Tracy Profiler GUI 无头自动化测试实战:emscripten Web 版与 X11 原生版双路线验证指南
【免费下载链接】tracyFrame profiler项目地址: https://gitcode.com/GitHub_Trending/tr/tracy
Tracy(Frame profiler)的图形界面(GUI)在无显示器(headless)环境中如何被自动化驱动与验证,是 CI 与回归测试中最棘手的问题之一。本文以仓库内.omp/skills/tracy-gui-verify/SKILL.md为核心,系统讲解两条可行的测试路线:Route A通过 emscripten 将 GUI 编译为 Web 应用,在无头浏览器中用 Puppeteer 驱动并借助debug_snapshot/debug_clear插桩获取应用侧鼠标/悬停状态的"地面真值";Route B在 Xvfb 虚拟 X 服务器 + openbox 上运行原生 GLFW GUI,用 xdotool 注入输入、以截图差异作为校验依据。读完本文,你将掌握两条路线的完整环境搭建、点击/拖拽/滚轮/键盘协议、坐标映射原理、空闲省电门控的行为陷阱,以及用于验证保存流程与实时连接的端到端检查方法。
路线总览:何时选 Web,何时选原生
两条路线都能在一台没有显示器(无 X11/Wayland/DISPLAY)的机器上运行 profiler GUI,但适用场景差异明显:
| 对比项 | Route A — Web(emscripten) | Route B — 原生(X11) |
|---|---|---|
| 显示方式 | 浏览器;无需 X11/Wayland/DISPLAY——浏览器本身就是"显示服务器" | Xvfb(虚拟 X 服务器)+ openbox(窗口管理器) |
| 输入方式 | Puppeteer 鼠标/键盘 | xdotool 鼠标/键盘 |
| 校验真值 | 插桩 harness(debug_snapshot/debug_clear)——应用侧鼠标/ID 状态 | 截图差异(PILImageChops)——无 harness |
| 可测试范围 | 已加载 trace 的 GUI:工具栏、面板、目标命中、DPI 缩放 | 与客户端的实时 TCP 连接、原生文件对话框(保存/打开)、X11/GLFW 输入语义 |
| 不可测试范围 | 实时客户端(浏览器无法开 TCP 服务端)、原生文件对话框 | 无已知 GUI 缺口;但校验精度较低(没有命中测试真值) |
| 前置依赖 | emsdk($EMSDK_DIR,默认~/emsdk)、CPM 缓存 | Xvfb、openbox、xdotool、ImageMagick(import)、mesa swrast |
简单概括:要验证加载 trace 后的 UI 交互与命中精度,选 Route A;要验证实时连接、原生保存/打开对话框和 X11 输入语义,选 Route B。
Route A — Web GUI(emscripten,无头浏览器)
Route A 的核心思想:把 profiler 的 emscripten 构建产物编译进一个被插桩的临时工作区,用 HTTP 服务托管,再用浏览器工具驱动。插桩 harness 导出debug_snapshot/debug_clear,把应用侧的鼠标/ID 状态暴露出来——这才是定位与验证的地面真值,而非截图。
文档中标记为M的常量是工作站相关的(例如本机无头浏览器的
dpr = 1.25),换机器必须重新验证;其余内容稳定,不需要重新推导。
环境搭建:setup.sh 与 harness 补丁
sh <this-skill-dir>/setup.sh # 工作区 workspace=/tmp/tracy-protocol # 或自定义:TRACY_WEB_WS=/elsewhere TRACY_WEB_REPO=<repo> sh setup.sh<this-skill-dir>指项目作用域下的.omp/skills/tracy-gui-verify/目录(即仓库根目录下的.omp/skills/tracy-gui-verify/),而不是固定的 home 目录。若多个项目各有一份该 skill,请使用被测试仓库内部的那份拷贝;TRACY_WEB_REPO决定把哪个仓库拷贝进工作区。
脚本执行流程(见 setup.sh):
- 全新源码拷贝到
$WS/src(排除.git与所有build*目录,通过 tar--exclude实现); - 应用 harness.patch——仅在拷贝中添加
debug_snapshot/debug_clear导出以及事件/帧记录逻辑(原仓库的 BackendEmscripten.cpp 不包含这些插桩,见下文"Harness API"); - 配置:
cmake -S $WS/src/profiler -B $WS/build,使用$EMSDK/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake工具链,CMAKE_BUILD_TYPE=Release,CPM_SOURCE_CACHE=$HOME/.cache/cpm; - 构建 Release(首次全量约 3–5 分钟,增量约 30 秒)。
脚本是幂等的:复用已有源码拷贝与构建目录,跳过已应用的 patch hunk,直接重编译。harness_complete()函数通过检查四处特征字符串(_debug_snapshot出现在 CMakeLists、EMSCRIPTEN_KEEPALIVE const char* debug_snapshot、DbgPushEvent( 1,与s_dbgTick++;出现在 BackendEmscripten.cpp)判断 harness 是否已完整就位;由于patch --forward在首个 hunk 已应用时会整文件跳过,必须用内容检查而非 patch 输出/退出码来判断。
陷阱 1:若工作区在tracy-web服务运行时被删除,先停掉 hub 进程——陈旧服务仍持有已删除的 cwd,端口依旧接受连接却静默返回空响应。
陷阱 2:上一会话死掉的tracy-web会把守护进程留在failed状态:stop只报告旧失败,start则一直返回 "Daemon tracy-web has unacknowledged completion notifications" 而端口空闲。用hub op=logs name=tracy-web排空守护进程(日志也会显示原因),再hub op=restart name=tracy-web重启(保留原 spec)。若端口不空闲,则是陈旧进程占用了它——往往是仍在服务旧(已过期)构建目录的进程:新的httpd.py会因 bind 失败而死,你的请求却默默命中旧目录。用ss -tlnp | grep 8000排查并结束占用者。任何 start/restart 之后都要验证服务真的在响应:curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8000/index.html必须返回 200——守护进程消息不代表服务可用。
修改 GUI 后重新构建:把改动文件拷入$WS/src/(保持相同相对路径),重跑cmake --build $WS/build -j$(nproc),然后刷新页面。若改动触及profiler/src/BackendEmscripten.cpp或profiler/CMakeLists.txt,harness 补丁可能不再适用——需要手动重新应用:保留Dbg*harness 区块、鼠标回调与NewFrame中的DbgPushEvent/DbgPushFrame调用,以及-sEXPORTED_FUNCTIONS中的两个debug_*导出。
服务、打开页面与就绪信号
服务端(长驻进程 → 交给 hub 管理):
hub op=start name=tracy-web application=python3 args=["$WS/build/httpd.py"] cwd=$WS/build ready={port:8000} persist=true必须用 httpd.py 而不是裸python -m http.server——它通过end_headers()注入 COOP/COEP 响应头(Cross-Origin-Embedder-Policy: require-corp与Cross-Origin-Opener-Policy: same-origin),这是 pthreads/SharedArrayBuffer 正常工作所必需的;同时它还设置了Cache-Control: no-cache等头,避免浏览器缓存旧构建。
浏览器:
browser open url=http://127.0.0.1:8000/index.html viewport={width:1600,height:900}就绪信号(在run单元格中执行;run代码运行在 Node 作用域,页面 JS 必须走tab.evaluate):
let title = null; for (let i = 0; i < 360; i++) { title = await tab.evaluate(() => document.title); if (/ - Tracy Profiler/.test(title)) break; await new Promise(r => setTimeout(r, 250)); }标题在预加载的embed.tracy(DarkRL 采集)完成加载前是Tracy Profiler X.Y.Z,加载完成后变成<trace> (embed.tracy) - Tracy Profiler X.Y.Z——这个拼接逻辑来自 main.cpp 的SetWindowTitleCallback("%s - Tracy Profiler %i.%i.%i")。标题匹配后再等约 1.5 秒,然后确认主循环活着:snap().frames.at(-1).tick > 0。
Harness API:应用侧地面真值
const snap = () => tab.evaluate(() => JSON.parse(Module.ccall('debug_snapshot','string',[],[]))); const clear = () => tab.evaluate(() => Module.ccall('debug_clear','void',[],[]));snap()返回{ dpr, innerW, innerH, bufW, bufH, rectL, rectT, rectW, rectH, title, cursor, frames:[…64], events:[…64] }(最新的在最后)。这两个 C 函数在 harness.patch 中以EMSCRIPTEN_KEEPALIVE导出,并被追加进-sEXPORTED_FUNCTIONS=_main,_nativeOpenFile,_tracy_paste_clipboard,_debug_snapshot,_debug_clear:
frames[]:每个 rAF tick 一条采样,包括空闲 tick。tick= 主循环计数(始终前进);f=ImGui::GetFrameCount()(只有真正渲染一帧才前进——受空闲门控控制,见"空闲与省电"节);mx/my= 应用侧鼠标位置;md[3]= 三个鼠标按键状态;q= 挂起的输入事件数;wa= tick 开始时tracy::s_wasActive的值;hi/ai= ImGui 的HoveredId/ActiveId。实现细节:DbgPushFrame()直接从ImGui::GetIO()与ImGui::GetCurrentContext()读取这些值(imgui_internal.h中的InputEventsQueue、HoveredId、ActiveId),环形缓冲容量 512 条。events[]:应用实际收到的原始回调数据。type:0=move、1=down、2=up、3=enter、4=leave、5=wheel;tx/ty= Emscripten 整型targetX/Y;ax/ay= 传给 ImGui 的位置(非 move 事件为 -1);t= DOMHighResTimeStamp。DbgPushEvent()被分别注入 mousedown/mouseup/mousemove/mouseleave/mouseenter/wheel 六个回调,与 BackendEmscripten.cpp 原始输入处理并存。
坐标映射:CSS 像素 ↔ 设备像素 ↔ 应用坐标
appX = floor( trunc(cssX - rectL) * dpr ) // 设备像素,ImGui 命中测试用的坐标 cssX = appX / dpr // 反向换算,用于定位 screenshot_px = cssX * dpr完整链路:DOMclientX→ EmscriptentargetX = int(clientX - (rect.left|0))(src/lib/libhtml5.js的fillMouseEventData把targetX以int写入 HEAP32)→ Tracy 回调AddMousePosEvent(targetX * dpr, …)(见 BackendEmscripten.cpp)→ ImGuiImFloor(imgui.cpp的AddMousePosEvent对坐标做 floor 与去重)。canvas 填满视口,所以rectL/rectT通常为 0(但脚本仍会重读)。
M:本工作站无头浏览器的dpr是 1.25——1600×900 视口对应 2000×1125 的 canvas 缓冲,截图也按设备像素捕获。永远不要假设 dpr 为 1。应用坐标是dpr的整数倍,用frames[].mx/my在 ±1 px 内验证命中。
点击协议:防吞点击的五步流程
点击 = 移动、按下、(按住期间的)刷新、释放、验证——全部对照已渲染的帧进行。空闲门控(见"空闲与省电")每次唤醒只渲染 3 帧,且完全没有鼠标事件的长按不会刷新任何帧,因此从冷空闲直接固定延时 down/up 可能被吞掉(两个事件都收到,但零帧渲染),或者在唤醒预算耗尽后才释放。按住期间持续发鼠标移动可以维持预算存活:同坐标重复 move 会被投递(已验证:能到达 canvas 处理器、唤醒应用、同坐标长按点击干净利落地跨越)且是最安全的刷新方式——零悬停漂移;±dxpx 的 wiggle 也可以,但必须保持在命中区域内:
async function click(cssX, cssY, holdMs = 400) { await page.mouse.move(cssX, cssY); // 1. 必须先移动 await new Promise(r => setTimeout(r, 150)); // 2. 保证 >= 1 帧已渲染 const pre = await snap(); const pf = pre.frames.at(-1); await clear(); await page.mouse.down(); // 3. 按下 let df = null, hiDuring = null; const t0 = Date.now(); while (Date.now() - t0 < holdMs) { await new Promise(r => setTimeout(r, 70)); await page.mouse.move(cssX, cssY); // 4. 同坐标重移刷新唤醒预算 const f = (await snap()).frames.at(-1); if (f.md[0] === 1) { df ??= f.f; hiDuring = f.hi; } } await page.mouse.up(); // 5. 释放 let uf = null; for (let i = 0; i < 40; i++) { await new Promise(r => setTimeout(r, 40)); const f = (await snap()).frames.at(-1); if (f.md[0] === 0) { uf = f.f; break; } } await new Promise(r => setTimeout(r, 400)); const s = await snap(); const d = s.dpr, ex = Math.floor(Math.trunc(cssX - s.rectL) * d), ey = Math.floor(Math.trunc(cssY - s.rectT) * d); return { hoverID: pf.hi, hiDuring, appPos: [pf.mx, pf.my], expected: [ex, ey], straddle: { df, uf }, ok: df !== null && uf !== null && uf > df && hiDuring === pf.hi && Math.abs(pf.mx - ex) <= 1 && Math.abs(pf.my - ey) <= 1 }; }规则要点:
- 每次点击前必须先移动。emscripten 的 mousemove 处理器是唯一设置应用鼠标位置的途径;mousedown/mouseup 只排队按钮状态。新页面位置是无效值(
-FLT_MAX)——没有先移动的点击激活不了任何东西。 - 按下与释放必须各自渲染在不同帧,且目标处于悬停状态(ImGui 的
PressedOnClickRelease语义,见imgui_widgets.cpp点击语义表;输入事件滴灌ConfigInputTrickleEventQueue会把同帧释放延迟到下一帧)。同坐标刷新正是保证冷空闲长按期间有帧渲染的手段。 - 每次点击都要验证:
ok: true(位置命中、按下/释放各渲染一帧、悬停保持hiDuring === hoverID),外加截图差异作为补充视觉确认。非 ImGui 目标(如时间线 zone 条)按设计hi = 0——这类目标用截图验证而非悬停 ID。 - 双击陷阱:同一位置两次点击间隔 < 300 ms 会被判定为双击(
MouseDoubleClickTime = 0.3 s)→ 触发缩放/线程信息操作。同一位置的重复点击之间至少等 350 ms。 - 拖拽:move → down →(move + 等 70 ms)× n → up。滚轮:定位后
page.mouse.wheel({ deltaY: ±500 })。按键:点击聚焦输入框后page.keyboard.type(..., { delay: 50 })输入,page.keyboard.press('Enter')提交。 - 危险区:工具栏设备 x 10–32(css 8–26)是红色电源按钮(最左工具栏图标,hover ID 3313137574),会关闭 trace 视图(回到 "Get started")。非预期时绝不点击。
空闲与省电:为什么点击会被"吞"
main.cpp 的DrawContents维护一个activeFrames = 3的预算:任何输入事件(置位tracy::s_wasActive,定义见 TracyImGui.cpp)、已连接客户端、视图动画或队列中的输入都会刷新预算;预算耗尽后sleep 16 ms,不调用ImGui::NewFrame、不做 GL 绘制(canvas 保留最后一帧)。预计空闲时约 97% 的 tick 跳过渲染。
- 冷空闲下的点击只有通过上述点击协议的按住刷新才能命中——固定延时 down/up 可能被空闲门控吞掉(两事件均收到、零帧渲染)。交互前先断言
tick/f在前进;主循环停滞(初始 trace 解析、隐藏标签页)时不渲染任何东西并丢弃一切输入。初始加载期间光标样式为wait。 focusLostLimit(失焦降低渲染率)仅桌面端存在,emscripten 路径没有这个逻辑(对比 BackendGlfw.cpp 与 BackendWayland.cpp 中基于tracy::s_config.focusLostLimit的 sleep)。canvas.style.cursor不是悬停真值:这个 ImGui 构建只在TextLink里设置 Hand 光标,按钮上不设置。
目标定位:用应用自身的命中测试代替目测
截图永远无法可靠地解决目标定位:整页截图到达时是降采样 JPEG(2000 设备像素缓冲通常只有 1024 px 宽,系数不定);page.screenshot({ clip })返回设备像素比例的 PNG——W×H 的 CSS clip 返回 W·dpr × H·dpr 像素。图标按钮只有 15–25 设备像素宽,目测误差 ±20 px 足以点中相邻按钮或空隙。正确做法是从应用自己的命中测试出发——横扫一行并点击命中的中心:
async function sweepRow(dpr, cssY, devFrom, devTo, step = 4) { const ranges = []; let cur = null; for (let dev = devFrom; dev <= devTo; dev += step) { await page.mouse.move(dev / dpr, cssY); await new Promise(r => setTimeout(r, 50)); const s = await snap(); const hi = s.frames[s.frames.length - 1].hi; if (cur && hi === cur.id) cur.end = dev; else { if (cur) ranges.push(cur); cur = { id: hi, start: dev, end: dev }; } } ranges.push(cur); return ranges.filter(r => r.id !== 0 && r.end - r.start >= 4); }- 连续步进中相同的非零
hi= 同一个控件;hi = 0= 空隙。点击该区间中心:click((r.start + r.end) / 2 / dpr, cssY)。 - 布局是状态相关的:停靠面板(Statistics、Flame 等)会缩放主窗口并移动工具栏——任何状态变化后都要重新横扫。
- 横扫前先关闭弹窗:弹窗打开时,弹窗外部的悬停可能读取
hi = 0(即使位于活控件之上,已用 ZoomPopup 悬停工具栏验证过)。点击 ZoomPopup 中的百分比选项不会关闭弹窗——先点击空白区域。 - Hover ID 跨会话稳定(标签哈希)。下面的映射表只对默认状态有效(新加载、无面板、1600×900、dpr 1.25)。
- 需要放大的区域截图(用于关联 ID 与图标):
page.screenshot({clip})返回 Buffer,用 Nodefs写入文件后读取:
const fs = require('fs'); const buf = await page.screenshot({ clip: { x: 0, y: 0, width: 720, height: 28 } }); fs.writeFileSync('/tmp/toolbar-zoom.png', buf);工具栏映射表——默认状态(设备像素;css = 设备值/1.25;行 y = css 15)
| 设备 x | hover ID | 功能 |
|---|---|---|
| 10–32 | 3313137574 | ⏻ 电源(红色,最左)——关闭 trace 视图(危险) |
| 44–68 | 608241489 | ⚙ 选项齿轮——打开 Options 窗口(可点击;勿与电源按钮混淆) |
| 81–185 | 2246346107 | 💬 Messages 面板开关 |
| 199–259 | 3177774389 | 🔍 Find zone 开关(搜索输入框 hover ID 3637961452,css 约 x 100–440,y 约 90) |
| 271–367 | 1419322276 | 📊 Statistics 开关 |
| 383–451 | 322727105 | 🔥 Flame graph 开关 |
| 463–551 | 37993378 | 💾 Memory graph 开关 |
| 567–667 | 2133895361 | ⚖ Compare 开关 |
| 679–735 | 1808024465 | ⌗ Info(trace 信息)开关 |
| 751–775 | 2535445525 | 🛠 扳手 → ToolsPopup(Playback、CPU data、Annotations、Limits、Wait stacks、Frame statistics) |
| 791–811 | 3101356487 | 📖 书本 → 用户手册开关 |
| 827–851 | 2335673581 | 🔍+ → ZoomPopup(50–300% 用户缩放) |
| 864–888 | 1263603982 | ◀ 上一帧——聚焦上一帧(视图范围收缩到该帧时间跨度) |
| ~890–1010 | 0 | 帧集名称 + 计数文本("Frames: N")——LMB 跳转到指定帧(自定义命中测试,类似时间线 zone) |
| 1012–1036 | 2142681495 | ▶ 下一帧——聚焦下一帧(已验证:视图范围 25.82 s → 203.38 ms) |
| 1052–1072 | 2961929287 | ▼ 帧集选择(切换活动帧集) |
按钮名称与功能见用户手册的Control menu一节(源码为 manual/tracy.tex,构建时生成build/profiler/manual/tracy.md到 profiler 构建目录)。Web 构建省略了Connection(仅实时采集)与Tracy Assist(仅桌面)。
用户缩放(DPI 缩放)
🔍+ 按钮(hover ID 2335673581)——即用户手册中的Display scale——打开含 50–300% 步进的 ZoomPopup。在 Web 构建中它只缩放 UI 布局:字体、Style::ScaleAllSizes与窗口尺寸(main.cpp的SetupDPIScale:scale = devicePixelRatio × userScale)。canvas 缓冲与鼠标→应用坐标映射仍停留在devicePixelRatio(见 BackendEmscripten.cpp)——命中区域在同一坐标空间里物理变大,定位精度不变,标准点击协议原样可用。
适合在小按钮难以点中或截图中细节难读时使用。在 200% 下每个工具栏命中区域约放大 2 倍(已验证:电源 16→40 px、齿轮 25→50 px、Messages 105→190 px 宽;行高 24→46 设备像素)。任何缩放下布局都会完全重排——改变缩放后以及恢复后都要重新横扫。userScale仅会话内有效,除非 Options → "Save UI scale" 开启;新页面加载从 100% 开始。
Route A 的限制
- 已连接的实时流客户端无法在 Web 构建中测试(浏览器 GUI 无法打开 TCP 服务端)。嵌入 trace 的 GUI 是实时应用的代理:真实 trace 数据、完整 GUI——实时客户端行为请用 Route B。
embed.tracy(10 MB)在配置时从 share.nereid.pl 下载——每个全新工作区需要一次网络;之后文件保存在$WS/build。
Route B — 原生 GUI(X11,无头)
在虚拟 X 服务器上运行原生(GLFW)GUI,用于 Route A 测不了的部分:与客户端的实时 TCP 连接、原生文件对话框(保存/打开流程)、X11 输入语义。技术栈:Xvfb + openbox + xdotool + ImageMagickimport。常量标记 M 在本工作站验证(1280×800 屏幕、openbox、profiler 窗口客户区位于 (1,20));屏幕/WM 变化后必须重新验证,点击前务必对照截图确认。
构建:LEGACY 与 GTK_FILESELECTOR 两个关键开关
cmake -S profiler -B /tmp/prof-x11 -DCMAKE_BUILD_TYPE=Release -DLEGACY=ON -DGTK_FILESELECTOR=ON cmake --build /tmp/prof-x11 --target tracy-profiler -j$(nproc)LEGACY=ON选择 X11(GLFW)后端;Linux 默认是 Wayland。该选项定义在 profiler/CMakeLists.txt。GTK_FILESELECTOR=ON让 NFD 走 GTK。不开它,Linux 默认 NFD 走 XDG 桌面门户(portal),无头环境会失败并弹出 "File selector is not available" 通知。底层实现见 cmake/vendor.cmake:GTK_FILESELECTOR打开时NFD_PORTAL置 OFF,并应用nfd-xdg-foreign-v2.patch。- 构建后验证:
ldd /tmp/prof-x11/tracy-profiler | grep -c gtk≥ 1。
服务:Xvfb + openbox
hub op=start name=xvfb application=Xvfb args=[":99","-screen","0","1280x800x24"]当/tmp/.X11-unix/X99存在时即就绪——检查该 socket。(若给 Xvfb 参数加-nolisten tcp,TCP 端口就绪检查将失效,UNIX socket 是唯一监听者。)然后:
DISPLAY=:99 nohup openbox >/dev/null 2>&1 &openbox 是必需的。没有窗口管理器,窗口落在任意位置,xdotool windowactivate会失败("WM claims not to support _NET_ACTIVE_WINDOW"),键盘焦点没有归属者。在 openbox 下 profiler 窗口被确定性放置(M:(1,20),1280×800 屏幕上 1278×775)。
运行与实时客户端
DISPLAY=:99 LIBGL_ALWAYS_SOFTWARE=1 nohup /tmp/prof-x11/tracy-profiler >gui.log 2>&1 &LIBGL_ALWAYS_SOFTWARE=1——无 GPU 时由 mesa swrast 软件渲染。- 窗口操作:
DISPLAY=:99 xdotool search --name "Tracy Profiler"→getwindowgeometry获取原点;windowactivate --sync聚焦。 - 实时测试客户端:
tracy-monitor -p $(pgrep -n sleep)风格——挂到一个长生命周期进程;进程内客户端监听 8086 并用 UDP 广播(无头环境下发现机制正常,客户端行会出现在 "Discovered clients" 下)。
交互规则
- 每次点击都用截图差异验证——
import -window root前后各拍一张,用 PILImageChops.difference(a, b).getbbox()比较:bbox 为空(或无相关变化)= 点击未命中。预期会有至少一次被吞的点击,重新发出并重新验证。 - 点击前先移动;同一位置两次点击间隔 ≥ 0.3 s(ImGui 双击阈值 0.3 s)。
- 键盘(
xdotool key)在 openbox 获得焦点后可靠。 - 首次启动会出现成就提示(右下角)——点击 "No"(M:725,585)。选择持久化在 Tracy 配置中,后续启动不再出现。
- 出现陈旧的 "Client not ready / another server" 通知,说明客户端在 GUI 之下被替换了——点击Reconnect(M:738,574);没有自动重试。
坐标表(M:屏幕像素,1280×800,窗口位于 (1,20))
| 目标 | 坐标 |
|---|---|
| Get started → Connect | 101,227 |
| Get started → 扳手(About 面板) | 317,120 |
| Connection 弹窗 → Save trace… | 64,202 |
| Connection 弹窗 → Reconnect | 738,574 |
| GTK 保存对话框 → Save 按钮 | 1195,773 |
| GTK 覆盖确认 → Replace | 790,482 |
| Tracy 保存确认 → Save trace | 757,540 |
已直接点击验证的:Connect、Save trace…、GTK Save、Replace、保存确认按钮。其余为"已验证窗口相对偏移 + openbox 窗口原点"推导(首次使用前确认):扳手、Reconnect、成就提示 "No"。
Get-started 面板、连接弹窗与两个确认面板在 profiler 窗口内保持固定偏移;GTK 对话框是独立顶层窗口,尺寸铺满屏幕。任何缺失坐标都从截图中推导,不要靠猜。
端到端保存流程
- 启动客户端,点击Connect(GUI 从不自动连接)。
- 连接弹窗自动打开;点击Save trace…。
- GTK 对话框:Name 输入框已聚焦、上次使用目录已预选;
xdotool type --delay 30 <name>输入名字;点击Save(1195,773)。 - 目标文件已存在 → 弹出独立的覆盖确认窗口;点击Replace(790,482)。
- 出现 Tracy 自己的确认面板(显示最终
Path:);点击其Save trace(757,540)。 - 按下面的方式验证文件。
文件与连接检查
- 实时连接:
ss -tnp | grep 8086显示tracy-profiler与客户端进程之间的ESTAB;窗口标题变为<program> @ <date> - Tracy Profiler X.Y.Z。 - 保存的
.tracy:前 4 字节 =74 72 FD 50;tail -c +11 file | zstd -t输出premature end表示帧解码完整(预期——裁剪点落在最后一帧之后、落入尾部元数据);unsupported format表示偏移错误或文件不是 trace。 - 保存完成后不得残留
file.tmp;保存中途被杀时旧文件保持完好(GUI 通过 server/TracySafeFileWrite.hpp 写入:先写final + ".tmp",提交时std::filesystem::rename原子替换——这正是上述不变量的来源)。 - 交互过程的视频证据:
ffmpeg -f x11grab -video_size 1280x800 -i :99 -t N -c:v libx264 out.mp4(后台运行;录制整个交互过程)。
Route B 的限制
- 无 debug harness——截图差异是唯一真值;定位靠截图坐标,比 Route A 的命中测试横扫慢且粗。
- 已验证坐标与 M 常量绑定(1280×800 屏幕、openbox 默认主题、100% UI 缩放);任何一项变化都需从截图重新推导。
- GTK 对话框是独立顶层窗口,其布局跟随系统 GTK 主题。
清理
pkill -f prof-x11; pkill -f tracy-monitor pkill -f "sleep 600" # §运行 一节中的测试目标——按你启动的进程调整 pkill openbox # hub op=stop name=xvfb关键源码索引:行为规则的底层依据
当某个常量需要复核、或某条规则突然失效时,以下文件是权威来源:
- profiler/src/BackendEmscripten.cpp — emscripten 输入处理、canvas/dpr 尺寸、光标映射(mousemove 中
targetX * dpr的换算、NewFrame中 canvas 缓冲按innerWidth × dpr重设)。 - profiler/src/main.cpp — 空闲门控(
activeFrames = 3预算与sleep 16 ms)。 - profiler/src/profiler/TracyImGui.cpp —
s_wasActive每帧重置;TracyImGui.hpp 中WasActive()接口。 - profiler/src/profiler/TracyMouse.cpp — 逐帧点击状态缓存(
MouseFrame)。 - profiler/CMakeLists.txt 与 cmake/vendor.cmake —
NO_FILESELECTOR、GTK_FILESELECTOR、LEGACY选项,NFD 打包与NFD_PORTAL默认开关、nfd-xdg-foreign-v2.patch。 - server/TracySafeFileWrite.hpp — GUI 保存的 tmp + rename 提交机制(保存检查不变量的来源)。
- profiler/src/profiler/TracyConfig.hpp —
focusLostLimit默认值(桌面端失焦降渲染率,emscripten 无此逻辑)。 - harness.patch 与 setup.sh — Route A 插桩与工作区构建的完整实现。
- ImGui 侧语义(vendored 拷贝在 CPM 缓存
~/.cache/cpm/imgui/):imgui_widgets.cpp的点击语义表(PressedOnClickRelease等)、imgui.cpp的事件滴灌(ConfigInputTrickleEventQueue默认开启)与AddMousePosEvent的 floor/去重。 - Emscripten 侧(
src/lib/libhtml5.js、system/include/emscripten/html5.h):targetX = e.clientX - (rect.left|0)以int写入 HEAP32;EmscriptenWheelEvent内嵌mouse子结构。
理解这些源码细节后,两条路线的所有常量、陷阱与交互协议都能随时在仓库内重新验证,而不是盲信文档——这正是本技能文档设计的初衷。
【免费下载链接】tracyFrame profiler项目地址: https://gitcode.com/GitHub_Trending/tr/tracy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考