From 310fee93d810db388ac4126e10ec05e9081057b4 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Sat, 24 Oct 2026 21:40:33 +0800 Subject: [PATCH 1/3] feat: add a Timeline panel of what each action re-ran - A Timeline tab lists each user action, such as a click or a key press, with the writes it made, the memos and effects that re-ran, and how long the update took. Updates from timers or promises are rows of their own. - A re-run shows the changes that caused it, down to the write that started them. A re-run that produced the same value is marked as no change. - The detail shows the input delay, handler time and settle time from the interaction record, and links each re-run to the reactivity graph. - The copy button puts a report of the action on the clipboard for an agent. - The toolbar turns on the attribution engine of solid-js when it mounts, with its console log off. - Clicks in the toolbar change nothing in the app, so they are left out. Co-Authored-By: Claude Opus 5.5 --- .changeset/timeline-panel.md | 9 + README.md | 20 ++ src/dev-toolbar/icons.tsx | 19 ++ src/dev-toolbar/index.tsx | 30 +- src/dev-toolbar/report/timeline-report.ts | 123 +++++++ src/dev-toolbar/timeline/index.tsx | 385 ++++++++++++++++++++++ src/dev-toolbar/timeline/styles.css | 312 ++++++++++++++++++ src/dev-toolbar/timeline/tracker.test.ts | 202 ++++++++++++ src/dev-toolbar/timeline/tracker.ts | 269 +++++++++++++++ tests/e2e/devtools.spec.ts | 45 +++ 10 files changed, 1411 insertions(+), 3 deletions(-) create mode 100644 .changeset/timeline-panel.md create mode 100644 src/dev-toolbar/report/timeline-report.ts create mode 100644 src/dev-toolbar/timeline/index.tsx create mode 100644 src/dev-toolbar/timeline/styles.css create mode 100644 src/dev-toolbar/timeline/tracker.test.ts create mode 100644 src/dev-toolbar/timeline/tracker.ts diff --git a/.changeset/timeline-panel.md b/.changeset/timeline-panel.md new file mode 100644 index 0000000..2dc74e5 --- /dev/null +++ b/.changeset/timeline-panel.md @@ -0,0 +1,9 @@ +--- +'@solidjs/start-devtools': minor +--- + +Add a Timeline panel that lists what each user action re-ran, and why. + +- Each click, key press or outside update is a row with its writes, re-runs and timing. +- A re-run shows the changes that caused it, down to the write that started them. +- The copy button puts a report of the action on the clipboard for an agent. diff --git a/README.md b/README.md index 8db67d7..a041c3d 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Development error and server-function tooling for Solid Start mode. - Runtime error inspection with source-mapped stack frames. - The issues the runtime reports, such as a signal read outside a tracking scope. +- A timeline of what each action re-ran, and why. - Server-function request and response inspection. - A reactivity graph of the live signals, memos and effects in the app. - An ownership tree of the components and scopes the app created. @@ -64,6 +65,25 @@ the same findings, mixed with everything else. The panel listens from the moment the toolbar mounts, so findings from the first render are there when you open it. Issues need a development build of `solid-js`. +## Timeline + +The timeline lists what each user action made the app do. A click, a key press or an +input is one row, with the signals it wrote and every memo and effect that re-ran because +of it. An update from outside a handler, such as a timer or a promise, is a row of its own. + +- A row shows the writes, the number of re-runs and how long the update took. +- Select a row for the input delay, the handler time, the time until the update settled, + and the writes with their old and new values. +- Open a re-run to see why it ran: the changes it saw, and the changes behind each of + those, down to the write that started them. +- A re-run marked **no change** produced the same value as before, so its work was wasted. +- The eye opens the memo or effect in the reactivity graph. + +The panel turns on the attribution engine of `solid-js` (`solid-js/attribution`) when the +toolbar mounts, so the first action is recorded even with the dock closed. Clicks in the +toolbar itself change nothing in the app, so they are left out. The timeline needs a +development build of `solid-js`. + ## Reactivity graph The graph panel maps the running reactive graph. Signals, memos and effects are nodes, diff --git a/src/dev-toolbar/icons.tsx b/src/dev-toolbar/icons.tsx index 172dd1b..76b1a77 100644 --- a/src/dev-toolbar/icons.tsx +++ b/src/dev-toolbar/icons.tsx @@ -740,3 +740,22 @@ export function IssueIcon(props: JSX.IntrinsicElements['svg'] & { title: string ); } + +export function TimelineIcon(props: JSX.IntrinsicElements['svg'] & { title: string }): JSX.Element { + return ( + + {props.title} + + + ); +} diff --git a/src/dev-toolbar/index.tsx b/src/dev-toolbar/index.tsx index 334e4be..a78415f 100644 --- a/src/dev-toolbar/index.tsx +++ b/src/dev-toolbar/index.tsx @@ -21,26 +21,30 @@ import { ErrorIcon, FunctionIcon, GraphIcon, + IssueIcon, MinimizeIcon, SolidIcon, + TimelineIcon, TreeIcon, - IssueIcon, } from './icons.js'; +import { addIssues, captureIssues, type IssueEntry } from './issues/tracker.js'; import { excludeOwner, includeOwner } from './ownership/registry.js'; import { excludeReactiveOwner, includeReactiveOwner } from './reactivity/registry.js'; -import { addIssues, captureIssues, type IssueEntry } from './issues/tracker.js'; +import { startTimeline, TimelineBuilder, type TimelineEntry } from './timeline/tracker.js'; import './index.css'; const ErrorViewer = clientOnly(() => import('./error-viewer/index.js'), { lazy: true }); const ReactivityViewer = clientOnly(() => import('./reactivity/index.js'), { lazy: true }); const OwnershipViewer = clientOnly(() => import('./ownership/index.js'), { lazy: true }); const IssuesViewer = clientOnly(() => import('./issues/index.js'), { lazy: true }); +const TimelineViewer = clientOnly(() => import('./timeline/index.js'), { lazy: true }); -type Tab = 'err' | 'issues' | 'fn' | 'rx' | 'own'; +type Tab = 'err' | 'issues' | 'tl' | 'fn' | 'rx' | 'own'; const TABS: { value: Tab; label: string; icon: typeof ErrorIcon }[] = [ { value: 'err', label: 'Errors', icon: ErrorIcon }, { value: 'issues', label: 'Issues', icon: IssueIcon }, + { value: 'tl', label: 'Timeline', icon: TimelineIcon }, { value: 'fn', label: 'Server functions', icon: FunctionIcon }, { value: 'rx', label: 'Reactivity', icon: GraphIcon }, { value: 'own', label: 'Ownership', icon: TreeIcon }, @@ -321,6 +325,14 @@ export function DevToolbar(props: DevToolbarProps) { } const issueCount = createMemo(() => issues().reduce((total, entry) => total + entry.count, 0)); + // The first action can come before the toolbar settles, so the timeline + // starts during setup too. + const timeline = new TimelineBuilder(); + const [timelineEntries, setTimelineEntries] = createSignal([]); + if (!isServer) { + onCleanup(startTimeline(timeline, () => setTimelineEntries(timeline.snapshot()))); + } + const [instances, setInstances] = createSignal< Record >({}); @@ -485,6 +497,18 @@ export function DevToolbar(props: DevToolbarProps) { show('rx'); }} /> + { + timeline.clear(); + setTimelineEntries([]); + }} + onViewInGraph={(node) => { + setGraphFocus({ node }); + show('rx'); + }} + />