源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s

This commit is contained in:
agent-radar
2026-09-01 19:05:14 +08:00
commit 7be535059e
3358 changed files with 1306357 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Renderer performance probes
Measuring what a session switch costs in the running Desktop app, over CDP. The
findings in #4109 were produced with these; they live here so the next
measurement is a command rather than a rebuild.
They are ad-hoc tools, not a benchmark suite: they attach to a dev app you
started, and they answer "how much" only for the interaction they drive.
## Running
Start the dev app with the debugger open:
```bash
npm run dev -w @maka/desktop -- --remote-debugging-port=9334
```
Then, with a populated sidebar (about 30 rows is what the #4109 numbers used):
```bash
node scripts/perf/session-switch-commits.mjs before
node scripts/perf/session-switch-busy-js.mjs
```
- `session-switch-commits.mjs` — React commits per switch, how many are
full-tree renders, and how many fibers actually re-rendered. Reloads the page
first, so `react-commit-probe.js` is in place before React boots.
- `session-switch-busy-js.mjs` — renderer busy JS per switch, with the top
self-time entries. It reports the running build, one configuration. A
before/after needs both configurations in one instance; see below.
- `react-commit-probe.js` — the in-page half: a minimal
`__REACT_DEVTOOLS_GLOBAL_HOOK__` plus a `Function.prototype.bind` wrapper that
catches React creating a `dispatchSetState`, so a commit can be attributed to
the `setState` that caused it.
- `cdp-client.mjs` — the protocol client and the row-clicking helper.
## The one rule
**Never compare numbers from two app launches.** Restarting the app shifts these
metrics by orders of magnitude, while the spread inside a single running
instance is small. An A/B means: one instance, a `globalThis` switch to select
the configuration, at least three repetitions per configuration, alternating,
compared pairwise. Two runs of "before" and "after" against two launches will
produce a confident number that means nothing.
The switch is a throwaway, not a shipped affordance. Branch a worktree, put a
`globalThis` flag in the one place that reads it — one that restores the old
behaviour when set, so the regressed configuration is reachable without
rebuilding the defect by hand — alternate the flag inside the running instance,
then delete the worktree. Measurement discipline is ours; product code carries
none of it. A flag that reaches `main` is a branch nothing executes and a
defect replica that has to be maintained against the code it replicates.
Two smaller ones that cost time to rediscover:
- Clicks must be `Input.dispatchMouseEvent`. A synthesised `element.click()`
does not activate an Astryx `SideNavItem`, so the app does nothing and the
measurement describes it.
- Disarm the commit probe (`__MAKA_PROBE__.arm = false`) before sampling. Its
fiber walk otherwise shows up in the profile as the app's own work;
`session-switch-busy-js.mjs` does this for you.
## What these do not tell you
The commit count is unweighted, and busy JS on this workload is spread across
about a thousand cheap fibers rather than concentrated in one component. A
change that lowers the commit count has not necessarily lowered the time, and a
change that moves the top self-time entry has not necessarily lowered anything.
Read both, and read the medians.
+155
View File
@@ -0,0 +1,155 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A Chrome DevTools Protocol client for the running dev app (#4109).
*
* Small on purpose: `/json/list` to find the renderer target, one WebSocket,
* `Runtime.evaluate` and `Input.dispatchMouseEvent`. Nothing here is specific
* to a measurement — the probes in this directory are.
*
* Clicks must go through `Input.dispatchMouseEvent`. A synthesised
* `element.click()` does not produce the event sequence Astryx's SideNavItem
* listens for, so the row never activates and the measurement silently
* describes an app that did nothing.
*/
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const WebSocket = require('ws');
export const DEFAULT_PORT = 9334;
export async function targets(port = DEFAULT_PORT) {
const response = await fetch(`http://127.0.0.1:${port}/json/list`);
return response.json();
}
export async function connectRenderer(port = DEFAULT_PORT, match = /index\.html|maka/i) {
const list = await targets(port);
const page = list.find(
(target) => target.type === 'page' && (match.test(target.url) || match.test(target.title)),
);
if (!page) {
const seen = list.map((target) => `${target.type} ${target.title} ${target.url}`).join('\n ');
throw new Error(`no renderer target on port ${port}. Saw:\n ${seen}`);
}
return connect(page.webSocketDebuggerUrl);
}
export function connect(url) {
const socket = new WebSocket(url, { perMessageDeflate: false, maxPayload: 512 * 1024 * 1024 });
let nextId = 0;
const pending = new Map();
const listeners = [];
const ready = new Promise((resolve, reject) => {
socket.once('open', resolve);
socket.once('error', reject);
});
socket.on('message', (data) => {
const message = JSON.parse(data.toString());
if (message.id === undefined) {
for (const listener of listeners) listener(message);
return;
}
const waiter = pending.get(message.id);
pending.delete(message.id);
if (!waiter) return;
if (message.error) waiter.reject(new Error(JSON.stringify(message.error)));
else waiter.resolve(message.result);
});
const send = (method, params = {}) =>
new Promise((resolve, reject) => {
const id = ++nextId;
pending.set(id, { resolve, reject });
socket.send(JSON.stringify({ id, method, params }));
});
const evaluate = async (expression, options = {}) => {
const result = await send('Runtime.evaluate', {
expression,
returnByValue: true,
awaitPromise: true,
...options,
});
if (result.exceptionDetails) {
throw new Error(
JSON.stringify(result.exceptionDetails.exception?.description ?? result.exceptionDetails),
);
}
return result.result.value;
};
return {
socket,
send,
evaluate,
on: (listener) => listeners.push(listener),
ready,
close: () => socket.close(),
};
}
export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
/**
* Click the nth Session row by real input events, at the centre of its box.
*
* Scoped to the rail and refused when the row is not on screen. A click at
* coordinates outside the viewport lands on whatever is there instead — one of
* them opened Settings mid-run and the measurement that followed described an
* app that was not showing the rail at all.
*/
export async function clickSessionRow(client, index) {
const box = JSON.parse(
await client.evaluate(
`(() => {
const rail = document.querySelector('nav.maka-session-panel');
if (!rail) throw new Error('the session rail is not mounted');
const row = [...rail.querySelectorAll('[data-session-id]')][${index}];
if (!row) throw new Error('no session row at index ${index}');
const b = row.getBoundingClientRect();
const x = b.x + b.width / 2;
const y = b.y + b.height / 2;
if (x < 0 || y < 0 || x > innerWidth || y > innerHeight) {
throw new Error('session row ${index} is off screen (' + x + ',' + y + ')');
}
return JSON.stringify({ x, y });
})()`,
),
);
const point = {
x: Math.round(box.x),
y: Math.round(box.y),
button: 'left',
clickCount: 1,
buttons: 1,
};
await client.send('Input.dispatchMouseEvent', { type: 'mouseMoved', ...point, buttons: 0 });
await client.send('Input.dispatchMouseEvent', { type: 'mousePressed', ...point });
await client.send('Input.dispatchMouseEvent', { type: 'mouseReleased', ...point, buttons: 0 });
}
export function median(values) {
const sorted = [...values].sort((a, b) => a - b);
return sorted[Math.floor(sorted.length / 2)];
}
+191
View File
@@ -0,0 +1,191 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Counts React commits, rendered fibers, and the `setState` calls that caused
* them, from inside the renderer (#4109).
*
* Injected with `Page.addScriptToEvaluateOnNewDocument` before the app loads,
* because both halves have to be in place first:
*
* 1. `__REACT_DEVTOOLS_GLOBAL_HOOK__` must exist before `react-dom` looks for
* it, or no commit is ever reported. This is a minimal stand-in for the
* DevTools hook — enough of the shape that react-dom injects into it.
*
* 2. `Function.prototype.bind` is wrapped to catch React creating a state
* dispatcher. React binds `dispatchSetState` as `(null, fiber, queue)`,
* and the queue is the only object in the runtime carrying
* `lastRenderedReducer`, so that triple identifies it with no version
* coupling to react-dom's internals.
*
* A fiber counts as rendered when it carries `Placement` (`flags & 1`) and its
* alternate was last seen in the previous commit. That is identity-insensitive:
* it reports what React actually re-rendered, not what a `memo` comparator was
* asked about.
*
* `arm` gates recording so a measurement covers one interaction rather than the
* app's whole life. Set `__MAKA_PROBE__.arm = false` before profiling with the
* sampler, or the probe's own walk lands in the profile.
*/
(() => {
if (globalThis.__MAKA_PROBE__) return 'already';
const probe = {
commits: [],
dispatches: [],
arm: false,
roots: new Set(),
rootIdx: new WeakMap(),
qid: 0,
qlabel: new Map(),
};
globalThis.__MAKA_PROBE__ = probe;
const originalBind = Function.prototype.bind;
Function.prototype.bind = function bind(...args) {
const bound = originalBind.apply(this, args);
try {
const queue = args[2];
if (
args.length === 3 &&
args[0] === null &&
queue &&
typeof queue === 'object' &&
'lastRenderedReducer' in queue &&
!queue.__qid
) {
queue.__qid = ++probe.qid;
const qid = queue.__qid;
return function dispatchWithRecord(action) {
if (probe.arm && probe.dispatches.length < 500) {
probe.dispatches.push({
t: performance.now(),
qid,
prev: brief(queue.lastRenderedState),
action: brief(action),
st: (new Error().stack || '')
.split('\n')
.slice(2, 8)
.map((line) => line.trim().slice(0, 130)),
});
}
return bound.apply(this, arguments);
};
}
} catch {
// A bind we cannot inspect is a bind we do not care about.
}
return bound;
};
function brief(value) {
try {
if (value == null) return String(value);
if (typeof value === 'function') return 'fn';
if (typeof value !== 'object') return String(value).slice(0, 40);
if (Array.isArray(value)) return `Arr${value.length}`;
return `{${Object.keys(value).slice(0, 6)}}`;
} catch {
return '?';
}
}
const nameOf = (fiber) => {
const type = fiber.type ?? fiber.elementType;
if (typeof type === 'string') return type;
if (typeof type === 'function') return type.displayName || type.name || 'anon';
if (type && typeof type === 'object') {
const inner = type.type ?? type.render;
return (
type.displayName ||
(typeof inner === 'function' ? inner.displayName || inner.name : null) ||
'obj'
);
}
return `tag${fiber.tag}`;
};
const hook = {
renderers: new Map(),
supportsFiber: true,
_nextId: 1,
inject(renderer) {
const id = hook._nextId++;
hook.renderers.set(id, renderer);
return id;
},
checkDCE() {},
onScheduleFiberRoot() {},
onCommitFiberUnmount() {},
onPostCommitFiberRoot() {},
setStrictMode() {},
getFiberRoots() {
return probe.roots;
},
on() {},
off() {},
sub() {
return () => {};
},
emit() {},
onCommitFiberRoot(_id, root) {
probe.roots.add(root);
const idx = (probe.rootIdx.get(root) || 0) + 1;
probe.rootIdx.set(root, idx);
const key = '__pc';
let total = 0;
let visited = 0;
const renderRoots = [];
const stack = [[root.current, 0, false]];
while (stack.length) {
const [fiber, depth, hasRenderedAncestor] = stack.pop();
if (!fiber || visited++ > 90000) break;
const alternate = fiber.alternate;
const rendered = fiber.flags & 1 && alternate && alternate[key] === idx - 1;
if (rendered) total++;
let ancestor = hasRenderedAncestor;
if (rendered && !hasRenderedAncestor && probe.arm) {
ancestor = true;
renderRoots.push({ d: depth, name: nameOf(fiber) });
}
let state = fiber.memoizedState;
let i = 0;
while (state && i < 1200) {
if (state.queue?.__qid && !probe.qlabel.has(state.queue.__qid)) {
probe.qlabel.set(state.queue.__qid, `${nameOf(fiber)}@d${depth}#h${i}`);
}
state = state.next;
i++;
}
fiber[key] = idx;
if (fiber.child) stack.push([fiber.child, depth + 1, ancestor]);
if (fiber.sibling) stack.push([fiber.sibling, depth, hasRenderedAncestor]);
}
if (probe.arm && probe.commits.length < 300) {
renderRoots.sort((a, b) => a.d - b.d);
probe.commits.push({ t: performance.now(), idx, total, rr: renderRoots.slice(0, 3) });
}
},
};
Object.defineProperty(globalThis, '__REACT_DEVTOOLS_GLOBAL_HOOK__', {
value: hook,
writable: false,
configurable: true,
});
return 'installed';
})();
+102
View File
@@ -0,0 +1,102 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Renderer busy JS per session switch, with the top self-time entries (#4109).
*
* Usage, against a running dev app with `--remote-debugging-port=9334`:
*
* node scripts/perf/session-switch-busy-js.mjs [port] [trials]
*
* This reports one configuration: whatever the running build does. A before/
* after needs both configurations reachable from ONE running instance — see the
* README for how to get that from a throwaway worktree instead of from a switch
* carried in product code.
*
* Sampling at 100µs. Busy time is every sample that is not `(idle)` or
* `(program)`; the top-8 self-time list is there to show WHERE the time is, and
* on this workload it is deliberately flat — about a thousand cheap fibers, no
* hot component. A change that only moves the top entry around has not moved
* the total.
*
* The same discipline as the commit counter: alternate configurations inside
* ONE running instance and compare paired trials. Numbers from two launches are
* not comparable.
*/
import { clickSessionRow, connectRenderer, median, sleep, DEFAULT_PORT } from './cdp-client.mjs';
const positional = process.argv.slice(2).filter((arg) => !arg.startsWith('--'));
const port = Number(positional[0] ?? DEFAULT_PORT);
const trials = Number(positional[1] ?? 5);
const SETTLE_MS = 2000;
const client = await connectRenderer(port);
await client.ready;
await client.send('Profiler.enable');
// The commit probe walks the fiber tree on every commit. Left armed it lands in
// the profile as the app's own work.
await client.evaluate('globalThis.__MAKA_PROBE__ && (__MAKA_PROBE__.arm = false)').catch(() => {});
async function measureSwitch(row) {
await client.send('Profiler.setSamplingInterval', { interval: 100 });
await client.send('Profiler.start');
await clickSessionRow(client, row);
await sleep(SETTLE_MS);
const { profile } = await client.send('Profiler.stop');
const byId = new Map(profile.nodes.map((node) => [node.id, node]));
const selfTime = new Map();
const deltas = profile.timeDeltas ?? [];
let busy = 0;
let idle = 0;
for (let i = 0; i < profile.samples.length; i++) {
const node = byId.get(profile.samples[i]);
const dt = (deltas[i] ?? 0) / 1000;
const name = node?.callFrame?.functionName || '(anonymous)';
if (name === '(idle)' || name === '(program)') {
idle += dt;
continue;
}
busy += dt;
const file = (node?.callFrame?.url || '').split('/').pop() || '';
const key = `${name} @${file}:${node?.callFrame?.lineNumber}`;
selfTime.set(key, (selfTime.get(key) ?? 0) + dt);
}
return { busy, idle, selfTime };
}
function printTop(selfTime) {
for (const [key, value] of [...selfTime.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8)) {
console.log(` ${value.toFixed(0)}ms ${key.slice(0, 110)}`);
}
}
const busyPerSwitch = [];
for (let trial = 0; trial < trials; trial++) {
const row = 2 + trial;
const { busy, idle, selfTime } = await measureSwitch(row);
busyPerSwitch.push(busy);
console.log(`row ${row}: busyJS=${busy.toFixed(0)}ms idle=${idle.toFixed(0)}ms`);
printTop(selfTime);
}
console.log(`\nmedian busy JS per switch = ${median(busyPerSwitch).toFixed(0)}ms`);
client.close();
+96
View File
@@ -0,0 +1,96 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* How many React commits one session switch costs, and how much of each is a
* full-tree render (#4109).
*
* Usage, against a running dev app with `--remote-debugging-port=9334`:
*
* node scripts/perf/session-switch-commits.mjs <label> [port]
*
* Reloads the page to install `react-commit-probe.js` before React boots, then
* switches sessions several times and reports the median. Read the medians, not
* a single row: the first switch after a reload pays for caches the rest do
* not, which is why row 1 is a warm-up and is excluded.
*
* NEVER compare two numbers from two app launches. Restarting the app moves
* these metrics by an order of magnitude while the spread inside one instance
* is small; an A/B has to alternate configurations inside one running instance
* and compare paired trials.
*/
import { readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { clickSessionRow, connectRenderer, median, sleep, DEFAULT_PORT } from './cdp-client.mjs';
const here = fileURLToPath(new URL('.', import.meta.url));
const label = process.argv[2] ?? 'run';
const port = Number(process.argv[3] ?? DEFAULT_PORT);
/** A full-tree render: React re-rendered essentially everything under the root. */
const FULL_TREE_FIBERS = 1000;
const SETTLE_MS = 2500;
const BOOT_MS = 9000;
const ROWS = [2, 3, 4, 5, 6, 7, 8, 9];
const client = await connectRenderer(port);
await client.ready;
await client.send('Page.enable');
await client.send('Page.addScriptToEvaluateOnNewDocument', {
source: readFileSync(join(here, 'react-commit-probe.js'), 'utf8'),
});
await client.send('Page.reload');
await sleep(BOOT_MS);
await clickSessionRow(client, 1);
await sleep(3000);
const results = [];
for (const row of ROWS) {
await client.evaluate(
'__MAKA_PROBE__.commits.length = 0; __MAKA_PROBE__.dispatches.length = 0; __MAKA_PROBE__.arm = true',
);
await clickSessionRow(client, row);
await sleep(SETTLE_MS);
await client.evaluate('__MAKA_PROBE__.arm = false');
const { commits } = JSON.parse(
await client.evaluate('JSON.stringify({ commits: __MAKA_PROBE__.commits })'),
);
const fullTree = commits.filter((commit) => commit.total > FULL_TREE_FIBERS);
const renderRoots = {};
for (const commit of fullTree) {
const name = commit.rr[0]?.name ?? '?';
renderRoots[name] = (renderRoots[name] ?? 0) + 1;
}
const rendered = commits.reduce((sum, commit) => sum + commit.total, 0);
results.push({ row, commits: commits.length, fullTree: fullTree.length, renderRoots, rendered });
console.log(
`row ${row}: commits=${commits.length} fullTree=${fullTree.length} renderedFibers=${rendered} roots=${JSON.stringify(renderRoots)}`,
);
}
console.log(
`\n[${label}] median commits=${median(results.map((r) => r.commits))} ` +
`fullTree=${median(results.map((r) => r.fullTree))} ` +
`renderedFibers=${median(results.map((r) => r.rendered))}`,
);
writeFileSync(`session-switch-commits-${label}.json`, `${JSON.stringify(results, null, 1)}\n`);
client.close();