Skip to content

Fix performance issues for large reports#32

Merged
elhil merged 8 commits into
mainfrom
indexeddb-storage
Feb 26, 2026
Merged

Fix performance issues for large reports#32
elhil merged 8 commits into
mainfrom
indexeddb-storage

Conversation

@elhil
Copy link
Copy Markdown
Contributor

@elhil elhil commented Feb 20, 2026

It turns out that browsers set a fairly small quota per website for localstorage. Since all our reports share the same localstorage pool, browsers were running out of space, causing spinalcordtoolbox/spinalcordtoolbox#5083

Hence, implements IndexedDB as our storage backend: asynchronous, fast, and allowing ~10GiB on most browsers.

In addition, applies several optimizations:

  • virtualized table rows
  • refactored sorting logic to prevent re-renders
  • better compartmentalization of prop changes to components

This also cleans up the code a little by consolidating state management, which needed to happen eventually anyway.

@github-actions
Copy link
Copy Markdown

Test QC report built! You can download it here.

@elhil elhil requested a review from joshuacwnewton February 20, 2026 21:13
@elhil
Copy link
Copy Markdown
Contributor Author

elhil commented Feb 20, 2026

What to test here: changing around:

  • columns
  • sorting
  • ranking rows
  • flagging rows.

For each of these, do they persist across:

  • closing & opening the tab
  • Save & Upload buttons

@elhil
Copy link
Copy Markdown
Contributor Author

elhil commented Feb 23, 2026

Also, as a bonus: test for #21 - related issues, like what happens when you run refresh_qc_entries while a report is open/flagged/ranked?

Comment thread src/components/Table/index.tsx
@elhil
Copy link
Copy Markdown
Contributor Author

elhil commented Feb 25, 2026

Okay, big changes that I think (??) solve the performance issues!

  • The biggest performance change has to do with the freezing of row order when editing. This is now done completely differently, in such a way as to prevent many re-renders whenever a row was edited. Along with moving around variables to make sure every component only registers the changes it needs to, this solved 95% of the performance issues.
  • For good measure, we also now "virtualize" the table, meaning only a fixed number of rows are rendered in the DOM, instead of many thousands of rows being rendered and then shoved off screen. This changes the look and feel of the table a bit: it's no longer forced into the width of its container, but now column resizing works much better. Also fast scrolling will produce a lag while the new rows are rendered. We can tweak this a bit, but I think it's overall the way to go if we want to support large reports. That being said, I made this change before the most important one above, thinking it would be the solution, so we can revert this if the UX isn't acceptable.

@elhil
Copy link
Copy Markdown
Contributor Author

elhil commented Feb 25, 2026

Looks like tagging a commit [skip ci] even upstream a bit caused the preview not to build, so you can just pull and yarn build @joshuacwnewton

@joshuacwnewton

This comment was marked as resolved.

@joshuacwnewton
Copy link
Copy Markdown
Member

((Oh, sorry! I never mentioned that the performance is FANTASTIC! And there is honestly no noticeable lag when fast-scrolling. I have no comments on the performance at all, it's perfect.))

@elhil
Copy link
Copy Markdown
Contributor Author

elhil commented Feb 25, 2026

Okay, I think I've found a comfortable middle ground between no-scrollbar-ever and the awkwardness you found. Now the table always fills the available width (no scrollbar) until enough columns are selected that it would squish them to add any, at which point a horizontal scrollbar appears.

While doing so, this refactor also lifts all table state up to the
App component so it can live in one place. This massively
simplifies state management, also by offering a setTableState
callback which will only change the keys provided, instead of
mutating the entire state.

IndexedDB values are updated asynchronously, using only React state
for local UI changes.
Instead of rendering all rows, use react-table's virtualization
(i.e., only display ~20 rows at a time instead of all). This comes
with some usability drawbacks, mainly because our rows aren't a
predictable size, so there can be some flicker if scrolling fast.

This seems to solve *some* of the lag issues, most importantly on
first selection of a row.
This keeps state regarding datasets out of App, which prevents
unnecessary re-renders when data changes
Instead of adding the invisible 'position' column, the injection of
which was causing lengthy laggy re-renders, we instead make use of
manual sorting. Switching it on any time a dataset is modified,
we achieve freezing the rows without a re-render. When sorting changes,
we flip this flag back off, and sorting resumes. This completely
eliminates the awkward RowOrder state as well :)

This requires moving useKeyboardShortcuts back down into Table, so
we can intercept the call to change a dataset and use it to toggle
sorting state. I think this leaves the components in a fairly
balanced state.

[skip ci]
@elhil elhil changed the title Use IndexedDB as storage backend Fix performance issues for large reports Feb 25, 2026
Copy link
Copy Markdown
Member

@joshuacwnewton joshuacwnewton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything works beautifully!

The only issue I noticed (outside the scope of this PR) is that refresh_qc_entries.py wipes out rank/QC. This is... quite severe! But, I don't know if anyone actually uses that feature (I found it hard to productively delete specific json files, since the filename don't include any subject/session details), so hopefully (🙏) no one has run into this before.

I wish I could review the actual code changes for like... TypeScript code quality, structure, maintainability, etc. but I don't have any experience in terms of like... TS patterns, whatever the equivalent to "Pythonic" is for webdev. Still, everything works like a charm, so... good to go? 😅

@elhil
Copy link
Copy Markdown
Contributor Author

elhil commented Feb 25, 2026

Everything works beautifully!

The only issue I noticed (outside the scope of this PR) is that refresh_qc_entries.py wipes out rank/QC. This is... quite severe! But, I don't know if anyone actually uses that feature (I found it hard to productively delete specific json files, since the filename don't include any subject/session details), so hopefully (🙏) no one has run into this before.

Oh, it shouldn't. That should work outside this PR, so that would be a regression. I'll look into it!

I wish I could review the actual code changes for like... TypeScript code quality, structure, maintainability, etc. but I don't have any experience in terms of like... TS patterns, whatever the equivalent to "Pythonic" is for webdev. Still, everything works like a charm, so... good to go? 😅

Happy to run through the codebase at any point, though the only way to really judge this would be to learn how to do React development, which I don't necessarily recommend 😅

@elhil
Copy link
Copy Markdown
Contributor Author

elhil commented Feb 25, 2026

Okay, I think the QC/rank wipeout is an upstream issue, tracked in spinalcordtoolbox/spinalcordtoolbox#5177 and spinalcordtoolbox/spinalcordtoolbox#5178. (Did this ever work?)

@elhil elhil force-pushed the indexeddb-storage branch from 9d2005d to ecea64d Compare February 25, 2026 21:18
@elhil
Copy link
Copy Markdown
Contributor Author

elhil commented Feb 25, 2026

(Force-pushed rebased version for one final test)

@github-actions
Copy link
Copy Markdown

Test QC report built! You can download it here.

@elhil
Copy link
Copy Markdown
Contributor Author

elhil commented Feb 26, 2026

Test QC report built! You can download it here.

Works great using index.html from this build in the example report from upstream. Using refresh_qc_entries.py from spinalcordtoolbox/spinalcordtoolbox#5178 also results in QC/rank persistence.

Merging! Thanks @joshuacwnewton as always for the careful testing and detailed feedback.

@elhil elhil merged commit a390b88 into main Feb 26, 2026
2 checks passed
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.

2 participants