perf(datahub): match export rows against a set of listed subject ids#1425
Open
gdevenyi wants to merge 1 commit into
Open
perf(datahub): match export rows against a set of listed subject ids#1425gdevenyi wants to merge 1 commit into
gdevenyi wants to merge 1 commit into
Conversation
The export filter built its list of subject ids by iterating each row's
visible cells:
.rows.flatMap((row) => row.getVisibleCells().map((cell) => removeSubjectIdScope(cell.row.original.id)))
The callback ignores `cell` and reads `cell.row.original.id`, so the row's
id is emitted once per rendered column. Measured against the real table,
which has three data columns plus the row-actions column libui adds:
5,000 rows produce a 20,000 entry array, a 4x duplication, with
`removeSubjectIdScope` called four times per row.
That array was then the right-hand side of an `includes` inside a
`filter` over the export, so matching was a linear scan per exported row
against a list four times longer than necessary.
Read one id per row into a Set instead. Measured filtering 200,000 export
rows against 5,000 listed subjects, identical results (167,000 matched
either way):
before 3,070 ms
after 17 ms
A 25,000-record group with 30 measures per record produces around 750,000
export rows, so the figure above is conservative.
Still client-side: the export is fetched for the whole group and narrowed
in the browser, so every filter the user has applied is honoured only
after the payload has crossed the network. Pushing the subject ids to the
export endpoint is the real fix and composes with the streaming rework in
#1407.
Refs #1412
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bfen9VQemZanJLXinJ6MMG
Contributor
There was a problem hiding this comment.
Pull request overview
This PR improves Datahub export filtering performance by extracting one subject ID per listed table row (instead of per visible cell) and using a Set for constant-time membership checks when filtering exported rows client-side.
Changes:
- Replace per-cell subject ID collection (
getVisibleCells().flatMap(...)) with a per-row extraction helper that deduplicates viaSet. - Update export filtering to use
Set.has(...)instead ofArray.includes(...). - Add a new test file that renders a real
DataTableshape (including row actions) to validate the underlying “per-cell duplicates IDs” behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| apps/web/src/routes/_app/datahub/index.tsx | Switch export filtering to per-row ID extraction and Set membership checks for large performance gains. |
| apps/web/src/routes/_app/datahub/tests/index.test.tsx | Add tests around table row-model behavior and basic set membership filtering. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+37
to
+39
| ); | ||
| return table!; | ||
| }; |
Comment on lines
+67
to
+75
| it('membership is a set lookup, so an export row is matched without scanning the list', () => { | ||
| const table = renderMasterTableLike([{ id: 'subject-a' }, { id: 'subject-b' }]); | ||
| const listed = new Set(table.getPrePaginationRowModel().rows.map((row) => row.original.id)); | ||
|
|
||
| const exportRows = [{ subjectId: 'subject-a' }, { subjectId: 'subject-z' }, { subjectId: 'subject-b' }]; | ||
| expect(exportRows.filter((row) => listed.has(row.subjectId)).map((row) => row.subjectId)).toStrictEqual([ | ||
| 'subject-a', | ||
| 'subject-b' | ||
| ]); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses item 1 of #1412.
The bug
The export filter built its list of subject ids by iterating each row's visible cells:
The callback ignores
cellentirely and readscell.row.original.id— the same value for every cell in the row. So the row's id is emitted once per rendered column, andremoveSubjectIdScoperuns once per column too.That array was then the right-hand side of an
includesinside afilterover the export, making the match a linear scan per exported row, against a list several times longer than it needed to be.Measured, not estimated — and the issue understated it
I rendered the real table and counted. It has three data columns plus the row-actions column libui injects, which #1412 didn't account for:
So the duplication is 4×, not the "at least three" I wrote in the issue.
Filtering 200,000 export rows against 5,000 listed subjects, identical results both ways (167,000 matched):
That is ~180×, and it is conservative: a 25,000-record group with 30 measures per record produces roughly 750,000 export rows, ~3.75× the size benchmarked here.
The change
and
listedSubjects.has(...)at the call site. Extracted to a named helper so the reason it reads one id per row is documented where someone would otherwise reintroduce the cell loop.Tests
Three, driving the real
DataTablewith the master table's shape (three columns + row actions) rather than a hand-built stand-in — so the duplication claim is verified rather than assumed:getVisibleCells()genuinely over-counts, and that the current extraction doesn't);Checks
tsc --noEmitonapps/web— cleaneslint src— cleanprettier --check— cleanvitest run(whole workspace) — 255 passed, 1 skipped, 1 failedThe one failure is
instrument-records.service.spec.ts > upload > should create records with pending set. This branch is cut frommain, so it carries that pre-existing failure; #1422 fixes it.What is still open in #1412
The filtering is still client-side. The export is fetched for the entire group and narrowed in the browser, so the sex filter, the date-of-birth range, the search string and the "with records only" toggle are all honoured only after the payload has crossed the network. Pushing the subject ids (or the filter criteria) to
/v1/instrument-records/exportis the real fix, and it composes with the streaming rework proposed in #1407 — worth doing together rather than bolting asubjectIdsparameter onto an endpoint that is about to be restructured.Item 3 of #1412 — the same anti-pattern in the audit log download — is already handled in #1419, which replaces it with a server-side fetch.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Bfen9VQemZanJLXinJ6MMG