Skip to content

feat(bench): desktop editor benchmark — gpui vs Tauri v2 vs Electron - #294

Merged
doodlewind merged 3 commits into
mainfrom
north-pancreas-bench
Aug 18, 2026
Merged

feat(bench): desktop editor benchmark — gpui vs Tauri v2 vs Electron#294
doodlewind merged 3 commits into
mainfrom
north-pancreas-bench

Conversation

@doodlewind

@doodlewind doodlewind commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

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-app host. 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-tree ps medians 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).

app procs cold start (ms) idle RSS (MB) idle CPU (%) storm CPU (%) storm RSS (MB) disk (MB)
pocket (gpui) 1 149 83 3.65 47.9 87 10
tauri v2 4 380 193 3.45 16.4 208 9
electron 5 301 382 0.55 39.6 487 242

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 in docs/bench/gpui-vs-tauri-electron-2026-08-18.md. The comparison apps are git-only: !tools/bench-desktop in the npm files map (#293) keeps their cargo target/ out of the tarball.

🤖 Generated with Claude Code

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
doodlewind force-pushed the north-pancreas-bench branch from 9e6d0da to e78840f Compare August 18, 2026 09:52
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>
Base automatically changed from north-pancreas to main August 18, 2026 10:07
doodlewind and others added 3 commits August 18, 2026 18:08
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
doodlewind force-pushed the north-pancreas-bench branch from e78840f to a2dcca0 Compare August 18, 2026 10:09
@doodlewind
doodlewind marked this pull request as ready for review August 18, 2026 10:09
@doodlewind
doodlewind merged commit ac77aaa into main Aug 18, 2026
@doodlewind
doodlewind deleted the north-pancreas-bench branch August 18, 2026 10:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant