@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
package/package.json
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buoy-gg/js-top",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.30",
|
|
4
4
|
"description": "Live Task Manager for the React Native JS thread — see which timers, Promises, and callbacks are eating your JS FPS, ranked in real time. Part of Buoy devtools.",
|
|
5
|
-
"main": "lib/commonjs/index",
|
|
6
|
-
"module": "lib/module/index",
|
|
5
|
+
"main": "lib/commonjs/index.js",
|
|
6
|
+
"module": "lib/module/index.js",
|
|
7
7
|
"types": "lib/typescript/index.d.ts",
|
|
8
|
-
"react-native": "
|
|
8
|
+
"react-native": "lib/module/index.js",
|
|
9
9
|
"source": "src/index.tsx",
|
|
10
10
|
"files": [
|
|
11
|
-
"src",
|
|
12
11
|
"lib"
|
|
13
12
|
],
|
|
14
13
|
"sideEffects": false,
|
|
15
14
|
"dependencies": {
|
|
16
|
-
"@buoy-gg/shared-ui": "^7.0.
|
|
15
|
+
"@buoy-gg/shared-ui": "^7.0.30"
|
|
17
16
|
},
|
|
18
17
|
"peerDependencies": {
|
|
19
18
|
"react": "*",
|
package/src/index.tsx
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @buoy-gg/js-top — live Task Manager for the React Native JS thread.
|
|
3
|
-
*
|
|
4
|
-
* Pure JS (Expo Go + release compatible): wraps timers / rAF / microtasks /
|
|
5
|
-
* Promise reactions / legacy-bridge call-ins and books exclusive time per
|
|
6
|
-
* scheduling origin; a calibrated busy probe measures true thread occupancy
|
|
7
|
-
* so unattributable work is surfaced honestly instead of hidden.
|
|
8
|
-
*
|
|
9
|
-
* Coverage tiers beyond the core (promises, bridge, longtask correlation)
|
|
10
|
-
* are registered here at module scope — the controller installs them lazily
|
|
11
|
-
* on first engine start, so importing this package costs nothing until a
|
|
12
|
-
* viewer opens the tool.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { JsTopController } from "./js-top/engine/JsTopController";
|
|
16
|
-
import { installPromisePatches } from "./js-top/engine/patches/promises";
|
|
17
|
-
import { installBridgePatch } from "./js-top/engine/patches/bridge";
|
|
18
|
-
import { installTimerPatches } from "./js-top/engine/patches/timers";
|
|
19
|
-
import { installMicrotaskPatches } from "./js-top/engine/patches/microtasks";
|
|
20
|
-
import {
|
|
21
|
-
clearLongtasks,
|
|
22
|
-
getFreezeStats,
|
|
23
|
-
getLongtasks,
|
|
24
|
-
installLongtaskObserver,
|
|
25
|
-
} from "./js-top/engine/longtasks";
|
|
26
|
-
|
|
27
|
-
JsTopController.registerTierInstaller(installPromisePatches);
|
|
28
|
-
JsTopController.registerTierInstaller(installBridgePatch);
|
|
29
|
-
JsTopController.registerTierInstaller(installLongtaskObserver);
|
|
30
|
-
JsTopController.registerLongtaskProvider(getLongtasks);
|
|
31
|
-
JsTopController.registerFreezeStatsProvider(getFreezeStats);
|
|
32
|
-
JsTopController.registerClearHook(clearLongtasks);
|
|
33
|
-
|
|
34
|
-
// Timer/microtask patches install at import so callbacks scheduled at APP
|
|
35
|
-
// BOOT — the classic forgotten setInterval, the tool's #1 target — are
|
|
36
|
-
// wrapped and attributable the moment any viewer opens the tool. Wrappers
|
|
37
|
-
// resolve their origin lazily and cost one boolean per fire while the
|
|
38
|
-
// engine is off. (Promise/bridge patches are more invasive and stay lazy —
|
|
39
|
-
// they install on first engine start and cover work scheduled after it.)
|
|
40
|
-
try {
|
|
41
|
-
installTimerPatches();
|
|
42
|
-
installMicrotaskPatches();
|
|
43
|
-
} catch {
|
|
44
|
-
// Never let instrumentation break an app at import time.
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export { JsTopController } from "./js-top/engine/JsTopController";
|
|
48
|
-
export { jsTopSyncAdapter } from "./js-top/sync/jsTopSyncAdapter";
|
|
49
|
-
export { JsTopModal } from "./js-top/components/JsTopModal";
|
|
50
|
-
export type {
|
|
51
|
-
JsTopModalProps,
|
|
52
|
-
JsTopRemoteActions,
|
|
53
|
-
} from "./js-top/components/JsTopModal";
|
|
54
|
-
export { JsTopHudOverlay } from "./js-top/components/JsTopHudOverlay";
|
|
55
|
-
export {
|
|
56
|
-
jsTopPreset,
|
|
57
|
-
jsTopModalPreset,
|
|
58
|
-
JS_TOP_ENABLED_COLOR,
|
|
59
|
-
JS_TOP_DISABLED_COLOR,
|
|
60
|
-
} from "./preset";
|
|
61
|
-
export type {
|
|
62
|
-
JsTopSnapshot,
|
|
63
|
-
JsTopRow,
|
|
64
|
-
JsTopLongtask,
|
|
65
|
-
JsTopOriginDetail,
|
|
66
|
-
JsTopTiers,
|
|
67
|
-
JsTopStatus,
|
|
68
|
-
TaskApi,
|
|
69
|
-
} from "./js-top/types";
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Busy% history sparkline — plain View bars (no SVG dependency), tinted by
|
|
3
|
-
* severity. Renders at most the trailing 60 buckets (15s).
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { memo } from "react";
|
|
7
|
-
import { StyleSheet, View } from "react-native";
|
|
8
|
-
import { macOSColors } from "@buoy-gg/shared-ui";
|
|
9
|
-
|
|
10
|
-
const MAX_POINTS = 60;
|
|
11
|
-
|
|
12
|
-
function barColor(pct: number): string {
|
|
13
|
-
if (pct >= 80) return macOSColors.semantic.error;
|
|
14
|
-
if (pct >= 40) return macOSColors.semantic.warning;
|
|
15
|
-
return macOSColors.semantic.success;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const BusySparkline = memo(function BusySparkline({
|
|
19
|
-
history,
|
|
20
|
-
}: {
|
|
21
|
-
history: number[];
|
|
22
|
-
}) {
|
|
23
|
-
const points = history.slice(-MAX_POINTS);
|
|
24
|
-
return (
|
|
25
|
-
<View style={styles.row} pointerEvents="none">
|
|
26
|
-
{points.map((pct, i) => (
|
|
27
|
-
<View
|
|
28
|
-
// Bars are positional by nature (a scrolling time axis); the array
|
|
29
|
-
// is fixed-length and rewritten wholesale each tick, so positional
|
|
30
|
-
// keys are correct here — this is not a prepend-list.
|
|
31
|
-
key={`b${i}`}
|
|
32
|
-
style={[
|
|
33
|
-
styles.bar,
|
|
34
|
-
{
|
|
35
|
-
height: Math.max(2, (pct / 100) * 28),
|
|
36
|
-
backgroundColor: barColor(pct),
|
|
37
|
-
},
|
|
38
|
-
]}
|
|
39
|
-
/>
|
|
40
|
-
))}
|
|
41
|
-
</View>
|
|
42
|
-
);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
const styles = StyleSheet.create({
|
|
46
|
-
row: {
|
|
47
|
-
flexDirection: "row",
|
|
48
|
-
alignItems: "flex-end",
|
|
49
|
-
gap: 1,
|
|
50
|
-
height: 28,
|
|
51
|
-
alignSelf: "stretch",
|
|
52
|
-
},
|
|
53
|
-
bar: {
|
|
54
|
-
flex: 1,
|
|
55
|
-
maxWidth: 6,
|
|
56
|
-
borderRadius: 1,
|
|
57
|
-
opacity: 0.9,
|
|
58
|
-
},
|
|
59
|
-
});
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal floating HUD pill: busy% + the current top offender. Rendered by
|
|
3
|
-
* FloatingDevTools (guarded require) when the JS TOP toggle chip is on —
|
|
4
|
-
* same auto-mount pattern as the perf-monitor overlay. Display-only in v1
|
|
5
|
-
* (no drag; sits under the status bar on the left).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { useEffect, useState } from "react";
|
|
9
|
-
import { StyleSheet, Text, View } from "react-native";
|
|
10
|
-
import { macOSColors, night } from "@buoy-gg/shared-ui";
|
|
11
|
-
import type { JsTopSnapshot } from "../types";
|
|
12
|
-
import { JsTopController } from "../engine/JsTopController";
|
|
13
|
-
|
|
14
|
-
function busyColor(pct: number): string {
|
|
15
|
-
if (pct >= 80) return macOSColors.semantic.error;
|
|
16
|
-
if (pct >= 40) return macOSColors.semantic.warning;
|
|
17
|
-
return macOSColors.semantic.success;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function JsTopHudOverlay() {
|
|
21
|
-
const [enabled, setEnabled] = useState(() => JsTopController.isEnabled());
|
|
22
|
-
const [snapshot, setSnapshot] = useState<JsTopSnapshot | null>(null);
|
|
23
|
-
|
|
24
|
-
useEffect(() => JsTopController.subscribeEnabled(setEnabled), []);
|
|
25
|
-
|
|
26
|
-
useEffect(() => {
|
|
27
|
-
if (!enabled) {
|
|
28
|
-
setSnapshot(null);
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
return JsTopController.subscribe(setSnapshot);
|
|
32
|
-
}, [enabled]);
|
|
33
|
-
|
|
34
|
-
if (!enabled || !snapshot) return null;
|
|
35
|
-
|
|
36
|
-
const top = snapshot.rows.find((r) => r.api !== "unattributed");
|
|
37
|
-
|
|
38
|
-
return (
|
|
39
|
-
<View style={styles.pill} pointerEvents="none">
|
|
40
|
-
<Text style={[styles.busy, { color: busyColor(snapshot.busyPct) }]}>
|
|
41
|
-
{snapshot.busyPct.toFixed(0)}%
|
|
42
|
-
</Text>
|
|
43
|
-
{top && (
|
|
44
|
-
<Text style={styles.label} numberOfLines={1}>
|
|
45
|
-
{top.label}
|
|
46
|
-
</Text>
|
|
47
|
-
)}
|
|
48
|
-
</View>
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const styles = StyleSheet.create({
|
|
53
|
-
pill: {
|
|
54
|
-
position: "absolute",
|
|
55
|
-
top: 60,
|
|
56
|
-
left: 8,
|
|
57
|
-
flexDirection: "row",
|
|
58
|
-
alignItems: "center",
|
|
59
|
-
gap: 6,
|
|
60
|
-
backgroundColor: night.color.buttonSurface,
|
|
61
|
-
borderWidth: 1,
|
|
62
|
-
borderColor: night.color.border,
|
|
63
|
-
borderRadius: 6,
|
|
64
|
-
paddingHorizontal: 8,
|
|
65
|
-
paddingVertical: 4,
|
|
66
|
-
maxWidth: 220,
|
|
67
|
-
},
|
|
68
|
-
busy: {
|
|
69
|
-
fontSize: 11,
|
|
70
|
-
fontWeight: "700",
|
|
71
|
-
fontVariant: ["tabular-nums"],
|
|
72
|
-
},
|
|
73
|
-
label: {
|
|
74
|
-
color: night.color.textSecondary,
|
|
75
|
-
fontSize: 10,
|
|
76
|
-
flexShrink: 1,
|
|
77
|
-
},
|
|
78
|
-
});
|
|
@@ -1,426 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* JS TOP modal — the Task Manager window.
|
|
3
|
-
*
|
|
4
|
-
* Layout: header (busy% pill + pause/clear controls) → busy sparkline →
|
|
5
|
-
* coverage banner (when bridge tier is unavailable) → ranked task table →
|
|
6
|
-
* blocking-task list.
|
|
7
|
-
*
|
|
8
|
-
* Data source is the local JsTopController by default. The desktop
|
|
9
|
-
* dashboard mounts this same component via react-native-web and injects the
|
|
10
|
-
* synced device snapshot + remote actions through `remoteSnapshot` /
|
|
11
|
-
* `remoteActions` — the modal never samples the Electron renderer.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
15
|
-
import {
|
|
16
|
-
StyleSheet,
|
|
17
|
-
Text,
|
|
18
|
-
TouchableOpacity,
|
|
19
|
-
View,
|
|
20
|
-
} from "react-native";
|
|
21
|
-
import {
|
|
22
|
-
Activity,
|
|
23
|
-
JsModal,
|
|
24
|
-
macOSColors,
|
|
25
|
-
night,
|
|
26
|
-
ModalHeader,
|
|
27
|
-
Pause,
|
|
28
|
-
Play,
|
|
29
|
-
Trash2,
|
|
30
|
-
type ModalState,
|
|
31
|
-
} from "@buoy-gg/shared-ui";
|
|
32
|
-
import type { JsTopSnapshot } from "../types";
|
|
33
|
-
import type { JsTopRow } from "../types";
|
|
34
|
-
import { JsTopController } from "../engine/JsTopController";
|
|
35
|
-
import { BusySparkline } from "./BusySparkline";
|
|
36
|
-
import { TaskTable } from "./TaskTable";
|
|
37
|
-
import { TaskDetailView } from "./TaskDetailView";
|
|
38
|
-
|
|
39
|
-
export interface JsTopRemoteActions {
|
|
40
|
-
pause: () => void;
|
|
41
|
-
resume: () => void;
|
|
42
|
-
clear: () => void;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface JsTopModalProps {
|
|
46
|
-
visible: boolean;
|
|
47
|
-
onClose: () => void;
|
|
48
|
-
/** App-stack back (from the floating menu's AppHost). */
|
|
49
|
-
onBack?: () => void;
|
|
50
|
-
onMinimize?: (modalState: ModalState) => void;
|
|
51
|
-
initialModalState?: ModalState;
|
|
52
|
-
minimizeTargetPosition?: { x: number; y: number };
|
|
53
|
-
enableSharedModalDimensions?: boolean;
|
|
54
|
-
/** Desktop dashboard: render from a synced device snapshot. */
|
|
55
|
-
remoteSnapshot?: JsTopSnapshot | null;
|
|
56
|
-
remoteActions?: JsTopRemoteActions;
|
|
57
|
-
/** Desktop Workbench: render inline, parent supplies window chrome. */
|
|
58
|
-
embedded?: boolean;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function busyColor(pct: number): string {
|
|
62
|
-
if (pct >= 80) return macOSColors.semantic.error;
|
|
63
|
-
if (pct >= 40) return macOSColors.semantic.warning;
|
|
64
|
-
return macOSColors.semantic.success;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** ≥100ms is a dropped-tap-level freeze — escalate to red. */
|
|
68
|
-
function freezeColor(worstMs: number): string {
|
|
69
|
-
return worstMs >= 100
|
|
70
|
-
? macOSColors.semantic.error
|
|
71
|
-
: macOSColors.semantic.warning;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* The one-line answer to "what should I look at?" — rendered above the
|
|
76
|
-
* table so triage never requires reading it. Priority: a named culprit >
|
|
77
|
-
* freezes > saturated-by-UI-work > healthy.
|
|
78
|
-
*/
|
|
79
|
-
function diagnose(s: JsTopSnapshot): {
|
|
80
|
-
color: string;
|
|
81
|
-
icon: string;
|
|
82
|
-
text: string;
|
|
83
|
-
} {
|
|
84
|
-
const culprit = s.rows.find(
|
|
85
|
-
(r) => r.api !== "unattributed" && r.api !== "other",
|
|
86
|
-
);
|
|
87
|
-
if (culprit && culprit.pctOfBusy >= 25 && s.busyPct >= 15) {
|
|
88
|
-
return {
|
|
89
|
-
color:
|
|
90
|
-
culprit.pctOfBusy >= 50
|
|
91
|
-
? macOSColors.semantic.error
|
|
92
|
-
: macOSColors.semantic.warning,
|
|
93
|
-
icon: "▲",
|
|
94
|
-
text: `${culprit.label} is eating ${Math.round(culprit.pctOfBusy)}% of thread time — start there.`,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
if (s.freezeSummary.count > 0) {
|
|
98
|
-
return {
|
|
99
|
-
color: freezeColor(s.freezeSummary.worstMs),
|
|
100
|
-
icon: "▲",
|
|
101
|
-
text: `${s.freezeSummary.count} freeze${s.freezeSummary.count === 1 ? "" : "s"} in the last 60s (worst ${s.freezeSummary.worstMs}ms) — check rows marked ⚠.`,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
if (s.busyPct >= 80) {
|
|
105
|
-
return {
|
|
106
|
-
color: macOSColors.semantic.warning,
|
|
107
|
-
icon: "▲",
|
|
108
|
-
text: "Thread saturated by UI/React work — open Highlight Updates to find re-renders.",
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
return {
|
|
112
|
-
color: macOSColors.semantic.success,
|
|
113
|
-
icon: "●",
|
|
114
|
-
text: `Thread healthy at ${Math.round(s.busyPct)}% load — nothing worth chasing.`,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function JsTopModal({
|
|
119
|
-
visible,
|
|
120
|
-
onClose,
|
|
121
|
-
onBack,
|
|
122
|
-
onMinimize,
|
|
123
|
-
initialModalState,
|
|
124
|
-
minimizeTargetPosition,
|
|
125
|
-
remoteSnapshot,
|
|
126
|
-
remoteActions,
|
|
127
|
-
embedded = false,
|
|
128
|
-
}: JsTopModalProps) {
|
|
129
|
-
const isRemote = remoteSnapshot !== undefined;
|
|
130
|
-
const [localSnapshot, setLocalSnapshot] = useState<JsTopSnapshot | null>(
|
|
131
|
-
null,
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
// Local mode: subscribing is what starts the engine (and unsubscribing on
|
|
135
|
-
// close is what stops it — zero overhead when the window isn't open).
|
|
136
|
-
useEffect(() => {
|
|
137
|
-
if (isRemote || !visible) return;
|
|
138
|
-
return JsTopController.subscribe(setLocalSnapshot);
|
|
139
|
-
}, [isRemote, visible]);
|
|
140
|
-
|
|
141
|
-
const snapshot = isRemote ? remoteSnapshot : localSnapshot;
|
|
142
|
-
|
|
143
|
-
const actions = useMemo<JsTopRemoteActions>(
|
|
144
|
-
() =>
|
|
145
|
-
remoteActions ?? {
|
|
146
|
-
pause: () => JsTopController.pause(),
|
|
147
|
-
resume: () => JsTopController.resume(),
|
|
148
|
-
clear: () => JsTopController.clear(),
|
|
149
|
-
},
|
|
150
|
-
[remoteActions],
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
const paused = snapshot?.paused ?? false;
|
|
154
|
-
const hasData = (snapshot?.rows.length ?? 0) > 0;
|
|
155
|
-
|
|
156
|
-
// Tap-through detail. Local mode re-pulls the detail on every snapshot
|
|
157
|
-
// tick (this component re-renders), so the detail view stays live.
|
|
158
|
-
// Remote mode (desktop) has no local engine — rows aren't tappable there.
|
|
159
|
-
const [detailKey, setDetailKey] = useState<string | null>(null);
|
|
160
|
-
const onSelectRow = useCallback(
|
|
161
|
-
(row: JsTopRow) => setDetailKey(row.key),
|
|
162
|
-
[],
|
|
163
|
-
);
|
|
164
|
-
const detail =
|
|
165
|
-
!isRemote && detailKey ? JsTopController.getOriginDetail(detailKey) : null;
|
|
166
|
-
const inDetail = detailKey !== null;
|
|
167
|
-
|
|
168
|
-
// Shared ModalHeader compound — same structure as Network/Events/Console:
|
|
169
|
-
// Content (brand icon + title + live busy pill) and 32px icon Actions.
|
|
170
|
-
// In the detail view the chevron pops back to the table.
|
|
171
|
-
const headerContent = (
|
|
172
|
-
<ModalHeader>
|
|
173
|
-
{inDetail ? (
|
|
174
|
-
<ModalHeader.Navigation onBack={() => setDetailKey(null)} />
|
|
175
|
-
) : onBack ? (
|
|
176
|
-
<ModalHeader.Navigation onBack={onBack} />
|
|
177
|
-
) : null}
|
|
178
|
-
<ModalHeader.Content title="" noMargin>
|
|
179
|
-
<View style={styles.titleRow}>
|
|
180
|
-
<Activity size={14} color="#34D399" />
|
|
181
|
-
<Text style={styles.title}>JS Top</Text>
|
|
182
|
-
{snapshot && (
|
|
183
|
-
<View
|
|
184
|
-
style={[
|
|
185
|
-
styles.busyPill,
|
|
186
|
-
{ borderColor: busyColor(snapshot.busyPct) },
|
|
187
|
-
]}
|
|
188
|
-
>
|
|
189
|
-
<Text
|
|
190
|
-
style={[
|
|
191
|
-
styles.busyText,
|
|
192
|
-
{ color: busyColor(snapshot.busyPct) },
|
|
193
|
-
]}
|
|
194
|
-
>
|
|
195
|
-
{snapshot.busyPct.toFixed(0)}% BUSY
|
|
196
|
-
</Text>
|
|
197
|
-
</View>
|
|
198
|
-
)}
|
|
199
|
-
{snapshot && snapshot.freezeSummary.count > 0 && (
|
|
200
|
-
<View
|
|
201
|
-
style={[
|
|
202
|
-
styles.busyPill,
|
|
203
|
-
{ borderColor: freezeColor(snapshot.freezeSummary.worstMs) },
|
|
204
|
-
]}
|
|
205
|
-
>
|
|
206
|
-
<Text
|
|
207
|
-
style={[
|
|
208
|
-
styles.busyText,
|
|
209
|
-
{ color: freezeColor(snapshot.freezeSummary.worstMs) },
|
|
210
|
-
]}
|
|
211
|
-
>
|
|
212
|
-
⚠ {snapshot.freezeSummary.count} ·{" "}
|
|
213
|
-
{snapshot.freezeSummary.worstMs}ms
|
|
214
|
-
</Text>
|
|
215
|
-
</View>
|
|
216
|
-
)}
|
|
217
|
-
</View>
|
|
218
|
-
</ModalHeader.Content>
|
|
219
|
-
<ModalHeader.Actions>
|
|
220
|
-
<TouchableOpacity
|
|
221
|
-
onPress={paused ? actions.resume : actions.pause}
|
|
222
|
-
style={[styles.actionBtn, paused && styles.actionBtnActive]}
|
|
223
|
-
accessibilityLabel={paused ? "Resume sampling" : "Pause sampling"}
|
|
224
|
-
>
|
|
225
|
-
{paused ? (
|
|
226
|
-
<Play size={14} color={night.color.accent} />
|
|
227
|
-
) : (
|
|
228
|
-
<Pause size={14} color={night.color.textSecondary} />
|
|
229
|
-
)}
|
|
230
|
-
</TouchableOpacity>
|
|
231
|
-
<TouchableOpacity
|
|
232
|
-
onPress={actions.clear}
|
|
233
|
-
disabled={!hasData}
|
|
234
|
-
style={[styles.actionBtn, !hasData && styles.actionBtnDisabled]}
|
|
235
|
-
accessibilityLabel="Clear stats"
|
|
236
|
-
>
|
|
237
|
-
<Trash2
|
|
238
|
-
size={14}
|
|
239
|
-
color={
|
|
240
|
-
hasData ? night.color.textTertiary : night.color.textTertiary
|
|
241
|
-
}
|
|
242
|
-
/>
|
|
243
|
-
</TouchableOpacity>
|
|
244
|
-
</ModalHeader.Actions>
|
|
245
|
-
</ModalHeader>
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
const body = (
|
|
249
|
-
<View style={styles.body}>
|
|
250
|
-
{inDetail ? (
|
|
251
|
-
detail ? (
|
|
252
|
-
<TaskDetailView detail={detail} />
|
|
253
|
-
) : (
|
|
254
|
-
<Text style={styles.waiting}>
|
|
255
|
-
This origin is no longer tracked (cleared or evicted).
|
|
256
|
-
</Text>
|
|
257
|
-
)
|
|
258
|
-
) : snapshot ? (
|
|
259
|
-
<>
|
|
260
|
-
{(() => {
|
|
261
|
-
const d = diagnose(snapshot);
|
|
262
|
-
return (
|
|
263
|
-
<View style={[styles.diagnosis, { borderLeftColor: d.color }]}>
|
|
264
|
-
<Text style={[styles.diagnosisIcon, { color: d.color }]}>
|
|
265
|
-
{d.icon}
|
|
266
|
-
</Text>
|
|
267
|
-
<Text style={styles.diagnosisText}>{d.text}</Text>
|
|
268
|
-
</View>
|
|
269
|
-
);
|
|
270
|
-
})()}
|
|
271
|
-
<View style={styles.sparklineRow}>
|
|
272
|
-
<Text style={styles.sparklineCaption}>
|
|
273
|
-
JS THREAD LOAD · LAST 30s
|
|
274
|
-
</Text>
|
|
275
|
-
<BusySparkline history={snapshot.busyHistory} />
|
|
276
|
-
</View>
|
|
277
|
-
{!snapshot.tiers.bridge && (
|
|
278
|
-
<Text style={styles.tierBanner}>
|
|
279
|
-
“System” rows are UI/React/native work pure JS can’t name —
|
|
280
|
-
timers, rAF and Promise chains are fully attributed.
|
|
281
|
-
</Text>
|
|
282
|
-
)}
|
|
283
|
-
<TaskTable
|
|
284
|
-
rows={snapshot.rows}
|
|
285
|
-
onSelectRow={isRemote ? undefined : onSelectRow}
|
|
286
|
-
/>
|
|
287
|
-
</>
|
|
288
|
-
) : (
|
|
289
|
-
<Text style={styles.waiting}>
|
|
290
|
-
{isRemote ? "Waiting for device…" : "Starting engine…"}
|
|
291
|
-
</Text>
|
|
292
|
-
)}
|
|
293
|
-
</View>
|
|
294
|
-
);
|
|
295
|
-
|
|
296
|
-
if (embedded) {
|
|
297
|
-
return (
|
|
298
|
-
<View style={styles.embeddedRoot}>
|
|
299
|
-
<View style={styles.embeddedHeader}>{headerContent}</View>
|
|
300
|
-
{body}
|
|
301
|
-
</View>
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return (
|
|
306
|
-
<JsModal
|
|
307
|
-
visible={visible}
|
|
308
|
-
onClose={onClose}
|
|
309
|
-
onMinimize={onMinimize}
|
|
310
|
-
minimizeTargetPosition={minimizeTargetPosition}
|
|
311
|
-
initialModalState={initialModalState}
|
|
312
|
-
header={{ showToggleButton: true, customContent: headerContent }}
|
|
313
|
-
persistenceKey="buoy-js-top-modal"
|
|
314
|
-
enablePersistence={true}
|
|
315
|
-
initialMode="bottomSheet"
|
|
316
|
-
disableScrollWrapper
|
|
317
|
-
>
|
|
318
|
-
{body}
|
|
319
|
-
</JsModal>
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const styles = StyleSheet.create({
|
|
324
|
-
titleRow: {
|
|
325
|
-
flexDirection: "row",
|
|
326
|
-
alignItems: "center",
|
|
327
|
-
gap: 8,
|
|
328
|
-
},
|
|
329
|
-
title: {
|
|
330
|
-
color: night.color.text,
|
|
331
|
-
fontSize: 14,
|
|
332
|
-
fontWeight: "500",
|
|
333
|
-
},
|
|
334
|
-
busyPill: {
|
|
335
|
-
borderWidth: 1,
|
|
336
|
-
borderRadius: 4,
|
|
337
|
-
paddingHorizontal: 6,
|
|
338
|
-
paddingVertical: 2,
|
|
339
|
-
},
|
|
340
|
-
busyText: {
|
|
341
|
-
fontSize: 10,
|
|
342
|
-
fontWeight: "700",
|
|
343
|
-
fontVariant: ["tabular-nums"],
|
|
344
|
-
},
|
|
345
|
-
actionBtn: {
|
|
346
|
-
width: 32,
|
|
347
|
-
height: 32,
|
|
348
|
-
borderRadius: 8,
|
|
349
|
-
backgroundColor: night.color.buttonSurface,
|
|
350
|
-
borderWidth: 1,
|
|
351
|
-
borderColor: night.color.border,
|
|
352
|
-
alignItems: "center",
|
|
353
|
-
justifyContent: "center",
|
|
354
|
-
},
|
|
355
|
-
actionBtnActive: {
|
|
356
|
-
backgroundColor: night.color.accentSoft,
|
|
357
|
-
borderColor: night.color.accentBorderStrong,
|
|
358
|
-
},
|
|
359
|
-
actionBtnDisabled: {
|
|
360
|
-
opacity: 0.55,
|
|
361
|
-
},
|
|
362
|
-
body: {
|
|
363
|
-
flex: 1,
|
|
364
|
-
minHeight: 0,
|
|
365
|
-
// Transparent so JsModal's ToolBackground shows through (the modal paints
|
|
366
|
-
// opaque base beneath it). The embedded path keeps `embeddedRoot`'s base fill.
|
|
367
|
-
backgroundColor: "transparent",
|
|
368
|
-
},
|
|
369
|
-
diagnosis: {
|
|
370
|
-
flexDirection: "row",
|
|
371
|
-
alignItems: "center",
|
|
372
|
-
gap: 8,
|
|
373
|
-
marginHorizontal: 10,
|
|
374
|
-
marginTop: 8,
|
|
375
|
-
paddingHorizontal: 10,
|
|
376
|
-
paddingVertical: 8,
|
|
377
|
-
backgroundColor: night.color.surface,
|
|
378
|
-
borderRadius: 6,
|
|
379
|
-
borderLeftWidth: 3,
|
|
380
|
-
},
|
|
381
|
-
diagnosisIcon: {
|
|
382
|
-
fontSize: 11,
|
|
383
|
-
fontWeight: "700",
|
|
384
|
-
},
|
|
385
|
-
diagnosisText: {
|
|
386
|
-
flex: 1,
|
|
387
|
-
color: night.color.text,
|
|
388
|
-
fontSize: 12,
|
|
389
|
-
lineHeight: 16,
|
|
390
|
-
},
|
|
391
|
-
sparklineRow: {
|
|
392
|
-
paddingHorizontal: 10,
|
|
393
|
-
paddingVertical: 8,
|
|
394
|
-
gap: 4,
|
|
395
|
-
},
|
|
396
|
-
sparklineCaption: {
|
|
397
|
-
color: night.color.textTertiary,
|
|
398
|
-
fontSize: 8,
|
|
399
|
-
fontWeight: "700",
|
|
400
|
-
letterSpacing: 0.5,
|
|
401
|
-
},
|
|
402
|
-
tierBanner: {
|
|
403
|
-
color: night.color.textTertiary,
|
|
404
|
-
fontSize: 10,
|
|
405
|
-
paddingHorizontal: 10,
|
|
406
|
-
paddingBottom: 6,
|
|
407
|
-
},
|
|
408
|
-
waiting: {
|
|
409
|
-
color: night.color.textTertiary,
|
|
410
|
-
fontSize: 12,
|
|
411
|
-
textAlign: "center",
|
|
412
|
-
paddingVertical: 32,
|
|
413
|
-
},
|
|
414
|
-
embeddedRoot: {
|
|
415
|
-
position: "absolute",
|
|
416
|
-
top: 0,
|
|
417
|
-
left: 0,
|
|
418
|
-
right: 0,
|
|
419
|
-
bottom: 0,
|
|
420
|
-
backgroundColor: macOSColors.background.base,
|
|
421
|
-
},
|
|
422
|
-
embeddedHeader: {
|
|
423
|
-
borderBottomWidth: 1,
|
|
424
|
-
borderBottomColor: macOSColors.border.default,
|
|
425
|
-
},
|
|
426
|
-
});
|