feat(bench): desktop editor benchmark — gpui vs Tauri v2 vs Electron - #294
Merged
Conversation
doodlewind
added a commit
that referenced
this pull request
Aug 18, 2026
… truthful svcOpen 1. The provider divergence check compared unlike predicates: layout gated on declared transforms down the path, the draw walk on the composed matrix being a translation — a parent scale canceled by a child's inverse satisfied one and not the other, oscillating layout.dirty into a permanent relayout loop, and every transform transition still painted one stale frame. Both sides now accumulate ONE shared predicate (Resolved::declares_transform) down identical recursions, so divergence can only mean a transform VALUE changed since the last relayout — and Ui::draw re-decides and REPAINTS before returning, so every frame that leaves draw() is provider-correct. Pinned by canceling_transforms_do_not_oscillate_the_provider (draws must not schedule spurious relayouts) and the zero-stale-frame rewrite of the transform enter/exit test. 2. svcOpen answered true for ANY service (allowlist default None), so the note believed its companion was live even when the host was launched without --editor — edit/pointer UI over a channel nobody feeds. The host now sets the allowlist before mount: exactly "note" with the adapter on, empty otherwise. Verified both ways: without --editor the scripted pencil-click + typing produces NO autosave (truthfully read-only); the --proof acceptance still passes with it. Minors: the macOS lane's path filter covers framework/compiler, the fonts, and the test files it executes; font-bake gains mono regression assertions (slot table stability + the monospace property itself: uniform baked advances where the proportional face differs); tools/macos.ts ships again with a git-checkout guard (the note.ts precedent — script and tool now agree) instead of the contradictory script-without-tool metadata; BACKENDS.md points at #294 for the benchmark; stale styleHash/bench comments in the host corrected; the vendored license loses its trailing whitespace. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
doodlewind
added a commit
that referenced
this pull request
Aug 18, 2026
…ADY, honest comments - pocket-ui-surface: svcOpen now DENIES by default (the allowlist is a plain list, empty unless the host declares its companions) — the shared API is truthful-by-default instead of relying on every future host to remember the footgun. note-widget declares its "note" companion explicitly; a new surface test pins the default-deny. - The host's READY first-frame marker moves behind --announce-ready (the benchmark runner's flag, PR #294) — a production launch prints nothing. - The two core comments still describing a one-frame-stale provider now state the same-draw re-decide + repaint the code actually performs. - Companion-by-manifest-name is registered as architecture debt (issue #295) and referenced where the convention lives (tools/macos.ts). - The macOS lane watches package.json + tests/npm-package.test.ts and runs the npm tarball guards. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
doodlewind
force-pushed
the
north-pancreas-bench
branch
from
August 18, 2026 09:52
9e6d0da to
e78840f
Compare
doodlewind
added a commit
that referenced
this pull request
Aug 18, 2026
…text layout (#293) * feat(gpui): gpui render backend + macos-app host with native text layout The portable backend family (wgpu, software raster, PPA, GLES2) keeps executing the DrawList against compile-time baked font atlases. This adds a second backend class on the same DrawList contract: engine/backends/gpui paints through Zed's gpui/Metal renderer, and text measurement + shaping move to the host text system (CoreText) when an app opts in. Core (backend-neutral, no behavior change unless a host installs the hook): - text::MeasureFn — a pluggable native text measurer on Fonts; Ui::set_text_measure installs it before mount. measure_run, the taffy MeasureCtx and the measureText op all route through the one provider. - DRAW_OP.TEXT_RUN (9): translation-only tracking-0 runs emit the run string + style through a DrawList side table; a styleHash word keeps identical word streams pixel-identical (demand-render hashes stay truthful). Tracked, scaled and rotated runs keep the baked GLYPH_RUN pair on BOTH the measure and paint sides. Partially clipped runs are scissor-bracketed. - raster/damage/wgpu interpreters learn to skip the op (it never reaches fixed-function backends); 5 new core tests pin the gates. Registry: text.layout.native capability + the macos-app target profile (hostAbi 3, window form, acceptsFixed — the slot platforms.ts reserved), so every fixed-viewport console demo admits unchanged, size-locked. engine/backends/gpui (standalone, like esp32p4-ppa): the DrawList -> gpui interpreter (quads, gradients, glyph-cell blitting for baked apps, content- mask scissors, TEXT_RUN shaping via shape_line with kern/liga off so prefix-sum caret math stays exact) plus a pixel-exact escape hatch: gouraud TRI / TEX_TRI batches raster through pocketjs_core::raster into cached local images at target density. hosts/macos (standalone lone-bin, like pocketbook): gpui window host of the macos-app target. Fixed 60 Hz guest ticks from a foreground timer governor (one guest.frame + surface.tick per tick, never from paint), demand renders off the DrawList hash, speaks note-widget's svc editor protocol (keyboard, pointer, scroll, clipboard, IME through EntityInputHandler), letterboxes fixed-viewport apps. bun run macos <app> resolves the manifest against macos-app and derives every host flag from the plan. Also: host string ops now decode JS strings lossily (LossyString) — an app measuring an emoji prefix sliced between surrogate halves is legal JS and must never abort the frame transaction. apps/note enhances text.layout.native: the same unmodified JSX markdown editor now runs with CoreText metrics, full CJK + color emoji, no runtime atlas baking. Proof: bun run macos note --proof (scripted click + typing, debounced autosave round-trips through the gpui host). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bench): desktop editor benchmark harness + gpui shaping caches tools/bench-desktop.ts measures the pocket note on the gpui host against byte-identical web editors shelled by Tauri v2 and Electron: cold start to each app's own first-painted-frame READY report, hands-off idle and a 120 chars/s typing storm through each stack's real edit path, ps process- tree medians (WebKit XPC helpers attributed by spawn-delta — Tauri's WebContent/GPU processes are launchd children), footprint for physical memory. The gpui renderer gains shaped-line and measured-width caches so a keystroke repaint reshapes one line, not the document. docs/BACKENDS.md names the backend split; .github/workflows/macos.yml is the first macOS CI lane (core tests + clippy + host build). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bench): gpui vs Tauri vs Electron results + tarball guard for bench fixtures Final measured run (M3 Max, methodology + fairness caveats in the report): the pocket note on the gpui host takes 1 process / 84 MB idle RSS / 145 ms cold start / 10 MB disk against Tauri v2 (4 procs, 192 MB, 391 ms) and Electron (5 procs, 382 MB, 328 ms, 242 MB). Storm completion is now verified (STORM-DONE), the Tauri window is explicitly focused (an unfocused WKWebView throttles timers and reads fiction), and the footprint column is dropped from the table — Electron's hardened helpers refuse task inspection, so RSS is the uniform metric. The benchmark fixtures' cargo target/ was packing into the npm tarball through the wholesale "tools" files entry (the v0.8.0 E415 failure mode — the tests/npm-package.test.ts tripwire caught it): tools/bench-desktop is now a governed negation in the files map. Tauri codegen (gen/) untracked; gpui backend passes clippy -D warnings for the macOS CI lane. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf(note): browser-style caret blink that demand rendering can skip The caret was animate-pulse — a continuous opacity sine that changes the DrawList every tick, so an idle editor repainted at 60 fps on every host (88% of ticks; ~3.6% CPU on both the gpui and wgpu hosts). Replace it with a browser-style square wave (app-local pocket.config.ts theme: 500 ms on / 500 ms off, hard edges via a same-frame twin keyframe) plus the browser input discipline: the caret is SOLID while typing or moving and resumes blinking from its ON phase after a 0.6 s rest (a <Show> swap remounts the animated node, restarting the baked timeline at frame 0). The square wave's constant segments keep the DrawList byte-stable between edges, so every demand-rendering host skips them: idle-in-edit repaints drop from 2115/2400 ticks to 83/2400 (~2 fps, exactly the edge count), CPU from 3.6% to 0.95% on the gpui host and 3.65% to 0.7% on the wgpu widget host. Same measurement protocol as docs/bench. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(bench): refresh desktop numbers after the square-wave caret Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(note): double-click word selection + truthful backend copy Double click selects the word under the pointer in BOTH modes, browser semantics throughout: whitespace and punctuation select as runs, a click just past a word's right half still selects the word (caretFromX rounds to the nearest boundary), CJK and surrogate pairs ride the non-ASCII word class, code blocks stay atomic in preview (rowSelSpan granularity), and a drag after the double click extends from the word start. Detection rides the virtual clock (0.4 s / 3 px on the svc press stream) so replays stay deterministic; wordRangeAt is regex-free QuickJS-portable math with unit coverage. The sample doc's 'same bytes as the PSP build' line predates the backend split — it now names the real contract (same core, same DrawList; wgpu paints baked atlases, gpui paints native text) and the two launch commands, and the charset-anchor comment drops the stale tofu caveat (runtime glyph baking and native shaping both exist now). Verified end-to-end on the gpui host: scripted double click on 'Pocket' + typing X autosaves '# X Note'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(gpui): review — exact TEXT_RUN words, one text provider per node, svc-independent capabilities Three review blockers on #293, each fixed at the contract level: 1. TEXT_RUN carried a 32-bit content hash while the run string lived in a side table — colliding texts could leave identical word streams with different pixels, silently defeating demand-render hashes and damage word-diffs, and breaking the "DrawList is the complete Vec<u32>" contract. The op now packs the run string's UTF-8 bytes INTO the words (8 header words + payload; slot/align/lineHeight all in-stream) and the side table is gone: snapshots, hashes and diffs are exact by construction, not probabilistic. All interpreters skip the variable length; the gpui backend decodes from the stream. 2. Measurement and paint could disagree on rotated/scaled text (native- sized box, baked glyphs). The provider is now chosen ONCE at layout build (native iff a measurer is installed, tracking is 0 and the subtree declares no non-translation transform) and RECORDED on the node (Node::text_native); paint follows the record unconditionally, and the style-dirty restyle path can only flip it for a node-local tracking change. Rotated/transformed text takes the baked pair on BOTH sides — pinned by tests including a transformed-ancestor case and a paint-only-transform consistency case. 3. macos-app capabilities were conflated with the note's svc protocol. display.viewport.live is now host-generic: every dynamic app receives the framework's __pocketResizeViewport hook inside the tick transaction (the hero-resize regression); --editor derives from the app being the note companion, never from input.text; and the registry comment states the delivery paths plainly — buttons + live viewport host-generic, pointer/text/IME/clipboard via the companion adapter today (the macos-widget stock-host bar), with the host-generic pointer feed named as follow-up (the touch packing's 9-bit axes cannot carry a 4096-px window, so it needs new framework surface). Rot: hosts/macos passes clippy -D warnings and CI now runs it; the macOS workflow path filter covers contracts/spec, tools/macos.ts and apps/note; tools/macos.ts is negated out of the npm tarball (its build inputs are git-only — a published entry point that cannot run). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(bench): split the desktop benchmark into its own PR The Tauri/Electron comparison apps, the runner and the results move to a stacked PR so the backend/host/contract surface reviews on its own boundary. The npm files negation stays — it guards the re-add. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(gpui): review round 2 — provider self-healing, truthful macos-app profile, windowed chrome, monospace code Review blockers: 1. Paint-only transforms (rotate/scale never relayout) could leave a text node's recorded provider permanently stale — native-measured box painted forever unrotated, and no way back. The draw walk now DETECTS the divergence (desired provider from the live world transform vs the record) and schedules the relayout that re-decides the pair: the stale frame lasts at most one tick, in both directions, with the 3D subtree path exempt (always the baked pair). Pinned by provider_self_heals_after_a_paint_only_transform. 2. macos-app declared input.text/pointer/ime/host.clipboard while only the note companion delivered them. The profile now registers exactly what the host implements for EVERY app — input.buttons, display.viewport.live, text.glyphs.baked, text.layout.native — and the registry comment names the companion delivery for the rest. The note's edit/pointer gates now track the COMPANION's runtime presence (connectSvc() !== null), not a capability id, so the flagship behaves identically while the contract stops over-promising. Flagship polish (user-reported): - Widget-era chrome is gated on platform.target === "macos-widget": a real window keeps OS corners (no rounded-xl card), resizes at its edges (no grip dots) and closes from its titlebar (no "Close widget" menu item). - Markdown code is monospace on every backend: MAX_FONT_SLOTS grows 16→24 with mono slots 16..18 (font-mono, 12/14/16 px), baked from a vendored JetBrains Mono (OFL) on the portable side and mapped through the host text system on gpui. The note's fenced blocks and inline code move to slot 17; note-widget's cjk mirror learns the mono rows. Rot: both new crates pass cargo fmt --check and CI enforces it; the macOS lane now actually exercises its TS path filter (contract drift, platform/ note/font-bake tests, and a real macos-app plan+bundle build of the note); docs/BACKENDS.md describes the bytes-in-words TEXT_RUN and the recorded provider instead of the deleted side table. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci(macos): wasm target + rustfmt component for the TS test steps note.test.ts boots the sim host, which builds engine/wasm — the runner needs wasm32-unknown-unknown (and fmt --check needs rustfmt declared). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(note): windowed header drops the wordmark; roomier narrow-width padding The POCKET NOTE label is widget identity — a real window already titles itself in the OS titlebar, so the wordmark hides with the rest of the widget chrome (the eye/pencil toggle and menu stay). The content column's minimum side padding rises 22 -> 28 px, the floor narrow windows pin to, so text stops hugging the window edge; wide windows keep the centered 560 px column. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(note): the standalone smoke pins sample content, not widget chrome The POCKET NOTE wordmark hides off macos-widget now, and the sim boot is not a widget — assert the sample document instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(gpui): review round 3 — one provider gate with zero stale frames, truthful svcOpen 1. The provider divergence check compared unlike predicates: layout gated on declared transforms down the path, the draw walk on the composed matrix being a translation — a parent scale canceled by a child's inverse satisfied one and not the other, oscillating layout.dirty into a permanent relayout loop, and every transform transition still painted one stale frame. Both sides now accumulate ONE shared predicate (Resolved::declares_transform) down identical recursions, so divergence can only mean a transform VALUE changed since the last relayout — and Ui::draw re-decides and REPAINTS before returning, so every frame that leaves draw() is provider-correct. Pinned by canceling_transforms_do_not_oscillate_the_provider (draws must not schedule spurious relayouts) and the zero-stale-frame rewrite of the transform enter/exit test. 2. svcOpen answered true for ANY service (allowlist default None), so the note believed its companion was live even when the host was launched without --editor — edit/pointer UI over a channel nobody feeds. The host now sets the allowlist before mount: exactly "note" with the adapter on, empty otherwise. Verified both ways: without --editor the scripted pencil-click + typing produces NO autosave (truthfully read-only); the --proof acceptance still passes with it. Minors: the macOS lane's path filter covers framework/compiler, the fonts, and the test files it executes; font-bake gains mono regression assertions (slot table stability + the monospace property itself: uniform baked advances where the proportional face differs); tools/macos.ts ships again with a git-checkout guard (the note.ts precedent — script and tool now agree) instead of the contradictory script-without-tool metadata; BACKENDS.md points at #294 for the benchmark; stale styleHash/bench comments in the host corrected; the vendored license loses its trailing whitespace. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(gpui): review round 4 residue — deny-by-default svcOpen, gated READY, honest comments - pocket-ui-surface: svcOpen now DENIES by default (the allowlist is a plain list, empty unless the host declares its companions) — the shared API is truthful-by-default instead of relying on every future host to remember the footgun. note-widget declares its "note" companion explicitly; a new surface test pins the default-deny. - The host's READY first-frame marker moves behind --announce-ready (the benchmark runner's flag, PR #294) — a production launch prints nothing. - The two core comments still describing a one-frame-stale provider now state the same-draw re-decide + repaint the code actually performs. - Companion-by-manifest-name is registered as architecture debt (issue #295) and referenced where the convention lives (tools/macos.ts). - The macOS lane watches package.json + tests/npm-package.test.ts and runs the npm tarball guards. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(gpui): review round 5 — invariant wording, surface tests in CI, honest flag comments docs/BACKENDS.md states the actual invariant (same-draw relayout+repaint, zero stale frames — not next-tick healing); the macOS lane runs the pocket-ui-surface tests (svcOpen deny-by-default landed there); the tools/macos.ts flag comment names its two non-plan derivations and points both at #295, which also gains the plan-completeness (viewport policy) and Apple-sidecar-allowlist items. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The benchmark from #293, split out for review-boundary hygiene: byte- identical web editors shelled by Tauri v2 and Electron against the pocket note on the gpui host, measured by tools/bench-desktop.ts (process-tree ps medians, WebKit XPC spawn-delta attribution, per-app first-frame READY, 120 chars/s typing storm through each stack's real edit path). Results + fairness caveats in docs/bench/gpui-vs-tauri-electron-2026-08-18.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… sampling Numbers now come from the post-review TEXT_RUN code. The runner settles 20 s after READY before idle sampling (pcpu is a decaying average — early samples carried launch work into every app's idle median), and the report reads idle through the structural metric: the pocket governor receipt's repaint rate (84/2400 idle ticks, the caret square wave's edge count), since pcpu medians at low single digits drift ±1.5 points across runs for pocket and Tauri alike. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…now) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
doodlewind
force-pushed
the
north-pancreas-bench
branch
from
August 18, 2026 10:09
e78840f to
a2dcca0
Compare
doodlewind
marked this pull request as ready for review
August 18, 2026 10:09
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #293 (split out per review: the benchmark tools and results review on their own boundary; retarget to main after #293 merges).
Byte-identical web editors shelled by Tauri v2 (WKWebView) and Electron against the pocket note on the gpui
macos-apphost.bun tools/bench-desktop.ts: cold start to each app's own first-painted-frame READY, a 20 s settle then 60 s hands-off idle (pcpu decays — the settle keeps launch work out of idle medians), 120 chars/s typing storm through each stack's real edit path, process-treepsmedians with WebKit XPC helpers attributed by spawn-delta (without that Tauri's WebContent/GPU processes hide under launchd and read ~0), locked-session refusal + caffeinate during runs. Measured on the post-review TEXT_RUN code (#293's exact-words format).Reading the idle column: at low single digits, pcpu medians drift ±1.5 points across runs for pocket and Tauri alike (footprint scans, caffeinate assertions, thermal state) — the structural number is the pocket governor receipt: 84 repaints over 2400 idle ticks (~2/s, exactly the caret square wave's edge count; 88% of ticks before the square-wave caret). Electron idles lower because its caret blinks in the compositor, not through an app repaint. The pocket storm CPU is the note's app-level O(n) re-wrap through QuickJS (profiled:
JS_CallInternal+ regex, not rendering); full fairness caveats indocs/bench/gpui-vs-tauri-electron-2026-08-18.md. The comparison apps are git-only:!tools/bench-desktopin the npm files map (#293) keeps their cargotarget/out of the tarball.🤖 Generated with Claude Code