mirror of
https://github.com/wolfpld/tracy.git
synced 2026-09-01 16:08:30 +00:00
Add web gui test skill.
This commit is contained in:
340
.omp/skills/tracy-web-gui-test/SKILL.md
Normal file
340
.omp/skills/tracy-web-gui-test/SKILL.md
Normal file
@@ -0,0 +1,340 @@
|
||||
---
|
||||
name: tracy-web-gui-test
|
||||
description: Build, serve, and drive the Tracy Profiler's emscripten (web) GUI in a headless browser with verified mouse/keyboard semantics. Use when developing or testing the profiler GUI.
|
||||
---
|
||||
|
||||
# Test the Tracy web GUI in a browser
|
||||
|
||||
Build the emscripten profiler GUI into an instrumented temporary workspace,
|
||||
serve it, and drive it with the browser tool. The harness exports
|
||||
(`debug_snapshot`/`debug_clear`) expose the app-side mouse/ID state — they
|
||||
are the ground truth for targeting and verification, not screenshots.
|
||||
|
||||
Applies to this repo's emscripten build with ImGui 1.92.9b-docking and
|
||||
Emscripten 5.0.7. Constants marked M are workstation-dependent — re-verify
|
||||
them. Everything else is stable; do not re-derive it.
|
||||
|
||||
## Setup
|
||||
|
||||
```sh
|
||||
sh <this-skill-dir>/setup.sh # workspace=/tmp/tracy-protocol
|
||||
# or: TRACY_WEB_WS=/elsewhere TRACY_WEB_REPO=<repo> sh setup.sh
|
||||
```
|
||||
|
||||
`<this-skill-dir>` is the project-scoped `.omp/skills/tracy-web-gui-test/`
|
||||
under the active project root — not a fixed home directory. If multiple
|
||||
copies exist (one per project that has used this skill), use the copy
|
||||
inside the repo being tested; `TRACY_WEB_REPO` controls which repo is
|
||||
copied into the workspace.
|
||||
|
||||
The script: fresh source copy (excludes `.git` and build dirs) → applies
|
||||
`harness.patch` (adds `debug_snapshot`/`debug_clear` exports + event/frame
|
||||
recording to the emscripten backend, in the COPY only) → configures → builds
|
||||
Release with the emsdk at `$EMSDK_DIR` (default `~/emsdk`) and the CPM cache at
|
||||
`~/.cache/cpm`.
|
||||
|
||||
**Pitfall:** if the workspace is deleted while a `tracy-web` server is running,
|
||||
stop the hub process first — a stale server keeps the *deleted* cwd and silently
|
||||
returns empty responses while the port still accepts connections.
|
||||
|
||||
**Pitfall:** a `tracy-web` that died in an earlier session leaves the daemon in
|
||||
`failed` state: `stop` only reports the old failure, and `start` keeps returning
|
||||
"Daemon tracy-web has unacknowledged completion notifications" while the port
|
||||
stays free. Drain the daemon with `hub op=logs name=tracy-web` (the log also
|
||||
shows the cause), then relaunch with `hub op=restart name=tracy-web` (retained
|
||||
spec). If the port is NOT free, a stale process owns it — often one serving an
|
||||
earlier (now-stale) build directory: the new `httpd.py` dies on the bind and
|
||||
your requests silently hit the old directory. Check `ss -tlnp | grep 8000` and
|
||||
kill the owner. After any start/restart, verify the server is actually
|
||||
serving: `curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8000/index.html`
|
||||
must return 200 — the daemon message is not proof of a server.
|
||||
|
||||
**After editing the GUI in the repo:** copy the changed files into
|
||||
`$WS/src/` (same relative path), re-run `cmake --build $WS/build -j$(nproc)`,
|
||||
reload the page. If a change touches `profiler/src/BackendEmscripten.cpp` or
|
||||
`profiler/CMakeLists.txt`, the harness patch may no longer apply — re-apply it
|
||||
manually (keep the `Dbg*` harness section, the `DbgPushEvent`/`DbgPushFrame`
|
||||
calls in the mouse callbacks + `NewFrame`, and the two `debug_*` exports in
|
||||
`-sEXPORTED_FUNCTIONS`).
|
||||
|
||||
## Serve + open + ready
|
||||
|
||||
Server (long-running → `hub`):
|
||||
|
||||
```
|
||||
hub op=start name=tracy-web application=python3 args=["$WS/build/httpd.py"]
|
||||
cwd=$WS/build ready={port:8000} persist=true
|
||||
```
|
||||
|
||||
`httpd.py` sends the COOP/COEP headers required for pthreads/SharedArrayBuffer —
|
||||
use it, not a bare http.server.
|
||||
|
||||
Browser:
|
||||
|
||||
```
|
||||
browser open url=http://127.0.0.1:8000/index.html viewport={width:1600,height:900}
|
||||
```
|
||||
|
||||
**Ready signal** (in a `run` cell; `run` code executes in Node scope — page JS
|
||||
goes through `tab.evaluate`):
|
||||
|
||||
```js
|
||||
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));
|
||||
}
|
||||
```
|
||||
|
||||
Title is `Tracy Profiler X.Y.Z` until the preloaded `embed.tracy` (DarkRL
|
||||
capture) finishes loading, then `<trace> (embed.tracy) - Tracy Profiler X.Y.Z`
|
||||
(`profiler/src/main.cpp` `SetWindowTitleCallback`). Wait ~1.5 s after, then
|
||||
confirm the app loop lives: `snap().frames.at(-1).tick > 0`.
|
||||
|
||||
## Harness API (ground truth)
|
||||
|
||||
```js
|
||||
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] }` (newest last).
|
||||
|
||||
- `frames[]`: one sample **per rAF tick, including idle ticks**. `tick` = main-loop
|
||||
count (always advances), `f` = `ImGui::GetFrameCount()` (advances only when a
|
||||
frame actually renders — the idle gate, §Idle), `mx/my` = app mouse position,
|
||||
`md[3]` = mouse button states, `q` = pending input events, `wa` =
|
||||
`tracy::s_wasActive` at tick start, `hi`/`ai` = ImGui `HoveredId`/`ActiveId`.
|
||||
- `events[]`: raw callback data as the app received it. `type` 0=move 1=down
|
||||
2=up 3=enter 4=leave 5=wheel; `tx/ty` = Emscripten int `targetX/Y`; `ax/ay` =
|
||||
position passed to ImGui (`-1` for non-move); `t` = DOMHighResTimeStamp.
|
||||
|
||||
## Coordinate mapping
|
||||
|
||||
```
|
||||
appX = floor( trunc(cssX - rectL) * dpr ) // device px, what ImGui hit-tests
|
||||
cssX = appX / dpr // inverse for targeting
|
||||
screenshot_px = cssX * dpr
|
||||
```
|
||||
|
||||
Chain: DOM `clientX` → Emscripten `targetX = int(clientX - (rect.left|0))`
|
||||
(`src/lib/libhtml5.js`, `EmscriptenMouseEvent.targetX` is an `int`) → Tracy
|
||||
`AddMousePosEvent(targetX * dpr, …)` (`BackendEmscripten.cpp`) → ImGui `ImFloor`
|
||||
(`imgui.cpp` `AddMousePosEvent`). Canvas fills the viewport, so `rectL/rectT`
|
||||
are 0 (re-read anyway).
|
||||
|
||||
**M: `dpr` on this workstation's headless browser is 1.25** (canvas buffer
|
||||
2000×1125 for a 1600×900 viewport; screenshots are captured at device px).
|
||||
Never assume 1. App positions come in multiples of `dpr`; verify targeting
|
||||
within ±1 px via `frames[].mx/my`.
|
||||
|
||||
## Click protocol
|
||||
|
||||
Click = move, press, refresh through the hold, release, verify — all
|
||||
against rendered frames. The idle gate (§Idle) renders only 3 frames per
|
||||
wake, and a hold with no mouse events at all refreshes nothing, so a
|
||||
fixed-wait down/up from a cold idle can be swallowed (both events received,
|
||||
zero frames rendered) or released after the wake budget expires. Issuing
|
||||
mouse moves while the button is down keeps the budget alive: a same-coord
|
||||
re-move is delivered (verified: it reaches the canvas handler, wakes the
|
||||
app, and a same-coord hold click straddles cleanly) and is the safest
|
||||
refresh — zero hover drift; a ±`dx` px wiggle works too, but must stay
|
||||
inside the hit area:
|
||||
|
||||
```js
|
||||
async function click(cssX, cssY, holdMs = 400) {
|
||||
await page.mouse.move(cssX, cssY); // 1. ALWAYS move first
|
||||
await new Promise(r => setTimeout(r, 150)); // 2. >= 1 rendered frame
|
||||
const pre = await snap();
|
||||
const pf = pre.frames.at(-1);
|
||||
await clear();
|
||||
await page.mouse.down(); // 3. press
|
||||
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. same-coord re-move refreshes the wake budget
|
||||
const f = (await snap()).frames.at(-1);
|
||||
if (f.md[0] === 1) { df ??= f.f; hiDuring = f.hi; }
|
||||
}
|
||||
await page.mouse.up(); // 5. release
|
||||
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 };
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
1. **Move before every click.** The emscripten mousemove handler is the only
|
||||
thing that sets the app mouse position; mousedown/mouseup only queue button
|
||||
state. Fresh page position is invalid (`-FLT_MAX`) — a click without a prior
|
||||
move activates nothing.
|
||||
2. **Press and release must each render** in separate frames with the item
|
||||
hovered (ImGui's `PressedOnClickRelease` — doc table at
|
||||
`imgui_widgets.cpp:489-497`; input trickling,
|
||||
`ConfigInputTrickleEventQueue`, defers a same-frame release). The
|
||||
same-coord refresh is what guarantees rendered frames across the hold
|
||||
from a cold idle.
|
||||
3. **Verify every click:** `ok: true` (position landed, press and release each
|
||||
rendered, hover held — `hiDuring === hoverID`) plus a screenshot diff.
|
||||
Non-ImGui targets (timeline zone bars) have `hi = 0` by design — for those
|
||||
verify by screenshot, not hover.
|
||||
4. **Double-click trap:** two same-position clicks <300 ms apart = double-click
|
||||
(`MouseDoubleClickTime = 0.3 s`) → zoom/thread-info actions fire. Wait
|
||||
≥350 ms between repeated clicks at one spot.
|
||||
5. **Drag:** move → down → (move + wait 70 ms)×n → up. **Wheel:**
|
||||
`page.mouse.wheel({ deltaY: ±500 })` after positioning. **Keys:**
|
||||
`page.keyboard.type(..., { delay: 50 })` after focusing the input by click;
|
||||
`page.keyboard.press('Enter')`.
|
||||
6. **Danger:** toolbar device x 10–32 (css 8–26) = red power button (leftmost
|
||||
toolbar icon, hover ID 3313137574), closes the trace view (back to "Get
|
||||
started"). Never click it unless intended.
|
||||
|
||||
## Idle and power saving
|
||||
|
||||
`main.cpp DrawContents` keeps an `activeFrames = 3` budget refreshed on any
|
||||
input event (`tracy::s_wasActive`), a connected client, view animation, or a
|
||||
queued input; exhausted → `sleep 16 ms` with **no `ImGui::NewFrame` and no GL
|
||||
draw** (canvas keeps the last frame). Expect ~97 % of ticks to skip rendering
|
||||
while idle.
|
||||
|
||||
- Clicks from a cold idle land only via the click protocol's hold refresh
|
||||
(§Click protocol) — a fixed-wait down/up can be swallowed by the idle gate
|
||||
(both events received, zero frames rendered). **Before interacting, assert
|
||||
`tick`/`f` is advancing**; a stalled main loop (initial trace parse, hidden
|
||||
tab) renders nothing and drops everything. During the initial load the
|
||||
cursor style is `wait`.
|
||||
- `focusLostLimit` (reduce render rate on focus loss) is desktop-only; it does
|
||||
not exist in the emscripten path.
|
||||
- `canvas.style.cursor` is **not** a hover oracle: this ImGui build sets the
|
||||
Hand cursor only in `TextLink`, not on buttons.
|
||||
|
||||
## Targeting
|
||||
|
||||
Screenshots never resolve targeting. Full-page shots arrive downscaled JPEG
|
||||
(1024 px wide for a 2000-px device buffer; the factor varies), and
|
||||
`page.screenshot({ clip })` returns a PNG at device scale — a W×H CSS clip
|
||||
returns W·dpr × H·dpr px (CSS = clip offset + px/dpr). Icon buttons are
|
||||
15–25 device px wide, so visual estimates carry ±20 px error, enough to hit
|
||||
the neighboring button or a gap. Target from the app's own hit-testing —
|
||||
sweep the row and click the run's center:
|
||||
|
||||
```js
|
||||
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);
|
||||
}
|
||||
```
|
||||
|
||||
- Same non-zero `hi` across consecutive steps = one widget; `hi = 0` = gap.
|
||||
Click the run's center: `click((r.start + r.end) / 2 / dpr, cssY)`.
|
||||
- **Layout is state-dependent:** docked panels (Statistics, Flame, …) resize the
|
||||
main window and shift the toolbar — **re-sweep after any state change.**
|
||||
- **Close popups before sweeping:** while a popup is open, hovers outside the
|
||||
popup can read `hi = 0` even over live widgets (verified with ZoomPopup over
|
||||
the toolbar). A % click in ZoomPopup does not close the popup — click an
|
||||
empty area first.
|
||||
- Hover IDs are stable across sessions (label hashes) — the map below holds for
|
||||
the default state (fresh load, no panels, 1600×900, dpr 1.25).
|
||||
- Zoomed region shots (for correlating IDs with icons): `page.screenshot({clip})`
|
||||
returns a Buffer — write it with Node `fs`, then `read` the file:
|
||||
|
||||
```js
|
||||
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);
|
||||
```
|
||||
|
||||
### Toolbar map — default state (device px; css = device/1.25; row y css 15)
|
||||
|
||||
| device x | hover ID | function |
|
||||
|---|---|---|
|
||||
| 10–32 | 3313137574 | ⏻ power (red, leftmost) — **closes the trace view (DANGER)** |
|
||||
| 44–68 | 608241489 | ⚙ Options gear — opens the Options window (clickable; do not confuse with the power button) |
|
||||
| 81–185 | 2246346107 | 💬 Messages panel toggle |
|
||||
| 199–259 | 3177774389 | 🔍 Find zone toggle (search input hover ID 3637961452, css ~x 100–440, y ~90) |
|
||||
| 271–367 | 1419322276 | 📊 Statistics toggle |
|
||||
| 383–451 | 322727105 | 🔥 Flame graph toggle |
|
||||
| 463–551 | 37993378 | 💾 Memory graph toggle |
|
||||
| 567–667 | 2133895361 | ⚖ Compare toggle |
|
||||
| 679–735 | 1808024465 | ⌗ Info (trace information) toggle |
|
||||
| 751–775 | 2535445525 | 🛠 wrench → ToolsPopup (Playback, CPU data, Annotations, Limits, Wait stacks, Frame statistics) |
|
||||
| 791–811 | 3101356487 | 📖 book → User manual toggle |
|
||||
| 827–851 | 2335673581 | 🔍+ → ZoomPopup (50–300 % user scale) |
|
||||
| 864–888 | 1263603982 | ◀ previous frame — focuses the timeline on the previous frame (view range shrinks to the frame's timespan) |
|
||||
| ~890–1010 | 0 | frame set name + count text ("Frames: N") — LMB jumps to a specified frame (custom hit-test, like timeline zones) |
|
||||
| 1012–1036 | 2142681495 | ▶ next frame — focuses the timeline on the next frame (verified: view range 25.82 s → 203.38 ms) |
|
||||
| 1052–1072 | 2961929287 | ▼ frame set selection (switch the active frame set) |
|
||||
|
||||
Button names and functions: the user manual, *Control menu* (`manual/tracy.md`). The
|
||||
web build omits *Connection* (live capture only) and *Tracy Assist* (desktop only).
|
||||
|
||||
## User scale (DPI zoom)
|
||||
|
||||
The 🔍+ button (hover ID 2335673581) — *Display scale* in the user manual —
|
||||
opens a ZoomPopup with 50–300 % steps.
|
||||
In the web build this scales the UI **layout only**: fonts,
|
||||
`Style::ScaleAllSizes`, and window sizes (`main.cpp SetupDPIScale`,
|
||||
`scale = devicePixelRatio × userScale`). The canvas buffer and the mouse→app
|
||||
mapping stay at `devicePixelRatio` (`BackendEmscripten.cpp`) — hit regions
|
||||
become physically larger in the same coordinate space, targeting precision is
|
||||
unchanged, and the standard click protocol works as-is.
|
||||
|
||||
Use it when small buttons are hard to hit or details are hard to read in
|
||||
screenshots. At 200 % every toolbar hit region is roughly 2× (verified: power
|
||||
16→40 px, gear 25→50 px, Messages 105→190 px wide; row height 24→46 device
|
||||
px). The layout **reflows completely** at any scale — re-sweep after changing
|
||||
it and after setting it back. `userScale` is session-only unless Options →
|
||||
"Save UI scale" is on; a fresh page load starts at 100 %.
|
||||
|
||||
## Limitations
|
||||
|
||||
- A *connected* live-streaming client cannot be tested in the web build (the
|
||||
browser GUI cannot open a TCP server). The embedded-trace GUI is the
|
||||
live-app proxy: real trace data, full GUI.
|
||||
- `embed.tracy` (10 MB) is downloaded from share.nereid.pl at configure time —
|
||||
network needed once per fresh workspace; the file then lives in `$WS/build`.
|
||||
|
||||
## References
|
||||
|
||||
Sources for the behavior the rules above encode — consult them when re-verifying
|
||||
a constant or a rule fails:
|
||||
|
||||
- `profiler/src/BackendEmscripten.cpp` — emscripten input handlers, canvas/dpr
|
||||
sizing, cursor mapping.
|
||||
- `profiler/src/main.cpp:540-574` — idle gate (`activeFrames` budget).
|
||||
- `profiler/src/profiler/TracyMouse.cpp` — per-frame click state cache.
|
||||
- `profiler/CMakeLists.txt:334-353` — emscripten link options.
|
||||
- ImGui 1.92.9b (vendored copy in the CPM cache, `~/.cache/cpm/imgui/`):
|
||||
`imgui_widgets.cpp:483-545` (click semantics table), `imgui.cpp:11278-11403`
|
||||
(event trickling), `imgui.cpp:1736` (trickle default on), `imgui.cpp:1992-2016`
|
||||
(AddMousePosEvent floor/dedup).
|
||||
- Emscripten 5.0.7: `src/lib/libhtml5.js` (`fillMouseEventData`,
|
||||
`targetX = e.clientX - (rect.left|0)` into HEAP32), `system/include/emscripten/html5.h`
|
||||
(`EmscriptenMouseEvent.targetX` is `int`; `EmscriptenWheelEvent` embeds a
|
||||
`mouse` sub-struct).
|
||||
- Upstream: imgui issue #4921 (new input event API — events spread over
|
||||
multiple frames), PR #2525 (same-frame click/release), issue #1992 (rapid
|
||||
down/up "soft clicks" dropped).
|
||||
221
.omp/skills/tracy-web-gui-test/harness.patch
Normal file
221
.omp/skills/tracy-web-gui-test/harness.patch
Normal file
@@ -0,0 +1,221 @@
|
||||
diff -r a/profiler/CMakeLists.txt b/profiler/CMakeLists.txt
|
||||
--- a/profiler/CMakeLists.txt
|
||||
+++ b/profiler/CMakeLists.txt
|
||||
@@ -340,7 +340,7 @@
|
||||
-sMAXIMUM_MEMORY=4gb
|
||||
-sSTACK_SIZE=1048576
|
||||
-sPTHREAD_POOL_SIZE=8
|
||||
- -sEXPORTED_FUNCTIONS=_main,_nativeOpenFile,_tracy_paste_clipboard
|
||||
+ -sEXPORTED_FUNCTIONS=_main,_nativeOpenFile,_tracy_paste_clipboard,_debug_snapshot,_debug_clear
|
||||
-sEXPORTED_RUNTIME_METHODS=ccall
|
||||
-sENVIRONMENT=web,worker
|
||||
--preload-file embed.tracy
|
||||
diff -r a/profiler/src/BackendEmscripten.cpp b/profiler/src/BackendEmscripten.cpp
|
||||
--- a/profiler/src/BackendEmscripten.cpp
|
||||
+++ b/profiler/src/BackendEmscripten.cpp
|
||||
@@ -1,6 +1,8 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
+#include <cstdarg>
|
||||
+#include <algorithm>
|
||||
#include <EGL/egl.h>
|
||||
#include <EGL/eglext.h>
|
||||
#include <GLES2/gl2.h>
|
||||
@@ -10,7 +12,146 @@
|
||||
#include "Backend.hpp"
|
||||
#include "RunQueue.hpp"
|
||||
#include "profiler/TracyImGui.hpp"
|
||||
+#include "imgui_internal.h"
|
||||
|
||||
+// ================= DEBUG HARNESS (temporary copy, not for upstream) =================
|
||||
+struct DbgEvent
|
||||
+{
|
||||
+ int type; // 0=move 1=down 2=up 3=enter 4=leave 5=wheel
|
||||
+ int button;
|
||||
+ int targetX;
|
||||
+ int targetY;
|
||||
+ double appX; // what was passed to ImGui (targetX * dpr), -1 if n/a
|
||||
+ double appY;
|
||||
+ double t; // DOMHighResTimeStamp
|
||||
+ int wheelX;
|
||||
+ int wheelY;
|
||||
+};
|
||||
+struct DbgFrame
|
||||
+{
|
||||
+ double t;
|
||||
+ int tick; // emscripten main-loop tick count (rAF)
|
||||
+ int imguiFrame; // ImGui::GetFrameCount()
|
||||
+ float mx, my; // io.MousePos
|
||||
+ int md0, md1, md2;
|
||||
+ int queue; // pending ImGui input events
|
||||
+ int wasActive; // tracy::s_wasActive at tick start
|
||||
+ unsigned hi; // ImGui hovered item ID
|
||||
+ unsigned ai; // ImGui active item ID
|
||||
+};
|
||||
+static DbgEvent s_dbgEvents[512];
|
||||
+static int s_dbgEventHead = 0, s_dbgEventN = 0;
|
||||
+static DbgFrame s_dbgFrames[512];
|
||||
+static int s_dbgFrameHead = 0, s_dbgFrameN = 0;
|
||||
+static int s_dbgTick = 0;
|
||||
+
|
||||
+static void DbgPushEvent( int type, int button, int tx, int ty, double ax, double ay, double t, int wx = 0, int wy = 0 )
|
||||
+{
|
||||
+ auto& e = s_dbgEvents[s_dbgEventHead];
|
||||
+ e.type = type; e.button = button; e.targetX = tx; e.targetY = ty;
|
||||
+ e.appX = ax; e.appY = ay; e.t = t; e.wheelX = wx; e.wheelY = wy;
|
||||
+ s_dbgEventHead = ( s_dbgEventHead + 1 ) % 512;
|
||||
+ if ( s_dbgEventN < 512 ) s_dbgEventN++;
|
||||
+}
|
||||
+
|
||||
+static void DbgPushFrame()
|
||||
+{
|
||||
+ auto& f = s_dbgFrames[s_dbgFrameHead];
|
||||
+ auto& io = ImGui::GetIO();
|
||||
+ f.t = EM_ASM_DOUBLE( { return performance.now(); } );
|
||||
+ f.tick = s_dbgTick;
|
||||
+ f.imguiFrame = ImGui::GetFrameCount();
|
||||
+ f.mx = io.MousePos.x; f.my = io.MousePos.y;
|
||||
+ f.md0 = io.MouseDown[0]; f.md1 = io.MouseDown[1]; f.md2 = io.MouseDown[2];
|
||||
+ auto ctx = ImGui::GetCurrentContext();
|
||||
+ f.queue = ctx ? int( ctx->InputEventsQueue.Size ) : -1;
|
||||
+ f.wasActive = tracy::s_wasActive;
|
||||
+ f.hi = ctx ? ctx->HoveredId : 0;
|
||||
+ f.ai = ctx ? ctx->ActiveId : 0;
|
||||
+ s_dbgFrameHead = ( s_dbgFrameHead + 1 ) % 512;
|
||||
+ if ( s_dbgFrameN < 512 ) s_dbgFrameN++;
|
||||
+}
|
||||
+
|
||||
+extern "C" EMSCRIPTEN_KEEPALIVE void debug_clear()
|
||||
+{
|
||||
+ s_dbgEventHead = s_dbgEventN = s_dbgFrameHead = s_dbgFrameN = 0;
|
||||
+}
|
||||
+
|
||||
+// Escape a C string for embedding in a JSON string literal (", \, control chars).
|
||||
+static void JsonEscape( char* out, size_t outsz, const char* in )
|
||||
+{
|
||||
+ size_t o = 0;
|
||||
+ for( const unsigned char* p = ( const unsigned char* )in; *p; p++ )
|
||||
+ {
|
||||
+ if ( *p == '"' || *p == '\\' )
|
||||
+ {
|
||||
+ if ( o + 2 >= outsz ) break;
|
||||
+ out[o++] = '\\';
|
||||
+ out[o++] = char( *p );
|
||||
+ }
|
||||
+ else if ( *p < 0x20 )
|
||||
+ {
|
||||
+ if ( o + 6 >= outsz ) break;
|
||||
+ o += size_t( snprintf( out + o, outsz - o, "\\u%04x", *p ) );
|
||||
+ }
|
||||
+ else
|
||||
+ {
|
||||
+ if ( o + 1 >= outsz ) break;
|
||||
+ out[o++] = char( *p );
|
||||
+ }
|
||||
+ }
|
||||
+ out[o] = 0;
|
||||
+}
|
||||
+
|
||||
+extern "C" EMSCRIPTEN_KEEPALIVE const char* debug_snapshot()
|
||||
+{
|
||||
+ static char buf[512 * 1024];
|
||||
+ size_t off = 0;
|
||||
+ auto put = [ & ]( const char* fmt, ... )
|
||||
+ {
|
||||
+ if ( off + 1 >= sizeof( buf ) ) return;
|
||||
+ va_list args;
|
||||
+ va_start( args, fmt );
|
||||
+ int n = vsnprintf( buf + off, sizeof( buf ) - off - 1, fmt, args );
|
||||
+ va_end( args );
|
||||
+ if ( n > 0 ) off = std::min( off + size_t( n ), sizeof( buf ) - 1 );
|
||||
+ };
|
||||
+ const auto dpr = EM_ASM_DOUBLE( { return window.devicePixelRatio; } );
|
||||
+ const auto innerW = EM_ASM_INT( { return window.innerWidth; } );
|
||||
+ const auto innerH = EM_ASM_INT( { return window.innerHeight; } );
|
||||
+ const auto bufW = EM_ASM_INT( { return document.getElementById( 'canvas' ).width; } );
|
||||
+ const auto bufH = EM_ASM_INT( { return document.getElementById( 'canvas' ).height; } );
|
||||
+ const auto rectL = EM_ASM_DOUBLE( { var r = document.getElementById( 'canvas' ).getBoundingClientRect(); return r.left; } );
|
||||
+ const auto rectT = EM_ASM_DOUBLE( { var r = document.getElementById( 'canvas' ).getBoundingClientRect(); return r.top; } );
|
||||
+ const auto rectW = EM_ASM_DOUBLE( { var r = document.getElementById( 'canvas' ).getBoundingClientRect(); return r.width; } );
|
||||
+ const auto rectH = EM_ASM_DOUBLE( { var r = document.getElementById( 'canvas' ).getBoundingClientRect(); return r.height; } );
|
||||
+ char title[256];
|
||||
+ EM_ASM( { stringToUTF8( document.title, $0, 256 ); }, title );
|
||||
+ char cursor[64];
|
||||
+ EM_ASM( { stringToUTF8( document.getElementById( 'canvas' ).style.cursor || "", $0, 64 ); }, cursor );
|
||||
+ char title_e[1536];
|
||||
+ char cursor_e[384];
|
||||
+ JsonEscape( title_e, sizeof( title_e ), title );
|
||||
+ JsonEscape( cursor_e, sizeof( cursor_e ), cursor );
|
||||
+ put( "{\"dpr\":%.6f,\"innerW\":%d,\"innerH\":%d,\"bufW\":%d,\"bufH\":%d,\"rectL\":%.3f,\"rectT\":%.3f,\"rectW\":%.3f,\"rectH\":%.3f,\"title\":\"%s\",\"cursor\":\"%s\",\"frames\":[", dpr, innerW, innerH, bufW, bufH, rectL, rectT, rectW, rectH, title_e, cursor_e );
|
||||
+ const int fn = std::min( s_dbgFrameN, 64 );
|
||||
+ for ( int i = 0; i < fn; i++ )
|
||||
+ {
|
||||
+ auto& f = s_dbgFrames[ ( s_dbgFrameHead - fn + i + 512 ) % 512 ];
|
||||
+ put( "%s{\"t\":%.3f,\"tick\":%d,\"f\":%d,\"mx\":%.1f,\"my\":%.1f,\"md\":[%d,%d,%d],\"q\":%d,\"wa\":%d,\"hi\":%u,\"ai\":%u}", i ? "," : "", f.t, f.tick, f.imguiFrame, f.mx, f.my, f.md0, f.md1, f.md2, f.queue, f.wasActive, f.hi, f.ai );
|
||||
+ }
|
||||
+ put( "],\"events\":[");
|
||||
+ const int en = std::min( s_dbgEventN, 64 );
|
||||
+ for ( int i = 0; i < en; i++ )
|
||||
+ {
|
||||
+ auto& e = s_dbgEvents[ ( s_dbgEventHead - en + i + 512 ) % 512 ];
|
||||
+ put( "%s{\"type\":%d,\"btn\":%d,\"tx\":%d,\"ty\":%d,\"ax\":%.3f,\"ay\":%.3f,\"t\":%.3f,\"wx\":%d,\"wy\":%d}", i ? "," : "", e.type, e.button, e.targetX, e.targetY, e.appX, e.appY, e.t, e.wheelX, e.wheelY );
|
||||
+ }
|
||||
+ put( "]}");
|
||||
+ buf[off] = 0;
|
||||
+ return buf;
|
||||
+}
|
||||
+
|
||||
static std::function<void()> s_redraw;
|
||||
static std::function<void(float)> s_scaleChanged;
|
||||
static std::function<int(void)> s_isBusy;
|
||||
@@ -223,32 +364,38 @@
|
||||
emscripten_set_mousedown_callback( "#canvas", nullptr, EM_TRUE, []( int, const EmscriptenMouseEvent* e, void* ) -> EM_BOOL {
|
||||
ImGui::GetIO().AddMouseButtonEvent( e->button == 0 ? 0 : 3 - e->button, true );
|
||||
tracy::s_wasActive = true;
|
||||
+ DbgPushEvent( 1, e->button, e->targetX, e->targetY, -1, -1, e->timestamp );
|
||||
return EM_TRUE;
|
||||
} );
|
||||
emscripten_set_mouseup_callback( "#canvas", nullptr, EM_TRUE, []( int, const EmscriptenMouseEvent* e, void* ) -> EM_BOOL {
|
||||
ImGui::GetIO().AddMouseButtonEvent( e->button == 0 ? 0 : 3 - e->button, false );
|
||||
tracy::s_wasActive = true;
|
||||
+ DbgPushEvent( 2, e->button, e->targetX, e->targetY, -1, -1, e->timestamp );
|
||||
return EM_TRUE;
|
||||
} );
|
||||
emscripten_set_mousemove_callback( "#canvas", nullptr, EM_TRUE, []( int, const EmscriptenMouseEvent* e, void* ) -> EM_BOOL {
|
||||
const auto scale = EM_ASM_DOUBLE( { return window.devicePixelRatio; } );
|
||||
ImGui::GetIO().AddMousePosEvent( e->targetX * scale, e->targetY * scale );
|
||||
tracy::s_wasActive = true;
|
||||
+ DbgPushEvent( 0, -1, e->targetX, e->targetY, e->targetX * scale, e->targetY * scale, e->timestamp );
|
||||
return EM_TRUE;
|
||||
} );
|
||||
- emscripten_set_mouseleave_callback( "#canvas", nullptr, EM_TRUE, []( int, const EmscriptenMouseEvent*, void* ) -> EM_BOOL {
|
||||
+ emscripten_set_mouseleave_callback( "#canvas", nullptr, EM_TRUE, []( int, const EmscriptenMouseEvent* e, void* ) -> EM_BOOL {
|
||||
ImGui::GetIO().AddFocusEvent( false );
|
||||
tracy::s_wasActive = true;
|
||||
+ DbgPushEvent( 4, -1, e->targetX, e->targetY, -1, -1, e->timestamp );
|
||||
return EM_TRUE;
|
||||
} );
|
||||
- emscripten_set_mouseenter_callback( "#canvas", nullptr, EM_TRUE, []( int, const EmscriptenMouseEvent*, void* ) -> EM_BOOL {
|
||||
+ emscripten_set_mouseenter_callback( "#canvas", nullptr, EM_TRUE, []( int, const EmscriptenMouseEvent* e, void* ) -> EM_BOOL {
|
||||
ImGui::GetIO().AddFocusEvent( true );
|
||||
tracy::s_wasActive = true;
|
||||
+ DbgPushEvent( 3, -1, e->targetX, e->targetY, -1, -1, e->timestamp );
|
||||
return EM_TRUE;
|
||||
} );
|
||||
emscripten_set_wheel_callback( "#canvas", nullptr, EM_TRUE, []( int, const EmscriptenWheelEvent* e, void* ) -> EM_BOOL {
|
||||
ImGui::GetIO().AddMouseWheelEvent( e->deltaX * -0.05, e->deltaY * -0.05 );
|
||||
tracy::s_wasActive = true;
|
||||
+ DbgPushEvent( 5, -1, e->mouse.targetX, e->mouse.targetY, -1, -1, e->mouse.timestamp, int( e->deltaX ), int( e->deltaY ) );
|
||||
return EM_TRUE;
|
||||
} );
|
||||
emscripten_set_keydown_callback( EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_TRUE, [] ( int, const EmscriptenKeyboardEvent* e, void* ) -> EM_BOOL {
|
||||
@@ -298,6 +445,8 @@
|
||||
|
||||
void Backend::NewFrame( int& w, int& h )
|
||||
{
|
||||
+ s_dbgTick++;
|
||||
+ DbgPushFrame();
|
||||
const auto scale = GetDpiScale();
|
||||
if( scale != s_prevScale )
|
||||
{
|
||||
102
.omp/skills/tracy-web-gui-test/setup.sh
Executable file
102
.omp/skills/tracy-web-gui-test/setup.sh
Executable file
@@ -0,0 +1,102 @@
|
||||
#!/bin/sh
|
||||
# setup.sh - build the instrumented Tracy web GUI workspace (see SKILL.md).
|
||||
#
|
||||
# Usage:
|
||||
# sh setup.sh
|
||||
# TRACY_WEB_WS=/custom/ws TRACY_WEB_REPO=<repo> sh setup.sh
|
||||
#
|
||||
# Idempotent: reuses an existing source copy and build dir, skips
|
||||
# already-applied harness patch hunks, rebuilds.
|
||||
set -e
|
||||
|
||||
WS="${TRACY_WEB_WS:-/tmp/tracy-protocol}"
|
||||
# The skill is project-scoped: <repo>/.omp/skills/tracy-web-gui-test/
|
||||
SKILL_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
if [ -n "$TRACY_WEB_REPO" ]; then
|
||||
REPO="$TRACY_WEB_REPO"
|
||||
else
|
||||
REPO=$(cd "$SKILL_DIR/../../.." 2>/dev/null && pwd) || REPO=""
|
||||
fi
|
||||
EMSDK="${EMSDK_DIR:-$HOME/emsdk}"
|
||||
|
||||
[ -x "$EMSDK/emsdk" ] || { echo "error: emsdk not found at $EMSDK (set EMSDK_DIR)" >&2; exit 1; }
|
||||
[ -f "$REPO/profiler/CMakeLists.txt" ] || { echo "error: not a Tracy repo: '$REPO' (set TRACY_WEB_REPO)" >&2; exit 1; }
|
||||
|
||||
. "$EMSDK/emsdk_env.sh" >/dev/null 2>&1 || true
|
||||
command -v emcc >/dev/null || { echo "error: emcc not on PATH after emsdk_env.sh" >&2; exit 1; }
|
||||
|
||||
if [ ! -f "$WS/src/profiler/CMakeLists.txt" ]; then
|
||||
echo "[setup] fresh source copy -> $WS/src (from $REPO)"
|
||||
mkdir -p "$WS/src"
|
||||
# -C applies to both archive creation and extraction; exclude all build dirs
|
||||
tar -C "$REPO" \
|
||||
--exclude=.git \
|
||||
--exclude='*/build*' \
|
||||
-cf - . | tar -xf - -C "$WS/src"
|
||||
else
|
||||
echo "[setup] reusing source copy in $WS/src (copy changed repo files into it first)"
|
||||
fi
|
||||
|
||||
# The harness is fully in place iff each patch hunk's signature line is present.
|
||||
# patch --forward skips a whole file once its first hunk is detected as already
|
||||
# applied, so a failed later hunk is invisible in patch's output and exit code —
|
||||
# verify the content instead of trusting either.
|
||||
harness_complete()
|
||||
{
|
||||
grep -q '_debug_snapshot' "$WS/src/profiler/CMakeLists.txt" \
|
||||
&& grep -qF 'EMSCRIPTEN_KEEPALIVE const char* debug_snapshot' "$WS/src/profiler/src/BackendEmscripten.cpp" \
|
||||
&& grep -qF 'DbgPushEvent( 1,' "$WS/src/profiler/src/BackendEmscripten.cpp" \
|
||||
&& grep -qF 's_dbgTick++;' "$WS/src/profiler/src/BackendEmscripten.cpp"
|
||||
}
|
||||
|
||||
echo "[setup] applying instrumentation harness"
|
||||
if harness_complete; then
|
||||
echo "[setup] harness already applied"
|
||||
rm -f "$WS/src/profiler/CMakeLists.txt.rej" "$WS/src/profiler/src/BackendEmscripten.cpp.rej"
|
||||
else
|
||||
rm -f "$WS/src/profiler/CMakeLists.txt.rej" "$WS/src/profiler/src/BackendEmscripten.cpp.rej"
|
||||
patch -p1 -d "$WS/src" --forward --no-backup-if-mismatch < "$SKILL_DIR/harness.patch" \
|
||||
|| {
|
||||
if harness_complete; then
|
||||
# mixed run: some hunks already present (skipped), the rest applied
|
||||
rm -f "$WS/src/profiler/CMakeLists.txt.rej" "$WS/src/profiler/src/BackendEmscripten.cpp.rej"
|
||||
else
|
||||
rej=$(ls "$WS/src/profiler/CMakeLists.txt.rej" "$WS/src/profiler/src/BackendEmscripten.cpp.rej" 2>/dev/null || true)
|
||||
if [ -n "$rej" ]; then
|
||||
kept="The unapplied hunks are kept in:
|
||||
$(printf '%s\n' $rej | sed 's/^/ /')"
|
||||
else
|
||||
kept="No .rej files were produced (fatal patch error); the patch output above is the only evidence."
|
||||
fi
|
||||
cat >&2 <<EOF
|
||||
error: harness patch did not fully apply — $WS/src has drifted from the repo.
|
||||
$kept
|
||||
Do not build from this workspace. Either re-apply the harness by hand
|
||||
(SKILL.md, "After editing the GUI in the repo"), or start fresh:
|
||||
rm -rf $WS && sh <this-skill-dir>/setup.sh
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
fi
|
||||
|
||||
if [ ! -f "$WS/build/CMakeCache.txt" ]; then
|
||||
echo "[setup] configuring"
|
||||
cmake -S "$WS/src/profiler" -B "$WS/build" \
|
||||
-DCMAKE_TOOLCHAIN_FILE="$EMSDK/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCPM_SOURCE_CACHE="$HOME/.cache/cpm"
|
||||
fi
|
||||
|
||||
echo "[setup] building (first full build ~3-5 min, incremental ~30 s)"
|
||||
cmake --build "$WS/build" -j "$(nproc)"
|
||||
|
||||
cat <<EOF
|
||||
[setup] done.
|
||||
workspace : $WS
|
||||
repo : $REPO
|
||||
emcc : $(emcc --version | sed -n 1p)
|
||||
serve : hub op=start name=tracy-web application=python3 args=[$WS/build/httpd.py] cwd=$WS/build ready={port:8000} persist=true
|
||||
open : browser open url=http://127.0.0.1:8000/index.html viewport={width:1600,height:900}
|
||||
ready : document.title matches / - Tracy Profiler/ (see SKILL.md)
|
||||
EOF
|
||||
Reference in New Issue
Block a user