Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/proud-pens-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@callstack/tracesift": minor
---

Add Explore View with Timeline, Flame graph, Call tree and culprits
11 changes: 11 additions & 0 deletions .claude/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "tracesift",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"port": 3000
}
]
}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,5 @@ next-env.d.ts

/packages/cli/release.json
/packages/cli/*.tgz

test-fixtures/local/
117 changes: 88 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

Finding performance bottlenecks in a CPU or React profile takes expertise. Handing the full profile to an agent can help, but it fills the context window and burns through tokens.

**TraceSift** identifies bottlenecks in your profile and presents them in a clear, readable format, with the slowest first.
**TraceSift** identifies bottlenecks in your profile and presents them in a clear, readable format, with the slowest first. Every finding is measured rather than guessed: CPU and Hermes profiles are split into the tasks the trace recorded, React profiles into the commits React committed, so a card's duration is a block of time that really passed and its figures add up.

Each bottleneck includes a button to generate or copy a handoff prompt, so your agent can continue the investigation in the source code. You decide what gets fixed; the agent does the work.
Each card carries a button to generate or copy a handoff prompt, so your agent can continue the investigation in the source code. You decide what gets fixed; the agent does the work.

Runs locally with OpenAI, Anthropic, or Callstack Apex. Your credentials stay with you.
> [!NOTE]
> No model is needed to analyze a profile. A model is optional and used only where you ask for it — **Explain with AI** on a single card. Runs locally with OpenAI, Anthropic, or Callstack Apex, and your credentials stay with you.

<img src="https://raw.githubusercontent.com/callstackincubator/tracesift/main/assets/landing.png" alt="Landing Page"/>

Expand All @@ -28,7 +29,10 @@ Installs the TraceSift command globally. Requires Node.js 22.19 or newer and npm
tracesift init
```

Run this once to download and verify the prebuilt web app for your platform. After updating the CLI, run `tracesift init` again to install its matching app release.
Run this once to download and verify the prebuilt web app for your platform.

> [!IMPORTANT]
> After updating the CLI, run `tracesift init` again to install its matching app release.

<!-- Add initialization screenshot here -->

Expand All @@ -44,75 +48,121 @@ Starts TraceSift at `http://127.0.0.1:3000` and opens it in your browser. Press

See the [CLI documentation](packages/cli/CLI.md) for storage, troubleshooting, and release details.

Contributors can run `npm run test:release:local` to verify the packaged download and startup flow against a loopback artifact server before any GitHub release. See [Contributing](CONTRIBUTING.md) for the manual browser checkpoint and Changesets release process.
> [!TIP]
> Contributors can run `npm run test:release:local` to verify the packaged download and startup flow against a loopback artifact server before any GitHub release. See [Contributing](CONTRIBUTING.md) for the manual browser checkpoint and Changesets release process.

## Using the web interface

### 1. Configure Analysis settings

Open **Settings** before your first analysis. Choose a provider, authentication method, and model, then select **Save model settings**.
Without a provider configured, TraceSift still reads a profile end to end and produces its cards. Configure one for the optional extras:

- the per-card **Explain with AI** reading;
- a sharper split of which frames are your code rather than the framework's or the engine's.

Open **Settings**, choose a provider, authentication method, and model, then select **Save model settings**.

- OpenAI and Callstack use API keys;
- ChatGPT Codex uses Plus/Pro subscription sign-in;
- Anthropic supports a Claude subscription or API key.
- Browser sign-in is available.
- Subscription sign-in uses PI OAuth experimentally.
- Connecting an account does not select a model automatically.
- Credentials stay in the local TraceSift home directory.
- TraceSift never reads PI's own auth file.
- Anthropic supports a Claude subscription or API key;
- browser sign-in is available, and subscription sign-in uses PI OAuth experimentally;
- connecting an account does not select a model automatically;
- credentials stay in the local TraceSift home directory, and TraceSift never reads PI's own auth file.

Enable **Save analyses automatically** to keep completed reports in local history, or disable it and save individual reports from their results page.

> Clicking on "Save model settings" is required.

<!-- Add Analysis settings screenshot here -->
> [!IMPORTANT]
> Nothing is applied until you select **Save model settings**.

<img src="https://raw.githubusercontent.com/callstackincubator/tracesift/main/assets/settings.png" alt="Analysis Settings"/>

### 2. Start a new analysis

Select **Get Started**, then choose the profile type that matches the profiler you used, then click on "Analyze Profile".
Select **Get Started**, choose the profile type that matches the profiler you used, then select **Analyze Profile**.

<img src="https://raw.githubusercontent.com/callstackincubator/tracesift/main/assets/drop-zone.png" alt="Profile Drop Zone"/>

> [!TIP]
> No profile at hand? **Open a sample CPU analysis** or **Open a sample React analysis** on the same screen loads a bundled recording with its cards, charts, prompts, and drill-down already in place.

> [!WARNING]
> Uploads are capped at 512 MB, the largest profile Node can still parse in one piece.
>
> - Set `TRACE_SIFT_MAX_UPLOAD_MB` before starting TraceSift to lower the cap; values above the ceiling are clamped.
> - TraceSift sizes its own heap for large profiles — up to 8 GB, and never more than half of physical memory. Setting `--max-old-space-size` through `NODE_OPTIONS` overrides that.

#### JavaScript CPU and Hermes profiles

Visualize the results sorted by slowest. Each card provides a short summary of from where the issue originates and its impact on the recorded flow.
One card per task, slowest first. A task is one block of main-thread work as recorded — Chrome's `RunTask`, or one top-level call in a Hermes duration trace — and tasks never overlap, so their shares add up. Each card carries:

- a heading naming the task and the feature it ran in;
- the **culprits**, ranked by the time they burned in their own body, each with the shape of how it burned it (`ran 125 times in this task · 140 ms total · longest single call 4 ms`) and the named callers that reached it;
- a chart of how that time is distributed across the task;
- a footnote describing the remainder, so a short list over a long task reads as cost spread thin rather than a short measurement.

<!-- Add CPU/Hermes analysis screenshot here -->
> [!NOTE]
> Where a trace carries no task boundaries, TraceSift splits sample runs on idle gaps and says on the card that the boundaries were inferred.

<img src="https://raw.githubusercontent.com/callstackincubator/tracesift/main/assets/cpu-result.png" alt="CPU analysis"/>

#### React profiles

Visualize the results sorted by longest to render. Each card provides a short summary of from where the issue originates and its impact on the recorded flow.

<!-- Add React analysis screenshot here -->
One card per commit, longest to render first. Each card names what the commit rendered, charts the components that held its time, and lists them with their render counts.

<img src="https://raw.githubusercontent.com/callstackincubator/tracesift/main/assets/react-result.png" alt="React analysis"/>

#### Ask for a reading on one card

**Explain with AI** sends that one task or commit — its timeline and culprits, framework and engine frames collapsed away — to your configured model, which answers with a few short technical bullets on what the issue is and where it comes from.

- The reading renders above the card's rows, labelled as a reading rather than a measurement; the card keeps its measured heading.
- It is a button rather than part of the upload because only some cards on a page are worth a model call, and which ones is your call.
- A card that has been read carries its reading as the first section of the copied hand-off, above the measured evidence an agent can check it against.

### 3. Copy the handoff

When the bottleneck or React issue cards appear, choose the card you want to investigate and select **Copy Prompt**.
Choose the card you want to investigate and select **Copy Prompt**. The prompt is written for an agent holding your codebase:

<!-- Add bottleneck card and handoff screenshot here -->
- the measured figures, and the culprit frames with the callers that locate them;
- one merged tree — **Where those frames sit** — joining the task root to every culprit, rather than a separate stack per frame;
- bundled frames named by chunk and position (`vendors.bundle.js:189:701931`) instead of a full hashed URL, while a path you can actually open is still printed whole.

<img src="https://raw.githubusercontent.com/callstackincubator/tracesift/main/assets/handoff.gif" alt="handoff"/>

### 4. Reopen an analysis
### 4. Explore the profile behind a card

Open **Analyses** to browse locally saved reports, including their findings and generated prompts. Select a report to reopen it, or delete reports you no longer need.
**Explore** opens that task or commit in a new tab, with the card's numbers at the top and the recording underneath. It answers what a card cannot: what else ran, and in what order.

For a CPU or Hermes task:

<!-- Add analysis history screenshot here -->
- **Timeline** — the task against the clock, so a function called five hundred times reads differently from one long call.
- **Flame graph** — the same frames drawn by eye, zoomable on a `1×`–`64×` ladder. Click a frame to lay the graph out across its subtree (Esc, or the `zoomed into` button, to come back), or hold ⌘/Ctrl and scroll to stretch the whole graph about the pointer so thin frames widen where they sit.
- **Call tree** — self and inclusive time per frame, where `total = self + sum(children)` holds exactly.
- **Culprits** — the ranked table behind the card's rows, with the callers that locate each frame.

Clicking into a frame gives that subtree its own **Flame graph**, **Call tree**, and **Repeated work** views.

<img src="https://raw.githubusercontent.com/callstackincubator/tracesift/main/assets/explore-cpu.gif" alt="explore-cpu"/>

A React commit opens on its commit timeline, with **Render tree** and **Components** tabs.

<img src="https://raw.githubusercontent.com/callstackincubator/tracesift/main/assets/explore-react.gif" alt="explore-react"/>

Every tree, table, and flame graph has an **Everything** / **Your code only** toggle. Collapsing a frame lifts its children into its place and charges the time it spent in its own body to the nearest kept frame above it, so the figures agree across every view.

> [!TIP]
> Each chart states how to read it in one line, keeps its controls beside that, and folds its caveats behind **What this doesn't show**.

### 5. Reopen an analysis

Open **Analyses** to browse locally saved reports, including their findings and generated prompts. Select a report to reopen it, or delete reports you no longer need.

<img src="https://raw.githubusercontent.com/callstackincubator/tracesift/main/assets/analyses.gif" alt="Analysis History"/>

### 5. Open the guides
### 6. Open the guides

Select **Guide** for built-in, step-by-step instructions for capturing and analyzing JavaScript CPU, Hermes, and React profiles.

<!-- Add guides screenshot here -->

<img src="https://raw.githubusercontent.com/callstackincubator/tracesift/main/assets/guides.png" alt="How to use"/>

## Development
Expand All @@ -121,6 +171,15 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, manual testing, automated chec

The CLI package lives in `packages/cli`; it exports shared configuration, catalog, and server-runtime modules and uses the same pinned agent SDK version as the app.

The analysis engines are deterministic and covered by tests under `tests/`:

- `src/lib/task-cards.ts` and `src/lib/call-tree.ts` build the CPU and Hermes cards;
- `src/lib/react-cards.ts` and `src/lib/react-commit-tree.ts` build the React ones;
- `src/lib/task-timeline.ts` and `src/lib/react-explore.ts` back the Explore views.

> [!NOTE]
> `TRACESIFT_CPU_ENGINE=legacy` and `TRACESIFT_REACT_ENGINE=analyzer` run the previous model-backed engines, so a suspicious profile can be checked against them.

## Made with ❤️ at Callstack

**TraceSift** is an open source project and will always remain free to use. If you think it's cool, please star it 🌟.
Expand Down
Binary file modified assets/analyses.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/cpu-result.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/drop-zone.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/explore-cpu.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/explore-react.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/guides.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/handoff.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/landing.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/react-result.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
"private": true,
"scripts": {
"postinstall": "patch-package --error-on-fail",
"dev": "node --import @callstack/tracesift/bootstrap node_modules/next/dist/bin/next dev",
"dev": "node --max-old-space-size=8192 --import @callstack/tracesift/bootstrap node_modules/next/dist/bin/next dev",
"build": "next build",
"artifact:package": "node scripts/package-standalone.mjs",
"changeset": "changeset",
"release:version": "changeset version && node scripts/sync-release-version.mjs && npm install --package-lock-only --ignore-scripts --no-audit --no-fund",
"release:publish": "changeset publish",
"start": "node --import @callstack/tracesift/bootstrap node_modules/next/dist/bin/next start",
"start": "node --max-old-space-size=8192 --import @callstack/tracesift/bootstrap node_modules/next/dist/bin/next start",
"lint": "eslint",
"test": "node --experimental-strip-types --test tests/*.test.mjs packages/cli/test/*.test.js",
"test:production": "node --test packages/cli/test/production.smoke.js tests/react-profile.production.mjs",
Expand Down
18 changes: 17 additions & 1 deletion packages/cli/src/models.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,23 @@ export const APEX_MODEL_ID = 'callstack/Apex';
export function registerApexProvider(runtime) {
runtime.registerProvider(APEX_PROVIDER_ID, {
name: 'Apex', baseUrl: 'https://api.callstack.ai/v1', api: 'openai-completions', authHeader: true,
models: [{ id: APEX_MODEL_ID, name: 'Apex', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 32000 }],
models: [{
id: APEX_MODEL_ID,
name: 'Apex',
// Apex can return hidden reasoning tokens even when the caller asks for
// no thinking. Declare that capability so the OpenAI-compatible adapter
// sends an explicit reasoning effort instead of leaving the provider to
// choose an unbounded default. Apex's lowest supported effort is `low`.
reasoning: true,
thinkingLevelMap: { off: 'low' },
// Keep the system-message wire format that Apex already accepted; only
// opt into the reasoning control needed for these bounded reports.
compat: { supportsReasoningEffort: true, supportsDeveloperRole: false },
input: ['text'],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 32000,
}],
});
}
export async function createModelRuntime() {
Expand Down
20 changes: 19 additions & 1 deletion packages/cli/src/start.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
import { join } from 'node:path';
import { spawn } from 'node:child_process';
import { createServer } from 'node:net';
import { totalmem } from 'node:os';
import { randomUUID } from 'node:crypto';
import { setTimeout as delay } from 'node:timers/promises';
import { getHome, ensureHome } from './config.js';
import { readRelease, verifyInstallation } from './install.js';
import { acquireLock, launch } from './process.js';

/**
* Profiles are parsed whole in memory, so the stock ~4 GB heap is the practical
* ceiling on profile size long before the upload limit is. Raising the cap costs
* nothing until it is used — V8 grows the heap on demand — but it should still
* stay well under physical memory so a runaway parse fails with a JavaScript
* out-of-memory error rather than pushing the machine into swap.
*/
export function heapLimitMb(total = totalmem()) {
const half = Math.round(total / 2 / 1024 / 1024);
return Math.min(8192, Math.max(2048, half));
}
/** A heap size the user set themselves always wins over ours. */
export function serverNodeArgs(env = process.env) {
if (/--max[-_]old[-_]space[-_]size/.test(env.NODE_OPTIONS ?? '')) return [];
return [`--max-old-space-size=${heapLimitMb()}`];
}

export function parsePort(value = '3000') {
if (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > 65535) throw new Error('--port must be an integer between 1 and 65535.');
return Number(value);
Expand Down Expand Up @@ -53,7 +71,7 @@ export async function start({ home = getHome(), port = 3000, open = true } = {})
const instance = randomUUID();
const url = `http://127.0.0.1:${port}`;
const app = join(home, 'app');
proc = launch(process.execPath, ['--import', '@callstack/tracesift/bootstrap', join(app, 'server.js')], {
proc = launch(process.execPath, [...serverNodeArgs(), '--import', '@callstack/tracesift/bootstrap', join(app, 'server.js')], {
cwd: app,
env: { ...process.env, HOSTNAME: '127.0.0.1', PORT: String(port), TRACE_SIFT_HOME: home, TRACE_SIFT_INSTANCE: instance },
});
Expand Down
7 changes: 6 additions & 1 deletion packages/cli/test/config-model.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,12 @@ test('offline catalog contains only supported providers and native adapters', as
assert(models.some(m => m.provider === 'openai-codex' && m.api === 'openai-codex-responses'));
assert(models.some(m => m.provider === 'openai' && m.api === 'openai-responses'));
assert(models.some(m => m.provider === 'anthropic' && m.api === 'anthropic-messages'));
assert.equal(models.find(m => m.provider === 'apex').id, 'callstack/Apex');
const apex = models.find(m => m.provider === 'apex');
assert.equal(apex.id, 'callstack/Apex');
assert.equal(apex.reasoning, true);
assert.equal(apex.thinkingLevelMap.off, 'low');
assert.equal(apex.compat.supportsReasoningEffort, true);
assert.equal(apex.compat.supportsDeveloperRole, false);
});

test('startup freezes configured, missing and malformed states before first agent access', async t => {
Expand Down
15 changes: 14 additions & 1 deletion packages/cli/test/install-process.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { createServer } from 'node:http';
import { EventEmitter } from 'node:events';
import { downloadArtifact, install, validateRelease, verifyInstallation } from '../src/install.js';
import { acquireLock, launch, alive, run } from '../src/process.js';
import { parsePort, assertPortAvailable, waitForReady, openBrowser } from '../src/start.js';
import { parsePort, assertPortAvailable, waitForReady, openBrowser, heapLimitMb, serverNodeArgs } from '../src/start.js';

async function home(t) { const dir = await mkdtemp(join(tmpdir(), 'tracesift-test-')); t.after(() => rm(dir, { recursive: true, force: true })); return dir; }
const bytes = Buffer.from('fake artifact');
Expand Down Expand Up @@ -184,3 +184,16 @@ test('browser launch detaches and handles opening failures without stopping the
assert(detached);
assert.doesNotThrow(() => browser.emit('error', new Error('missing opener')));
});

test('the server heap is sized from physical memory and yields to an explicit NODE_OPTIONS', () => {
const gb = 1024 ** 3;
assert.equal(heapLimitMb(4 * gb), 2048, 'small machines keep the floor rather than half of 4 GB');
assert.equal(heapLimitMb(16 * gb), 8192);
assert.equal(heapLimitMb(48 * gb), 8192, 'large machines are capped, not given half of 48 GB');
assert.ok(heapLimitMb(2 * gb) <= 2048 * 2);

assert.deepEqual(serverNodeArgs({}), [`--max-old-space-size=${heapLimitMb()}`]);
assert.deepEqual(serverNodeArgs({ NODE_OPTIONS: '--max-old-space-size=2048' }), []);
assert.deepEqual(serverNodeArgs({ NODE_OPTIONS: '--max_old_space_size=2048' }), [], 'node accepts underscores too');
assert.deepEqual(serverNodeArgs({ NODE_OPTIONS: '--trace-warnings' }), [`--max-old-space-size=${heapLimitMb()}`]);
});
111 changes: 111 additions & 0 deletions patches/@rozenite+ui+2.4.0.patch

Large diffs are not rendered by default.

Loading
Loading