news 2026/9/13 12:15:05

Tracy Profiler GUI 无头自动化测试实战:emscripten Web 版与 X11 原生版双路线验证指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Tracy Profiler GUI 无头自动化测试实战:emscripten Web 版与 X11 原生版双路线验证指南

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):

  1. 全新源码拷贝$WS/src(排除.git与所有build*目录,通过 tar--exclude实现);
  2. 应用 harness.patch——仅在拷贝中添加debug_snapshot/debug_clear导出以及事件/帧记录逻辑(原仓库的 BackendEmscripten.cpp 不包含这些插桩,见下文"Harness API");
  3. 配置cmake -S $WS/src/profiler -B $WS/build,使用$EMSDK/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake工具链,CMAKE_BUILD_TYPE=ReleaseCPM_SOURCE_CACHE=$HOME/.cache/cpm
  4. 构建 Release(首次全量约 3–5 分钟,增量约 30 秒)。

脚本是幂等的:复用已有源码拷贝与构建目录,跳过已应用的 patch hunk,直接重编译。harness_complete()函数通过检查四处特征字符串(_debug_snapshot出现在 CMakeLists、EMSCRIPTEN_KEEPALIVE const char* debug_snapshotDbgPushEvent( 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.cppprofiler/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-corpCross-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 一条采样,包括空闲 ticktick= 主循环计数(始终前进);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中的InputEventsQueueHoveredIdActiveId),环形缓冲容量 512 条。
  • events[]:应用实际收到的原始回调数据。type:0=move、1=down、2=up、3=enter、4=leave、5=wheel;tx/ty= Emscripten 整型targetX/Yax/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.jsfillMouseEventDatatargetXint写入 HEAP32)→ Tracy 回调AddMousePosEvent(targetX * dpr, …)(见 BackendEmscripten.cpp)→ ImGuiImFloorimgui.cppAddMousePosEvent对坐标做 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 }; }

规则要点:

  1. 每次点击前必须先移动。emscripten 的 mousemove 处理器是唯一设置应用鼠标位置的途径;mousedown/mouseup 只排队按钮状态。新页面位置是无效值(-FLT_MAX)——没有先移动的点击激活不了任何东西。
  2. 按下与释放必须各自渲染在不同帧,且目标处于悬停状态(ImGui 的PressedOnClickRelease语义,见imgui_widgets.cpp点击语义表;输入事件滴灌ConfigInputTrickleEventQueue会把同帧释放延迟到下一帧)。同坐标刷新正是保证冷空闲长按期间有帧渲染的手段。
  3. 每次点击都要验证:ok: true(位置命中、按下/释放各渲染一帧、悬停保持hiDuring === hoverID),外加截图差异作为补充视觉确认。非 ImGui 目标(如时间线 zone 条)按设计hi = 0——这类目标用截图验证而非悬停 ID。
  4. 双击陷阱:同一位置两次点击间隔 < 300 ms 会被判定为双击(MouseDoubleClickTime = 0.3 s)→ 触发缩放/线程信息操作。同一位置的重复点击之间至少等 350 ms。
  5. 拖拽:move → down →(move + 等 70 ms)× n → up。滚轮:定位后page.mouse.wheel({ deltaY: ±500 })按键:点击聚焦输入框后page.keyboard.type(..., { delay: 50 })输入,page.keyboard.press('Enter')提交。
  6. 危险区:工具栏设备 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)
设备 xhover ID功能
10–323313137574⏻ 电源(红色,最左)——关闭 trace 视图(危险)
44–68608241489⚙ 选项齿轮——打开 Options 窗口(可点击;勿与电源按钮混淆)
81–1852246346107💬 Messages 面板开关
199–2593177774389🔍 Find zone 开关(搜索输入框 hover ID 3637961452,css 约 x 100–440,y 约 90)
271–3671419322276📊 Statistics 开关
383–451322727105🔥 Flame graph 开关
463–55137993378💾 Memory graph 开关
567–6672133895361⚖ Compare 开关
679–7351808024465⌗ Info(trace 信息)开关
751–7752535445525🛠 扳手 → ToolsPopup(Playback、CPU data、Annotations、Limits、Wait stacks、Frame statistics)
791–8113101356487📖 书本 → 用户手册开关
827–8512335673581🔍+ → ZoomPopup(50–300% 用户缩放)
864–8881263603982◀ 上一帧——聚焦上一帧(视图范围收缩到该帧时间跨度)
~890–10100帧集名称 + 计数文本("Frames: N")——LMB 跳转到指定帧(自定义命中测试,类似时间线 zone)
1012–10362142681495▶ 下一帧——聚焦下一帧(已验证:视图范围 25.82 s → 203.38 ms)
1052–10722961929287▼ 帧集选择(切换活动帧集)

按钮名称与功能见用户手册的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.cppSetupDPIScalescale = 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" 下)。

交互规则

  1. 每次点击都用截图差异验证——import -window root前后各拍一张,用 PILImageChops.difference(a, b).getbbox()比较:bbox 为空(或无相关变化)= 点击未命中。预期会有至少一次被吞的点击,重新发出并重新验证。
  2. 点击前先移动;同一位置两次点击间隔 ≥ 0.3 s(ImGui 双击阈值 0.3 s)。
  3. 键盘(xdotool key)在 openbox 获得焦点后可靠。
  4. 首次启动会出现成就提示(右下角)——点击 "No"(M:725,585)。选择持久化在 Tracy 配置中,后续启动不再出现。
  5. 出现陈旧的 "Client not ready / another server" 通知,说明客户端在 GUI 之下被替换了——点击Reconnect(M:738,574);没有自动重试。

坐标表(M:屏幕像素,1280×800,窗口位于 (1,20))

目标坐标
Get started → Connect101,227
Get started → 扳手(About 面板)317,120
Connection 弹窗 → Save trace…64,202
Connection 弹窗 → Reconnect738,574
GTK 保存对话框 → Save 按钮1195,773
GTK 覆盖确认 → Replace790,482
Tracy 保存确认 → Save trace757,540

已直接点击验证的:Connect、Save trace…、GTK Save、Replace、保存确认按钮。其余为"已验证窗口相对偏移 + openbox 窗口原点"推导(首次使用前确认):扳手、Reconnect、成就提示 "No"。

Get-started 面板、连接弹窗与两个确认面板在 profiler 窗口内保持固定偏移;GTK 对话框是独立顶层窗口,尺寸铺满屏幕。任何缺失坐标都从截图中推导,不要靠猜。

端到端保存流程

  1. 启动客户端,点击Connect(GUI 从不自动连接)。
  2. 连接弹窗自动打开;点击Save trace…
  3. GTK 对话框:Name 输入框已聚焦、上次使用目录已预选;xdotool type --delay 30 <name>输入名字;点击Save(1195,773)。
  4. 目标文件已存在 → 弹出独立的覆盖确认窗口;点击Replace(790,482)。
  5. 出现 Tracy 自己的确认面板(显示最终Path:);点击其Save trace(757,540)。
  6. 按下面的方式验证文件。

文件与连接检查

  • 实时连接:ss -tnp | grep 8086显示tracy-profiler与客户端进程之间的ESTAB;窗口标题变为<program> @ <date> - Tracy Profiler X.Y.Z
  • 保存的.tracy:前 4 字节 =74 72 FD 50tail -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_FILESELECTORGTK_FILESELECTORLEGACY选项,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.jssystem/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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 12:13:55

智能信贷审批系统:架构设计与机器学习实践

1. 智能信贷审批系统的行业背景与核心价值信贷审批流程的智能化改造正在深刻重塑金融行业格局。传统人工审批模式平均需要3-7个工作日完成全流程&#xff0c;而智能审批系统能将这个时间压缩到分钟级。某股份制银行的实际案例显示&#xff0c;部署智能系统后审批效率提升40倍&a…

作者头像 李华
网站建设 2026/9/13 12:12:57

SAP Fiori Launchpad配置与权限管理实战指南

1. 项目概述&#xff1a;从SAP GUI到Fiori Launchpad的转型之路 在SAP生态系统中工作了十多年的老用户&#xff0c;应该都记得那个被事务码&#xff08;T-Code&#xff09;支配的时代。每天上班第一件事就是打开厚重的SAP GUI客户端&#xff0c;在命令行输入SE38、MM01、VA01这…

作者头像 李华
网站建设 2026/9/13 12:12:36

光热电站储热容量配置优化与经济性分析

1. 光热电站储热容量配置的背景与挑战光热发电技术&#xff08;CSP&#xff09;作为可再生能源领域的重要分支&#xff0c;近年来在全球范围内获得了快速发展。与传统光伏发电不同&#xff0c;光热电站通过聚光系统将太阳能转化为热能&#xff0c;再通过热力循环发电&#xff0…

作者头像 李华
网站建设 2026/9/13 12:11:24

TypeScript在AI Agent开发中的优势与实践

1. 为什么TypeScript成为AI Agent开发的首选语言在AI Agent开发领域&#xff0c;TypeScript近年来呈现出爆发式增长。根据GitHub官方统计&#xff0c;2025-2026年间新开源的AI Agent项目中&#xff0c;75%以上采用TypeScript/JavaScript技术栈。这种压倒性优势的形成并非偶然&a…

作者头像 李华