@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.
@@ -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
- }
@@ -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
- }