blob: 155e6834ddc05106d820358f42bbf736ed33229b [file]
/*
* 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';
})();