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.
- package/README.md +330 -81
- package/package.json +132 -14
- package/src/ClickToComponent.js +9 -2
- package/src/DevToolsIntegration.js +440 -32
- package/src/Reconciler.js +463 -165
- package/src/a11y.js +1299 -0
- package/src/acceleratorhooks.js +86 -0
- package/src/accelerators.js +128 -0
- package/src/activate.js +168 -0
- package/src/anchor.js +360 -0
- package/src/appcontext.js +161 -0
- package/src/appearance.js +748 -0
- package/src/appearancehooks.js +96 -0
- package/src/apphooks.js +73 -0
- package/src/application.js +748 -0
- package/src/atspi.js +1953 -0
- package/src/bus.js +545 -0
- package/src/bushooks.js +136 -0
- package/src/clientmessage.js +140 -0
- package/src/clipboard.js +121 -0
- package/src/components/Button.js +148 -33
- package/src/components/Checkbox.js +66 -32
- package/src/components/Dialog.js +96 -29
- package/src/components/FileDialog.js +491 -0
- package/src/components/Icon.js +329 -0
- package/src/components/Menu.js +797 -133
- package/src/components/PasswordInput.js +434 -0
- package/src/components/ProgressBar.js +91 -12
- package/src/components/Radio.js +78 -35
- package/src/components/Select.js +255 -65
- package/src/components/Slider.js +102 -41
- package/src/components/SplitPane.js +206 -0
- package/src/components/Switch.js +100 -22
- package/src/components/Table.js +529 -0
- package/src/components/Tabs.js +211 -0
- package/src/components/Tooltip.js +528 -54
- package/src/components/anchor.js +154 -107
- package/src/components/change.js +34 -0
- package/src/components/dnd.js +103 -0
- package/src/components/index.js +16 -3
- package/src/components/keys.js +17 -20
- package/src/components/scribble.js +247 -0
- package/src/components/theme.js +350 -42
- package/src/components/typeahead.js +12 -2
- package/src/compose.js +868 -0
- package/src/compositing.js +224 -0
- package/src/dbusmenu.js +384 -0
- package/src/debug.d.ts +63 -0
- package/src/debug.js +642 -0
- package/src/decorations.js +486 -0
- package/src/desktopsettings.js +209 -0
- package/src/desktopsettingshooks.js +62 -0
- package/src/dnd.js +1725 -0
- package/src/editmenu.js +272 -0
- package/src/errors.js +98 -0
- package/src/events.js +1406 -162
- package/src/extensions.js +45 -0
- package/src/filedialog.js +375 -0
- package/src/filedialoghooks.js +132 -0
- package/src/fonthooks.js +64 -0
- package/src/fonts.js +301 -0
- package/src/foreignnodes.js +519 -0
- package/src/frame/child.js +33 -0
- package/src/frame/childmain.js +274 -0
- package/src/frame/env.js +140 -0
- package/src/frame/index.js +444 -0
- package/src/frame/lifecycle.js +67 -0
- package/src/frame/protocol.js +179 -0
- package/src/frames.js +73 -0
- package/src/glbackend.js +114 -0
- package/src/glnodes.js +125 -67
- package/src/globalmenu.js +683 -0
- package/src/host.d.ts +88 -0
- package/src/host.js +42 -0
- package/src/idle.js +490 -0
- package/src/idlehooks.js +100 -0
- package/src/imagesource.js +349 -0
- package/src/index.d.ts +475 -0
- package/src/index.js +83 -9
- package/src/inputtime.js +206 -0
- package/src/jsx-dev-runtime.d.ts +21 -0
- package/src/jsx-dev-runtime.js +2 -0
- package/src/jsx-runtime.d.ts +39 -0
- package/src/jsx-runtime.js +6 -0
- package/src/keyboard.js +256 -0
- package/src/keyboardstate.js +278 -0
- package/src/keyboardstatehooks.js +58 -0
- package/src/keysyms.d.ts +149 -0
- package/src/keysyms.js +270 -0
- package/src/locale.js +170 -0
- package/src/localehooks.js +47 -0
- package/src/menuitem.js +223 -0
- package/src/node.d.ts +597 -0
- package/src/node.js +44 -0
- package/src/nodes.js +9546 -690
- package/src/ntk.d.ts +44 -0
- package/src/ntk.js +25 -0
- package/src/paintcache.js +366 -0
- package/src/palette.js +380 -0
- package/src/pastestate.js +66 -0
- package/src/portal.js +461 -0
- package/src/priority.js +26 -0
- package/src/refresh/index.d.ts +40 -0
- package/src/refresh/index.js +122 -0
- package/src/refresh/loader.d.ts +37 -0
- package/src/refresh/loader.js +401 -0
- package/src/refresh/register.d.ts +5 -0
- package/src/refresh/register.js +13 -0
- package/src/registry.js +232 -0
- package/src/scale.js +626 -0
- package/src/scalehooks.js +27 -0
- package/src/screencolor.js +640 -0
- package/src/screencolorhooks.js +101 -0
- package/src/screens.js +754 -0
- package/src/screenshooks.js +137 -0
- package/src/startup.js +302 -0
- package/src/style.d.ts +126 -0
- package/src/style.js +33 -0
- package/src/styles.js +1482 -6
- package/src/svgnodes.js +306 -0
- package/src/testing/a11y.js +484 -0
- package/src/testing/components.js +414 -0
- package/src/testing/events.js +407 -0
- package/src/testing/harness.js +455 -0
- package/src/testing/index.d.ts +558 -0
- package/src/testing/index.js +85 -0
- package/src/testing/mock-app.js +463 -0
- package/src/testing/pixels.js +152 -0
- package/src/testing/queries.js +224 -0
- package/src/textrange.js +83 -0
- package/src/textselection.js +439 -0
- package/src/trace-registry.js +63 -0
- package/src/transfer.js +93 -0
- package/src/types/appearance.d.ts +84 -0
- package/src/types/application.d.ts +173 -0
- package/src/types/components.d.ts +820 -0
- package/src/types/dbus.d.ts +177 -0
- package/src/types/elements.d.ts +897 -0
- package/src/types/events.d.ts +549 -0
- package/src/types/filedialog.d.ts +260 -0
- package/src/types/fonts.d.ts +124 -0
- package/src/types/frame.d.ts +146 -0
- package/src/types/globalmenu.d.ts +42 -0
- package/src/types/nodes.d.ts +199 -0
- package/src/types/screencolor.d.ts +84 -0
- package/src/types/style.d.ts +434 -0
- package/src/types/system.d.ts +287 -0
- package/src/windowid.js +151 -0
- package/src/windowstate.js +393 -0
- package/src/xsettings.js +336 -0
- package/src/yoga.d.ts +55 -0
- package/src/yoga.js +135 -0
- package/src/components/Canvas3D.js +0 -28
- package/src/geometry3d.js +0 -223
- package/src/pointer3d.js +0 -158
- package/src/raycast3d.js +0 -146
- package/src/richnodes.js +0 -436
- 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
|
+
}
|