termcast 1.5.0 → 1.7.0
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/dist/build.d.ts.map +1 -1
- package/dist/build.js +22 -5
- package/dist/build.js.map +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +7 -1
- package/dist/compile.js.map +1 -1
- package/dist/components/bar-chart.d.ts.map +1 -1
- package/dist/components/bar-chart.js +14 -3
- package/dist/components/bar-chart.js.map +1 -1
- package/dist/components/bar-graph.d.ts +4 -4
- package/dist/components/bar-graph.d.ts.map +1 -1
- package/dist/components/bar-graph.js +23 -5
- package/dist/components/bar-graph.js.map +1 -1
- package/dist/components/candle-chart.d.ts +15 -0
- package/dist/components/candle-chart.d.ts.map +1 -1
- package/dist/components/candle-chart.js +41 -3
- package/dist/components/candle-chart.js.map +1 -1
- package/dist/components/chart-tooltip.d.ts +83 -0
- package/dist/components/chart-tooltip.d.ts.map +1 -0
- package/dist/components/chart-tooltip.js +127 -0
- package/dist/components/chart-tooltip.js.map +1 -0
- package/dist/components/dotted-line-graph.d.ts +11 -0
- package/dist/components/dotted-line-graph.d.ts.map +1 -1
- package/dist/components/dotted-line-graph.js +43 -2
- package/dist/components/dotted-line-graph.js.map +1 -1
- package/dist/components/graph.d.ts +11 -0
- package/dist/components/graph.d.ts.map +1 -1
- package/dist/components/graph.js +53 -4
- package/dist/components/graph.js.map +1 -1
- package/dist/components/horizontal-bar-graph.d.ts.map +1 -1
- package/dist/components/horizontal-bar-graph.js +16 -5
- package/dist/components/horizontal-bar-graph.js.map +1 -1
- package/dist/components/list.d.ts +7 -0
- package/dist/components/list.d.ts.map +1 -1
- package/dist/components/list.js +75 -14
- package/dist/components/list.js.map +1 -1
- package/dist/examples/chart-tooltips.d.ts +2 -0
- package/dist/examples/chart-tooltips.d.ts.map +1 -0
- package/dist/examples/chart-tooltips.js +16 -0
- package/dist/examples/chart-tooltips.js.map +1 -0
- package/dist/examples/list-detail-height-ratchet.d.ts +2 -0
- package/dist/examples/list-detail-height-ratchet.d.ts.map +1 -0
- package/dist/examples/list-detail-height-ratchet.js +26 -0
- package/dist/examples/list-detail-height-ratchet.js.map +1 -0
- package/dist/extensions/dev.d.ts.map +1 -1
- package/dist/extensions/dev.js +1 -0
- package/dist/extensions/dev.js.map +1 -1
- package/dist/globals.js +8 -0
- package/dist/globals.js.map +1 -1
- package/dist/package-json.d.ts +2 -0
- package/dist/package-json.d.ts.map +1 -1
- package/dist/package-json.js +20 -17
- package/dist/package-json.js.map +1 -1
- package/dist/profiler.d.ts +2 -0
- package/dist/profiler.d.ts.map +1 -0
- package/dist/profiler.js +390 -0
- package/dist/profiler.js.map +1 -0
- package/package.json +14 -15
- package/src/build.tsx +27 -5
- package/src/cli.tsx +0 -0
- package/src/compile.tsx +9 -1
- package/src/compile.vitest.tsx +8 -8
- package/src/components/bar-chart.tsx +23 -3
- package/src/components/bar-graph.tsx +32 -13
- package/src/components/candle-chart.tsx +63 -16
- package/src/components/chart-tooltip.tsx +191 -0
- package/src/components/dotted-line-graph.tsx +49 -3
- package/src/components/graph.tsx +76 -18
- package/src/components/horizontal-bar-graph.tsx +24 -4
- package/src/components/list.tsx +93 -20
- package/src/examples/action-shortcut.vitest.tsx +4 -4
- package/src/examples/actions-context.vitest.tsx +2 -2
- package/src/examples/bar-graph-weekly.vitest.tsx +97 -97
- package/src/examples/chart-tooltips.tsx +54 -0
- package/src/examples/form-basic.vitest.tsx +8 -8
- package/src/examples/github.vitest.tsx +19 -28
- package/src/examples/graph-bar-chart.vitest.tsx +40 -40
- package/src/examples/graph-polymarket.vitest.tsx +24 -24
- package/src/examples/graph-row.vitest.tsx +8 -8
- package/src/examples/graph-styles.vitest.tsx +65 -65
- package/src/examples/horizontal-bar-graph-weekly.vitest.tsx +52 -52
- package/src/examples/list-detail-height-ratchet.tsx +48 -0
- package/src/examples/list-detail-height-ratchet.vitest.tsx +161 -0
- package/src/examples/list-detail-metadata.vitest.tsx +49 -49
- package/src/examples/list-dropdown-default.vitest.tsx +27 -27
- package/src/examples/list-fetch-data.vitest.tsx +3 -3
- package/src/examples/list-item-accessories.vitest.tsx +2 -2
- package/src/examples/list-loading-empty-view.vitest.tsx +1 -1
- package/src/examples/list-no-actions.vitest.tsx +3 -3
- package/src/examples/list-scrollbox.vitest.tsx +6 -6
- package/src/examples/list-spacing-mode.vitest.tsx +3 -3
- package/src/examples/list-with-detail.vitest.tsx +11 -11
- package/src/examples/list-with-dropdown.vitest.tsx +7 -7
- package/src/examples/list-with-sections.vitest.tsx +32 -32
- package/src/examples/list-with-toast.vitest.tsx +4 -4
- package/src/examples/simple-candle-chart.vitest.tsx +63 -61
- package/src/examples/simple-grid.vitest.tsx +13 -13
- package/src/examples/simple-navigation.vitest.tsx +25 -25
- package/src/examples/simple-progress-bar.vitest.tsx +8 -8
- package/src/examples/swift-extension.vitest.tsx +3 -3
- package/src/examples/toast-action.vitest.tsx +4 -4
- package/src/extensions/dev.tsx +2 -1
- package/src/extensions/dev.vitest.tsx +17 -17
- package/src/globals.ts +9 -0
- package/src/package-json.tsx +24 -23
- package/src/profiler.tsx +487 -0
package/dist/profiler.js
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
// React component profiler for termcast.
|
|
2
|
+
//
|
|
3
|
+
// Captures React 19.2+ PerformanceMeasure entries emitted by the development
|
|
4
|
+
// reconciler (react-reconciler/cjs/react-reconciler.development.js) and writes
|
|
5
|
+
// a .cpuprofile file on process exit. Analyze with profano:
|
|
6
|
+
//
|
|
7
|
+
// TERMCAST_REACT_PROFILE=1 termcast dev ./my-extension
|
|
8
|
+
// bunx profano ./tmp/react-profile-*.cpuprofile --sort self
|
|
9
|
+
// bunx profano ./tmp/react-profile-*.cpuprofile --sort total
|
|
10
|
+
//
|
|
11
|
+
// The reconciler emits performance.measure() calls with:
|
|
12
|
+
// - name: "\u200b" + componentName (component renders)
|
|
13
|
+
// - name: trigger string like "Mount", "Cascading Update", etc.
|
|
14
|
+
// - detail.devtools.track: "Components ⚛" for component renders
|
|
15
|
+
//
|
|
16
|
+
// NOTE: React also emits some entries via console.timeStamp() which are not
|
|
17
|
+
// captured by PerformanceObserver. This profiler captures a useful subset of
|
|
18
|
+
// React's performance track data, not every component render.
|
|
19
|
+
//
|
|
20
|
+
// The profile builds a best-effort call tree from time containment: if measure
|
|
21
|
+
// A fully contains measure B in time, B becomes a child of A. Partially
|
|
22
|
+
// overlapping measures are attached to the nearest containing ancestor.
|
|
23
|
+
// This gives meaningful self vs total times in profano output:
|
|
24
|
+
// - total time = all time inside a measure (including children)
|
|
25
|
+
// - self time = time not attributed to any child measure
|
|
26
|
+
//
|
|
27
|
+
// Requirements:
|
|
28
|
+
// - React 19.2+ in development mode (NODE_ENV !== 'production')
|
|
29
|
+
// - PerformanceObserver available (Node.js 16+, Bun)
|
|
30
|
+
import fs from 'node:fs';
|
|
31
|
+
import path from 'node:path';
|
|
32
|
+
import { logger } from './logger';
|
|
33
|
+
const measures = [];
|
|
34
|
+
let observerInstalled = false;
|
|
35
|
+
let profileWritten = false;
|
|
36
|
+
let perfObserver = null;
|
|
37
|
+
function writeProfileOnce() {
|
|
38
|
+
if (profileWritten) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
profileWritten = true;
|
|
42
|
+
// Drain any queued records not yet delivered by the observer callback,
|
|
43
|
+
// so the last measures before exit are captured.
|
|
44
|
+
if (perfObserver) {
|
|
45
|
+
for (const entry of perfObserver.takeRecords()) {
|
|
46
|
+
const detail = entry.detail;
|
|
47
|
+
if (!detail?.devtools?.track) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
measures.push({
|
|
51
|
+
name: entry.name,
|
|
52
|
+
duration: entry.duration,
|
|
53
|
+
startTime: entry.startTime,
|
|
54
|
+
track: detail.devtools.track,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
writeProfile();
|
|
59
|
+
}
|
|
60
|
+
export function installProfiler() {
|
|
61
|
+
if (observerInstalled) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (typeof PerformanceObserver === 'undefined') {
|
|
65
|
+
logger.error('PerformanceObserver not available, profiling disabled');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
observerInstalled = true;
|
|
69
|
+
perfObserver = new PerformanceObserver((list) => {
|
|
70
|
+
for (const entry of list.getEntries()) {
|
|
71
|
+
const detail = entry.detail;
|
|
72
|
+
if (!detail?.devtools?.track) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
measures.push({
|
|
76
|
+
name: entry.name,
|
|
77
|
+
duration: entry.duration,
|
|
78
|
+
startTime: entry.startTime,
|
|
79
|
+
track: detail.devtools.track,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
perfObserver.observe({ type: 'measure', buffered: true });
|
|
84
|
+
// Hook into the devtools fiber tree to collect component source locations
|
|
85
|
+
// from _debugStack on each commit. This builds the componentSourceMap
|
|
86
|
+
// incrementally as components render.
|
|
87
|
+
installFiberHook();
|
|
88
|
+
// Write profile on exit signals. The named function reference is used so
|
|
89
|
+
// removeListener actually removes the correct handler, preventing recursion
|
|
90
|
+
// when process.kill re-raises the signal.
|
|
91
|
+
const handleSignal = (signal) => {
|
|
92
|
+
writeProfileOnce();
|
|
93
|
+
process.removeListener(signal, handleSignal);
|
|
94
|
+
process.kill(process.pid, signal);
|
|
95
|
+
};
|
|
96
|
+
process.on('SIGINT', handleSignal);
|
|
97
|
+
process.on('SIGTERM', handleSignal);
|
|
98
|
+
process.on('exit', writeProfileOnce);
|
|
99
|
+
logger.log('React profiler installed. Profile will be written on exit.');
|
|
100
|
+
}
|
|
101
|
+
// Build a call tree from time containment: if span A fully contains span B,
|
|
102
|
+
// B is a child of A. Each unique (track, name) pair can appear at multiple
|
|
103
|
+
// tree positions when it occurs inside different parent spans.
|
|
104
|
+
//
|
|
105
|
+
// The algorithm: sort spans longest-first so parents come before children.
|
|
106
|
+
// For each span, walk the tree from root finding the deepest ancestor that
|
|
107
|
+
// contains it, then attach a new node there. Each tree node gets a unique ID
|
|
108
|
+
// even if the same component name appears multiple times (different call sites).
|
|
109
|
+
function buildCallTree({ spans, sourceMap }) {
|
|
110
|
+
const ROOT_ID = 1;
|
|
111
|
+
const IDLE_ID = 2;
|
|
112
|
+
const nodes = [
|
|
113
|
+
{
|
|
114
|
+
id: ROOT_ID,
|
|
115
|
+
callFrame: { functionName: '(root)', scriptId: '0', url: '', lineNumber: -1, columnNumber: -1 },
|
|
116
|
+
children: [IDLE_ID],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: IDLE_ID,
|
|
120
|
+
callFrame: { functionName: '(idle)', scriptId: '0', url: '', lineNumber: -1, columnNumber: -1 },
|
|
121
|
+
children: [],
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
let nextId = 3;
|
|
125
|
+
const rootEntry = {
|
|
126
|
+
nodeId: ROOT_ID,
|
|
127
|
+
startUs: -Infinity,
|
|
128
|
+
endUs: Infinity,
|
|
129
|
+
children: [],
|
|
130
|
+
};
|
|
131
|
+
// Sort spans by duration descending so parents (longer) are inserted first
|
|
132
|
+
const indexed = spans.map((s, i) => ({ ...s, originalIndex: i }));
|
|
133
|
+
indexed.sort((a, b) => (b.endUs - b.startUs) - (a.endUs - a.startUs));
|
|
134
|
+
// Map from original span index to the leaf node ID for sampling
|
|
135
|
+
const spanToLeafId = new Map();
|
|
136
|
+
for (const span of indexed) {
|
|
137
|
+
// Find deepest ancestor in the tree that fully contains this span
|
|
138
|
+
const parent = findDeepestContainer(rootEntry, span.startUs, span.endUs);
|
|
139
|
+
const id = nextId++;
|
|
140
|
+
const newEntry = {
|
|
141
|
+
nodeId: id,
|
|
142
|
+
startUs: span.startUs,
|
|
143
|
+
endUs: span.endUs,
|
|
144
|
+
children: [],
|
|
145
|
+
};
|
|
146
|
+
// Resolve source file path from the component name.
|
|
147
|
+
// Falls back to the React track name (e.g. "Components ⚛") for scheduler
|
|
148
|
+
// events and components not found in source.
|
|
149
|
+
// scriptId is stable per source identity so profano aggregates repeated
|
|
150
|
+
// renders of the same component into one row.
|
|
151
|
+
const sourcePath = sourceMap.get(span.name);
|
|
152
|
+
const sourceMatch = sourcePath ? /^(.*):(\d+)$/.exec(sourcePath) : null;
|
|
153
|
+
const url = sourceMatch ? sourceMatch[1] : (sourcePath || span.track);
|
|
154
|
+
const lineNumber = sourceMatch ? Number(sourceMatch[2]) : -1;
|
|
155
|
+
const scriptId = sourcePath || `${span.track}:${span.name}`;
|
|
156
|
+
nodes.push({
|
|
157
|
+
id,
|
|
158
|
+
callFrame: {
|
|
159
|
+
functionName: span.name,
|
|
160
|
+
scriptId,
|
|
161
|
+
url,
|
|
162
|
+
lineNumber,
|
|
163
|
+
columnNumber: -1,
|
|
164
|
+
},
|
|
165
|
+
children: [],
|
|
166
|
+
});
|
|
167
|
+
// Add as child of parent in both the tree and the profile nodes
|
|
168
|
+
parent.children.push(newEntry);
|
|
169
|
+
const parentNode = nodes.find((n) => n.id === parent.nodeId);
|
|
170
|
+
if (!parentNode.children.includes(id)) {
|
|
171
|
+
parentNode.children.push(id);
|
|
172
|
+
}
|
|
173
|
+
spanToLeafId.set(span.originalIndex, id);
|
|
174
|
+
}
|
|
175
|
+
return { nodes, spanToLeafId };
|
|
176
|
+
}
|
|
177
|
+
function findDeepestContainer(entry, startUs, endUs) {
|
|
178
|
+
// Check children for a tighter fit
|
|
179
|
+
for (const child of entry.children) {
|
|
180
|
+
if (child.startUs <= startUs && child.endUs >= endUs) {
|
|
181
|
+
return findDeepestContainer(child, startUs, endUs);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return entry;
|
|
185
|
+
}
|
|
186
|
+
// Component name → source file:line mapping built from React's fiber _debugStack.
|
|
187
|
+
// Populated at runtime by hooking into __REACT_DEVTOOLS_GLOBAL_HOOK__.onCommitFiberRoot
|
|
188
|
+
// which gives us the actual fiber tree with debug stack traces. Each fiber's _debugStack
|
|
189
|
+
// is an Error whose second stack frame points to where the JSX element was created.
|
|
190
|
+
// This is much more accurate than regex scanning source files.
|
|
191
|
+
const componentSourceMap = new Map();
|
|
192
|
+
// Extract "file:line" from a fiber's _debugStack Error.
|
|
193
|
+
// The stack trace format varies between Bun and Node:
|
|
194
|
+
// at functionName (/path/to/file.tsx:42:5) — named frame
|
|
195
|
+
// at /path/to/file.tsx:42:5 — anonymous frame
|
|
196
|
+
// We want the first non-React-internal frame. node_modules paths are kept
|
|
197
|
+
// so components from third-party packages show their real location.
|
|
198
|
+
function parseFrameLocation(frame) {
|
|
199
|
+
// Named frame: at fn (/path/file.tsx:1:2)
|
|
200
|
+
const parenthesized = /\((.+):(\d+):\d+\)\s*$/.exec(frame);
|
|
201
|
+
if (parenthesized) {
|
|
202
|
+
return { filePath: parenthesized[1], line: parenthesized[2] };
|
|
203
|
+
}
|
|
204
|
+
// Anonymous frame: at /path/file.tsx:1:2
|
|
205
|
+
const bare = /^\s*at (.+):(\d+):\d+\s*$/.exec(frame);
|
|
206
|
+
if (bare) {
|
|
207
|
+
return { filePath: bare[1], line: bare[2] };
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
function extractSourceFromFiber(fiber) {
|
|
212
|
+
const debugStack = fiber._debugStack;
|
|
213
|
+
if (!debugStack) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const stack = debugStack.stack || String(debugStack);
|
|
217
|
+
const frames = stack.split('\n');
|
|
218
|
+
for (const frame of frames) {
|
|
219
|
+
// Skip only React internals, keep everything else including node_modules
|
|
220
|
+
if (frame.includes('react.development') ||
|
|
221
|
+
frame.includes('react-jsx') ||
|
|
222
|
+
frame.includes('react-reconciler')) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const loc = parseFrameLocation(frame);
|
|
226
|
+
if (loc) {
|
|
227
|
+
const relativePath = path.relative(process.cwd(), loc.filePath);
|
|
228
|
+
return `${relativePath}:${loc.line}`;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
// Get display name from a fiber, handling memo/forwardRef wrappers.
|
|
234
|
+
// React wraps components in objects with .type or .render for these HOCs.
|
|
235
|
+
function getFiberComponentName(fiber) {
|
|
236
|
+
const type = fiber.type;
|
|
237
|
+
if (!type) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
// Direct function component or class
|
|
241
|
+
if (type.displayName || type.name) {
|
|
242
|
+
return type.displayName || type.name;
|
|
243
|
+
}
|
|
244
|
+
// memo(Component) — type is { $$typeof: REACT_MEMO_TYPE, type: innerComponent }
|
|
245
|
+
if (type.type?.displayName || type.type?.name) {
|
|
246
|
+
return type.type.displayName || type.type.name;
|
|
247
|
+
}
|
|
248
|
+
// forwardRef(Component) — type is { $$typeof: REACT_FORWARD_REF_TYPE, render: fn }
|
|
249
|
+
if (type.render?.displayName || type.render?.name) {
|
|
250
|
+
return type.render.displayName || type.render.name;
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
// Iterative fiber tree walk to avoid stack overflow on large flat lists.
|
|
255
|
+
// Sibling chains can be hundreds deep; recursion would overflow.
|
|
256
|
+
function walkFiberTree(root) {
|
|
257
|
+
const stack = root ? [root] : [];
|
|
258
|
+
while (stack.length > 0) {
|
|
259
|
+
const fiber = stack.pop();
|
|
260
|
+
const name = getFiberComponentName(fiber);
|
|
261
|
+
if (name && !componentSourceMap.has(name)) {
|
|
262
|
+
const source = extractSourceFromFiber(fiber);
|
|
263
|
+
if (source) {
|
|
264
|
+
componentSourceMap.set(name, source);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (fiber.sibling) {
|
|
268
|
+
stack.push(fiber.sibling);
|
|
269
|
+
}
|
|
270
|
+
if (fiber.child) {
|
|
271
|
+
stack.push(fiber.child);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function installFiberHook() {
|
|
276
|
+
const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
277
|
+
if (!hook) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
// Preserve all arguments and `this` so React Refresh and other hooks
|
|
281
|
+
// that depend on priorityLevel and didError continue to work.
|
|
282
|
+
const originalOnCommit = hook.onCommitFiberRoot;
|
|
283
|
+
hook.onCommitFiberRoot = function (...args) {
|
|
284
|
+
const root = args[1];
|
|
285
|
+
try {
|
|
286
|
+
walkFiberTree(root?.current);
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
return originalOnCommit?.apply(this, args);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function writeProfile() {
|
|
294
|
+
if (measures.length === 0) {
|
|
295
|
+
logger.log('No React performance measures captured, skipping profile write');
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const TICK = 1000; // microseconds per sample (1ms resolution)
|
|
299
|
+
// componentSourceMap was populated incrementally by the fiber hook during rendering
|
|
300
|
+
const sourceMap = componentSourceMap;
|
|
301
|
+
const sorted = [...measures].sort((a, b) => a.startTime - b.startTime);
|
|
302
|
+
const t0 = sorted[0].startTime;
|
|
303
|
+
const endUs = Math.round((Math.max(...sorted.map((m) => m.startTime + m.duration)) - t0) * 1000);
|
|
304
|
+
// Convert measures to spans with microsecond timestamps
|
|
305
|
+
const spans = sorted.map((m) => ({
|
|
306
|
+
startUs: Math.round((m.startTime - t0) * 1000),
|
|
307
|
+
endUs: Math.round((m.startTime + m.duration - t0) * 1000),
|
|
308
|
+
name: m.name.replace('\u200b', ''),
|
|
309
|
+
track: m.track,
|
|
310
|
+
}));
|
|
311
|
+
// Build call tree from time containment, passing sourceMap for file paths
|
|
312
|
+
const { nodes, spanToLeafId } = buildCallTree({ spans, sourceMap });
|
|
313
|
+
// Generate samples only over active span windows and compress idle gaps.
|
|
314
|
+
// Instead of iterating every tick across the full timeline (which is O(ticks * spans)
|
|
315
|
+
// and can hang for long sessions), collect all span boundaries, sort them, and only
|
|
316
|
+
// sample within active windows. Idle gaps between windows become a single idle sample
|
|
317
|
+
// with a large timeDelta.
|
|
318
|
+
const samples = [];
|
|
319
|
+
const timeDeltas = [];
|
|
320
|
+
const IDLE_ID = 2;
|
|
321
|
+
// Collect unique boundary times from all spans
|
|
322
|
+
const boundaries = new Set();
|
|
323
|
+
for (const span of spans) {
|
|
324
|
+
boundaries.add(span.startUs);
|
|
325
|
+
boundaries.add(span.endUs);
|
|
326
|
+
}
|
|
327
|
+
// Add timeline start/end
|
|
328
|
+
boundaries.add(0);
|
|
329
|
+
boundaries.add(endUs);
|
|
330
|
+
const sortedBoundaries = [...boundaries].sort((a, b) => a - b);
|
|
331
|
+
// Sort spans narrowest-first for fast deepest-leaf lookup
|
|
332
|
+
const spansByNarrowest = spans
|
|
333
|
+
.map((s, i) => ({ ...s, idx: i }))
|
|
334
|
+
.sort((a, b) => (a.endUs - a.startUs) - (b.endUs - b.startUs));
|
|
335
|
+
// For each window between consecutive boundaries, determine if any span is
|
|
336
|
+
// active. If yes, sample at TICK resolution. If no, emit one idle sample.
|
|
337
|
+
for (let w = 0; w < sortedBoundaries.length - 1; w++) {
|
|
338
|
+
const windowStart = sortedBoundaries[w];
|
|
339
|
+
const windowEnd = sortedBoundaries[w + 1];
|
|
340
|
+
if (windowStart >= windowEnd) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
// Check if any span is active at the midpoint of this window
|
|
344
|
+
const mid = windowStart + Math.floor((windowEnd - windowStart) / 2);
|
|
345
|
+
let hasActiveSpan = false;
|
|
346
|
+
for (const span of spansByNarrowest) {
|
|
347
|
+
if (mid >= span.startUs && mid < span.endUs) {
|
|
348
|
+
hasActiveSpan = true;
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (!hasActiveSpan) {
|
|
353
|
+
// Compress idle window into a single sample
|
|
354
|
+
samples.push(IDLE_ID);
|
|
355
|
+
timeDeltas.push(windowEnd - windowStart);
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
// Sample at TICK resolution within this active window.
|
|
359
|
+
// Use Math.min so the last sample's timeDelta covers only the remainder,
|
|
360
|
+
// preventing inflation when the window is shorter than TICK or not divisible.
|
|
361
|
+
for (let t = windowStart; t < windowEnd; t += TICK) {
|
|
362
|
+
const nextT = Math.min(t + TICK, windowEnd);
|
|
363
|
+
let leafId = IDLE_ID;
|
|
364
|
+
for (const span of spansByNarrowest) {
|
|
365
|
+
if (t >= span.startUs && t < span.endUs) {
|
|
366
|
+
leafId = spanToLeafId.get(span.idx) ?? IDLE_ID;
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
samples.push(leafId);
|
|
371
|
+
timeDeltas.push(nextT - t);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const profile = {
|
|
375
|
+
nodes,
|
|
376
|
+
samples,
|
|
377
|
+
startTime: 0,
|
|
378
|
+
endTime: endUs,
|
|
379
|
+
timeDeltas,
|
|
380
|
+
};
|
|
381
|
+
// Write to ./tmp/react-profile.cpuprofile relative to cwd
|
|
382
|
+
const outDir = path.join(process.cwd(), 'tmp');
|
|
383
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
384
|
+
const outPath = path.join(outDir, `react-profile-${Date.now()}.cpuprofile`);
|
|
385
|
+
fs.writeFileSync(outPath, JSON.stringify(profile));
|
|
386
|
+
const activeSamples = samples.filter((s) => s !== 2).length;
|
|
387
|
+
logger.log(`Wrote React profile: ${outPath} (${measures.length} measures, ${nodes.length} nodes, ${activeSamples} active / ${samples.length} total samples)`);
|
|
388
|
+
console.error(`\nReact profile written: ${outPath}\nAnalyze with: npx profano ${outPath} --sort self`);
|
|
389
|
+
}
|
|
390
|
+
//# sourceMappingURL=profiler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profiler.js","sourceRoot":"","sources":["../src/profiler.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,18 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "termcast",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Raycast for the terminal",
|
|
5
5
|
"repository": "https://github.com/remorses/termcast",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
|
-
"scripts": {
|
|
8
|
-
"build": "tsc",
|
|
9
|
-
"prepublishOnly": "bun x tsc && chmod +x dist/cli.js",
|
|
10
|
-
"release": "cd .. && bun changeset version && /Users/morse/Documents/GitHub/changesets/packages/cli/bin.js publish # my own fork to support github releases",
|
|
11
|
-
"see-raycast-types": "zed node_modules/@raycast/api/types/index.d.ts",
|
|
12
|
-
"play": "bun src/cli.tsx",
|
|
13
|
-
"test": "bun test && bun e2e",
|
|
14
|
-
"e2e": "vitest -u"
|
|
15
|
-
},
|
|
16
7
|
"bin": {
|
|
17
8
|
"termcast": "./dist/cli.js"
|
|
18
9
|
},
|
|
@@ -94,7 +85,6 @@
|
|
|
94
85
|
"@parcel/watcher": "^2.5.6",
|
|
95
86
|
"@tanstack/react-query": "^5.85.5",
|
|
96
87
|
"@tanstack/react-query-persist-client": "^5.85.5",
|
|
97
|
-
"@termcast/utils": "^2.2.8",
|
|
98
88
|
"change-case": "^5.4.4",
|
|
99
89
|
"colord": "^2.9.3",
|
|
100
90
|
"goke": "^6.6.0",
|
|
@@ -107,7 +97,8 @@
|
|
|
107
97
|
"react-refresh": "^0.18.0",
|
|
108
98
|
"simple-plist": "^1.3.1",
|
|
109
99
|
"string-dedent": "^3.0.2",
|
|
110
|
-
"zustand": "^5.0.8"
|
|
100
|
+
"zustand": "^5.0.8",
|
|
101
|
+
"@termcast/utils": "^2.2.8"
|
|
111
102
|
},
|
|
112
103
|
"peerDependencies": {
|
|
113
104
|
"@opentui/core": "^0.2.12",
|
|
@@ -126,10 +117,18 @@
|
|
|
126
117
|
"bun-pty": "0.4.8",
|
|
127
118
|
"zigpty": "^0.0.4",
|
|
128
119
|
"react": "^19.2.4",
|
|
129
|
-
"
|
|
130
|
-
"
|
|
120
|
+
"vitest": "^4.0.16",
|
|
121
|
+
"tuistory": "^0.8.1"
|
|
131
122
|
},
|
|
132
123
|
"optionalDependencies": {
|
|
133
124
|
"sharp": "^0.34.5"
|
|
125
|
+
},
|
|
126
|
+
"scripts": {
|
|
127
|
+
"build": "tsc",
|
|
128
|
+
"release": "cd .. && pnpm changeset version && /Users/morse/Documents/GitHub/changesets/packages/cli/bin.js publish # my own fork to support github releases",
|
|
129
|
+
"see-raycast-types": "zed node_modules/@raycast/api/types/index.d.ts",
|
|
130
|
+
"play": "bun src/cli.tsx",
|
|
131
|
+
"test": "bun test && pnpm e2e",
|
|
132
|
+
"e2e": "vitest -u"
|
|
134
133
|
}
|
|
135
|
-
}
|
|
134
|
+
}
|
package/src/build.tsx
CHANGED
|
@@ -244,13 +244,35 @@ export async function buildExtensionCommands({
|
|
|
244
244
|
packageJsonPath: path.join(resolvedPath, 'package.json'),
|
|
245
245
|
})
|
|
246
246
|
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
247
|
+
// Build entrypoints. For directory-style entries (e.g. src/tui/index.tsx),
|
|
248
|
+
// generate a wrapper file named {command}.tsx in the bundle dir so Bun
|
|
249
|
+
// outputs {command}.js instead of index.js. This avoids collisions when
|
|
250
|
+
// multiple commands use directory entries (Bun fails with "Multiple files
|
|
251
|
+
// share the same output path ./index.js" otherwise).
|
|
252
|
+
const existingCommands = commandsData.commands.filter((cmd) => cmd.exists)
|
|
253
|
+
const entrypoints = existingCommands.map((cmd) => {
|
|
254
|
+
const isDirectoryEntry = path.basename(cmd.filePath).startsWith('index.')
|
|
255
|
+
if (isDirectoryEntry) {
|
|
256
|
+
const wrapperPath = path.join(bundleDir, `${cmd.name}.tsx`)
|
|
257
|
+
fs.writeFileSync(
|
|
258
|
+
wrapperPath,
|
|
259
|
+
`export * from ${JSON.stringify(cmd.filePath)};\nexport { default } from ${JSON.stringify(cmd.filePath)};\n`,
|
|
260
|
+
)
|
|
261
|
+
return wrapperPath
|
|
262
|
+
}
|
|
263
|
+
return cmd.filePath
|
|
264
|
+
})
|
|
251
265
|
|
|
252
266
|
if (entrypoints.length === 0) {
|
|
253
|
-
|
|
267
|
+
const tried = commandsData.commands
|
|
268
|
+
.map((cmd) => {
|
|
269
|
+
const paths = cmd.checkedPaths.map((p) => ` ${p}`).join('\n')
|
|
270
|
+
return ` command "${cmd.name}" checked:\n${paths}`
|
|
271
|
+
})
|
|
272
|
+
.join('\n')
|
|
273
|
+
throw new Error(
|
|
274
|
+
`No command files found to build.\n${tried}\n\nEach command in package.json "commands" needs a matching file: {name}.tsx or {name}/index.tsx in the project root or src/`,
|
|
275
|
+
)
|
|
254
276
|
}
|
|
255
277
|
|
|
256
278
|
logger.log(`Building ${entrypoints.length} commands...`)
|
package/src/cli.tsx
CHANGED
|
File without changes
|
package/src/compile.tsx
CHANGED
|
@@ -190,7 +190,15 @@ export async function compileExtension({
|
|
|
190
190
|
if (!entry) {
|
|
191
191
|
const existingCommands = commands.filter((cmd) => cmd.exists)
|
|
192
192
|
if (existingCommands.length === 0) {
|
|
193
|
-
|
|
193
|
+
const tried = commands
|
|
194
|
+
.map((cmd) => {
|
|
195
|
+
const paths = cmd.checkedPaths.map((p) => ` ${p}`).join('\n')
|
|
196
|
+
return ` command "${cmd.name}" checked:\n${paths}`
|
|
197
|
+
})
|
|
198
|
+
.join('\n')
|
|
199
|
+
throw new Error(
|
|
200
|
+
`No command files found to build.\n${tried}\n\nEach command in package.json "commands" needs a matching file: {name}.tsx or {name}/index.tsx in the project root or src/`,
|
|
201
|
+
)
|
|
194
202
|
}
|
|
195
203
|
logger.log(`Compiling ${existingCommands.length} commands...`)
|
|
196
204
|
} else {
|
package/src/compile.vitest.tsx
CHANGED
|
@@ -90,13 +90,13 @@ test('compile extension and run executable', async () => {
|
|
|
90
90
|
|
|
91
91
|
Commands
|
|
92
92
|
›List Items Displays a simple list with some items view
|
|
93
|
-
|
|
93
|
+
Search ItemsSearch and filter...h a list of items view
|
|
94
94
|
Google Oauth view
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
usePr...e Dem Shows how to use...m @raycast/utils view
|
|
96
|
+
Show Stat Shows the current ...ate in JSON format view
|
|
97
|
+
With ...mentsDemonstrates com...ssword, dropdown) view
|
|
98
|
+
Quic...tionCopies current t...t showing a view no-view
|
|
99
|
+
Throw ErrorCommand that thro...rror at root scope view
|
|
100
100
|
"
|
|
101
101
|
`)
|
|
102
102
|
}, 60000)
|
|
@@ -142,7 +142,7 @@ test('compiled executable can run command', async () => {
|
|
|
142
142
|
○ Fifth Item This is the fifth item
|
|
143
143
|
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
|
|
146
146
|
"
|
|
147
147
|
`)
|
|
148
148
|
}, 60000)
|
|
@@ -198,7 +198,7 @@ test('compiled executable can navigate back', async () => {
|
|
|
198
198
|
○ Fifth Item This is the fifth item
|
|
199
199
|
|
|
200
200
|
|
|
201
|
-
|
|
201
|
+
|
|
202
202
|
"
|
|
203
203
|
`)
|
|
204
204
|
}, 60000)
|
|
@@ -12,9 +12,11 @@
|
|
|
12
12
|
* primary, accent, info, success, warning, error, secondary (cycles with %)
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import React, { ReactNode, useMemo } from 'react'
|
|
15
|
+
import React, { ReactNode, useMemo, useRef } from 'react'
|
|
16
|
+
import type { MouseEvent as OpenTUIMouseEvent } from '@opentui/core'
|
|
16
17
|
import { useTheme, getThemePalette } from 'termcast/src/theme'
|
|
17
18
|
import { Color, resolveColor } from 'termcast/src/colors'
|
|
19
|
+
import { ChartTooltip, useChartTooltip, formatTooltipLine } from 'termcast/src/components/chart-tooltip'
|
|
18
20
|
|
|
19
21
|
// ── Types ────────────────────────────────────────────────────────────
|
|
20
22
|
|
|
@@ -173,6 +175,8 @@ function LabelRow({ segments, labelMap, position, color }: {
|
|
|
173
175
|
const BarChart: BarChartType = (props) => {
|
|
174
176
|
const theme = useTheme()
|
|
175
177
|
const { height = 1, showLabels = true, children } = props
|
|
178
|
+
const containerRef = useRef<any>(null)
|
|
179
|
+
const { tooltip, show: showTooltip, hide: hideTooltip } = useChartTooltip()
|
|
176
180
|
|
|
177
181
|
// Collect segment data from BarChart.Segment children
|
|
178
182
|
const segments = useMemo<SegmentData[]>(() => {
|
|
@@ -242,7 +246,8 @@ const BarChart: BarChartType = (props) => {
|
|
|
242
246
|
const belowMap = new Map(below.map((l) => [l.segmentIndex, l]))
|
|
243
247
|
|
|
244
248
|
return (
|
|
245
|
-
<box flexDirection="column" width="100%" flexShrink={0}>
|
|
249
|
+
<box ref={containerRef} flexDirection="column" width="100%" flexShrink={0} onMouseOut={hideTooltip}>
|
|
250
|
+
<ChartTooltip tooltip={tooltip} containerRef={containerRef} />
|
|
246
251
|
<LabelRow
|
|
247
252
|
segments={visibleSegments}
|
|
248
253
|
labelMap={aboveMap}
|
|
@@ -252,7 +257,22 @@ const BarChart: BarChartType = (props) => {
|
|
|
252
257
|
<box flexDirection="row" height={height} width="100%" flexShrink={0}>
|
|
253
258
|
{visibleSegments.map((seg, i) => {
|
|
254
259
|
return (
|
|
255
|
-
<box
|
|
260
|
+
<box
|
|
261
|
+
key={i}
|
|
262
|
+
flexGrow={seg.value}
|
|
263
|
+
flexShrink={0}
|
|
264
|
+
backgroundColor={seg.resolvedColor}
|
|
265
|
+
height={height}
|
|
266
|
+
onMouseMove={(evt: OpenTUIMouseEvent) => {
|
|
267
|
+
const pct = formatValue(seg.value, total)
|
|
268
|
+
const label = seg.label || `#${i + 1}`
|
|
269
|
+
showTooltip({
|
|
270
|
+
x: evt.x,
|
|
271
|
+
y: evt.y,
|
|
272
|
+
lines: [formatTooltipLine(label, `${seg.value} (${pct})`)],
|
|
273
|
+
})
|
|
274
|
+
}}
|
|
275
|
+
/>
|
|
256
276
|
)
|
|
257
277
|
})}
|
|
258
278
|
</box>
|