react-x11 2.52.0 → 2.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -10
- package/docs/README.md +160 -314
- package/docs/accessibility.md +176 -229
- package/docs/appearance.md +77 -176
- package/docs/click-to-component.md +18 -51
- package/docs/clipboard.md +40 -59
- package/docs/components.md +518 -1036
- package/docs/dbus.md +61 -133
- package/docs/debugging.md +152 -245
- package/docs/desktop-calendar.md +51 -88
- package/docs/desktop.md +222 -347
- package/docs/devtools.md +50 -167
- package/docs/drag-and-drop.md +126 -204
- package/docs/ecosystem/animation.md +91 -125
- package/docs/ecosystem/data-fetching.md +38 -53
- package/docs/ecosystem/dev-tooling.md +159 -202
- package/docs/ecosystem/forms.md +61 -97
- package/docs/ecosystem/headless.md +64 -89
- package/docs/ecosystem/i18n.md +18 -21
- package/docs/ecosystem/icons.md +93 -126
- package/docs/ecosystem/layout.md +42 -52
- package/docs/ecosystem/routing.md +18 -29
- package/docs/ecosystem/state.md +71 -88
- package/docs/ecosystem/testing.md +131 -157
- package/docs/ecosystem/theming.md +119 -165
- package/docs/ecosystem.md +145 -179
- package/docs/elements.md +1091 -1774
- package/docs/embedding.md +83 -132
- package/docs/events.md +412 -695
- package/docs/extending.md +784 -1326
- package/docs/eyedropper.md +63 -122
- package/docs/filedialog.md +71 -107
- package/docs/frame.md +195 -339
- package/docs/gl.md +65 -62
- package/docs/globalmenu.md +63 -146
- package/docs/macos.md +531 -3234
- package/docs/notifications.md +33 -47
- package/docs/packaging.md +166 -309
- package/docs/permissions.md +9 -14
- package/docs/react-features.md +81 -106
- package/docs/remote.md +32 -76
- package/docs/scale.md +72 -186
- package/docs/styling.md +329 -547
- package/docs/system.md +200 -280
- package/docs/testing.md +63 -83
- package/docs/typescript.md +6 -48
- package/docs/uri-schemes.md +67 -107
- package/docs/wayland-backend.md +200 -459
- package/docs/windows-integrations.md +75 -131
- package/docs/windows.md +343 -1367
- package/package.json +3 -2
- package/src/Reconciler.js +2 -2
- package/src/a11y.js +1 -1
- package/src/backend/context2d.js +3 -3
- package/src/bootstrap.js +1 -1
- package/src/cocoa/app.js +6 -6
- package/src/cocoa/bezels.js +1 -1
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/globalmenu.js +1 -1
- package/src/cocoa/keymap.js +1 -1
- package/src/cocoa/panewindow.js +1 -1
- package/src/cocoa/player.js +1 -1
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/promotion.js +1 -1
- package/src/cocoa/relaunch.js +1 -1
- package/src/cocoa/threaded.js +1 -1
- package/src/cocoa/video.js +1 -1
- package/src/cocoa/window.js +4 -4
- package/src/components/Menu.js +7 -2
- package/src/components/Switch.js +1 -1
- package/src/components/native.js +1 -1
- package/src/dnd.js +2 -2
- package/src/glnodes.js +1 -1
- package/src/grid.js +1 -1
- package/src/layouts.js +1 -1
- package/src/nodes/animation.js +1 -1
- package/src/nodes/invalidate.js +1 -1
- package/src/nodes/layouthost.js +1 -1
- package/src/nodes/video.js +19 -8
- package/src/nodes/window/droptarget.js +1 -1
- package/src/pacing.js +1 -1
- package/src/refresh/index.js +2 -1
- package/src/types/video.d.ts +5 -2
- package/src/videobackend.js +28 -0
- package/src/videoframes.js +29 -16
- package/src/wayland/app.js +27 -0
- package/src/wayland/video.js +376 -0
- package/src/win32/a11y.js +1 -1
- package/src/win32/app.js +2 -2
- package/src/win32/fonts.js +1 -1
- package/src/win32/glarea.js +4 -4
- package/src/win32/overlay.js +1 -1
- package/src/win32/panewindow.js +3 -3
- package/src/win32/shell.js +1 -1
- package/src/win32/window.js +2 -2
- package/src/windowid.js +1 -1
- package/src/xvideo.js +189 -0
- package/docs/architecture/accessibility.md +0 -470
- package/docs/architecture/animation.md +0 -707
- package/docs/architecture/container-queries.md +0 -525
- package/docs/architecture/custom-layout.md +0 -334
- package/docs/architecture/drag-and-drop.md +0 -941
- package/docs/architecture/element-layer-contents.md +0 -622
- package/docs/architecture/frame-pacing.md +0 -255
- package/docs/architecture/grid-layout.md +0 -358
- package/docs/architecture/protocol-efficiency.md +0 -357
- package/docs/architecture/video.md +0 -1149
- package/docs/architecture/windowed-regions.md +0 -854
- package/docs/glx.md +0 -137
- package/docs/wayland.md +0 -545
- package/docs/windows-embedding.md +0 -271
- package/docs/windows-gl.md +0 -221
|
@@ -1,357 +0,0 @@
|
|
|
1
|
-
# Protocol efficiency: the checkbox-hover audit
|
|
2
|
-
|
|
3
|
-
_Research document, 2026-08-22. Measured on the `claude/x11-protocol-efficiency`
|
|
4
|
-
checkout with ntk 8.2.0 / x11 3.9.0 installed, on one machine (macOS 15.2,
|
|
5
|
-
node v26): live XQuartz (X.Org 21.x) through the
|
|
6
|
-
[x11-protocol-visualizer](https://github.com/sidorares/x11-protocol-visualizer)
|
|
7
|
-
MITM proxy, and node-x11's in-process pure-JS X server via `scripts/bench/`.
|
|
8
|
-
File:line references will drift with the code; request counts will not, because
|
|
9
|
-
the bench now prices them._
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## 0. TL;DR
|
|
14
|
-
|
|
15
|
-
A protocol trace of _hovering a `<Checkbox>`_ in `examples/form/index.jsx` — a frame
|
|
16
|
-
whose visible result is one 18×18 rounded rectangle changing colour — showed
|
|
17
|
-
~26 requests per frame, among them a `CreatePixmap` of a **420×380 depth-8
|
|
18
|
-
pixmap created and freed every frame**. The audit attributed every request in
|
|
19
|
-
that frame and in the mount that precedes it, across all three layers
|
|
20
|
-
(react-x11, ntk, node-x11).
|
|
21
|
-
|
|
22
|
-
**Findings, compressed:**
|
|
23
|
-
|
|
24
|
-
1. **[ntk] `_fillRect` misses the rectangular-clip fast path** — the dominant
|
|
25
|
-
cost, and the whole explanation of the per-frame pixmap. Every `fillRect`
|
|
26
|
-
under _any_ clip materializes a **window-sized a8 clip mask** and
|
|
27
|
-
composites through it, when the glyph, trapezoid and image routes all
|
|
28
|
-
already special-case a rectangular clip stack. A one-function fix
|
|
29
|
-
(intersect the fill rect with the clip rect, composite directly) removes
|
|
30
|
-
7 requests and ~2 full surfaces of server pixel work from every partial
|
|
31
|
-
repaint. Verified byte-identical.
|
|
32
|
-
2. **[ntk] Picture-clip churn** — the rounded-rect glyph route brackets every
|
|
33
|
-
shape in `SetPictureClipRectangles` + reset, so a box's fill and border
|
|
34
|
-
pay 4 clip requests where 1 would do, and consecutive shapes re-set the
|
|
35
|
-
clip they just reset. A sticky, lazily-reset picture clip removes ~75% of
|
|
36
|
-
them. Verified byte-identical.
|
|
37
|
-
3. **[react-x11] Event mask declared one listener at a time** — nine
|
|
38
|
-
`ChangeWindowAttributes` per window for a constant value. Fixed on this
|
|
39
|
-
branch: the union goes in `CreateWindow`.
|
|
40
|
-
4. **[react-x11 / ntk / node-x11] Startup round trips serialized** — ~10
|
|
41
|
-
dependent round trips before the first `CreateWindow`. Partially fixed on
|
|
42
|
-
this branch (`createRoot`'s two probes now run concurrently); the ntk and
|
|
43
|
-
node-x11 halves are proposed below.
|
|
44
|
-
5. The rest of the hover frame is honest work (corner glyphs, border,
|
|
45
|
-
blit) — and the event side (XI2 Motion at ~136 B/event) is the price of
|
|
46
|
-
smooth scrolling, with one narrow refinement available.
|
|
47
|
-
|
|
48
|
-
Prototyped fixes 1+2 against the whole bench: **requests −35…−80% and
|
|
49
|
-
composite pixel area −45…−95% on every partial-repaint scenario**, pixels
|
|
50
|
-
byte-identical (SHA-256 of full-window `GetImage` across five interaction
|
|
51
|
-
states), full test suite green.
|
|
52
|
-
|
|
53
|
-
---
|
|
54
|
-
|
|
55
|
-
## 1. The trace, explained line by line
|
|
56
|
-
|
|
57
|
-
The captured hover frame (reproduced byte-for-byte on the in-process server
|
|
58
|
-
and on live XQuartz through the proxy — same requests, same sizes):
|
|
59
|
-
|
|
60
|
-
| request | bytes | why it is there |
|
|
61
|
-
| --------------------------------------------------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
62
|
-
| `ChangeWindowAttributes {cursor}` | 16 | the hover path crossed into/out of a node with a `cursor`; react-x11 already dedupes (`EventManager._updateCursor`), so one request per boundary crossing is correct |
|
|
63
|
-
| `GetInputFocus` | 4 | **node-x11's void-sync**, not the frame fence (ntk#309 settled this from the reply positions: it follows the cursor write and precedes the paint, where a fence would trail `Present:Pixmap`). ntk passes a discarded `() => {}` to `ChangeWindowAttributes`, and node-x11 guarantees a callback on a void request eventually fires by injecting a `GetInputFocus` round trip. The fence proper (`Window#_armFence`) only runs where Present completions are not clocking frames |
|
|
64
|
-
| `CreatePixmap 420×380 depth=8` | 16 | **the bug.** `_fillRect` under the damage-rect clip takes the mask path: this is a full-window a8 clip mask |
|
|
65
|
-
| `RENDER:CreatePicture` | 20 | picture over that mask |
|
|
66
|
-
| `RENDER:FillRectangles Src (0,0,0,0)` | 28 | clear the _entire_ 420×380 mask (159,600 px of server work) |
|
|
67
|
-
| `RENDER:FillRectangles Src (0,0,0,1) [18×18 rect]` | 28 | write the clip rect into the mask |
|
|
68
|
-
| `RENDER:Composite Over src=solid mask=a8 → backing` | 36 | the window background, painted through the mask |
|
|
69
|
-
| `RENDER:SetPictureClipRectangles [18×18]` | 20 | clip for the checkbox body's corner glyphs |
|
|
70
|
-
| `RENDER:CompositeGlyphs8` | 76 | the body's rounded corners, as cached server-side glyphs (`shapeglyphs.js` — this part is the fast path working as designed) |
|
|
71
|
-
| `RENDER:FillRectangles Over ×3 rects` | 44 | the body's straight strips |
|
|
72
|
-
| `RENDER:SetPictureClipRectangles [reset]` | 20 | eager reset after the body |
|
|
73
|
-
| `RENDER:SetPictureClipRectangles [the same 18×18]` | 20 | …immediately re-set for the border |
|
|
74
|
-
| `RENDER:CompositeGlyphs8` | 76 | border corner glyphs |
|
|
75
|
-
| `RENDER:FillRectangles Over ×4 rects` | 52 | border strips |
|
|
76
|
-
| `RENDER:SetPictureClipRectangles [reset]` | 20 | eager reset again |
|
|
77
|
-
| `RENDER:FreePicture` + `FreePixmap` | 16 | the mask ceremony's teardown |
|
|
78
|
-
| `XFIXES:SetRegion` | 16 | the frame's damage as the present region (region id reused — correct) |
|
|
79
|
-
| `Present:Pixmap` | 72 | the blit, scheduled against vblank |
|
|
80
|
-
|
|
81
|
-
So of ~26 requests, **8 are the clip-mask ceremony** (finding 1), **3 of 4
|
|
82
|
-
clip set/resets are redundant** (finding 2), and the remaining ~12 are the
|
|
83
|
-
actual ink plus the blit. The pixel-work story is worse than the request
|
|
84
|
-
count: the ceremony touches ~2 × 160k px per frame for an 18×18 change.
|
|
85
|
-
|
|
86
|
-
The damage scoping above all of this is _correct_: react-x11 repaints exactly
|
|
87
|
-
the 18×18 checkbox box (`_takeDamage` → one clipped `_paintRegion` pass), and
|
|
88
|
-
a 120-motion hover sweep coalesces into 7 presents. The waste is entirely in
|
|
89
|
-
how the bounded repaint is translated to RENDER.
|
|
90
|
-
|
|
91
|
-
## 2. How to measure (the e2e methodology)
|
|
92
|
-
|
|
93
|
-
Two complementary rigs, both now in-repo or one command away:
|
|
94
|
-
|
|
95
|
-
**Deterministic (CI-grade): `npm run bench`.** Runs scenarios against
|
|
96
|
-
node-x11's in-process X server and counts requests / bytesOut / replies /
|
|
97
|
-
`Composite` count / **composite pixel area** / **convolved pixels** per
|
|
98
|
-
scenario (`scripts/bench/xcount.js`). Requests and bytes miss pixel work
|
|
99
|
-
entirely — a Composite is 36 bytes whether it touches ten pixels or the whole
|
|
100
|
-
surface — so the Mpx column is the one that catches "correct but does far too
|
|
101
|
-
much". The convMpx column catches the layer under _that_: area prices every
|
|
102
|
-
destination pixel the same, but a source or mask picture carrying RENDER's
|
|
103
|
-
`convolution` filter is re-convolved by the server on every composite that
|
|
104
|
-
reads it, so `kernel_w × kernel_h × area` is what the server actually runs.
|
|
105
|
-
Issue #413 is the case that motivated it — a blurred `boxShadow` left the
|
|
106
|
-
blur as a picture filter rather than baking it into the pixels, which made
|
|
107
|
-
every shadowed repaint ~700x slower with requests, bytes, composites, area,
|
|
108
|
-
stalls, dups, churn and the rendered pixels all unchanged.
|
|
109
|
-
Since #380 the same counting proxy also analyses the stream: `stalls`
|
|
110
|
-
(blocking round trips nothing was pipelined behind), `dupQueries` (repeated
|
|
111
|
-
identical reply-carrying requests) and `churn` (server resources created and
|
|
112
|
-
freed inside the measured window, keyed by creation parameters — the
|
|
113
|
-
"window-sized mask pixmap rebuilt every frame" class reads as one line with
|
|
114
|
-
a count), with `--hotspots` for the per-scenario detail, `--record-dir` for
|
|
115
|
-
x11vis-format captures of each scenario, and a CI job gating `--check`.
|
|
116
|
-
This audit added the two scenarios the trace exposed as unpriced:
|
|
117
|
-
|
|
118
|
-
- `fills: 20 fillRects each under a damage clip` — the window background of
|
|
119
|
-
every partial repaint;
|
|
120
|
-
- `hover: rounded box state flip, on and off` — the whole per-frame tail of a
|
|
121
|
-
pointer interaction (damage scoping → background under clip → corner
|
|
122
|
-
glyphs → border → blit).
|
|
123
|
-
|
|
124
|
-
**Live (ground truth): x11vis.** The MITM proxy records byte-exact captures
|
|
125
|
-
against a real server, with decode, per-request RTT, stats and resource-
|
|
126
|
-
lifecycle lints:
|
|
127
|
-
|
|
128
|
-
```bash
|
|
129
|
-
# terminal 1 — proxy the real display, record
|
|
130
|
-
x11vis --no-ui --quiet --record hover.x11cap
|
|
131
|
-
|
|
132
|
-
# terminal 2 — the app under test, through the proxy
|
|
133
|
-
DISPLAY=127.0.0.1:1 npm run examples:form
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
Driving the interaction reproducibly on XQuartz (no XTEST there): the app's
|
|
137
|
-
own connection can sweep the real pointer with core-protocol `WarpPointer`
|
|
138
|
-
across the widget row — real `EnterNotify`/`MotionNotify`/XI2 events follow,
|
|
139
|
-
so the full input path is exercised. The capture is then sliced between
|
|
140
|
-
`Present:Pixmap` requests to isolate steady-state frames, and
|
|
141
|
-
`x11vis --diff a.x11cap b.x11cap` compares before/after sessions.
|
|
142
|
-
|
|
143
|
-
**The correctness bar.** "No visual degradation" is verified as _pixel_
|
|
144
|
-
byte-identity, not request-stream identity (an optimized stream is different
|
|
145
|
-
by definition): render the same tree through the same interaction script
|
|
146
|
-
under both implementations and compare SHA-256 digests of full-window
|
|
147
|
-
`GetImage` readbacks at each settled state. The in-process server makes this
|
|
148
|
-
deterministic (no font/AA variance across runs). Five states were compared
|
|
149
|
-
for this audit — mounted, hover on, hover wiggle, hover off, clicked — all
|
|
150
|
-
identical.
|
|
151
|
-
|
|
152
|
-
**The latency metric.** Requests are a proxy; the goal posts are (a) **round
|
|
153
|
-
trips on the critical path** (the `replies` column; each is a full RTT on a
|
|
154
|
-
remote display) and (b) **server pixel work per frame** (Mpx), which is what
|
|
155
|
-
turns into input→photon latency once the server is the bottleneck.
|
|
156
|
-
|
|
157
|
-
## 3. Findings in detail
|
|
158
|
-
|
|
159
|
-
### 3.1 [ntk] `_fillRect` + any clip = a window-sized mask (the headline)
|
|
160
|
-
|
|
161
|
-
`renderingcontext_2d.js` has a rectangular-clip fast path — "a stack of
|
|
162
|
-
rectangles stays virtual" — and `_drawGlyphsDevice`, `drawTraps` and
|
|
163
|
-
`_beginDirectComposite` (images) all use it. `_fillRect` is the one direct
|
|
164
|
-
route that never checks: it calls `_compositeMask()` unconditionally, whose
|
|
165
|
-
own comment claims it is "reached only when the clip is not a rectangle".
|
|
166
|
-
Under react-x11 every partial repaint clips to the damage rect first, so
|
|
167
|
-
_every window-background fill of every bounded frame_ takes the mask path:
|
|
168
|
-
`CreatePixmap` (window-sized, depth 8) + `CreatePicture` + full-surface
|
|
169
|
-
clear + rect write + `Composite` through the mask + `FreePicture` +
|
|
170
|
-
`FreePixmap` — recreated per frame because `restore()` drops the mask.
|
|
171
|
-
|
|
172
|
-
**Fix.** In the identity-transform branch of `_fillRect`: if the clip stack
|
|
173
|
-
is all rectangles, intersect the fill rect with `_clipRect()` and composite
|
|
174
|
-
directly (`mask=0`, or a 1×1 repeating solid for `globalAlpha < 1`, exactly
|
|
175
|
-
as `_beginDirectComposite` already does). Fall back to `_compositeMask()`
|
|
176
|
-
only for a genuinely non-rectangular stack. The same treatment applies to
|
|
177
|
-
the remaining `_compositeMask()` call sites (the two `drawImage` legs).
|
|
178
|
-
|
|
179
|
-
**Why byte-identical:** compositing `src IN mask(=1 inside R, 0 outside)`
|
|
180
|
-
bounded to rect D writes exactly the pixels of D∩R; compositing unmasked
|
|
181
|
-
bounded to D∩R writes the same pixels with the same operator arithmetic.
|
|
182
|
-
Verified empirically (§5).
|
|
183
|
-
|
|
184
|
-
**Bonus:** this also stops per-frame XID allocate/free churn — the capture's
|
|
185
|
-
lint pass flags the freed mask pixmap's id being recycled into a Present
|
|
186
|
-
event id, which is legal but makes captures harder to read.
|
|
187
|
-
|
|
188
|
-
### 3.2 [ntk] Sticky picture clip
|
|
189
|
-
|
|
190
|
-
`_emitShapeGlyphs` (and the text fast path) bracket each run in
|
|
191
|
-
`SetPictureClipRectangles(clip)` … `SetPictureClipRectangles(0,0,32767,32767)`.
|
|
192
|
-
A rounded box paints as _fill bracket + border bracket_: four clip requests,
|
|
193
|
-
two of them setting the value the previous one just reset.
|
|
194
|
-
|
|
195
|
-
**Fix.** Make the destination picture's rectangular clip a piece of tracked
|
|
196
|
-
state: a set that matches the applied clip is skipped, and the reset is
|
|
197
|
-
recorded lazily and only flushed by the next operation that draws to the
|
|
198
|
-
picture under a different (or no) clip. Every RENDER op that names the
|
|
199
|
-
context's picture as destination goes through one accessor, which is what
|
|
200
|
-
makes the laziness safe — an unobserved reset-then-set-same pair is, by
|
|
201
|
-
construction, unobservable to the server. In the hover frame this turns 4
|
|
202
|
-
clip requests into 1.
|
|
203
|
-
|
|
204
|
-
### 3.3 [react-x11, landed] The event mask, one request instead of nine
|
|
205
|
-
|
|
206
|
-
ntk grows a window's server-side event mask from a `newListener` hook — one
|
|
207
|
-
`ChangeWindowAttributes` per first listener of each kind. react-x11's
|
|
208
|
-
subscriptions are a constant (pointer ×4, keys ×2, focus ×2, plus ntk's own
|
|
209
|
-
Exposure for the backing store), so mount paid nine requests for a value
|
|
210
|
-
known before the window existed. `WindowNode.realize()` now passes the union
|
|
211
|
-
as a `CreateWindow` attribute; ntk ORs it in and every lazy grow becomes a
|
|
212
|
-
no-op. _(A complementary ntk fix — coalescing mask grows per tick — would
|
|
213
|
-
serve non-react ntk apps too.)_
|
|
214
|
-
|
|
215
|
-
### 3.4 [all three, partially landed] Startup round-trip pipelining
|
|
216
|
-
|
|
217
|
-
Before the first `CreateWindow`, instrumented reply-by-reply:
|
|
218
|
-
|
|
219
|
-
| layer | chain | round trips |
|
|
220
|
-
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
|
|
221
|
-
| node-x11 | connection handshake | 1 |
|
|
222
|
-
| ntk `createClient` | `require('glx')` → `require('render')` (QueryExtension → QueryPictFormats, nested) → `Render.QueryVersion` | 4–5, **serial** |
|
|
223
|
-
| react-x11 `createRoot` | `beginCompositing` (InternAtom → GetSelectionOwner → XFixes QueryExtension) then `beginScreens` (QueryExtension → InternAtom → GetProperty) | 6, **was serial** |
|
|
224
|
-
|
|
225
|
-
On a remote display at 20 ms RTT that is ~200 ms of pure serialization before
|
|
226
|
-
the first pixel. Landed here: `createRoot` awaits its two probes with
|
|
227
|
-
`Promise.all` (the trace now shows them interleaved — the floor is the
|
|
228
|
-
slower chain, not the sum). Proposed for ntk: run the `glx` and `render`
|
|
229
|
-
requires concurrently and pipeline `QueryVersion` behind `QueryExtension`'s
|
|
230
|
-
reply (both only need the major opcode). Proposed for node-x11: the render
|
|
231
|
-
extension's own `QueryVersion`/`QueryPictFormats` can go out together.
|
|
232
|
-
|
|
233
|
-
Also verified, no action needed: node-x11's write path already batches
|
|
234
|
-
Xlib-style (16–64 KB buffer, flush on reply-bearing requests, `setImmediate`
|
|
235
|
-
backstop), atoms are cached with the 68 standard atoms pre-seeded, and the
|
|
236
|
-
17 `InternAtom`s react-x11's dnd module issues at mount are pipelined (one
|
|
237
|
-
RTT total). The one node-x11 nicety worth having: dedupe _concurrent_
|
|
238
|
-
in-flight `InternAtom`s for the same name (the cache only catches repeats
|
|
239
|
-
after the reply).
|
|
240
|
-
|
|
241
|
-
### 3.5 [ntk] Smaller findings
|
|
242
|
-
|
|
243
|
-
- **`_compositeMask` alpha fill is window-sized** — when `globalAlpha < 1`
|
|
244
|
-
it fills the whole scratch mask with the alpha; a 1×1 repeating solid (as
|
|
245
|
-
`_beginDirectComposite` uses) or a fill bounded to the composite rect is
|
|
246
|
-
the same result without the full-surface write.
|
|
247
|
-
- **The backing store's birth clear** — every backing pixmap is
|
|
248
|
-
`PolyFillRectangle`d white at creation, and react-x11's first frame then
|
|
249
|
-
paints every pixel of it. Skipping the clear when a full-window paint is
|
|
250
|
-
already queued would save one full-surface fill per window/resize.
|
|
251
|
-
- **`AddGlyphs` per batch** — a first paint uploads new glyphs as
|
|
252
|
-
encountered (20 `AddGlyphs` at form mount). Same bytes, fewer requests if
|
|
253
|
-
accumulated per (frame × glyphset).
|
|
254
|
-
|
|
255
|
-
### 3.6 The event side (bytes _in_)
|
|
256
|
-
|
|
257
|
-
In the live hover capture, `XInputExtension:Motion` is the single largest
|
|
258
|
-
item: 120 events, 16,320 bytes — 96% of event traffic, ~136 B per motion
|
|
259
|
-
against 32 B for core `MotionNotify`. This is the cost of XI2 Motion
|
|
260
|
-
selection, which is what smooth (sub-notch, valuator-based) scrolling rides
|
|
261
|
-
on, so it is bought deliberately (issue #273). One refinement is available:
|
|
262
|
-
ntk already queries the seat's scroll axes for its wheel bookkeeping, so on
|
|
263
|
-
a seat with _no_ scroll valuators the XI2 Motion selection buys nothing and
|
|
264
|
-
could be skipped, keeping 32-byte core motion. Not worth conditioning on
|
|
265
|
-
anything finer — device hot-plug would re-open the question mid-session.
|
|
266
|
-
|
|
267
|
-
Also checked and fine: motion coalescing (120 injected motions → 7 painted
|
|
268
|
-
frames), `PointerMotion` selection itself (hover styling needs it), and the
|
|
269
|
-
cursor updates (deduped; one request per hover-boundary crossing).
|
|
270
|
-
|
|
271
|
-
### 3.7 Why the traced session showed `GetInputFocus` _and_ `Present:Pixmap`
|
|
272
|
-
|
|
273
|
-
_Corrected 2026-08-23 — the first version of this section blamed the frame
|
|
274
|
-
fence; ntk#309's position analysis showed otherwise._
|
|
275
|
-
|
|
276
|
-
The per-frame `GetInputFocus` in the original capture is **node-x11's
|
|
277
|
-
void-sync**, not ntk's fence. ntk's `setCursor` (and its lazy event-mask
|
|
278
|
-
grows) pass a no-op `() => {}` callback to `ChangeWindowAttributes`, and
|
|
279
|
-
node-x11 honours its guarantee that a callback on a void request eventually
|
|
280
|
-
fires by injecting a `GetInputFocus` round trip when no reply-bearing
|
|
281
|
-
request follows in the same tick. The tell is the position: each
|
|
282
|
-
`GetInputFocus` follows a cursor write and precedes the frame's drawing,
|
|
283
|
-
where a fence would trail `Present:Pixmap` in the same synchronous run —
|
|
284
|
-
and the client never waited on the reply. The Present completion clock was
|
|
285
|
-
active in that capture all along (a live probe on this machine's XQuartz
|
|
286
|
-
confirms `frameClock: 'present'`); its 20.5 ms RTT is still informative —
|
|
287
|
-
that is the server draining the frame's mask ceremony, which finding 1
|
|
288
|
-
removes. Fixes: drop the discarded callbacks (ntk#309 / PR ntk#311, which
|
|
289
|
-
also memoizes the cursor id ntk-side). The fence itself needs no change: it
|
|
290
|
-
is the flow control everywhere Present completions cannot clock frames, and
|
|
291
|
-
one 4-byte request per frame is the correct price there.
|
|
292
|
-
|
|
293
|
-
## 4. Numbers
|
|
294
|
-
|
|
295
|
-
Bench, ntk 8.2.0 as published ("before") vs the same with fixes 3.1+3.2
|
|
296
|
-
prototyped ("after"). Composites and text/glyph bytes are unchanged
|
|
297
|
-
throughout — the ink is the same ink.
|
|
298
|
-
|
|
299
|
-
| scenario | reqs before → after | Mpx before → after |
|
|
300
|
-
| -------------------------------------------- | ------------------- | ------------------ |
|
|
301
|
-
| fills: 20 fillRects each under a damage clip | **150 → 30** | **3.36 → 0.18** |
|
|
302
|
-
| hover: rounded box state flip, on and off | **37 → 20** | 0.00 → 0.00 |
|
|
303
|
-
| clips: 40 nested rect clips with text | 148 → 108 | 0.16 → 0.16 |
|
|
304
|
-
| scroll: 10 notches over 500 rows | 329 → 207 | 0.64 → 0.39 |
|
|
305
|
-
| svg: 10 unchanged icons in a damaged row | 41 → 16 | 0.01 → 0.01 |
|
|
306
|
-
| update: 5 color flips, no layout | 49 → 19 | 0.03 → 0.03 |
|
|
307
|
-
| update: 5 absolute box moves (layout) | 48 → 18 | 0.03 → 0.03 |
|
|
308
|
-
| scene: 5 drag steps over 300 cells | 240 → 146 | 0.08 → 0.01 |
|
|
309
|
-
| scene: 5 pan steps over 374 cells | 983 → 623 | 0.30 → 0.06 |
|
|
310
|
-
| scene: 5 pan steps with a HUD strip | 3060 → 1992 | 1.10 → 0.43 |
|
|
311
|
-
|
|
312
|
-
Full-repaint scenarios (mount, icon wall, gradient cards) are unchanged —
|
|
313
|
-
they never clip, so they never paid the tax. The `Mpx` collapses are the
|
|
314
|
-
through-mask composites and full-surface mask clears disappearing; on a
|
|
315
|
-
slow or software-rendering server that column is frame time.
|
|
316
|
-
|
|
317
|
-
react-x11 changes landed with this document (measured on the mount trace):
|
|
318
|
-
9 `ChangeWindowAttributes` → 0, and the pre-window probe serialization
|
|
319
|
-
6 round trips → the slower of two ~3-trip chains.
|
|
320
|
-
|
|
321
|
-
Verification: the full test suite passes with the prototyped ntk (the six
|
|
322
|
-
known macOS-only appearance failures and nothing else), and SHA-256 digests
|
|
323
|
-
of full-window `GetImage` readbacks across five interaction states are
|
|
324
|
-
identical between published and prototyped ntk.
|
|
325
|
-
|
|
326
|
-
## 5. What lands where
|
|
327
|
-
|
|
328
|
-
- **This branch (react-x11):** event mask at `CreateWindow`; concurrent
|
|
329
|
-
`createRoot` probes; the two bench scenarios + baseline refresh; this
|
|
330
|
-
document.
|
|
331
|
-
- **ntk (proposed, prototype validated):** `_fillRect` rectangular-clip fast
|
|
332
|
-
path (§3.1, also the `drawImage` `_compositeMask` legs); sticky picture
|
|
333
|
-
clip (§3.2); bounded/solid alpha mask (§3.5); optional: coalesced event-
|
|
334
|
-
mask grows, deferred backing clear, per-frame `AddGlyphs` batching,
|
|
335
|
-
scroll-axis-gated XI2 Motion. Once released, react-x11 re-saves the bench
|
|
336
|
-
baseline to lock the improvement in (the skipped-on-CI trap: CI installs
|
|
337
|
-
published ntk, so the baseline must always be saved against it).
|
|
338
|
-
- **node-x11 (proposed):** pipeline extension init round trips; dedupe
|
|
339
|
-
concurrent `InternAtom`s.
|
|
340
|
-
|
|
341
|
-
---
|
|
342
|
-
|
|
343
|
-
## Addendum, 2026-08-23: where the proposals went
|
|
344
|
-
|
|
345
|
-
Filed and in flight the day after the audit:
|
|
346
|
-
|
|
347
|
-
- react-x11#380 (landed) — the bench's counting proxy grew the `stalls` /
|
|
348
|
-
`dupQueries` / `churn` analysis, a pointer-driven Checkbox hover scenario,
|
|
349
|
-
`--hotspots`, x11vis-format `--record-dir`, and a CI `--check` gate.
|
|
350
|
-
- ntk#307 → PR ntk#312 — the `fillRect` rectangular-clip bypass (§3.1),
|
|
351
|
-
including `globalAlpha` as a 1×1 solid on every direct composite.
|
|
352
|
-
- ntk#308 — the sticky, lazily-reset picture clip (§3.2).
|
|
353
|
-
- ntk#309 → PR ntk#311 — drop the no-op callbacks whose void-sync this
|
|
354
|
-
section originally misread as the fence (§3.7), and memoize the cursor.
|
|
355
|
-
- ntk#313 — the residual `_compositeMask` fallback: full-surface alpha
|
|
356
|
-
stamp and clip intersection bounded to the composite box (§3.5's first
|
|
357
|
-
bullet, narrowed to what PR ntk#312 leaves behind).
|