react-x11 1.2.0 → 2.0.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.
Files changed (158) hide show
  1. package/README.md +330 -81
  2. package/package.json +132 -14
  3. package/src/ClickToComponent.js +9 -2
  4. package/src/DevToolsIntegration.js +440 -32
  5. package/src/Reconciler.js +463 -165
  6. package/src/a11y.js +1299 -0
  7. package/src/acceleratorhooks.js +86 -0
  8. package/src/accelerators.js +128 -0
  9. package/src/activate.js +168 -0
  10. package/src/anchor.js +360 -0
  11. package/src/appcontext.js +161 -0
  12. package/src/appearance.js +748 -0
  13. package/src/appearancehooks.js +96 -0
  14. package/src/apphooks.js +73 -0
  15. package/src/application.js +748 -0
  16. package/src/atspi.js +1953 -0
  17. package/src/bus.js +545 -0
  18. package/src/bushooks.js +136 -0
  19. package/src/clientmessage.js +140 -0
  20. package/src/clipboard.js +121 -0
  21. package/src/components/Button.js +148 -33
  22. package/src/components/Checkbox.js +66 -32
  23. package/src/components/Dialog.js +96 -29
  24. package/src/components/FileDialog.js +491 -0
  25. package/src/components/Icon.js +329 -0
  26. package/src/components/Menu.js +797 -133
  27. package/src/components/PasswordInput.js +434 -0
  28. package/src/components/ProgressBar.js +91 -12
  29. package/src/components/Radio.js +78 -35
  30. package/src/components/Select.js +255 -65
  31. package/src/components/Slider.js +102 -41
  32. package/src/components/SplitPane.js +206 -0
  33. package/src/components/Switch.js +100 -22
  34. package/src/components/Table.js +529 -0
  35. package/src/components/Tabs.js +211 -0
  36. package/src/components/Tooltip.js +528 -54
  37. package/src/components/anchor.js +154 -107
  38. package/src/components/change.js +34 -0
  39. package/src/components/dnd.js +103 -0
  40. package/src/components/index.js +16 -3
  41. package/src/components/keys.js +17 -20
  42. package/src/components/scribble.js +247 -0
  43. package/src/components/theme.js +350 -42
  44. package/src/components/typeahead.js +12 -2
  45. package/src/compose.js +868 -0
  46. package/src/compositing.js +224 -0
  47. package/src/dbusmenu.js +384 -0
  48. package/src/debug.d.ts +63 -0
  49. package/src/debug.js +642 -0
  50. package/src/decorations.js +486 -0
  51. package/src/desktopsettings.js +209 -0
  52. package/src/desktopsettingshooks.js +62 -0
  53. package/src/dnd.js +1725 -0
  54. package/src/editmenu.js +272 -0
  55. package/src/errors.js +98 -0
  56. package/src/events.js +1406 -162
  57. package/src/extensions.js +45 -0
  58. package/src/filedialog.js +375 -0
  59. package/src/filedialoghooks.js +132 -0
  60. package/src/fonthooks.js +64 -0
  61. package/src/fonts.js +301 -0
  62. package/src/foreignnodes.js +519 -0
  63. package/src/frame/child.js +33 -0
  64. package/src/frame/childmain.js +274 -0
  65. package/src/frame/env.js +140 -0
  66. package/src/frame/index.js +444 -0
  67. package/src/frame/lifecycle.js +67 -0
  68. package/src/frame/protocol.js +179 -0
  69. package/src/frames.js +73 -0
  70. package/src/glbackend.js +114 -0
  71. package/src/glnodes.js +125 -67
  72. package/src/globalmenu.js +683 -0
  73. package/src/host.d.ts +88 -0
  74. package/src/host.js +42 -0
  75. package/src/idle.js +490 -0
  76. package/src/idlehooks.js +100 -0
  77. package/src/imagesource.js +349 -0
  78. package/src/index.d.ts +475 -0
  79. package/src/index.js +83 -9
  80. package/src/inputtime.js +206 -0
  81. package/src/jsx-dev-runtime.d.ts +21 -0
  82. package/src/jsx-dev-runtime.js +2 -0
  83. package/src/jsx-runtime.d.ts +39 -0
  84. package/src/jsx-runtime.js +6 -0
  85. package/src/keyboard.js +256 -0
  86. package/src/keyboardstate.js +278 -0
  87. package/src/keyboardstatehooks.js +58 -0
  88. package/src/keysyms.d.ts +149 -0
  89. package/src/keysyms.js +270 -0
  90. package/src/locale.js +170 -0
  91. package/src/localehooks.js +47 -0
  92. package/src/menuitem.js +223 -0
  93. package/src/node.d.ts +597 -0
  94. package/src/node.js +44 -0
  95. package/src/nodes.js +9546 -690
  96. package/src/ntk.d.ts +44 -0
  97. package/src/ntk.js +25 -0
  98. package/src/paintcache.js +366 -0
  99. package/src/palette.js +380 -0
  100. package/src/pastestate.js +66 -0
  101. package/src/portal.js +461 -0
  102. package/src/priority.js +26 -0
  103. package/src/refresh/index.d.ts +40 -0
  104. package/src/refresh/index.js +122 -0
  105. package/src/refresh/loader.d.ts +37 -0
  106. package/src/refresh/loader.js +401 -0
  107. package/src/refresh/register.d.ts +5 -0
  108. package/src/refresh/register.js +13 -0
  109. package/src/registry.js +232 -0
  110. package/src/scale.js +626 -0
  111. package/src/scalehooks.js +27 -0
  112. package/src/screencolor.js +640 -0
  113. package/src/screencolorhooks.js +101 -0
  114. package/src/screens.js +754 -0
  115. package/src/screenshooks.js +137 -0
  116. package/src/startup.js +302 -0
  117. package/src/style.d.ts +126 -0
  118. package/src/style.js +33 -0
  119. package/src/styles.js +1482 -6
  120. package/src/svgnodes.js +306 -0
  121. package/src/testing/a11y.js +484 -0
  122. package/src/testing/components.js +414 -0
  123. package/src/testing/events.js +407 -0
  124. package/src/testing/harness.js +455 -0
  125. package/src/testing/index.d.ts +558 -0
  126. package/src/testing/index.js +85 -0
  127. package/src/testing/mock-app.js +463 -0
  128. package/src/testing/pixels.js +152 -0
  129. package/src/testing/queries.js +224 -0
  130. package/src/textrange.js +83 -0
  131. package/src/textselection.js +439 -0
  132. package/src/trace-registry.js +63 -0
  133. package/src/transfer.js +93 -0
  134. package/src/types/appearance.d.ts +84 -0
  135. package/src/types/application.d.ts +173 -0
  136. package/src/types/components.d.ts +820 -0
  137. package/src/types/dbus.d.ts +177 -0
  138. package/src/types/elements.d.ts +897 -0
  139. package/src/types/events.d.ts +549 -0
  140. package/src/types/filedialog.d.ts +260 -0
  141. package/src/types/fonts.d.ts +124 -0
  142. package/src/types/frame.d.ts +146 -0
  143. package/src/types/globalmenu.d.ts +42 -0
  144. package/src/types/nodes.d.ts +199 -0
  145. package/src/types/screencolor.d.ts +84 -0
  146. package/src/types/style.d.ts +434 -0
  147. package/src/types/system.d.ts +287 -0
  148. package/src/windowid.js +151 -0
  149. package/src/windowstate.js +393 -0
  150. package/src/xsettings.js +336 -0
  151. package/src/yoga.d.ts +55 -0
  152. package/src/yoga.js +135 -0
  153. package/src/components/Canvas3D.js +0 -28
  154. package/src/geometry3d.js +0 -223
  155. package/src/pointer3d.js +0 -158
  156. package/src/raycast3d.js +0 -146
  157. package/src/richnodes.js +0 -436
  158. package/src/scene3d.js +0 -683
package/src/debug.js ADDED
@@ -0,0 +1,642 @@
1
+ // react-x11/debug — runtime X11 protocol tracing and frame diagnostics.
2
+ //
3
+ // The environment switch (read once by the Reconciler at import time):
4
+ //
5
+ // REACT_X11_TRACE=summary opcode histogram + byte totals at exit
6
+ // REACT_X11_TRACE=requests one stderr line per request + per frame
7
+ // REACT_X11_TRACE=chrome:/tmp/trace.json Chrome Trace Event JSON at exit
8
+ //
9
+ // append `+stacks` to `requests` or `chrome` to capture a JS stack per
10
+ // request (node-x11's seq2stack), so an async X error names the call that
11
+ // sent the offending request. That costs an Error capture per request, so
12
+ // it is not on by default.
13
+ //
14
+ // The API (any connection, not only the renderer's):
15
+ //
16
+ // import { startTrace } from 'react-x11/debug';
17
+ // const trace = startTrace({ sink: 'chrome', path: '/tmp/t.json' });
18
+ // ...
19
+ // const { requests, bytesOut, replies, byOpcode } = trace.stop();
20
+ //
21
+ // With no `app`, the trace follows every connection the renderer has open
22
+ // or opens later. Pass `app` to trace one specific connection instead.
23
+ //
24
+ // The tracer attaches to a connection *after* its handshake: it wraps the
25
+ // pack stream that carries framed requests and adds a listener beside
26
+ // node-x11's own on the socket. The connection-setup packet — the one that
27
+ // carries the X auth cookie — was written before either hook existed, so
28
+ // the tracer cannot observe it. Redaction by construction, not by filter.
29
+ // Payload bytes are never recorded either — only opcode, length and
30
+ // direction — so window contents and property values stay off the log.
31
+ import { writeFileSync } from 'node:fs';
32
+ import { onApp, hooks } from './trace-registry.js';
33
+
34
+ // --- names --------------------------------------------------------------
35
+
36
+ // Core request opcodes 1..119 in protocol order (`-` marks a hole), plus
37
+ // 127. Kept as one word list so the table reads like the protocol index.
38
+ const CORE_REQUESTS = `- CreateWindow ChangeWindowAttributes
39
+ GetWindowAttributes DestroyWindow DestroySubwindows ChangeSaveSet
40
+ ReparentWindow MapWindow MapSubwindows UnmapWindow UnmapSubwindows
41
+ ConfigureWindow CirculateWindow GetGeometry QueryTree InternAtom
42
+ GetAtomName ChangeProperty DeleteProperty GetProperty ListProperties
43
+ SetSelectionOwner GetSelectionOwner ConvertSelection SendEvent GrabPointer
44
+ UngrabPointer GrabButton UngrabButton ChangeActivePointerGrab GrabKeyboard
45
+ UngrabKeyboard GrabKey UngrabKey AllowEvents GrabServer UngrabServer
46
+ QueryPointer GetMotionEvents TranslateCoordinates WarpPointer
47
+ SetInputFocus GetInputFocus QueryKeymap OpenFont CloseFont QueryFont
48
+ QueryTextExtents ListFonts ListFontsWithInfo SetFontPath GetFontPath
49
+ CreatePixmap FreePixmap CreateGC ChangeGC CopyGC SetDashes
50
+ SetClipRectangles FreeGC ClearArea CopyArea CopyPlane PolyPoint PolyLine
51
+ PolySegment PolyRectangle PolyArc FillPoly PolyFillRectangle PolyFillArc
52
+ PutImage GetImage PolyText8 PolyText16 ImageText8 ImageText16
53
+ CreateColormap FreeColormap CopyColormapAndFree InstallColormap
54
+ UninstallColormap ListInstalledColormaps AllocColor AllocNamedColor
55
+ AllocColorCells AllocColorPlanes FreeColors StoreColors StoreNamedColor
56
+ QueryColors LookupColor CreateCursor CreateGlyphCursor FreeCursor
57
+ RecolorCursor QueryBestSize QueryExtension ListExtensions
58
+ ChangeKeyboardMapping GetKeyboardMapping ChangeKeyboardControl
59
+ GetKeyboardControl Bell ChangePointerControl GetPointerControl
60
+ SetScreenSaver GetScreenSaver ChangeHosts ListHosts SetAccessControl
61
+ SetCloseDownMode KillClient RotateProperties ForceScreenSaver
62
+ SetPointerMapping GetPointerMapping SetModifierMapping
63
+ GetModifierMapping`
64
+ .trim()
65
+ .split(/\s+/);
66
+ CORE_REQUESTS[127] = 'NoOperation';
67
+
68
+ const RENDER_MINORS = `QueryVersion QueryPictFormats QueryPictIndexValues -
69
+ CreatePicture ChangePicture SetPictureClipRectangles FreePicture
70
+ Composite - Trapezoids Triangles TriStrip TriFan - - - CreateGlyphSet
71
+ ReferenceGlyphSet FreeGlyphSet AddGlyphs - FreeGlyphs CompositeGlyphs8
72
+ CompositeGlyphs16 CompositeGlyphs32 FillRectangles CreateCursor
73
+ SetPictureTransform QueryFilters SetPictureFilter CreateAnimCursor
74
+ AddTraps CreateSolidFill CreateLinearGradient CreateRadialGradient
75
+ CreateConicalGradient`
76
+ .trim()
77
+ .split(/\s+/);
78
+
79
+ const CORE_ERRORS = `- Request Value Window Pixmap Atom Cursor Font Match
80
+ Drawable Access Alloc Colormap GC IDChoice Name Length Implementation`
81
+ .trim()
82
+ .split(/\s+/);
83
+
84
+ /** A `-` in the word lists above is a hole, not a name. */
85
+ const named = (table, index) => {
86
+ const name = table[index];
87
+ return name && name !== '-' ? name : null;
88
+ };
89
+
90
+ /** Extension major opcodes this connection negotiated, from the display. */
91
+ function extensionTable(app) {
92
+ const table = new Map();
93
+ const display = app?.display ?? app?.X?.display;
94
+ if (!display || typeof display !== 'object') return table;
95
+ for (const [key, value] of Object.entries(display)) {
96
+ const major = value?.majorOpcode;
97
+ if (typeof major === 'number' && major >= 128) {
98
+ table.set(major, {
99
+ name: key,
100
+ minors: key === 'Render' ? RENDER_MINORS : null,
101
+ });
102
+ }
103
+ }
104
+ return table;
105
+ }
106
+
107
+ function opcodeName(extensions, major, minor) {
108
+ if (major < 128) return named(CORE_REQUESTS, major) ?? `core${major}`;
109
+ const ext = extensions.get(major);
110
+ if (!ext) return `ext${major}.${minor}`;
111
+ const minorName = ext.minors ? named(ext.minors, minor) : null;
112
+ return `${ext.name}.${minorName ?? minor}`;
113
+ }
114
+
115
+ // --- framing ------------------------------------------------------------
116
+
117
+ const EMPTY = Buffer.alloc(0);
118
+ // Nothing ntk sends comes near this; a length beyond it means the parser
119
+ // lost the frame boundary (it attached mid-burst), so it resynchronises by
120
+ // dropping its buffer rather than swallowing the stream forever.
121
+ const MAX_SANE_LENGTH = 1 << 26;
122
+
123
+ /**
124
+ * Incremental parser for the client->server direction of a post-handshake
125
+ * X11 stream: framed requests, BIG-REQUESTS aware (a 16-bit length of 0
126
+ * means the real 32-bit length follows the header).
127
+ */
128
+ function requestParser(onRequest) {
129
+ let buf = EMPTY;
130
+ return (chunk) => {
131
+ buf = buf.length ? Buffer.concat([buf, chunk]) : chunk;
132
+ for (;;) {
133
+ if (buf.length < 4) return;
134
+ const major = buf[0];
135
+ const minor = buf[1];
136
+ let len = buf.readUInt16LE(2) * 4;
137
+ if (len === 0) {
138
+ if (buf.length < 8) return;
139
+ len = buf.readUInt32LE(4) * 4;
140
+ }
141
+ if (len < 4 || len > MAX_SANE_LENGTH) {
142
+ buf = EMPTY; // desynchronised; drop and re-align on the next put
143
+ return;
144
+ }
145
+ if (buf.length < len) return;
146
+ onRequest(major, minor, len);
147
+ buf = buf.subarray(len);
148
+ }
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Incremental parser for the server->client direction: 32-byte units, with
154
+ * replies (type 1) and GenericEvents (code 35) carrying extra length.
155
+ */
156
+ function inboundParser(onUnit) {
157
+ let buf = EMPTY;
158
+ return (chunk) => {
159
+ buf = buf.length ? Buffer.concat([buf, chunk]) : chunk;
160
+ for (;;) {
161
+ if (buf.length < 32) return;
162
+ const type = buf[0];
163
+ const code = type & 0x7f;
164
+ let total = 32;
165
+ if (type === 1 || code === 35) {
166
+ total = 32 + buf.readUInt32LE(4) * 4;
167
+ if (total > MAX_SANE_LENGTH) {
168
+ buf = EMPTY;
169
+ return;
170
+ }
171
+ if (buf.length < total) return;
172
+ }
173
+ if (type === 0) {
174
+ onUnit('error', total, {
175
+ errorCode: buf[1],
176
+ seq: buf.readUInt16LE(2),
177
+ minor: buf.readUInt16LE(8),
178
+ major: buf[10],
179
+ });
180
+ } else if (type === 1) {
181
+ onUnit('reply', total, null);
182
+ } else {
183
+ onUnit('event', total, { code });
184
+ }
185
+ buf = buf.subarray(total);
186
+ }
187
+ };
188
+ }
189
+
190
+ // --- seq2stack ----------------------------------------------------------
191
+
192
+ // How many recent requests keep their captured stack. A trace can run for
193
+ // minutes; an unbounded map of Error objects would not.
194
+ const STACK_WINDOW = 2048;
195
+
196
+ /**
197
+ * node-x11 captures a stack per request when the client was created with
198
+ * `{ debug: true }` (its "seq2stack" mode). A connection made without it
199
+ * can still get the same accessor installed after the fact — same
200
+ * mechanism, plus a sliding window so a long trace stays bounded.
201
+ */
202
+ function enableSeq2Stack(X) {
203
+ if (!X || X.seq2stack) return Boolean(X?.seq2stack);
204
+ const desc = Object.getOwnPropertyDescriptor(X, 'seq_num');
205
+ if (desc && !('value' in desc)) return false; // someone else's accessor
206
+ try {
207
+ let value = X.seq_num ?? 0;
208
+ X.seq2stack = {};
209
+ Object.defineProperty(X, 'seq_num', {
210
+ configurable: true,
211
+ get: () => value,
212
+ set: function seqNumSetter(v) {
213
+ value = v;
214
+ const err = new Error();
215
+ Error.captureStackTrace(err, seqNumSetter);
216
+ err.timestamp = Date.now();
217
+ X.seq2stack[v] = err;
218
+ delete X.seq2stack[v - STACK_WINDOW];
219
+ },
220
+ });
221
+ return true;
222
+ } catch {
223
+ return false;
224
+ }
225
+ }
226
+
227
+ /** The stack captured for a wire sequence number (16-bit, so it has to be
228
+ * widened against the client's full counter before the lookup). */
229
+ function stackForSeq(X, seq16) {
230
+ const map = X?.seq2stack;
231
+ if (!map) return null;
232
+ const current = X.seq_num ?? 0;
233
+ const base = current & ~0xffff;
234
+ for (const candidate of [base + seq16, base - 0x10000 + seq16]) {
235
+ if (candidate >= 0 && candidate <= current && map[candidate]) {
236
+ return map[candidate].stack;
237
+ }
238
+ }
239
+ return null;
240
+ }
241
+
242
+ // --- the rounded-box fast path ------------------------------------------
243
+
244
+ /**
245
+ * Every 2d context this app paints windows through. react-x11 caches one
246
+ * per window node (nodes.js), which is where ntk keeps `shapeStats`.
247
+ */
248
+ function contextsOf(app) {
249
+ return (app?._rootChildren ?? [])
250
+ .map((node) => node?._ctx)
251
+ .filter((ctx) => ctx?.shapeStats);
252
+ }
253
+
254
+ /** `{ hits, misses }` summed over an app's contexts; zeroes on older ntk. */
255
+ function readShapes(app) {
256
+ const total = { hits: 0, misses: {} };
257
+ for (const ctx of contextsOf(app)) {
258
+ const { hits = 0, misses = {} } = ctx.shapeStats;
259
+ total.hits += hits;
260
+ for (const [reason, count] of Object.entries(misses)) {
261
+ total.misses[reason] = (total.misses[reason] ?? 0) + count;
262
+ }
263
+ }
264
+ return total;
265
+ }
266
+
267
+ /**
268
+ * One human line for a `stats.shapes` tally, or null when nothing drew a
269
+ * rounded box at all. A bail-out is a silent perf cliff — a box that misses
270
+ * the fast path is rasterized as a polygon instead — so the reasons are
271
+ * spelled out rather than summed: `gradient`, `transform`, `clip-mask`,
272
+ * `fractional`, `dashes`, `radius-cap`, `composite-op`, `geometry`,
273
+ * `radii-mix`, `join`. docs/debugging.md explains what each one means.
274
+ */
275
+ export function formatShapes(shapes) {
276
+ if (!shapes) return null;
277
+ const missed = Object.entries(shapes.misses ?? {}).sort(
278
+ (a, b) => b[1] - a[1],
279
+ );
280
+ const total = missed.reduce((sum, [, count]) => sum + count, 0);
281
+ if (!shapes.hits && !total) return null;
282
+ const why = missed.map(([reason, count]) => `${reason} ${count}`).join(', ');
283
+ return (
284
+ `${shapes.hits} fast (glyph+rect), ${total} fell back` +
285
+ (total ? ` (${why})` : '')
286
+ );
287
+ }
288
+
289
+ /** b - a, clamped at zero: a context replaced mid-trace restarts at 0. */
290
+ function shapesDelta(a, b) {
291
+ const delta = { hits: Math.max(0, b.hits - a.hits), misses: {} };
292
+ for (const [reason, count] of Object.entries(b.misses)) {
293
+ const grew = count - (a.misses[reason] ?? 0);
294
+ if (grew > 0) delta.misses[reason] = grew;
295
+ }
296
+ return delta;
297
+ }
298
+
299
+ // --- the session --------------------------------------------------------
300
+
301
+ const usec = () => Math.round(performance.now() * 1000);
302
+
303
+ // A Chrome trace is buffered in memory until stop; this is the ceiling.
304
+ const MAX_TRACE_EVENTS = 1_000_000;
305
+
306
+ function createSession({ sink, path }) {
307
+ const stats = {
308
+ requests: 0,
309
+ bytesOut: 0,
310
+ replies: 0,
311
+ events: 0,
312
+ errors: 0,
313
+ bytesIn: 0,
314
+ /** decoded request name -> count, e.g. 'Render.CompositeGlyphs32' */
315
+ byOpcode: new Map(),
316
+ /**
317
+ * ntk's rounded-rect fast path (ntk >= 6.7.0): boxes drawn as cached
318
+ * corner glyphs + FillRectangles, and the ones that fell back to
319
+ * polygon rasterization, by reason. Zeroes when the toolkit predates
320
+ * it — see collectShapes.
321
+ */
322
+ shapes: { hits: 0, misses: {} },
323
+ };
324
+ /** app -> per-context shapeStats at attach time, so a trace started mid
325
+ * session reports its own window rather than the process's history */
326
+ const shapeBaselines = new Map();
327
+ const chrome = sink === 'chrome' ? [] : null;
328
+ let dropped = 0;
329
+ let frames = 0;
330
+ let commitStarted = 0;
331
+ let finished = false;
332
+
333
+ const record = (event) => {
334
+ if (!chrome) return;
335
+ if (chrome.length >= MAX_TRACE_EVENTS) {
336
+ dropped += 1;
337
+ return;
338
+ }
339
+ chrome.push(event);
340
+ };
341
+
342
+ const line = (text) => process.stderr.write(`${text}\n`);
343
+
344
+ return {
345
+ stats,
346
+
347
+ /**
348
+ * Take an app's fast-path counters as the zero point. Contexts are
349
+ * created lazily on first paint, so a window that has not painted yet
350
+ * simply has no baseline and counts in full.
351
+ */
352
+ watchApp(app) {
353
+ if (app) shapeBaselines.set(app, readShapes(app));
354
+ },
355
+
356
+ request(name, bytes) {
357
+ stats.requests += 1;
358
+ stats.bytesOut += bytes;
359
+ stats.byOpcode.set(name, (stats.byOpcode.get(name) ?? 0) + 1);
360
+ if (sink === 'requests') {
361
+ line(`x11 → ${name} ${bytes}B`);
362
+ }
363
+ record({
364
+ name,
365
+ ph: 'i',
366
+ s: 't',
367
+ ts: usec(),
368
+ pid: process.pid,
369
+ tid: 1,
370
+ cat: 'x11',
371
+ args: { bytes },
372
+ });
373
+ },
374
+
375
+ inbound(kind, bytes, info, X, extensions) {
376
+ stats.bytesIn += bytes;
377
+ if (kind === 'reply') stats.replies += 1;
378
+ else if (kind === 'event') stats.events += 1;
379
+ else {
380
+ stats.errors += 1;
381
+ const errorName =
382
+ named(CORE_ERRORS, info.errorCode) ?? `#${info.errorCode}`;
383
+ const request = opcodeName(extensions, info.major, info.minor);
384
+ const stack = stackForSeq(X, info.seq);
385
+ if (sink === 'requests') {
386
+ line(`x11 ← Error ${errorName} on ${request} (seq ${info.seq})`);
387
+ if (stack) line(stack.replace(/^Error\n?/, ''));
388
+ }
389
+ record({
390
+ name: `Error ${errorName} (${request})`,
391
+ ph: 'i',
392
+ s: 'p',
393
+ ts: usec(),
394
+ pid: process.pid,
395
+ tid: 1,
396
+ cat: 'x11,error',
397
+ args: { seq: info.seq, stack: stack ?? undefined },
398
+ });
399
+ }
400
+ },
401
+
402
+ frame({ rects, reasons, start, end, landed }) {
403
+ frames += 1;
404
+ const full = !rects;
405
+ const area = full
406
+ ? null
407
+ : rects.reduce((sum, r) => sum + r.width * r.height, 0);
408
+ if (sink === 'requests') {
409
+ const where = full
410
+ ? 'FULL WINDOW'
411
+ : rects.map((r) => `${r.width}x${r.height}@${r.x},${r.y}`).join(' ');
412
+ const why = reasons?.length ? ` reasons=${reasons.join('+')}` : '';
413
+ // How long the previous frame took to be answered — ntk's
414
+ // `frameLatency`, whose meaning follows the clock it is on. On the
415
+ // vertical-blank clock (ntk >= 7, the default) it is time-to-display
416
+ // and reads about one refresh period, so a 16ms figure on a 60Hz
417
+ // screen is the system working. On the fence clock it is a server
418
+ // round trip and sits under a millisecond locally, where a figure
419
+ // that grows with window area is a server-side bottleneck —
420
+ // software-fallback RENDER ops, a virtualized GPU — that client
421
+ // timings cannot show. Either way it is paint-vs-everything-else.
422
+ const wait =
423
+ typeof landed === 'number' ? ` landed=${landed.toFixed(1)}ms` : '';
424
+ line(`frame ${frames}: ${where}${why}${wait}`);
425
+ }
426
+ record({
427
+ name: 'frame',
428
+ ph: 'X',
429
+ ts: Math.round(start * 1000),
430
+ dur: Math.max(1, Math.round((end - start) * 1000)),
431
+ pid: process.pid,
432
+ tid: 1,
433
+ cat: 'react-x11',
434
+ args: {
435
+ full,
436
+ rects: rects?.length ?? 0,
437
+ area,
438
+ reasons: reasons ?? [],
439
+ landedMs: typeof landed === 'number' ? +landed.toFixed(2) : undefined,
440
+ },
441
+ });
442
+ },
443
+
444
+ commitStart() {
445
+ commitStarted = usec();
446
+ },
447
+
448
+ commitEnd() {
449
+ if (!commitStarted) return;
450
+ record({
451
+ name: 'commit',
452
+ ph: 'X',
453
+ ts: commitStarted,
454
+ dur: Math.max(1, usec() - commitStarted),
455
+ pid: process.pid,
456
+ tid: 1,
457
+ cat: 'react-x11',
458
+ });
459
+ commitStarted = 0;
460
+ },
461
+
462
+ finish() {
463
+ if (finished) return stats;
464
+ finished = true;
465
+ for (const [app, baseline] of shapeBaselines) {
466
+ const delta = shapesDelta(baseline, readShapes(app));
467
+ stats.shapes.hits += delta.hits;
468
+ for (const [reason, count] of Object.entries(delta.misses)) {
469
+ stats.shapes.misses[reason] =
470
+ (stats.shapes.misses[reason] ?? 0) + count;
471
+ }
472
+ }
473
+ if (sink === 'summary') {
474
+ const top = [...stats.byOpcode.entries()]
475
+ .sort((a, b) => b[1] - a[1])
476
+ .slice(0, 8);
477
+ const kb = (n) => `${(n / 1024).toFixed(1)}KB`;
478
+ line(
479
+ `react-x11 trace: ${stats.requests} requests (${kb(stats.bytesOut)} out), ` +
480
+ `${stats.replies} replies, ${stats.events} events, ` +
481
+ `${stats.errors} errors (${kb(stats.bytesIn)} in)`,
482
+ );
483
+ for (const [name, count] of top) {
484
+ line(` ${String(count).padStart(7)} ${name}`);
485
+ }
486
+ const shapeLine = formatShapes(stats.shapes);
487
+ if (shapeLine) line(` rounded boxes: ${shapeLine}`);
488
+ }
489
+ if (chrome) {
490
+ if (dropped) {
491
+ chrome.push({
492
+ name: `trace truncated: ${dropped} events dropped`,
493
+ ph: 'i',
494
+ s: 'g',
495
+ ts: usec(),
496
+ pid: process.pid,
497
+ tid: 1,
498
+ cat: 'react-x11',
499
+ });
500
+ }
501
+ writeFileSync(
502
+ path,
503
+ JSON.stringify({ traceEvents: chrome, displayTimeUnit: 'ms' }),
504
+ );
505
+ line(`react-x11 trace: wrote ${chrome.length} events to ${path}`);
506
+ }
507
+ return stats;
508
+ },
509
+ };
510
+ }
511
+
512
+ // --- attaching ----------------------------------------------------------
513
+
514
+ /**
515
+ * Attach the session's counters to one connection. Returns a detacher, or
516
+ * null when the connection has nothing to attach to (a mock app in tests).
517
+ */
518
+ function attachApp(app, session, { seq2stack }) {
519
+ const X = app?.X;
520
+ const packStream = X?.pack_stream;
521
+ if (!packStream || typeof packStream.put !== 'function') return null;
522
+ const extensions = extensionTable(app);
523
+ if (seq2stack) enableSeq2Stack(X);
524
+
525
+ let active = true;
526
+ const parseOut = requestParser((major, minor, len) => {
527
+ session.request(opcodeName(extensions, major, minor), len);
528
+ });
529
+ const parseIn = inboundParser((kind, bytes, info) => {
530
+ session.inbound(kind, bytes, info, X, extensions);
531
+ });
532
+
533
+ const origPut = packStream.put;
534
+ const tracedPut = function (...args) {
535
+ if (active && Buffer.isBuffer(args[0])) parseOut(args[0]);
536
+ return origPut.apply(this, args);
537
+ };
538
+ packStream.put = tracedPut;
539
+
540
+ const onData = (chunk) => {
541
+ if (active && Buffer.isBuffer(chunk)) parseIn(chunk);
542
+ };
543
+ const stream = X.stream;
544
+ if (typeof stream?.on === 'function') stream.on('data', onData);
545
+
546
+ return () => {
547
+ active = false;
548
+ // only unhook what is still ours — another trace may have wrapped on top
549
+ if (packStream.put === tracedPut) packStream.put = origPut;
550
+ if (typeof stream?.removeListener === 'function') {
551
+ stream.removeListener('data', onData);
552
+ }
553
+ };
554
+ }
555
+
556
+ // --- public API ---------------------------------------------------------
557
+
558
+ /**
559
+ * Start tracing. `sink` is 'summary' (default), 'requests' or 'chrome'
560
+ * (which needs `path`). With no `app` the trace follows every connection
561
+ * the renderer has open or opens later; pass one to trace it alone.
562
+ * `seq2stack: true` captures a JS stack per request so an asynchronous X
563
+ * error can name the call that sent it — an Error capture per request, so
564
+ * off by default.
565
+ *
566
+ * Returns `{ stats, stop }`; `stop()` detaches everything, flushes the
567
+ * sink and returns the totals.
568
+ */
569
+ export function startTrace(options = {}) {
570
+ const {
571
+ app,
572
+ sink = 'summary',
573
+ path = 'react-x11-trace.json',
574
+ seq2stack = false,
575
+ } = options;
576
+ if (!['summary', 'requests', 'chrome'].includes(sink)) {
577
+ throw new Error(
578
+ `react-x11/debug: unknown trace sink ${JSON.stringify(sink)} — ` +
579
+ "expected 'summary', 'requests' or 'chrome'.",
580
+ );
581
+ }
582
+ const session = createSession({ sink, path });
583
+ const detachers = [];
584
+ const attach = (a) => {
585
+ const detach = attachApp(a, session, { seq2stack });
586
+ if (detach) detachers.push(detach);
587
+ // independent of the pack-stream hook: the fast path is counted by the
588
+ // toolkit, not observed on the wire, so a mock app with no stream to
589
+ // wrap can still report it
590
+ session.watchApp(a);
591
+ };
592
+ const unsubscribe = app ? null : onApp(attach);
593
+ if (app) attach(app);
594
+
595
+ const frame = (info) => session.frame(info);
596
+ if (sink === 'requests' || sink === 'chrome') hooks.frame = frame;
597
+ const commitStart = () => session.commitStart();
598
+ const commitEnd = () => session.commitEnd();
599
+ if (sink === 'chrome') {
600
+ hooks.commitStart = commitStart;
601
+ hooks.commitEnd = commitEnd;
602
+ }
603
+
604
+ let stopped = false;
605
+ return {
606
+ stats: session.stats,
607
+ stop() {
608
+ if (stopped) return session.stats;
609
+ stopped = true;
610
+ unsubscribe?.();
611
+ for (const detach of detachers) detach();
612
+ if (hooks.frame === frame) hooks.frame = null;
613
+ if (hooks.commitStart === commitStart) hooks.commitStart = null;
614
+ if (hooks.commitEnd === commitEnd) hooks.commitEnd = null;
615
+ return session.finish();
616
+ },
617
+ };
618
+ }
619
+
620
+ /**
621
+ * The REACT_X11_TRACE entry point, called by the Reconciler at import time.
622
+ * Grammar: `summary`, `requests[+stacks]`, `chrome[+stacks]:<path>`.
623
+ * The trace runs until the process exits, then flushes its sink.
624
+ */
625
+ export function startEnvTrace(spec) {
626
+ const match = /^(summary|requests|chrome)(\+stacks)?(?::(.*))?$/.exec(spec);
627
+ if (!match) {
628
+ console.warn(
629
+ `react-x11: REACT_X11_TRACE=${JSON.stringify(spec)} not understood — ` +
630
+ "expected 'summary', 'requests[+stacks]' or 'chrome[+stacks]:<path>'.",
631
+ );
632
+ return null;
633
+ }
634
+ const [, sink, stacks, path] = match;
635
+ const trace = startTrace({
636
+ sink,
637
+ path: path || 'react-x11-trace.json',
638
+ seq2stack: Boolean(stacks),
639
+ });
640
+ process.on('exit', () => trace.stop());
641
+ return trace;
642
+ }