@buoy-gg/js-top 7.0.29 → 7.0.31
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/package.json +5 -6
- package/src/index.tsx +0 -69
- package/src/js-top/components/BusySparkline.tsx +0 -59
- package/src/js-top/components/JsTopHudOverlay.tsx +0 -78
- package/src/js-top/components/JsTopModal.tsx +0 -426
- package/src/js-top/components/TaskDetailView.tsx +0 -350
- package/src/js-top/components/TaskTable.tsx +0 -365
- package/src/js-top/engine/JsTopController.ts +0 -401
- package/src/js-top/engine/__tests__/engine.test.ts +0 -337
- package/src/js-top/engine/aggregator.ts +0 -148
- package/src/js-top/engine/busyProbe.ts +0 -100
- package/src/js-top/engine/frameStack.ts +0 -142
- package/src/js-top/engine/longtasks.ts +0 -137
- package/src/js-top/engine/now.ts +0 -33
- package/src/js-top/engine/originRegistry.ts +0 -495
- package/src/js-top/engine/patches/bridge.ts +0 -69
- package/src/js-top/engine/patches/microtasks.ts +0 -25
- package/src/js-top/engine/patches/promises.ts +0 -57
- package/src/js-top/engine/patches/timers.ts +0 -80
- package/src/js-top/engine/state.ts +0 -66
- package/src/js-top/engine/symbolicate.ts +0 -109
- package/src/js-top/sync/jsTopSyncAdapter.ts +0 -72
- package/src/js-top/types/index.ts +0 -144
- package/src/js-top/utils/preferences.ts +0 -35
- package/src/preset.tsx +0 -67
|
@@ -1,495 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Origin registry — the "process table". An origin is the identity of a task
|
|
3
|
-
* scheduling site.
|
|
4
|
-
*
|
|
5
|
-
* Two-stage identity keeps the hot path free of stack captures:
|
|
6
|
-
* - Cheap key `${api}|${fnName}` — computed on every schedule, just string
|
|
7
|
-
* concat + Map lookup.
|
|
8
|
-
* - Once an origin gets hot (PROMOTE_MIN_CALLS schedules or
|
|
9
|
-
* PROMOTE_MIN_TOTAL_MS booked), the next schedule captures ONE
|
|
10
|
-
* `Error().stack` (throttled per-key and globally) and refines the key
|
|
11
|
-
* with the scheduling site's caller frame. Hermes dev stacks reference
|
|
12
|
-
* bundle offsets, not source files — the CALLER FUNCTION NAME is the
|
|
13
|
-
* readable part, so labels read `setInterval ← startPolling`; raw frames
|
|
14
|
-
* are kept on the origin for future desktop symbolication.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
MAX_ORIGINS,
|
|
19
|
-
PROMOTE_MIN_CALLS,
|
|
20
|
-
PROMOTE_MIN_TOTAL_MS,
|
|
21
|
-
WINDOW_BUCKETS,
|
|
22
|
-
HISTORY_BUCKETS,
|
|
23
|
-
BUCKET_MS,
|
|
24
|
-
type JsTopRow,
|
|
25
|
-
type JsTopOriginDetail,
|
|
26
|
-
type OriginStackFrame,
|
|
27
|
-
type TaskApi,
|
|
28
|
-
} from "../types";
|
|
29
|
-
import { bookAttributedRange, epochOf } from "./aggregator";
|
|
30
|
-
import { buoyJsTopSymbolicateOrigin } from "./symbolicate";
|
|
31
|
-
|
|
32
|
-
const STACK_CAPTURE_MIN_INTERVAL_PER_KEY_MS = 500;
|
|
33
|
-
const STACK_CAPTURES_PER_SEC_GLOBAL = 20;
|
|
34
|
-
|
|
35
|
-
export interface Origin {
|
|
36
|
-
key: string;
|
|
37
|
-
api: TaskApi;
|
|
38
|
-
label: string;
|
|
39
|
-
/** Caller function name from the scheduling stack, once refined. */
|
|
40
|
-
caller?: string;
|
|
41
|
-
file?: string;
|
|
42
|
-
line?: number;
|
|
43
|
-
frames?: OriginStackFrame[];
|
|
44
|
-
calls: number;
|
|
45
|
-
totalMs: number;
|
|
46
|
-
maxMs: number;
|
|
47
|
-
scheduleCount: number;
|
|
48
|
-
lastSeenAt: number;
|
|
49
|
-
promoted: boolean;
|
|
50
|
-
/** On cheap origins: the refined key new work currently attributes to. */
|
|
51
|
-
activeRefinedKey?: string;
|
|
52
|
-
lastCaptureAt: number;
|
|
53
|
-
/** Dev-only Metro symbolication attempted (one shot per origin). */
|
|
54
|
-
symbolicated?: boolean;
|
|
55
|
-
ring: Float64Array;
|
|
56
|
-
ringStamps: Float64Array;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const origins = new Map<string, Origin>();
|
|
60
|
-
|
|
61
|
-
/** Pinned bucket that absorbs evicted cold origins. */
|
|
62
|
-
export const OTHER_KEY = "other";
|
|
63
|
-
|
|
64
|
-
let globalCaptureWindowStart = 0;
|
|
65
|
-
let globalCapturesInWindow = 0;
|
|
66
|
-
|
|
67
|
-
function makeOrigin(key: string, api: TaskApi, label: string): Origin {
|
|
68
|
-
return {
|
|
69
|
-
key,
|
|
70
|
-
api,
|
|
71
|
-
label,
|
|
72
|
-
calls: 0,
|
|
73
|
-
totalMs: 0,
|
|
74
|
-
maxMs: 0,
|
|
75
|
-
scheduleCount: 0,
|
|
76
|
-
lastSeenAt: 0,
|
|
77
|
-
promoted: false,
|
|
78
|
-
lastCaptureAt: 0,
|
|
79
|
-
ring: new Float64Array(WINDOW_BUCKETS),
|
|
80
|
-
ringStamps: new Float64Array(WINDOW_BUCKETS).fill(-1),
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function windowMsOf(o: Origin, nowT: number): number {
|
|
85
|
-
const current = epochOf(nowT);
|
|
86
|
-
let sum = 0;
|
|
87
|
-
for (let e = current - WINDOW_BUCKETS + 1; e <= current; e++) {
|
|
88
|
-
const i = e % WINDOW_BUCKETS;
|
|
89
|
-
if (i >= 0 && o.ringStamps[i] === e) sum += o.ring[i];
|
|
90
|
-
}
|
|
91
|
-
return sum;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function evictColdest(nowT: number): void {
|
|
95
|
-
let coldest: Origin | null = null;
|
|
96
|
-
let coldestScore = Infinity;
|
|
97
|
-
for (const o of origins.values()) {
|
|
98
|
-
if (o.key === OTHER_KEY) continue;
|
|
99
|
-
// Rank by recent cost, tiebreak by staleness.
|
|
100
|
-
const score = windowMsOf(o, nowT) * 1e6 + o.lastSeenAt;
|
|
101
|
-
if (score < coldestScore) {
|
|
102
|
-
coldestScore = score;
|
|
103
|
-
coldest = o;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (!coldest) return;
|
|
107
|
-
const other = getOrCreate(OTHER_KEY, "other", "other (evicted origins)");
|
|
108
|
-
other.calls += coldest.calls;
|
|
109
|
-
other.totalMs += coldest.totalMs;
|
|
110
|
-
other.maxMs = Math.max(other.maxMs, coldest.maxMs);
|
|
111
|
-
other.lastSeenAt = Math.max(other.lastSeenAt, coldest.lastSeenAt);
|
|
112
|
-
origins.delete(coldest.key);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function getOrCreate(key: string, api: TaskApi, label: string): Origin {
|
|
116
|
-
let o = origins.get(key);
|
|
117
|
-
if (!o) {
|
|
118
|
-
o = makeOrigin(key, api, label);
|
|
119
|
-
origins.set(key, o);
|
|
120
|
-
}
|
|
121
|
-
return o;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Parse a Hermes stack into frames, skipping engine/wrapper frames. Engine
|
|
126
|
-
* functions are deliberately named with a `buoyJsTop` prefix so they are
|
|
127
|
-
* identifiable here even in bundle-offset stacks.
|
|
128
|
-
*/
|
|
129
|
-
export function parseStack(stack: string): {
|
|
130
|
-
caller?: string;
|
|
131
|
-
frames: OriginStackFrame[];
|
|
132
|
-
} {
|
|
133
|
-
const frames: OriginStackFrame[] = [];
|
|
134
|
-
let caller: string | undefined;
|
|
135
|
-
const lines = stack.split("\n");
|
|
136
|
-
for (const lineText of lines) {
|
|
137
|
-
const m =
|
|
138
|
-
/at\s+(.*?)\s+\((?:address at\s+)?(.*?):(\d+):(\d+)\)/.exec(lineText) ??
|
|
139
|
-
/at\s+()(.*?):(\d+):(\d+)$/.exec(lineText.trim());
|
|
140
|
-
if (!m) continue;
|
|
141
|
-
const fnName = m[1] ?? "";
|
|
142
|
-
if (
|
|
143
|
-
fnName.includes("buoyJsTop") ||
|
|
144
|
-
fnName === "Error" ||
|
|
145
|
-
m[2]?.includes("/js-top/")
|
|
146
|
-
) {
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
frames.push({
|
|
150
|
-
file: m[2] ?? "",
|
|
151
|
-
line: Number(m[3]),
|
|
152
|
-
col: Number(m[4]),
|
|
153
|
-
});
|
|
154
|
-
// React splices internal frame names around effect callbacks
|
|
155
|
-
// (react_stack_bottom_frame etc.) — their OFFSETS map to user code, so
|
|
156
|
-
// keep the frame, but never present them as the "caller".
|
|
157
|
-
const isReactInternal =
|
|
158
|
-
/^(react_stack_bottom_frame|runWithFiberInDEV|commitHookEffectList|flushPassive|performWork|renderWithHooks|safelyCall)/.test(
|
|
159
|
-
fnName,
|
|
160
|
-
) ||
|
|
161
|
-
// Promise/polyfill machinery — technically the caller, never the answer.
|
|
162
|
-
/^(then|catch|finally|resolve|reject|fulfilled|rejected|step|tryCallOne|tryCallTwo|tryCatch|invokeCallback|notify)$/.test(
|
|
163
|
-
fnName,
|
|
164
|
-
);
|
|
165
|
-
if (!caller && fnName && fnName !== "anonymous" && !isReactInternal) {
|
|
166
|
-
caller = fnName;
|
|
167
|
-
}
|
|
168
|
-
if (frames.length >= 3) break;
|
|
169
|
-
}
|
|
170
|
-
return { caller, frames };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function allowCapture(o: Origin, nowT: number): boolean {
|
|
174
|
-
if (nowT - o.lastCaptureAt < STACK_CAPTURE_MIN_INTERVAL_PER_KEY_MS) {
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
if (nowT - globalCaptureWindowStart > 1000) {
|
|
178
|
-
globalCaptureWindowStart = nowT;
|
|
179
|
-
globalCapturesInWindow = 0;
|
|
180
|
-
}
|
|
181
|
-
if (globalCapturesInWindow >= STACK_CAPTURES_PER_SEC_GLOBAL) return false;
|
|
182
|
-
globalCapturesInWindow++;
|
|
183
|
-
o.lastCaptureAt = nowT;
|
|
184
|
-
return true;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function shortFile(file: string): string | undefined {
|
|
188
|
-
if (!file) return undefined;
|
|
189
|
-
// Strip scheme://host:port/ and query — keeps the bundle-relative tail.
|
|
190
|
-
// Metro dev URLs carry params after "//&" instead of "?" — strip both.
|
|
191
|
-
const noQuery = (file.split("?")[0] ?? file).replace(/\/\/&.*$/, "");
|
|
192
|
-
const m = /^[a-z]+:\/\/[^/]+\/(.*)$/.exec(noQuery);
|
|
193
|
-
const tail = m ? m[1] : noQuery;
|
|
194
|
-
const parts = tail.split("/");
|
|
195
|
-
return parts.slice(-2).join("/");
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Resolve the origin key for a schedule. Called from patched globals at
|
|
200
|
-
* schedule time — this is the hot path; the common case is one Map get and
|
|
201
|
-
* two counter bumps.
|
|
202
|
-
*/
|
|
203
|
-
export function buoyJsTopOriginExists(key: string): boolean {
|
|
204
|
-
return origins.has(key);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export function buoyJsTopScheduleOrigin(
|
|
208
|
-
api: TaskApi,
|
|
209
|
-
fn: { name?: string; displayName?: string } | ((...args: never[]) => unknown),
|
|
210
|
-
nowT: number,
|
|
211
|
-
/**
|
|
212
|
-
* False when identity is resolved at FIRE time (callback scheduled while
|
|
213
|
-
* the engine was off, or stats cleared since): a runtime stack is not a
|
|
214
|
-
* scheduling site, so stack capture is skipped on that path.
|
|
215
|
-
*/
|
|
216
|
-
allowStackCapture = true,
|
|
217
|
-
): string {
|
|
218
|
-
const fnLike = fn as { name?: string; displayName?: string };
|
|
219
|
-
const name =
|
|
220
|
-
(typeof fnLike.displayName === "string" && fnLike.displayName) ||
|
|
221
|
-
(typeof fnLike.name === "string" && fnLike.name) ||
|
|
222
|
-
"anonymous";
|
|
223
|
-
const cheapKey = `${api}|${name}`;
|
|
224
|
-
let o = origins.get(cheapKey);
|
|
225
|
-
if (!o) {
|
|
226
|
-
if (origins.size >= MAX_ORIGINS) evictColdest(nowT);
|
|
227
|
-
o = makeOrigin(
|
|
228
|
-
cheapKey,
|
|
229
|
-
api,
|
|
230
|
-
name === "anonymous" ? apiLabel(api) : `${apiLabel(api)} ${name}`,
|
|
231
|
-
);
|
|
232
|
-
origins.set(cheapKey, o);
|
|
233
|
-
}
|
|
234
|
-
o.scheduleCount++;
|
|
235
|
-
if (
|
|
236
|
-
!o.promoted &&
|
|
237
|
-
(o.scheduleCount >= PROMOTE_MIN_CALLS || o.totalMs >= PROMOTE_MIN_TOTAL_MS)
|
|
238
|
-
) {
|
|
239
|
-
o.promoted = true;
|
|
240
|
-
}
|
|
241
|
-
// Intervals schedule exactly ONCE — this call is the only chance to see
|
|
242
|
-
// their scheduling stack, so capture eagerly instead of waiting for
|
|
243
|
-
// promotion (which is driven by schedule counts they'll never accrue).
|
|
244
|
-
if (
|
|
245
|
-
(o.promoted || api === "interval") &&
|
|
246
|
-
allowStackCapture &&
|
|
247
|
-
allowCapture(o, nowT)
|
|
248
|
-
) {
|
|
249
|
-
const { caller, frames } = parseStack(new Error().stack ?? "");
|
|
250
|
-
const top = frames[0];
|
|
251
|
-
if (top || caller) {
|
|
252
|
-
// Keys stay short + token-cheap: bundle URLs are stripped down to the
|
|
253
|
-
// trailing path segment (full frames live on the origin record).
|
|
254
|
-
const site = top ? `${shortFile(top.file) ?? "?"}:${top.line}` : "?";
|
|
255
|
-
const refinedKey = `${cheapKey}@${caller ?? "?"}:${site}`;
|
|
256
|
-
let refined = origins.get(refinedKey);
|
|
257
|
-
if (!refined) {
|
|
258
|
-
if (origins.size >= MAX_ORIGINS) evictColdest(nowT);
|
|
259
|
-
// Label rule: the function name (when it exists) is the anchor and
|
|
260
|
-
// is never discarded; a readable caller or site is appended.
|
|
261
|
-
const base =
|
|
262
|
-
name === "anonymous" ? apiLabel(api) : `${apiLabel(api)} ${name}`;
|
|
263
|
-
refined = makeOrigin(
|
|
264
|
-
refinedKey,
|
|
265
|
-
api,
|
|
266
|
-
caller
|
|
267
|
-
? `${base} ← ${caller}`
|
|
268
|
-
: name === "anonymous"
|
|
269
|
-
? `${base} @ ${shortFile(top?.file ?? "") ?? "?"}`
|
|
270
|
-
: base,
|
|
271
|
-
);
|
|
272
|
-
refined.caller = caller;
|
|
273
|
-
refined.file = top ? shortFile(top.file) : undefined;
|
|
274
|
-
refined.line = top?.line;
|
|
275
|
-
refined.frames = frames;
|
|
276
|
-
refined.promoted = true;
|
|
277
|
-
// Migrate the cheap origin's accumulated stats into the refined one
|
|
278
|
-
// and zero the cheap record — the table must never show the same
|
|
279
|
-
// scheduling site as two rows (pre- and post-refinement).
|
|
280
|
-
refined.calls = o.calls;
|
|
281
|
-
refined.totalMs = o.totalMs;
|
|
282
|
-
refined.maxMs = o.maxMs;
|
|
283
|
-
refined.scheduleCount = o.scheduleCount;
|
|
284
|
-
refined.lastSeenAt = o.lastSeenAt;
|
|
285
|
-
refined.ring.set(o.ring);
|
|
286
|
-
refined.ringStamps.set(o.ringStamps);
|
|
287
|
-
o.calls = 0;
|
|
288
|
-
o.totalMs = 0;
|
|
289
|
-
o.maxMs = 0;
|
|
290
|
-
o.ring.fill(0);
|
|
291
|
-
o.ringStamps.fill(-1);
|
|
292
|
-
origins.set(refinedKey, refined);
|
|
293
|
-
// Upgrade the label to real source coordinates when Metro is around.
|
|
294
|
-
buoyJsTopSymbolicateOrigin(refined, apiLabel(api));
|
|
295
|
-
}
|
|
296
|
-
o.activeRefinedKey = refinedKey;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
return o.activeRefinedKey ?? cheapKey;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function apiLabel(api: TaskApi): string {
|
|
303
|
-
switch (api) {
|
|
304
|
-
case "timeout":
|
|
305
|
-
return "setTimeout";
|
|
306
|
-
case "interval":
|
|
307
|
-
return "setInterval";
|
|
308
|
-
case "immediate":
|
|
309
|
-
return "setImmediate";
|
|
310
|
-
case "raf":
|
|
311
|
-
return "requestAnimationFrame";
|
|
312
|
-
case "microtask":
|
|
313
|
-
return "queueMicrotask";
|
|
314
|
-
case "then":
|
|
315
|
-
return "Promise.then";
|
|
316
|
-
case "bridge":
|
|
317
|
-
return "bridge";
|
|
318
|
-
default:
|
|
319
|
-
return api;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/** Book a completed call's exclusive time onto its origin + global buckets. */
|
|
324
|
-
export function buoyJsTopBookExclusive(
|
|
325
|
-
originKey: string,
|
|
326
|
-
exclusiveMs: number,
|
|
327
|
-
start: number,
|
|
328
|
-
end: number,
|
|
329
|
-
): void {
|
|
330
|
-
const o = origins.get(originKey);
|
|
331
|
-
if (!o) return;
|
|
332
|
-
o.calls++;
|
|
333
|
-
o.totalMs += exclusiveMs;
|
|
334
|
-
if (exclusiveMs > o.maxMs) o.maxMs = exclusiveMs;
|
|
335
|
-
o.lastSeenAt = end;
|
|
336
|
-
// Promotion by accumulated cost must ALSO happen at book time: an
|
|
337
|
-
// interval never re-schedules, so the schedule-time check alone would
|
|
338
|
-
// leave a 1000ms/run interval "not hot" forever.
|
|
339
|
-
if (!o.promoted && o.totalMs >= PROMOTE_MIN_TOTAL_MS) o.promoted = true;
|
|
340
|
-
// Per-origin ring, spread across the call's span like the global buckets.
|
|
341
|
-
const span = end - start;
|
|
342
|
-
const ratio = span > 0 ? Math.min(1, exclusiveMs / span) : 1;
|
|
343
|
-
const clampedStart = Math.max(start, end - WINDOW_BUCKETS * BUCKET_MS);
|
|
344
|
-
let e = epochOf(clampedStart);
|
|
345
|
-
const lastEpoch = epochOf(end);
|
|
346
|
-
let cursor = clampedStart;
|
|
347
|
-
while (e <= lastEpoch) {
|
|
348
|
-
const bucketEnd = (e + 1) * BUCKET_MS;
|
|
349
|
-
const sliceEnd = Math.min(end, bucketEnd);
|
|
350
|
-
const ms = sliceEnd - cursor;
|
|
351
|
-
if (ms > 0) {
|
|
352
|
-
const i = e % WINDOW_BUCKETS;
|
|
353
|
-
if (i >= 0) {
|
|
354
|
-
if (o.ringStamps[i] !== e) {
|
|
355
|
-
o.ringStamps[i] = e;
|
|
356
|
-
o.ring[i] = 0;
|
|
357
|
-
}
|
|
358
|
-
o.ring[i] += span > 0 ? ms * ratio : exclusiveMs;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
cursor = sliceEnd;
|
|
362
|
-
e++;
|
|
363
|
-
}
|
|
364
|
-
bookAttributedRange(start, end, exclusiveMs);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
export function labelForOrigin(key: string): string | null {
|
|
368
|
-
return origins.get(key)?.label ?? null;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/** Build the ranked table rows (excludes the pinned unattributed row — the
|
|
372
|
-
* controller assembles that from aggregator residuals). */
|
|
373
|
-
export function buildRows(
|
|
374
|
-
nowT: number,
|
|
375
|
-
windowBusy: number,
|
|
376
|
-
topN: number,
|
|
377
|
-
): JsTopRow[] {
|
|
378
|
-
const rows: JsTopRow[] = [];
|
|
379
|
-
for (const o of origins.values()) {
|
|
380
|
-
// Cheap keys that have handed off to a refined key stop accumulating new
|
|
381
|
-
// work but keep their pre-promotion totals; hide them once idle.
|
|
382
|
-
const windowMs = windowMsOf(o, nowT);
|
|
383
|
-
if (windowMs <= 0 && o.totalMs < 1) continue;
|
|
384
|
-
rows.push({
|
|
385
|
-
key: o.key,
|
|
386
|
-
label: o.label,
|
|
387
|
-
api: o.api,
|
|
388
|
-
file: o.file,
|
|
389
|
-
line: o.line,
|
|
390
|
-
windowMs: Math.round(windowMs * 10) / 10,
|
|
391
|
-
pctOfBusy:
|
|
392
|
-
windowBusy > 0
|
|
393
|
-
? Math.min(100, Math.round((windowMs / windowBusy) * 1000) / 10)
|
|
394
|
-
: 0,
|
|
395
|
-
totalMs: Math.round(o.totalMs * 10) / 10,
|
|
396
|
-
calls: o.calls,
|
|
397
|
-
avgMs: o.calls > 0 ? Math.round((o.totalMs / o.calls) * 100) / 100 : 0,
|
|
398
|
-
maxMs: Math.round(o.maxMs * 100) / 100,
|
|
399
|
-
lastSeenAgoMs: o.lastSeenAt > 0 ? Math.max(0, nowT - o.lastSeenAt) : 0,
|
|
400
|
-
// Filled in by the controller from the freeze-stats provider.
|
|
401
|
-
freezes: 0,
|
|
402
|
-
worstFreezeMs: 0,
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
rows.sort((a, b) => b.windowMs - a.windowMs || b.totalMs - a.totalMs);
|
|
406
|
-
// Noise gate: rows that can't matter (<1ms window, <0.5% busy, no freezes)
|
|
407
|
-
// collapse into a single aggregate so the table stays a shortlist.
|
|
408
|
-
const majors: JsTopRow[] = [];
|
|
409
|
-
const minors: JsTopRow[] = [];
|
|
410
|
-
for (const r of rows) {
|
|
411
|
-
(r.windowMs >= 1 || r.pctOfBusy >= 0.5 || r.freezes > 0
|
|
412
|
-
? majors
|
|
413
|
-
: minors
|
|
414
|
-
).push(r);
|
|
415
|
-
}
|
|
416
|
-
if (minors.length >= 2) {
|
|
417
|
-
majors.push({
|
|
418
|
-
key: "minor",
|
|
419
|
-
label: `${minors.length} minor origins (≤1ms each)`,
|
|
420
|
-
api: "other",
|
|
421
|
-
windowMs:
|
|
422
|
-
Math.round(minors.reduce((sum, r) => sum + r.windowMs, 0) * 10) / 10,
|
|
423
|
-
pctOfBusy:
|
|
424
|
-
Math.round(minors.reduce((sum, r) => sum + r.pctOfBusy, 0) * 10) / 10,
|
|
425
|
-
totalMs:
|
|
426
|
-
Math.round(minors.reduce((sum, r) => sum + r.totalMs, 0) * 10) / 10,
|
|
427
|
-
calls: minors.reduce((sum, r) => sum + r.calls, 0),
|
|
428
|
-
avgMs: 0,
|
|
429
|
-
maxMs: Math.max(0, ...minors.map((r) => r.maxMs)),
|
|
430
|
-
lastSeenAgoMs: Math.min(...minors.map((r) => r.lastSeenAgoMs)),
|
|
431
|
-
freezes: 0,
|
|
432
|
-
worstFreezeMs: 0,
|
|
433
|
-
});
|
|
434
|
-
return majors.slice(0, topN);
|
|
435
|
-
}
|
|
436
|
-
return rows.slice(0, topN);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/**
|
|
440
|
-
* Full origin detail minus freeze stats (the controller merges those). The
|
|
441
|
-
* bucket array is the origin's 5s activity trend, oldest→newest.
|
|
442
|
-
*/
|
|
443
|
-
export function getOriginDetailBase(
|
|
444
|
-
key: string,
|
|
445
|
-
nowT: number,
|
|
446
|
-
windowBusy: number,
|
|
447
|
-
): Omit<JsTopOriginDetail, "freezes" | "worstFreezeMs"> | null {
|
|
448
|
-
const o = origins.get(key);
|
|
449
|
-
if (!o) return null;
|
|
450
|
-
const current = epochOf(nowT);
|
|
451
|
-
const buckets: number[] = [];
|
|
452
|
-
let windowMs = 0;
|
|
453
|
-
for (let e = current - WINDOW_BUCKETS + 1; e <= current; e++) {
|
|
454
|
-
const i = e % WINDOW_BUCKETS;
|
|
455
|
-
const ms =
|
|
456
|
-
i >= 0 && o.ringStamps[i] === e ? Math.round(o.ring[i] * 10) / 10 : 0;
|
|
457
|
-
buckets.push(ms);
|
|
458
|
-
windowMs += ms;
|
|
459
|
-
}
|
|
460
|
-
return {
|
|
461
|
-
key: o.key,
|
|
462
|
-
label: o.label,
|
|
463
|
-
api: o.api,
|
|
464
|
-
caller: o.caller,
|
|
465
|
-
file: o.file,
|
|
466
|
-
line: o.line,
|
|
467
|
-
frames: o.frames,
|
|
468
|
-
promoted: o.promoted,
|
|
469
|
-
scheduleCount: o.scheduleCount,
|
|
470
|
-
calls: o.calls,
|
|
471
|
-
totalMs: Math.round(o.totalMs * 10) / 10,
|
|
472
|
-
avgMs: o.calls > 0 ? Math.round((o.totalMs / o.calls) * 100) / 100 : 0,
|
|
473
|
-
maxMs: Math.round(o.maxMs * 100) / 100,
|
|
474
|
-
windowMs: Math.round(windowMs * 10) / 10,
|
|
475
|
-
pctOfBusy:
|
|
476
|
-
windowBusy > 0
|
|
477
|
-
? Math.min(100, Math.round((windowMs / windowBusy) * 1000) / 10)
|
|
478
|
-
: 0,
|
|
479
|
-
lastSeenAgoMs: o.lastSeenAt > 0 ? Math.max(0, nowT - o.lastSeenAt) : 0,
|
|
480
|
-
buckets,
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
export function resetOrigins(): void {
|
|
485
|
-
origins.clear();
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/** Test-only introspection. */
|
|
489
|
-
export function debugGetOrigin(key: string): Origin | undefined {
|
|
490
|
-
return origins.get(key);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
export function originCount(): number {
|
|
494
|
-
return origins.size;
|
|
495
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tier 2 patch: legacy-bridge call-ins. On Paper/legacy-bridge runtimes,
|
|
3
|
-
* every native→JS call (touch events via RCTEventEmitter, timer flushes via
|
|
4
|
-
* JSTimers.callTimers, module callbacks) enters through
|
|
5
|
-
* `__fbBatchedBridge.callFunctionReturnFlushedQueue` /
|
|
6
|
-
* `invokeCallbackAndReturnFlushedQueue`. Wrapping them attributes event and
|
|
7
|
-
* native-callback cost by module.method.
|
|
8
|
-
*
|
|
9
|
-
* On New Architecture (bridgeless) these globals don't exist — install
|
|
10
|
-
* reports `bridge: false` and the UI banners the coverage gap. Nested
|
|
11
|
-
* wrapped work (e.g. our wrapped rAF callbacks running inside a
|
|
12
|
-
* callTimers call-in) books exclusively via the frame stack, so this tier
|
|
13
|
-
* only ever adds its own un-nested remainder.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { buoyJsTopRunAs } from "../frameStack";
|
|
17
|
-
import { g, isActive, PATCHED_TAG } from "../state";
|
|
18
|
-
import type { JsTopTiers } from "../../types";
|
|
19
|
-
|
|
20
|
-
export function installBridgePatch(): Partial<JsTopTiers> {
|
|
21
|
-
const bridge = g.__fbBatchedBridge;
|
|
22
|
-
if (!bridge) return { bridge: false };
|
|
23
|
-
|
|
24
|
-
let patchedAny = false;
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
typeof bridge.callFunctionReturnFlushedQueue === "function" &&
|
|
28
|
-
!bridge.callFunctionReturnFlushedQueue[PATCHED_TAG]
|
|
29
|
-
) {
|
|
30
|
-
const original = bridge.callFunctionReturnFlushedQueue.bind(bridge);
|
|
31
|
-
const buoyJsTopCallFunction = function (
|
|
32
|
-
module: string,
|
|
33
|
-
method: string,
|
|
34
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
|
-
args: any[],
|
|
36
|
-
) {
|
|
37
|
-
if (!isActive()) return original(module, method, args);
|
|
38
|
-
return buoyJsTopRunAs("bridge", `${module}.${method}`, () =>
|
|
39
|
-
original(module, method, args),
|
|
40
|
-
);
|
|
41
|
-
};
|
|
42
|
-
buoyJsTopCallFunction[PATCHED_TAG] = true;
|
|
43
|
-
bridge.callFunctionReturnFlushedQueue = buoyJsTopCallFunction;
|
|
44
|
-
patchedAny = true;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (
|
|
48
|
-
typeof bridge.invokeCallbackAndReturnFlushedQueue === "function" &&
|
|
49
|
-
!bridge.invokeCallbackAndReturnFlushedQueue[PATCHED_TAG]
|
|
50
|
-
) {
|
|
51
|
-
const original = bridge.invokeCallbackAndReturnFlushedQueue.bind(bridge);
|
|
52
|
-
// Callback ids are per-call and meaningless as identity — one shared
|
|
53
|
-
// origin covers all native-module completion callbacks.
|
|
54
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
55
|
-
const buoyJsTopInvokeCallback = function (cbID: number, args: any[]) {
|
|
56
|
-
if (!isActive()) return original(cbID, args);
|
|
57
|
-
return buoyJsTopRunAs("bridge", "nativeModuleCallback", () =>
|
|
58
|
-
original(cbID, args),
|
|
59
|
-
);
|
|
60
|
-
};
|
|
61
|
-
buoyJsTopInvokeCallback[PATCHED_TAG] = true;
|
|
62
|
-
bridge.invokeCallbackAndReturnFlushedQueue = buoyJsTopInvokeCallback;
|
|
63
|
-
patchedAny = true;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const alreadyPatched =
|
|
67
|
-
bridge.callFunctionReturnFlushedQueue?.[PATCHED_TAG] === true;
|
|
68
|
-
return { bridge: patchedAny || alreadyPatched };
|
|
69
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tier 1 patch: queueMicrotask. Same install-once contract as timers.ts.
|
|
3
|
-
* (Promise reactions are Tier 1.5 — see promises.ts.)
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { buoyJsTopWrapTask } from "../frameStack";
|
|
7
|
-
import { captureOriginals, g, PATCHED_TAG } from "../state";
|
|
8
|
-
|
|
9
|
-
export function installMicrotaskPatches(): void {
|
|
10
|
-
const originals = captureOriginals();
|
|
11
|
-
if (
|
|
12
|
-
typeof g.queueMicrotask === "function" &&
|
|
13
|
-
!g.queueMicrotask[PATCHED_TAG] &&
|
|
14
|
-
originals.queueMicrotask
|
|
15
|
-
) {
|
|
16
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
-
const buoyJsTopQueueMicrotask = function (fn: any) {
|
|
18
|
-
const cb = buoyJsTopWrapTask(fn, "microtask");
|
|
19
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
-
return (originals.queueMicrotask as any)(cb);
|
|
21
|
-
};
|
|
22
|
-
buoyJsTopQueueMicrotask[PATCHED_TAG] = true;
|
|
23
|
-
g.queueMicrotask = buoyJsTopQueueMicrotask;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tier 1.5 patch: Promise reactions via Promise.prototype.then/catch/finally.
|
|
3
|
-
*
|
|
4
|
-
* All three are patched: in Hermes, catch/finally delegate to then, and the
|
|
5
|
-
* WRAPPED_TAG check in wrapTask makes the delegation harmless (a reaction
|
|
6
|
-
* wrapped by our catch patch passes through our then patch untouched).
|
|
7
|
-
*
|
|
8
|
-
* Known, documented gap: native async/await resumption never goes through
|
|
9
|
-
* `.then` — async-function bodies land in the "unattributed" row, not here.
|
|
10
|
-
*
|
|
11
|
-
* The wrapper is passthrough-first: non-function reactions
|
|
12
|
-
* (`.then(undefined, fn)`), `this` binding, and return values are preserved
|
|
13
|
-
* exactly; when the engine is inactive the original is called with the raw
|
|
14
|
-
* arguments.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { buoyJsTopWrapTask } from "../frameStack";
|
|
18
|
-
import { g, PATCHED_TAG } from "../state";
|
|
19
|
-
import type { JsTopTiers } from "../../types";
|
|
20
|
-
|
|
21
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
-
type AnyFn = (...args: any[]) => any;
|
|
23
|
-
|
|
24
|
-
function patchMethod(
|
|
25
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
-
proto: any,
|
|
27
|
-
name: "then" | "catch" | "finally",
|
|
28
|
-
): boolean {
|
|
29
|
-
const original: AnyFn | undefined = proto[name];
|
|
30
|
-
if (typeof original !== "function") return false;
|
|
31
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
-
if ((original as any)[PATCHED_TAG]) return true;
|
|
33
|
-
|
|
34
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
|
-
const patched = function (this: any, ...reactions: any[]) {
|
|
36
|
-
for (let i = 0; i < reactions.length; i++) {
|
|
37
|
-
if (typeof reactions[i] === "function") {
|
|
38
|
-
reactions[i] = buoyJsTopWrapTask(reactions[i], "then");
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return original.apply(this, reactions);
|
|
42
|
-
};
|
|
43
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
44
|
-
(patched as any)[PATCHED_TAG] = true;
|
|
45
|
-
Object.defineProperty(patched, "name", { value: name, configurable: true });
|
|
46
|
-
proto[name] = patched;
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function installPromisePatches(): Partial<JsTopTiers> {
|
|
51
|
-
const proto = g.Promise?.prototype;
|
|
52
|
-
if (!proto) return { promises: false };
|
|
53
|
-
const ok = patchMethod(proto, "then");
|
|
54
|
-
patchMethod(proto, "catch");
|
|
55
|
-
patchMethod(proto, "finally");
|
|
56
|
-
return { promises: ok };
|
|
57
|
-
}
|