Commit 066a662
feat(timeline): HTML virtualization + collapse idle time (#3627)
* perf(timeline): grouped-event-buffer, window virtualization, sticky canvas
Introduce a module-level grouped-event-buffer singleton that owns event
state via callbacks rather than Svelte reactive stores, eliminating memory
pressure when handling 50k+ events.
Implement window-based SVG virtualization with a sentinel-scroll pattern:
only ~101 rows stay in the DOM (OVERSCAN=20) with hysteresis preventing
window thrashing on every pixel of scroll. CSS transform + will-change on
the SVG element lets the compositor pan a cached layer between updates.
Supporting changes:
- Extract timeline-positioning.ts for row Y calculation logic
- Cache getGroupArray() to eliminate O(N log N) sort on each call
- Short-circuit expensive onMount scan in TimelineGraphRow for non-local-activity
- Remove incremental windowing (applyWindowStep) that invalidated the GPU
compositor layer every frame
- Add perf-events / perf-signals scripts for local load testing
* perf(timeline): virtualization simplification, WFT filter, sorted eventList
Virtualization
- Replace hysteresis $effect + $state window tracking with a single
$derived; scrollY is already rAF-throttled so per-frame overhead is
just O(1) arithmetic in getWindowBounds, not DOM reconciliation
- Reduce OVERSCAN 20 → 8 rows (480 → 192 px buffer per side); rows are
now icon + text only so a smaller buffer is sufficient
Timeline layout
- Filter WorkflowTask groups from the timeline via
getGroupArray({ excludeWorkflowTasks: true }) at all call sites;
WFTs remain visible on the workflow events tab
- Add a second getGroupArray cache (_cachedGroupsNoWFT) so the
WFT-excluded path is O(1) after first build, same as the full cache
- Fix spacerHeight to account for stickyHeight and panelHeight so the
scroll range correctly covers the full content height
- Expose panelHeight as $bindable() from TimelineGraph so the layout can
incorporate open detail panel height into the spacer calculation
Event ordering
- Replace eventList.push() with insertEventById (insertion sort by
numeric event ID) in attachFollowerToPool and appendLiveEvent; the
descending cursor delivers higher IDs first, causing detail panels and
grouping predicates to see events out of order without this fix
- Add unit test covering the desc-cursor out-of-order arrival case
Rendering
- Replace SVG dashed tick lines with a CSS repeating-linear-gradient
background; recomputes only on resize, never during scroll
- Replace foreignObject skeleton placeholder with an SVG <rect> to
avoid reflows during loading
- Clip border lines to the virtualized window bounds
* perf(timeline): reduce reactive signal count and stabilize scroll height
Signal reduction
- timeline-graph-row: convert accessibleName to a plain const (translate
called once at mount, not on every reactive flush); move hover-width
calculations into {#if hovering} with {@const} so they only run while
hovered; keep pendingActivity/pauseTime/pendingLine as $derived since
enrichGroups mutates the group in place after fetch completes
- dot: consolidate two $derived color signals into one (colorPair); use
point[0]/point[1] directly in template, removing the [x,y] destructure
signal
- line: replace $derived strokeColor with a plain computeStrokeColor()
function called in the template; replace [x1,y1]/[x2,y2] destructure
signals with direct array access — template expressions are reactive
without needing script-level $derived
- text: remove [x,y] $derived destructure, use point[0]/point[1] directly
Net: ~17 fewer reactive signals per mounted row. At 50 visible rows that
is ~850 fewer Signal objects created/tracked by Svelte's reactive graph,
and ~850 fewer to GC as the virtualization window scrolls.
Scroll height stability
- pendingGroupCount now uses totalExpectedEvents directly as the row
estimate instead of recalculating density on every streaming batch;
totalExpectedEvents is a server constant so pendingGroupCount decrements
by exactly 1 per new group — totalForY stays flat and no existing rows
shift position as pages arrive
- spacerHeight (the page-level scroll range) uses totalExpectedEvents
while loading so the scrollbar thumb size and position are stable from
the first paint; switches to actual group count once fetchComplete
* Get rid of some warnings
* fix(timeline): restore A/B parity with master — auto-refresh, canvas height, WFT filter
Functional fixes
- Auto Refresh toggle now wires up correctly: imports pauseLiveUpdates,
syncs refresh_off URL param on load, calls onAutoRefreshToggle on click,
and reflects paused state in the dot colour / button label
- Compact (event history) view no longer leaks WorkflowTask groups — all
getGroupArray() call sites in workflow-history-layout now pass
{ excludeWorkflowTasks: true }
- Fix import ordering in events.ts and scripts/start-long-running.ts
(pnpm lint 0 errors)
Visual fixes
- Remove -mx-4 / md:-mx-8 negative margins from the sticky canvas wrapper
so the inner timeline-graph border is visible, matching master's look
- Introduce canvasContentHeight = max((rows+2)*ROW_PX,120) + panel + 120
and use min(canvasContentHeight, 100dvh-nav) as the sticky height;
small histories render a compact canvas while large ones still use the
full-viewport virtual-scroll path
New tests
- event-filter-params.test.ts: parseEventFilterParams / updateEventFilterParams
round-trip refresh_off correctly
- events.test.ts (locale): expand-details + collapse-details keys exist
- grouped-event-buffer.test.ts: compact-view regression — getGroupArray with
excludeWorkflowTasks never returns WFT groups even when interleaved
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: event row expand chevrons, zoom-svg controls, long-running test workflows
UI regressions restored
- event-summary-row: add expand/collapse chevron (IconButton) for inline
event detail expansion; detailsId ties aria-controls to the detail row;
onLinkClick made optional so programmatic calls don't throw
- zoom-svg: implement panBy / zoomBy helpers, keyboard handler (arrows to
pan, +/- to zoom), and a bottom-right button cluster (pan ↑↓←→, zoom
in/out, fit/reset) matching the master branch Relationships tab controls
Data / store fixes
- grouped-event-buffer: WorkflowExecutionStarted solo events now included
in getEventArray() output so child workflow input fields are populated
- workflow-details: migrate totalActions derivation from fullEventHistory
store to getEventArray() + bufferVersion signal, eliminating the stale
second copy of event objects
Test infrastructure
- Add longSleep + alwaysFails activities (long-sleep.ts) for realistic
pending-activity and retrying-activity test scenarios
- Add PendingActivityWorkflow, PendingTimerWorkflow, RetryingActivityWorkflow,
PendingChildWorkflow, MixedOpenWorkflow, SignalUpdateWaitWorkflow,
LongSleepChildWorkflow, ConcurrentPendingWorkflow to temporal/workflows.ts
- scripts/start-long-running.ts: runner that starts all long-running
workflows against a local Temporal server; registered as
pnpm run-workflows:long-running in package.json
- workflow-history-json: thread events prop through so JSON view reflects
the current filtered set
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(timeline): eliminate controls-canvas gap and restore top border
Three layout bugs fixed:
1. 32px phantom gap — the parent flex container uses gap-4 (16px).
The sentinel (h-0) was a standalone flex child, contributing two gaps
(one before, one after) = 32 px of empty space between the controls
bar and the sticky canvas. Fix: wrap controls + sentinel + canvas +
spacer in a single block-flow div so the parent gap only fires once
(above the whole block) and the children stack with zero internal gaps.
2. Missing top border — timeline-graph.svelte uses border-t-0 (designed
to share the controls bar's border-b in master's inline layout). The
sticky canvas wrapper now carries border-t border-subtle, completing
the four-sided border rectangle.
3. Controls bar overlap when scrolled — both the controls bar and the
sticky canvas were sticky at top: var(--top-nav-height), so on scroll
the controls bar covered the top ~53 px of the canvas. The canvas
sticky top is now calc(var(--top-nav-height) + {controlsHeight}px),
measured live via bind:clientHeight, so the canvas always sits flush
below the controls bar. The CSS height cap is updated to subtract
controlsHeight from 100dvh so the canvas never overflows the viewport.
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs: timeline architecture and virtualization reference
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs: expand reactivity section — buffer signals vs Svelte primitives
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs(timeline): add group assembly section to architecture doc
Documents the park-and-flush pattern for split groups: the four
follower resolution paths, what pendingFollowers vs livePendingFollowers
each store, the dedup guard in attachFollowerToPool, and how live
groups transition out of getGroupArray() once the pool claims the head.
Also expands the new-tests table to cover all test files added during
this work.
* Drop some old architecture stuffs
* Update arch doc
* Fix pending issues
* refactor(event-groups): eventList-based group model
* feat(services): streaming event buffer, bidirectional fetch, live poll
* feat(stores): event-view/actions-ready stores and history context
* feat(utilities): time formatting, timeline sorting, type predicates
* feat(timeline): HTML timeline-graph rendering layer
* feat(timeline): history-graph and shared lines-and-dots pieces
* refactor(timeline): remove the old SVG timeline
* refactor(components): shared holocene tweaks and composited heartbeat
* refactor(events): event/payload/activity component updates
* feat(timeline): wire the timeline into layouts and pages
* chore(i18n): timeline strings
* chore(timeline): perf/test harness, e2e specs, docs, tooling
* Dedupe change
* use transform
* perf(timeline): recycle row-pool slots to cut scroll recomputes
Map group i to a fixed slot (i % poolSize) and reuse the prior slot
object when unchanged, so a scroll re-points only the slots that
actually change instead of shifting every slot's group each frame. The
stable object identity keeps the {#each} item unchanged so a row's
point-computation derived no longer re-runs for rows that didn't move.
---------
Co-authored-by: alex thomsen <alex.scott.thomsen@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>1 parent af5c940 commit 066a662
95 files changed
Lines changed: 10360 additions & 2086 deletions
File tree
- docs
- scripts
- src/lib
- components
- activity
- event
- lines-and-dots
- history-graph
- svg
- timeline-graph
- payload
- contexts
- holocene
- icon
- svg
- i18n/locales/en
- layouts
- models/event-groups
- pages
- services
- test-helpers
- stores
- utilities
- temporal
- activities
- tests/integration
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
| 40 | + | |
40 | 41 | | |
41 | 42 | | |
42 | 43 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| 41 | + | |
| 42 | + | |
41 | 43 | | |
42 | 44 | | |
43 | 45 | | |
| |||
53 | 55 | | |
54 | 56 | | |
55 | 57 | | |
| 58 | + | |
| 59 | + | |
56 | 60 | | |
57 | 61 | | |
58 | 62 | | |
| |||
65 | 69 | | |
66 | 70 | | |
67 | 71 | | |
| 72 | + | |
68 | 73 | | |
69 | 74 | | |
70 | 75 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
0 commit comments