Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
revert(toolpath-desktop): remove trace caching; fix perf mutation error
The perf tracer showed the real bottleneck sits between model-update
and component-mount (~205ms of Svelte render work) rather than in the
Rust derive (~80ms), so the two-tier cache added in the earlier
commit was optimising the wrong thing. Stripping it removes a lot of
complexity (cache.rs, disk persistence, prewarm threading, in-flight
slots, LRU eviction) that wasn't buying the user anything.

Kept: the perf tracer, the overlay, the `buildTree` /
`flattenChatHead` marks — these are what lets us now see exactly
where the remaining time goes.

Also fix `state_unsafe_mutation` thrown when `perfMark` is called
from inside a `$derived` (which happens when ChatView's `turns`
derivation runs `buildTree` + `flattenChatHead`): defer `perf.latest`
writes to a microtask so mutation never happens during derivation.
  • Loading branch information
eliothedeman committed Apr 23, 2026
commit 697e453c84fef4bdaa0418649f27d33ca1f8b14d
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ Tests live alongside the code (`#[cfg(test)] mod tests`), plus `toolpath-cli` ha
- `toolpath-pi`: ~88 unit tests (types, paths, error, reader, io, provider)
- `toolpath-dot`: 30 unit + 2 doc tests (render, visual conventions, escaping)
- `toolpath-cli`: 126 unit + 24 integration tests (all commands, track sessions, merge, validate, roundtrip, render-md snapshots)
- `toolpath-desktop`: 32 unit tests (IPC command modules — source listing, derive validation + cache-hit short-circuit for claude/pi, export round-trip, upload stub, keychain input checks; tray activity-window bucketing, stats-snapshot smoke, session-id/basename helpers, Quick View provider routing; trace cache memory-tier get/insert/freshness/LRU-eviction/in-flight slots plus disk-tier persistence-across-restart, freshness-across-restart, corrupt-file recovery, disk prune, disk-dir creation fallback)
- `toolpath-desktop`: 17 unit tests (IPC command modules — source listing, derive validation, export round-trip, upload stub, keychain input checks; tray activity-window bucketing, stats-snapshot smoke, session-id/basename helpers)

Validate example documents: `for f in examples/*.json; do cargo run -p toolpath-cli -- validate --input "$f"; done`

Expand Down
20 changes: 15 additions & 5 deletions crates/toolpath-desktop/frontend/src/lib/perf.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,26 +40,36 @@ function now(): number {
return typeof performance !== "undefined" ? performance.now() : Date.now();
}

// `perf.latest` is `$state`, so any assignment inside a Svelte `$derived`
// (e.g. `perfMark` from within `buildTree` reached via a `$derived` in
// ChatView) throws `state_unsafe_mutation`. Defer writes to a microtask so
// the mutation always happens outside derivation. The visible ordering is
// unchanged — multiple marks in one task resolve in order, last write wins.
function publish(trace: PerfTrace): void {
const snapshot = { ...trace, marks: trace.marks };
queueMicrotask(() => {
perf.latest = snapshot;
});
}

export function perfStart(label: string): void {
const t = now();
current = { label, startedAt: t, marks: [], durationMs: null };
// Make an early-visible copy so the overlay can show the label even before
// the first mark lands.
perf.latest = { ...current };
publish(current);
}

export function perfMark(name: string): void {
if (!current) return;
const t = now() - current.startedAt;
current.marks = [...current.marks, { name, t }];
perf.latest = { ...current, marks: current.marks };
publish(current);
}

export function perfEnd(): void {
if (!current) return;
const dur = now() - current.startedAt;
current.durationMs = dur;
perf.latest = { ...current, durationMs: dur };
publish(current);

// Summary to console. Each mark shows absolute-from-start and delta from
// the previous mark so the slow phase is easy to spot.
Expand Down
32 changes: 28 additions & 4 deletions crates/toolpath-desktop/frontend/src/lib/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import { classify, type ChatTurnKind } from "./classify";
import { renderMarkdown } from "./markdown";
import { perfMark } from "./perf.svelte";
import type { StepRef, TreeFilter } from "./types";
import { actorName, actorType, normalize, type Normalized } from "./viz";

Expand Down Expand Up @@ -108,8 +109,15 @@ export function matchesFilter(
export function buildTree(
doc: Parameters<typeof normalize>[0],
): { norm: Normalized; nodes: FlatNode[] } {
const t0 = performance.now();
const norm = normalize(doc);
return { norm, nodes: flattenTree(norm) };
const tNorm = performance.now() - t0;
const nodes = flattenTree(norm);
const tTotal = performance.now() - t0;
perfMark(
`buildTree (${norm.steps.length}st ${tTotal.toFixed(0)}ms: norm ${tNorm.toFixed(0)} + flat ${(tTotal - tNorm).toFixed(0)})`,
);
return { norm, nodes };
}

// ─── Chat / transcript view ──────────────────────────────────────────────
Expand Down Expand Up @@ -187,6 +195,7 @@ function firstRawDiff(
*/
export function flattenChatHead(norm: Normalized): ChatTurn[] {
const { steps, head, actors, stepMap, childrenMap } = norm;
const t0 = performance.now();

let ordered: StepRef[];
if (head && stepMap.has(head)) {
Expand All @@ -210,6 +219,16 @@ export function flattenChatHead(norm: Normalized): ChatTurn[] {
// Build each turn. For assistant turns, also collect tool.invoke sibling
// children so the renderer can fold them inline inside the bubble instead
// of scattering them as separate cards in the transcript.
let mdMs = 0;
let mdCount = 0;
const timedMarkdown = (src: string | null | undefined): string => {
if (!src) return "";
const t = performance.now();
const out = renderMarkdown(src);
mdMs += performance.now() - t;
mdCount += 1;
return out;
};
const turnFor = (s: StepRef): ChatTurn => {
const c = classify(s);
return {
Expand All @@ -224,10 +243,10 @@ export function flattenChatHead(norm: Normalized): ChatTurn[] {
changeKeys: s.change ? Object.keys(s.change) : [],
kind: c.kind,
text: c.text,
textHtml: c.text ? renderMarkdown(c.text) : "",
textHtml: timedMarkdown(c.text),
toolNames: c.toolNames,
thinking: c.thinking,
thinkingHtml: c.thinking ? renderMarkdown(c.thinking) : "",
thinkingHtml: timedMarkdown(c.thinking),
model: c.model,
toolName: c.toolName,
toolDiff: firstRawDiff(s),
Expand All @@ -236,7 +255,7 @@ export function flattenChatHead(norm: Normalized): ChatTurn[] {
};

const onChain = new Set(ordered.map((s) => s.step.id));
return ordered.map((s) => {
const out = ordered.map((s) => {
const turn = turnFor(s);
if (turn.kind === "assistant") {
const kids = childrenMap.get(s.step.id) ?? [];
Expand All @@ -250,4 +269,9 @@ export function flattenChatHead(norm: Normalized): ChatTurn[] {
}
return turn;
});
const total = performance.now() - t0;
perfMark(
`flattenChatHead (${out.length}t ${total.toFixed(0)}ms: md ${mdMs.toFixed(0)}ms × ${mdCount})`,
);
return out;
}
Loading