Skip to content

perf: speed up filtering and graph data preparation for large datasets (#364) - #365

Merged
timdegroot1996 merged 2 commits into
mainfrom
feat/364-filter-performance
Sep 28, 2026
Merged

timdegroot1996 merged 2 commits into
mainfrom
feat/364-filter-performance

Conversation

@timdegroot1996

@timdegroot1996 timdegroot1996 commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #364

Problem

On large dashboards, applying a filter spent most of its time in our own JavaScript rather than in Chart.js. On a 504-run / 208k-test dataset, filtering took 40–66% of the time to apply a filter. #364 has the full profiling.

Changes

  • filter_data: looks up run starts in a Set instead of scanning an array for every suite, test and keyword row (rows × runs).
  • Timestamp transforms: the milliseconds, timezone-conversion and timezone-display copies are cached per settings combination instead of copying every row on every filter apply. The filter option availability now uses the same cache instead of its own.
  • Timeline views of Most Failed, Most Flaky and Messages: rows are grouped once by label and run start (group_timeline_values) instead of scanning all rows for every run and label. With a message config, each rule's regex is built once.
  • Suite section filters: get_suite_data_exclusion reads the selected suite filters from the DOM once and returns the per-row check. exclude_from_suite_data used to read them for every row.
  • Smaller fixes:
    • sort_wall_clock computes its sort keys once.
    • The test select no longer does a quadratic duplicate check.
    • The base64 payload is decoded with a plain loop instead of Uint8Array.from with a callback.
  • Message config timeline bug: it relied on a block-scoped function and an undeclared variable, which both throw in strict mode. They are now declared properly.

Results

504 runs / 208k tests, headless Chrome, median of 3:

Scenario main this PR
Apply filter, default amount 1.18 s 0.37 s
Apply filter, all runs 8.07 s 3.14 s
Initial load 3.19 s 2.29 s
Messages with a message config, all runs, 306 runs (graph update) 1.49 s 0.65 s

Chart.js is the largest remaining cost. The follow-up items are listed in #364.

Tests

  • Unchanged output: I ran the same scenarios on main and this branch and compared chart data, filtered data and select options:
    • scenarios: the timestamp settings, default and all-runs amounts, bar and timeline views, suite paths, and a suite folder
    • dashboards: base, medium, and medium with a message config
    • All 1080 comparisons were identical, with no page errors.
  • JS unit tests: 373 passed. New tests cover:
    • the transform cache
    • stable wall-clock sorting
    • group_timeline_values
    • get_suite_data_exclusion (the four selection cases, plus reading the DOM only once)
    • the Messages bar and timeline with a message config
  • Robot tests (Docker): 124 tests, 124 passed, 0 failed. This run was before a comment-only cleanup.

Second commit: Overview, Tables, data loading

  • Overview: a run-card donut is created only when its card comes near the viewport, and destroyed before the card is removed.
    • Each update used to create every donut twice and never free the old charts, so memory grew with every filter change.
    • Update with 504 runs: 4.0 s → 0.07 s.
  • Tables: the columns get the type DataTables detected, so it no longer checks every cell on every update. version is left to detection. Sort and search results are identical to main. Update: 3.9 s → 2.4 s.
  • Data loading: the payloads are inflated in parallel with the native DecompressionStream (based on @HuntTheSun's branch), and pako is removed. main() awaits load_data() before anything reads the data.
  • Tooltips: build_tooltip_meta parses each run start once (180 → 51 ms).
  • Initial load, 504-run dashboard: 3.8 s → 2.3 s.
  • Browser support: DecompressionStream needs Chrome 80+, Firefox 113+ or Safari 16.4+.
  • Tests: JS 373 passed, Python 406 passed, robot (Docker) 124 passed.

🤖 Generated with Claude Code

timdegroot1996 and others added 2 commits September 28, 2026 14:25
#364)

Applying a filter on a large dataset spent most of its time in our own
JavaScript rather than in Chart.js:

- filter_data looked up every suite/test/keyword row in an array of run
  starts (rows x runs); it now uses a Set.
- The run_start transformations (milliseconds, timezone conversion,
  timezone display) copied every row on every filter apply. They only
  depend on three settings, so the copies are cached per combination and
  shared with the filter option availability, which had its own cache.
- The timeline views of Most Failed, Most Flaky and Messages scanned all
  rows for every run and label. Rows are now grouped once by label and
  run_start. With a message config every rule's regex is built once.
- The suite section filters were read from the DOM for every row;
  get_suite_data_exclusion reads them once and returns the check.
- sort_wall_clock computes its keys once, the test select no longer does
  a quadratic duplicate check, and the base64 payload is decoded with a
  plain loop instead of Uint8Array.from with a callback.

On a 504 run / 208k test dataset applying the default filter goes from
1.18 s to 0.37 s, all runs from 8.1 s to 3.1 s, and the initial load
from 3.2 s to 2.3 s. Chart data, filtered data and select options are
identical to before across timestamp settings, amounts, bar/timeline
views, suite paths and a message config.

The message config timeline also no longer relies on a block-scoped
function and an undeclared variable, which threw in strict mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ion (#364)

- Overview: every run card created its own Chart.js donut, twice, on
  every update, and the donuts of removed cards were never destroyed, so
  Chart.js kept all of them alive. A donut is now created when its card
  comes near the viewport and destroyed before its card is removed.
  Updating the overview with 504 runs goes from 4.0 s to 0.07 s.
- Tables: DataTables detected the type of every column by checking every
  cell on every update. The columns now get the type it detected, except
  the version which depends on the project. 3.9 s to 2.4 s.
- Data loading: the embedded payloads are inflated in parallel with the
  native DecompressionStream instead of pako, which is removed. main()
  awaits load_data() before anything reads the data, so the transform
  cache in the filter pipeline reads the arrays when it is called.
- build_tooltip_meta parses every run_start once instead of per row.

The initial load of the 504 run dashboard goes from 3.8 s to 2.3 s.

Co-Authored-By: HuntTheSun <53446567+HuntTheSun@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@timdegroot1996
timdegroot1996 merged commit 50cbb6f into main Sep 28, 2026
3 checks passed
@timdegroot1996 timdegroot1996 mentioned this pull request Sep 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Improvement] Speed up filtering and graph data preparation for large datasets

1 participant