@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,80 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tier 1 patches: setTimeout / setInterval / setImmediate /
|
|
3
|
-
* requestAnimationFrame. Public globals only — RN internals (JSTimers) are
|
|
4
|
-
* private API and churn across versions.
|
|
5
|
-
*
|
|
6
|
-
* Install-once, never unpatch (see state.ts). Handles are returned from the
|
|
7
|
-
* originals unchanged, so clearTimeout/clearInterval/cancelAnimationFrame
|
|
8
|
-
* need no patching. Extra setTimeout args pass through.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { buoyJsTopWrapTask } from "../frameStack";
|
|
12
|
-
import { captureOriginals, g, PATCHED_TAG } from "../state";
|
|
13
|
-
|
|
14
|
-
export function installTimerPatches(): void {
|
|
15
|
-
const originals = captureOriginals();
|
|
16
|
-
|
|
17
|
-
if (typeof g.setTimeout === "function" && !g.setTimeout[PATCHED_TAG]) {
|
|
18
|
-
const buoyJsTopSetTimeout = function (
|
|
19
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
-
fn: any,
|
|
21
|
-
timeout?: number,
|
|
22
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
-
...args: any[]
|
|
24
|
-
) {
|
|
25
|
-
const cb = buoyJsTopWrapTask(fn, "timeout");
|
|
26
|
-
return (originals.setTimeout as typeof setTimeout)(cb, timeout, ...args);
|
|
27
|
-
};
|
|
28
|
-
buoyJsTopSetTimeout[PATCHED_TAG] = true;
|
|
29
|
-
g.setTimeout = buoyJsTopSetTimeout;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (typeof g.setInterval === "function" && !g.setInterval[PATCHED_TAG]) {
|
|
33
|
-
const buoyJsTopSetInterval = function (
|
|
34
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
|
-
fn: any,
|
|
36
|
-
timeout?: number,
|
|
37
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
|
-
...args: any[]
|
|
39
|
-
) {
|
|
40
|
-
const cb = buoyJsTopWrapTask(fn, "interval");
|
|
41
|
-
return (originals.setInterval as typeof setInterval)(
|
|
42
|
-
cb,
|
|
43
|
-
timeout,
|
|
44
|
-
...args,
|
|
45
|
-
);
|
|
46
|
-
};
|
|
47
|
-
buoyJsTopSetInterval[PATCHED_TAG] = true;
|
|
48
|
-
g.setInterval = buoyJsTopSetInterval;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
typeof g.setImmediate === "function" &&
|
|
53
|
-
!g.setImmediate[PATCHED_TAG] &&
|
|
54
|
-
originals.setImmediate
|
|
55
|
-
) {
|
|
56
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
57
|
-
const buoyJsTopSetImmediate = function (fn: any, ...args: any[]) {
|
|
58
|
-
const cb = buoyJsTopWrapTask(fn, "immediate");
|
|
59
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
60
|
-
return (originals.setImmediate as any)(cb, ...args);
|
|
61
|
-
};
|
|
62
|
-
buoyJsTopSetImmediate[PATCHED_TAG] = true;
|
|
63
|
-
g.setImmediate = buoyJsTopSetImmediate;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (
|
|
67
|
-
typeof g.requestAnimationFrame === "function" &&
|
|
68
|
-
!g.requestAnimationFrame[PATCHED_TAG] &&
|
|
69
|
-
originals.requestAnimationFrame
|
|
70
|
-
) {
|
|
71
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
72
|
-
const buoyJsTopRequestAnimationFrame = function (fn: any) {
|
|
73
|
-
const cb = buoyJsTopWrapTask(fn, "raf");
|
|
74
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
75
|
-
return (originals.requestAnimationFrame as any)(cb);
|
|
76
|
-
};
|
|
77
|
-
buoyJsTopRequestAnimationFrame[PATCHED_TAG] = true;
|
|
78
|
-
g.requestAnimationFrame = buoyJsTopRequestAnimationFrame;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared engine state + pre-patch originals.
|
|
3
|
-
*
|
|
4
|
-
* `active` is the single gate every patched global checks at schedule time:
|
|
5
|
-
* patches install once and are NEVER removed (restoring originals would
|
|
6
|
-
* clobber any wrapper another library — Sentry, Reanimated — layered on top
|
|
7
|
-
* of ours after install; same reasoning as highlight-updates' CommitProfiler
|
|
8
|
-
* hook). When inactive, a patched global forwards the raw callback to the
|
|
9
|
-
* original at the cost of one boolean check.
|
|
10
|
-
*
|
|
11
|
-
* `originals` are captured BEFORE patching so the engine's own probe and
|
|
12
|
-
* notify timers never flow through the wrappers — JS TOP must not list
|
|
13
|
-
* itself as a task.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
-
export const g: any =
|
|
18
|
-
// eslint-disable-next-line no-restricted-globals
|
|
19
|
-
typeof globalThis !== "undefined" ? globalThis : {};
|
|
20
|
-
|
|
21
|
-
let active = false;
|
|
22
|
-
|
|
23
|
-
export function isActive(): boolean {
|
|
24
|
-
return active;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function setActive(on: boolean): void {
|
|
28
|
-
active = on;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
interface Originals {
|
|
32
|
-
setTimeout: typeof setTimeout;
|
|
33
|
-
setInterval: typeof setInterval;
|
|
34
|
-
clearInterval: typeof clearInterval;
|
|
35
|
-
clearTimeout: typeof clearTimeout;
|
|
36
|
-
setImmediate: typeof setImmediate | undefined;
|
|
37
|
-
requestAnimationFrame: typeof requestAnimationFrame | undefined;
|
|
38
|
-
queueMicrotask: typeof queueMicrotask | undefined;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
let originals: Originals | null = null;
|
|
42
|
-
|
|
43
|
-
/** Capture pre-patch globals. Idempotent; first call wins. */
|
|
44
|
-
export function captureOriginals(): Originals {
|
|
45
|
-
if (!originals) {
|
|
46
|
-
originals = {
|
|
47
|
-
setTimeout: g.setTimeout?.bind(g),
|
|
48
|
-
setInterval: g.setInterval?.bind(g),
|
|
49
|
-
clearInterval: g.clearInterval?.bind(g),
|
|
50
|
-
clearTimeout: g.clearTimeout?.bind(g),
|
|
51
|
-
setImmediate: g.setImmediate?.bind(g),
|
|
52
|
-
requestAnimationFrame: g.requestAnimationFrame?.bind(g),
|
|
53
|
-
queueMicrotask: g.queueMicrotask?.bind(g),
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
return originals;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function getOriginals(): Originals {
|
|
60
|
-
return captureOriginals();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** Tag interned on patched globals so duplicate module instances skip re-patching. */
|
|
64
|
-
export const PATCHED_TAG = Symbol.for("buoy.jsTop.patched");
|
|
65
|
-
/** Marker set on wrapper functions so a wrapped fn is never wrapped twice. */
|
|
66
|
-
export const WRAPPED_TAG = Symbol.for("buoy.jsTop.wrapped");
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dev-only origin symbolication via Metro's /symbolicate endpoint (the same
|
|
3
|
-
* one LogBox uses). Hermes stacks give function names + bundle offsets; for
|
|
4
|
-
* anonymous scheduling sites (the typical `useEffect(() => setInterval(…))`)
|
|
5
|
-
* that leaves an unreadable label. Metro maps the offset back to the real
|
|
6
|
-
* source file — so a row can say `setInterval ← ChatScreen.tsx:42`, which is
|
|
7
|
-
* component-level attribution in practice.
|
|
8
|
-
*
|
|
9
|
-
* Fire-and-forget with hard caps: one attempt per origin, few in flight,
|
|
10
|
-
* silent on any failure (release builds, tunnels, Metro gone). The origin's
|
|
11
|
-
* label/file/line are upgraded in place when the answer lands — the table
|
|
12
|
-
* re-renders every tick and picks it up.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import type { Origin } from "./originRegistry";
|
|
16
|
-
|
|
17
|
-
declare const __DEV__: boolean;
|
|
18
|
-
|
|
19
|
-
const MAX_IN_FLIGHT = 3;
|
|
20
|
-
let inFlight = 0;
|
|
21
|
-
|
|
22
|
-
function isDev(): boolean {
|
|
23
|
-
return typeof __DEV__ !== "undefined" && __DEV__;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Last two path segments of a source path — "screens/ChatScreen.tsx". */
|
|
27
|
-
function tailPath(file: string): string {
|
|
28
|
-
const clean = file.split("?")[0] ?? file;
|
|
29
|
-
const parts = clean.split("/").filter(Boolean);
|
|
30
|
-
return parts.slice(-2).join("/");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** True for names like ChatScreen / PokemonScreen — reads as a component. */
|
|
34
|
-
function looksLikeComponent(name: string): boolean {
|
|
35
|
-
return /^[A-Z][A-Za-z0-9]*$/.test(name);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function buoyJsTopSymbolicateOrigin(
|
|
39
|
-
o: Origin,
|
|
40
|
-
apiLabelText: string,
|
|
41
|
-
): void {
|
|
42
|
-
if (!isDev()) return;
|
|
43
|
-
if (o.symbolicated || !o.frames || o.frames.length === 0) return;
|
|
44
|
-
if (inFlight >= MAX_IN_FLIGHT) return; // retried implicitly? no — one shot
|
|
45
|
-
o.symbolicated = true;
|
|
46
|
-
|
|
47
|
-
const top = o.frames[0];
|
|
48
|
-
if (!top || !/^https?:\/\//.test(top.file)) return;
|
|
49
|
-
let endpoint: string;
|
|
50
|
-
try {
|
|
51
|
-
endpoint = `${new URL(top.file).origin}/symbolicate`;
|
|
52
|
-
} catch {
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
inFlight++;
|
|
57
|
-
const body = JSON.stringify({
|
|
58
|
-
stack: o.frames.map((f) => ({
|
|
59
|
-
file: f.file,
|
|
60
|
-
lineNumber: f.line,
|
|
61
|
-
column: f.col,
|
|
62
|
-
methodName: "",
|
|
63
|
-
})),
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
fetch(endpoint, { method: "POST", body })
|
|
67
|
-
.then((res) => (res.ok ? res.json() : null))
|
|
68
|
-
.then(
|
|
69
|
-
(data: {
|
|
70
|
-
stack?: { file?: string; lineNumber?: number; methodName?: string }[];
|
|
71
|
-
error?: string;
|
|
72
|
-
} | null) => {
|
|
73
|
-
let frame = data?.stack?.find(
|
|
74
|
-
(f) => f.file && !f.file.includes("node_modules"),
|
|
75
|
-
) ?? data?.stack?.[0];
|
|
76
|
-
// Some Metro configs (serialized source maps) reject full
|
|
77
|
-
// symbolication but still NAME the module in the error — that file
|
|
78
|
-
// path is exactly the scheduling site, so harvest it.
|
|
79
|
-
if (!frame?.file && data?.error) {
|
|
80
|
-
const m = /found: (\/.+)$/.exec(data.error);
|
|
81
|
-
if (m) frame = { file: m[1] };
|
|
82
|
-
}
|
|
83
|
-
if (!frame?.file) return;
|
|
84
|
-
const file = tailPath(frame.file);
|
|
85
|
-
const line = frame.lineNumber;
|
|
86
|
-
const method =
|
|
87
|
-
frame.methodName && frame.methodName !== "<unknown>"
|
|
88
|
-
? frame.methodName
|
|
89
|
-
: undefined;
|
|
90
|
-
o.file = file;
|
|
91
|
-
o.line = line ?? o.line;
|
|
92
|
-
// The existing label is name-anchored — append the source site
|
|
93
|
-
// once, and promote a component-looking method name.
|
|
94
|
-
const site = `${file.split("/").pop()}${line != null ? `:${line}` : ""}`;
|
|
95
|
-
if (method && looksLikeComponent(method) && !o.caller) {
|
|
96
|
-
o.caller = method;
|
|
97
|
-
o.label = `${o.label} ← <${method}> · ${site}`;
|
|
98
|
-
} else {
|
|
99
|
-
o.label = `${o.label} · ${site}`;
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
|
-
)
|
|
103
|
-
.catch(() => {
|
|
104
|
-
// Symbolication is best-effort decoration — never surface failures.
|
|
105
|
-
})
|
|
106
|
-
.finally(() => {
|
|
107
|
-
inFlight--;
|
|
108
|
-
});
|
|
109
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sync adapter for the JS TOP tool, consumed by @buoy-gg/external-sync's
|
|
3
|
-
* `useExternalSync` (structurally matches its ToolSyncAdapter interface so
|
|
4
|
-
* this package doesn't need a dependency on it — same pattern as
|
|
5
|
-
* perf-monitor's adapter).
|
|
6
|
-
*
|
|
7
|
-
* `subscribe` is only called while a dashboard is watching, and subscribing
|
|
8
|
-
* to JsTopController is exactly what starts the engine — so a desktop
|
|
9
|
-
* watcher auto-starts sampling and closing the desktop tool stops it. The
|
|
10
|
-
* snapshot IS the live channel; there is no heavy persisted state.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { JsTopController } from "../engine/JsTopController";
|
|
14
|
-
import type { JsTopSnapshot } from "../types";
|
|
15
|
-
|
|
16
|
-
export const jsTopSyncAdapter = {
|
|
17
|
-
version: 1,
|
|
18
|
-
getSnapshot: (): { live: JsTopSnapshot; enabled: boolean } => ({
|
|
19
|
-
live: JsTopController.getSnapshot(),
|
|
20
|
-
// "enabled" mirrors perf-monitor semantics: is the device sampling for
|
|
21
|
-
// any viewer (its own HUD/modal or a desktop's silent remote sampling).
|
|
22
|
-
enabled:
|
|
23
|
-
JsTopController.isEnabled() || JsTopController.isRemoteSampling(),
|
|
24
|
-
}),
|
|
25
|
-
subscribe: (onChange: () => void) => {
|
|
26
|
-
const unsubSnapshot = JsTopController.subscribe(() => onChange());
|
|
27
|
-
const unsubEnabled = JsTopController.subscribeEnabled(() => onChange());
|
|
28
|
-
return () => {
|
|
29
|
-
unsubSnapshot();
|
|
30
|
-
unsubEnabled();
|
|
31
|
-
};
|
|
32
|
-
},
|
|
33
|
-
actions: {
|
|
34
|
-
/**
|
|
35
|
-
* Silent sampling for a desktop viewer: streams snapshots without
|
|
36
|
-
* showing the device's own HUD. (Subscribing via `subscribe` already
|
|
37
|
-
* runs the engine while watched; this exists so MCP / scripted callers
|
|
38
|
-
* can force sampling on without a watch.)
|
|
39
|
-
*/
|
|
40
|
-
setEnabled: (params?: unknown) => {
|
|
41
|
-
const enabled = (params as { enabled: boolean }).enabled;
|
|
42
|
-
JsTopController.setRemoteSampling(enabled);
|
|
43
|
-
},
|
|
44
|
-
pause: () => JsTopController.pause(),
|
|
45
|
-
resume: () => JsTopController.resume(),
|
|
46
|
-
clear: () => JsTopController.clear(),
|
|
47
|
-
/** Full detail for one origin (desktop tap-through parity). */
|
|
48
|
-
getOriginDetail: (params?: unknown) =>
|
|
49
|
-
JsTopController.getOriginDetail((params as { key: string }).key),
|
|
50
|
-
/**
|
|
51
|
-
* One-shot sample for MCP/automation: ensure the engine is running,
|
|
52
|
-
* observe for `durationMs` (default 3s, capped 30s), return the
|
|
53
|
-
* resulting snapshot. `clearFirst` resets all stats before sampling so
|
|
54
|
-
* the result attributes exactly one interaction/window. Leaves sampling
|
|
55
|
-
* in its prior state.
|
|
56
|
-
*/
|
|
57
|
-
sample: async (params: unknown) => {
|
|
58
|
-
const p = params as { durationMs?: number; clearFirst?: boolean };
|
|
59
|
-
const durationMs = Math.min(Math.max(p?.durationMs ?? 3000, 250), 30_000);
|
|
60
|
-
const wasSampling =
|
|
61
|
-
JsTopController.isRemoteSampling() || JsTopController.isEnabled();
|
|
62
|
-
if (!wasSampling) JsTopController.setRemoteSampling(true);
|
|
63
|
-
if (p?.clearFirst) JsTopController.clear();
|
|
64
|
-
try {
|
|
65
|
-
await new Promise((resolve) => setTimeout(resolve, durationMs));
|
|
66
|
-
return JsTopController.getSnapshot();
|
|
67
|
-
} finally {
|
|
68
|
-
if (!wasSampling) JsTopController.setRemoteSampling(false);
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
};
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* JS TOP — live Task Manager for the React Native JS thread.
|
|
3
|
-
*
|
|
4
|
-
* Attribution model: the engine wraps every JS-thread entry point it can
|
|
5
|
-
* reach (timers, rAF, microtasks, Promise reactions, legacy-bridge call-ins)
|
|
6
|
-
* and books EXCLUSIVE time per "origin" (the place a callback was scheduled
|
|
7
|
-
* from). A high-frequency probe measures true thread occupancy; the gap
|
|
8
|
-
* between measured busy time and attributed time is surfaced honestly as an
|
|
9
|
-
* "unattributed" row rather than hidden.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
/** Which entry-point API a task came through. */
|
|
13
|
-
export type TaskApi =
|
|
14
|
-
| "timeout"
|
|
15
|
-
| "interval"
|
|
16
|
-
| "immediate"
|
|
17
|
-
| "raf"
|
|
18
|
-
| "microtask"
|
|
19
|
-
| "then"
|
|
20
|
-
| "bridge"
|
|
21
|
-
| "other"
|
|
22
|
-
| "unattributed";
|
|
23
|
-
|
|
24
|
-
/** One captured stack frame from an origin's scheduling site (dev-readable). */
|
|
25
|
-
export interface OriginStackFrame {
|
|
26
|
-
file: string;
|
|
27
|
-
line: number;
|
|
28
|
-
col: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Which coverage tiers are live on this device. UIs gray out / banner what a
|
|
33
|
-
* runtime can't see (e.g. bridge call-ins don't exist on bridgeless).
|
|
34
|
-
*/
|
|
35
|
-
export interface JsTopTiers {
|
|
36
|
-
/** Tier 1: timers/rAF/queueMicrotask — always true once the engine runs. */
|
|
37
|
-
timers: boolean;
|
|
38
|
-
/** Tier 1.5: Promise.prototype reactions (async/await bodies excluded). */
|
|
39
|
-
promises: boolean;
|
|
40
|
-
/** Tier 2: legacy-bridge call-ins (absent on New Architecture). */
|
|
41
|
-
bridge: boolean;
|
|
42
|
-
/** PerformanceObserver('longtask') available on this runtime. */
|
|
43
|
-
longtask: boolean;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** One row of the live table — a task origin ("process" in Task Manager terms). */
|
|
47
|
-
export interface JsTopRow {
|
|
48
|
-
/** Stable identity (cheap or refined origin key). */
|
|
49
|
-
key: string;
|
|
50
|
-
/** Human label, e.g. `setInterval pollFeed` or `Promise.then @ api.ts:88`. */
|
|
51
|
-
label: string;
|
|
52
|
-
api: TaskApi;
|
|
53
|
-
/** Present once the origin was promoted and a scheduling site resolved. */
|
|
54
|
-
file?: string;
|
|
55
|
-
line?: number;
|
|
56
|
-
/** Exclusive ms booked in the rolling window (the default sort column). */
|
|
57
|
-
windowMs: number;
|
|
58
|
-
/** windowMs as a share of measured busy time in the same window, 0–100. */
|
|
59
|
-
pctOfBusy: number;
|
|
60
|
-
/** Exclusive ms since engine start / last clear. */
|
|
61
|
-
totalMs: number;
|
|
62
|
-
calls: number;
|
|
63
|
-
avgMs: number;
|
|
64
|
-
maxMs: number;
|
|
65
|
-
lastSeenAgoMs: number;
|
|
66
|
-
/** 50ms+ freezes attributed to this origin in the last 60s. */
|
|
67
|
-
freezes: number;
|
|
68
|
-
/** Worst attributed freeze duration in that window (0 when none). */
|
|
69
|
-
worstFreezeMs: number;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** A 50ms+ blocking task reported by the runtime, with our best attribution. */
|
|
73
|
-
export interface JsTopLongtask {
|
|
74
|
-
/** performance.now()-based start of the block. */
|
|
75
|
-
start: number;
|
|
76
|
-
duration: number;
|
|
77
|
-
/** Wall-clock time (Date.now()) the block was reported — for "Ns ago" UI. */
|
|
78
|
-
at: number;
|
|
79
|
-
/** Label of the wrapped origin that overlapped the block most, if any. */
|
|
80
|
-
originLabel: string | null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** The JSON-serializable live snapshot (device UI + external-sync payload). */
|
|
84
|
-
export interface JsTopSnapshot {
|
|
85
|
-
ts: number;
|
|
86
|
-
paused: boolean;
|
|
87
|
-
/** Thread busy% over the rolling window, 0–100. */
|
|
88
|
-
busyPct: number;
|
|
89
|
-
/** Per-bucket busy% history, oldest→newest, ≤120 points (30s @ 250ms). */
|
|
90
|
-
busyHistory: number[];
|
|
91
|
-
tiers: JsTopTiers;
|
|
92
|
-
/** Top origins by windowMs, plus pinned "unattributed"/"other" rows. */
|
|
93
|
-
rows: JsTopRow[];
|
|
94
|
-
totals: {
|
|
95
|
-
attributedWindowMs: number;
|
|
96
|
-
unattributedWindowMs: number;
|
|
97
|
-
/** Wall span of the rolling window actually covered, in ms. */
|
|
98
|
-
windowMs: number;
|
|
99
|
-
};
|
|
100
|
-
/** Freezes (50ms+ blocks) over the trailing 60s — header summary chip. */
|
|
101
|
-
freezeSummary: { count: number; worstMs: number };
|
|
102
|
-
longtasks: JsTopLongtask[];
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export type JsTopStatus = "disabled" | "running" | "paused";
|
|
106
|
-
|
|
107
|
-
/** Everything the engine knows about one origin — the tap-through detail. */
|
|
108
|
-
export interface JsTopOriginDetail {
|
|
109
|
-
key: string;
|
|
110
|
-
label: string;
|
|
111
|
-
api: TaskApi;
|
|
112
|
-
/** Scheduling site (present once the origin was promoted + captured). */
|
|
113
|
-
caller?: string;
|
|
114
|
-
file?: string;
|
|
115
|
-
line?: number;
|
|
116
|
-
frames?: OriginStackFrame[];
|
|
117
|
-
promoted: boolean;
|
|
118
|
-
/** Times a callback was scheduled from this site. */
|
|
119
|
-
scheduleCount: number;
|
|
120
|
-
/** Times a callback actually ran. */
|
|
121
|
-
calls: number;
|
|
122
|
-
totalMs: number;
|
|
123
|
-
avgMs: number;
|
|
124
|
-
maxMs: number;
|
|
125
|
-
windowMs: number;
|
|
126
|
-
pctOfBusy: number;
|
|
127
|
-
lastSeenAgoMs: number;
|
|
128
|
-
/** Exclusive ms per 250ms bucket, oldest→newest (the 5s trend). */
|
|
129
|
-
buckets: number[];
|
|
130
|
-
freezes: number;
|
|
131
|
-
worstFreezeMs: number;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Engine tuning constants — exported for tests. */
|
|
135
|
-
export const BUCKET_MS = 250;
|
|
136
|
-
/** Rolling "recent" window: 20 buckets = 5s. */
|
|
137
|
-
export const WINDOW_BUCKETS = 20;
|
|
138
|
-
/** Global busy history ring: 120 buckets = 30s. */
|
|
139
|
-
export const HISTORY_BUCKETS = 120;
|
|
140
|
-
export const MAX_ORIGINS = 500;
|
|
141
|
-
/** Promotion thresholds: capture a scheduling stack once an origin is hot. */
|
|
142
|
-
export const PROMOTE_MIN_CALLS = 25;
|
|
143
|
-
export const PROMOTE_MIN_TOTAL_MS = 50;
|
|
144
|
-
export const SNAPSHOT_ROWS = 30;
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Persisted JS TOP preferences. Same load-then-save contract as
|
|
3
|
-
* perf-monitor's hudPreferences: rehydrate async, and only persist
|
|
4
|
-
* user-driven changes after the load resolves so defaults never clobber
|
|
5
|
-
* last session's state.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { persistentStorage } from "@buoy-gg/shared-ui";
|
|
9
|
-
|
|
10
|
-
const KEY = "@react_buoy/js-top/prefs";
|
|
11
|
-
|
|
12
|
-
interface JsTopPrefs {
|
|
13
|
-
enabled: boolean;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const DEFAULTS: JsTopPrefs = { enabled: false };
|
|
17
|
-
|
|
18
|
-
export async function loadJsTopPrefs(): Promise<JsTopPrefs> {
|
|
19
|
-
try {
|
|
20
|
-
const raw = await persistentStorage.getItem(KEY);
|
|
21
|
-
if (!raw) return DEFAULTS;
|
|
22
|
-
const parsed = JSON.parse(raw) as Partial<JsTopPrefs>;
|
|
23
|
-
return { enabled: parsed.enabled === true };
|
|
24
|
-
} catch {
|
|
25
|
-
return DEFAULTS;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export async function saveJsTopPrefs(prefs: JsTopPrefs): Promise<void> {
|
|
30
|
-
try {
|
|
31
|
-
await persistentStorage.setItem(KEY, JSON.stringify(prefs));
|
|
32
|
-
} catch {
|
|
33
|
-
// Storage unavailable — non-fatal.
|
|
34
|
-
}
|
|
35
|
-
}
|
package/src/preset.tsx
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pre-configured JS TOP tools for FloatingDevTools.
|
|
3
|
-
*
|
|
4
|
-
* Two presets:
|
|
5
|
-
* - jsTopPreset: toggle-only chip that turns the floating busy% pill on/off.
|
|
6
|
-
* - jsTopModalPreset: opens the Task Manager window (ranked live table).
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* ```tsx
|
|
10
|
-
* import { jsTopPreset, jsTopModalPreset } from '@buoy-gg/js-top';
|
|
11
|
-
*
|
|
12
|
-
* <FloatingDevTools apps={[jsTopPreset, jsTopModalPreset]} />
|
|
13
|
-
* ```
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { useEffect, useState } from "react";
|
|
17
|
-
import { Activity } from "@buoy-gg/shared-ui";
|
|
18
|
-
import { JsTopController } from "./js-top/engine/JsTopController";
|
|
19
|
-
import { JsTopModal } from "./js-top/components/JsTopModal";
|
|
20
|
-
|
|
21
|
-
export const JS_TOP_ENABLED_COLOR = "#34D399";
|
|
22
|
-
export const JS_TOP_DISABLED_COLOR = "#6B7280";
|
|
23
|
-
|
|
24
|
-
function JsTopToggleIcon({ size }: { size: number }) {
|
|
25
|
-
const [enabled, setEnabled] = useState(() => JsTopController.isEnabled());
|
|
26
|
-
useEffect(() => JsTopController.subscribeEnabled(setEnabled), []);
|
|
27
|
-
return (
|
|
28
|
-
<Activity
|
|
29
|
-
size={size}
|
|
30
|
-
color={enabled ? JS_TOP_ENABLED_COLOR : JS_TOP_DISABLED_COLOR}
|
|
31
|
-
/>
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function EmptyComponent() {
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Toggle-only preset: show/hide the floating busy% pill. */
|
|
40
|
-
export const jsTopPreset = {
|
|
41
|
-
id: "js-top",
|
|
42
|
-
name: "JS TOP",
|
|
43
|
-
description: "Toggle the floating JS-thread busy pill",
|
|
44
|
-
slot: "row" as const,
|
|
45
|
-
icon: JsTopToggleIcon,
|
|
46
|
-
component: EmptyComponent,
|
|
47
|
-
props: {},
|
|
48
|
-
launchMode: "toggle-only" as const,
|
|
49
|
-
onPress: () => {
|
|
50
|
-
JsTopController.setEnabled(!JsTopController.isEnabled());
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
/** Modal preset: the live Task Manager window. */
|
|
55
|
-
export const jsTopModalPreset = {
|
|
56
|
-
id: "js-top-modal",
|
|
57
|
-
name: "TASKS",
|
|
58
|
-
description: "Live Task Manager for the JS thread",
|
|
59
|
-
slot: "both" as const,
|
|
60
|
-
icon: ({ size }: { size: number }) => (
|
|
61
|
-
<Activity size={size} color={JS_TOP_ENABLED_COLOR} />
|
|
62
|
-
),
|
|
63
|
-
component: JsTopModal,
|
|
64
|
-
props: {
|
|
65
|
-
enableSharedModalDimensions: false,
|
|
66
|
-
},
|
|
67
|
-
};
|