@buoy-gg/js-top 7.0.28 → 7.0.30
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,142 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* wrapTask — the single wrapper every patched entry point uses, plus the
|
|
3
|
-
* frame stack that makes nested wrapped calls book EXCLUSIVE time.
|
|
4
|
-
*
|
|
5
|
-
* The no-double-count rule: every wrapper at any depth books
|
|
6
|
-
* `(end − start) − childrenMs` to its own origin and adds its TOTAL to its
|
|
7
|
-
* parent frame's childrenMs. Thread occupancy is then the sum of exclusive
|
|
8
|
-
* time at all depths, which equals the union of wrapped wall intervals —
|
|
9
|
-
* nesting (e.g. rAF callbacks fired inside a wrapped bridge call-in) can
|
|
10
|
-
* never inflate totals.
|
|
11
|
-
*
|
|
12
|
-
* Naming: exported/inner functions carry a `buoyJsTop` prefix on purpose —
|
|
13
|
-
* Hermes dev stacks only show function names + bundle offsets, and the
|
|
14
|
-
* registry's stack parser filters engine frames by that prefix.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { now } from "./now";
|
|
18
|
-
import { isActive, WRAPPED_TAG } from "./state";
|
|
19
|
-
import type { TaskApi } from "../types";
|
|
20
|
-
import {
|
|
21
|
-
buoyJsTopBookExclusive,
|
|
22
|
-
buoyJsTopOriginExists,
|
|
23
|
-
buoyJsTopScheduleOrigin,
|
|
24
|
-
} from "./originRegistry";
|
|
25
|
-
|
|
26
|
-
const DEPTH_CAP = 32;
|
|
27
|
-
|
|
28
|
-
interface Frame {
|
|
29
|
-
originKey: string;
|
|
30
|
-
start: number;
|
|
31
|
-
childrenMs: number;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const stack: Frame[] = [];
|
|
35
|
-
|
|
36
|
-
/** Depth-0 completed intervals ≥ this feed the longtask correlator. */
|
|
37
|
-
const NOTABLE_INTERVAL_MS = 20;
|
|
38
|
-
const INTERVAL_RING_SIZE = 64;
|
|
39
|
-
|
|
40
|
-
export interface CompletedInterval {
|
|
41
|
-
originKey: string;
|
|
42
|
-
start: number;
|
|
43
|
-
end: number;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const intervalRing: (CompletedInterval | null)[] = new Array(
|
|
47
|
-
INTERVAL_RING_SIZE,
|
|
48
|
-
).fill(null);
|
|
49
|
-
let intervalRingIndex = 0;
|
|
50
|
-
|
|
51
|
-
/** Recent depth-0 wrapped intervals, unordered — consumers scan all slots. */
|
|
52
|
-
export function recentIntervals(): (CompletedInterval | null)[] {
|
|
53
|
-
return intervalRing;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
57
|
-
type AnyFn = (...args: any[]) => any;
|
|
58
|
-
|
|
59
|
-
/** Push a frame (unless depth-capped), run `invoke`, book on the way out. */
|
|
60
|
-
function buoyJsTopRunFrame<T>(originKey: string, invoke: () => T): T {
|
|
61
|
-
const start = now();
|
|
62
|
-
const parent = stack.length > 0 ? stack[stack.length - 1] : null;
|
|
63
|
-
let frame: Frame | null = null;
|
|
64
|
-
if (stack.length < DEPTH_CAP) {
|
|
65
|
-
frame = { originKey, start, childrenMs: 0 };
|
|
66
|
-
stack.push(frame);
|
|
67
|
-
}
|
|
68
|
-
try {
|
|
69
|
-
// The user code runs OUTSIDE any catch of ours: errors propagate to the
|
|
70
|
-
// runtime's normal unhandled-error path untouched.
|
|
71
|
-
return invoke();
|
|
72
|
-
} finally {
|
|
73
|
-
try {
|
|
74
|
-
if (frame) {
|
|
75
|
-
stack.pop();
|
|
76
|
-
const end = now();
|
|
77
|
-
const total = end - start;
|
|
78
|
-
const exclusive = Math.max(0, total - frame.childrenMs);
|
|
79
|
-
if (parent) parent.childrenMs += total;
|
|
80
|
-
buoyJsTopBookExclusive(originKey, exclusive, start, end);
|
|
81
|
-
if (!parent && total >= NOTABLE_INTERVAL_MS) {
|
|
82
|
-
intervalRing[intervalRingIndex] = { originKey, start, end };
|
|
83
|
-
intervalRingIndex = (intervalRingIndex + 1) % INTERVAL_RING_SIZE;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
} catch {
|
|
87
|
-
// Bookkeeping must never break the app.
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Wrap a callback at schedule time. ALWAYS wraps once patches are installed
|
|
94
|
-
* (installation itself only happens on first engine start): a long-lived
|
|
95
|
-
* callback — the classic forgotten setInterval — must stay attributable
|
|
96
|
-
* even when it was scheduled while the engine was off, or after clear()
|
|
97
|
-
* deleted its origin record. Identity is therefore resolved eagerly at
|
|
98
|
-
* schedule time when possible (best stack context) and re-resolved lazily
|
|
99
|
-
* at fire time otherwise. Inactive-engine cost per fire: one boolean.
|
|
100
|
-
*/
|
|
101
|
-
export function buoyJsTopWrapTask<T extends AnyFn>(fn: T, api: TaskApi): T {
|
|
102
|
-
if (typeof fn !== "function") return fn;
|
|
103
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
104
|
-
if ((fn as any)[WRAPPED_TAG]) return fn;
|
|
105
|
-
let originKey: string | null = isActive()
|
|
106
|
-
? buoyJsTopScheduleOrigin(api, fn, now())
|
|
107
|
-
: null;
|
|
108
|
-
|
|
109
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
110
|
-
const buoyJsTopTaskWrapper = function (this: any, ...args: any[]) {
|
|
111
|
-
if (!isActive()) return fn.apply(this, args);
|
|
112
|
-
if (originKey === null || !buoyJsTopOriginExists(originKey)) {
|
|
113
|
-
// Scheduled while the engine was off, or stats cleared since —
|
|
114
|
-
// (re)resolve identity now, without stack capture (see scheduleOrigin).
|
|
115
|
-
originKey = buoyJsTopScheduleOrigin(api, fn, now(), false);
|
|
116
|
-
}
|
|
117
|
-
return buoyJsTopRunFrame(originKey, () => fn.apply(this, args));
|
|
118
|
-
} as unknown as T;
|
|
119
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
120
|
-
(buoyJsTopTaskWrapper as any)[WRAPPED_TAG] = true;
|
|
121
|
-
return buoyJsTopTaskWrapper;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Run a thunk as a named task frame — for entry points that aren't
|
|
126
|
-
* schedule-a-callback shaped (e.g. bridge call-ins, where the "origin" is
|
|
127
|
-
* the module.method being invoked right now).
|
|
128
|
-
*/
|
|
129
|
-
export function buoyJsTopRunAs<T>(
|
|
130
|
-
api: TaskApi,
|
|
131
|
-
name: string,
|
|
132
|
-
thunk: () => T,
|
|
133
|
-
): T {
|
|
134
|
-
if (!isActive()) return thunk();
|
|
135
|
-
const originKey = buoyJsTopScheduleOrigin(api, { name }, now());
|
|
136
|
-
return buoyJsTopRunFrame(originKey, thunk);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** Test-only: current nesting depth. */
|
|
140
|
-
export function debugDepth(): number {
|
|
141
|
-
return stack.length;
|
|
142
|
-
}
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Freeze tracking. RN ships a real `PerformanceObserver('longtask')` (C++
|
|
3
|
-
* TurboModule, present in Expo Go and release): entries fire for any task
|
|
4
|
-
* that blocks the JS event loop ≥ 50ms — what a user feels as a dropped tap
|
|
5
|
-
* or stuttered animation — but carry NO attribution. We supply it: each
|
|
6
|
-
* entry is overlap-matched against the ring of recent depth-0 wrapped
|
|
7
|
-
* intervals; the biggest overlap names the offender.
|
|
8
|
-
*
|
|
9
|
-
* Freeze data doesn't get its own UI section — it decorates the places you
|
|
10
|
-
* already look: per-row ⚠ badges in the task table and a summary chip in
|
|
11
|
-
* the header, both fed by getFreezeStats() (trailing 60s window). The raw
|
|
12
|
-
* entry list is still exported for the desktop panel and MCP payloads.
|
|
13
|
-
*
|
|
14
|
-
* The observer installs once and stays connected (uninstalling/reinstalling
|
|
15
|
-
* observers has its own churn); its handler early-outs when the engine is
|
|
16
|
-
* inactive — longtasks only fire on 50ms+ blocks, so the idle cost is nil.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { g, isActive } from "./state";
|
|
20
|
-
import { recentIntervals } from "./frameStack";
|
|
21
|
-
import { labelForOrigin } from "./originRegistry";
|
|
22
|
-
import { bookLongtaskRange } from "./aggregator";
|
|
23
|
-
import type { JsTopLongtask, JsTopTiers } from "../types";
|
|
24
|
-
|
|
25
|
-
const MAX_ENTRIES = 40;
|
|
26
|
-
/** Freeze badges/summary only consider the trailing window. */
|
|
27
|
-
const FREEZE_WINDOW_MS = 60_000;
|
|
28
|
-
|
|
29
|
-
/** Per-origin key for freezes we couldn't attribute to a wrapped callback. */
|
|
30
|
-
export const SYSTEM_FREEZE_KEY = "unattributed";
|
|
31
|
-
|
|
32
|
-
interface FreezeEntry {
|
|
33
|
-
start: number;
|
|
34
|
-
duration: number;
|
|
35
|
-
at: number;
|
|
36
|
-
originKey: string | null;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface FreezeStats {
|
|
40
|
-
/** Freezes in the trailing window. */
|
|
41
|
-
count: number;
|
|
42
|
-
worstMs: number;
|
|
43
|
-
/** Keyed by origin key (SYSTEM_FREEZE_KEY for unattributed). */
|
|
44
|
-
byOrigin: Record<string, { count: number; maxMs: number }>;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
let installed = false;
|
|
48
|
-
let supported = false;
|
|
49
|
-
const entries: FreezeEntry[] = [];
|
|
50
|
-
|
|
51
|
-
function correlate(start: number, duration: number): string | null {
|
|
52
|
-
const end = start + duration;
|
|
53
|
-
let bestKey: string | null = null;
|
|
54
|
-
let bestOverlap = 0;
|
|
55
|
-
for (const interval of recentIntervals()) {
|
|
56
|
-
if (!interval) continue;
|
|
57
|
-
const overlap =
|
|
58
|
-
Math.min(end, interval.end) - Math.max(start, interval.start);
|
|
59
|
-
if (overlap > bestOverlap) {
|
|
60
|
-
bestOverlap = overlap;
|
|
61
|
-
bestKey = interval.originKey;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return bestKey;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function installLongtaskObserver(): Partial<JsTopTiers> {
|
|
68
|
-
if (installed) return { longtask: supported };
|
|
69
|
-
installed = true;
|
|
70
|
-
const PerformanceObserverCtor = g.PerformanceObserver;
|
|
71
|
-
if (typeof PerformanceObserverCtor !== "function") {
|
|
72
|
-
return { longtask: false };
|
|
73
|
-
}
|
|
74
|
-
try {
|
|
75
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
76
|
-
const observer = new PerformanceObserverCtor((list: any) => {
|
|
77
|
-
if (!isActive()) return;
|
|
78
|
-
try {
|
|
79
|
-
for (const entry of list.getEntries()) {
|
|
80
|
-
if (entry.entryType !== "longtask") continue;
|
|
81
|
-
const start = entry.startTime as number;
|
|
82
|
-
const duration = entry.duration as number;
|
|
83
|
-
bookLongtaskRange(start, start + duration);
|
|
84
|
-
entries.push({
|
|
85
|
-
start,
|
|
86
|
-
duration,
|
|
87
|
-
at: Date.now(),
|
|
88
|
-
originKey: correlate(start, duration),
|
|
89
|
-
});
|
|
90
|
-
if (entries.length > MAX_ENTRIES) entries.shift();
|
|
91
|
-
}
|
|
92
|
-
} catch {
|
|
93
|
-
// Never let observation break the app.
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
observer.observe({ entryTypes: ["longtask"] });
|
|
97
|
-
supported = true;
|
|
98
|
-
} catch {
|
|
99
|
-
supported = false;
|
|
100
|
-
}
|
|
101
|
-
return { longtask: supported };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Raw recent entries (desktop panel + MCP payload shape). */
|
|
105
|
-
export function getLongtasks(): JsTopLongtask[] {
|
|
106
|
-
return entries.map((e) => ({
|
|
107
|
-
start: e.start,
|
|
108
|
-
duration: e.duration,
|
|
109
|
-
at: e.at,
|
|
110
|
-
originLabel: e.originKey ? labelForOrigin(e.originKey) : null,
|
|
111
|
-
}));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** Windowed per-origin freeze aggregation for badges + header summary. */
|
|
115
|
-
export function getFreezeStats(now: number): FreezeStats {
|
|
116
|
-
const byOrigin: FreezeStats["byOrigin"] = {};
|
|
117
|
-
let count = 0;
|
|
118
|
-
let worstMs = 0;
|
|
119
|
-
for (const e of entries) {
|
|
120
|
-
if (e.at > 0 && now - e.at > FREEZE_WINDOW_MS) continue;
|
|
121
|
-
count++;
|
|
122
|
-
if (e.duration > worstMs) worstMs = e.duration;
|
|
123
|
-
const key = e.originKey ?? SYSTEM_FREEZE_KEY;
|
|
124
|
-
const s = byOrigin[key];
|
|
125
|
-
if (s) {
|
|
126
|
-
s.count++;
|
|
127
|
-
if (e.duration > s.maxMs) s.maxMs = e.duration;
|
|
128
|
-
} else {
|
|
129
|
-
byOrigin[key] = { count: 1, maxMs: e.duration };
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return { count, worstMs, byOrigin };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function clearLongtasks(): void {
|
|
136
|
-
entries.length = 0;
|
|
137
|
-
}
|
package/src/js-top/engine/now.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* High-res monotonic clock for the engine's hot path. Resolved once at module
|
|
3
|
-
* load: `nativePerformanceNow` (JSI global, cheapest) → `performance.now` →
|
|
4
|
-
* `Date.now`. All engine timestamps share this single time base.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
-
const g: any =
|
|
9
|
-
// eslint-disable-next-line no-restricted-globals
|
|
10
|
-
typeof globalThis !== "undefined" ? globalThis : undefined;
|
|
11
|
-
|
|
12
|
-
const resolved: () => number = (() => {
|
|
13
|
-
if (typeof g?.nativePerformanceNow === "function") {
|
|
14
|
-
const native = g.nativePerformanceNow as () => number;
|
|
15
|
-
return () => native();
|
|
16
|
-
}
|
|
17
|
-
if (typeof g?.performance?.now === "function") {
|
|
18
|
-
const perf = g.performance as { now: () => number };
|
|
19
|
-
return () => perf.now();
|
|
20
|
-
}
|
|
21
|
-
return () => Date.now();
|
|
22
|
-
})();
|
|
23
|
-
|
|
24
|
-
let clock = resolved;
|
|
25
|
-
|
|
26
|
-
export function now(): number {
|
|
27
|
-
return clock();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Test-only: swap the engine clock for a controllable one. */
|
|
31
|
-
export function setNowForTests(fn: (() => number) | null): void {
|
|
32
|
-
clock = fn ?? resolved;
|
|
33
|
-
}
|