Repository navigation
perf(io): read several DataStub selections in one pass - #912
Draft
ehennestad wants to merge 5 commits into
Draft
ehennestad wants to merge 5 commits into
ehennestad wants to merge 5 commits into
Conversation
ehennestad
added this pull request to stack #913
September 30, 2026 19:29
2 of 3 tasks
ehennestad
force-pushed
the
read-selections-in-one-pass
branch
4 times, most recently
from
October 2, 2026 10:09
aae7a02 to
033d213
Compare
1 task done
ehennestad
force-pushed
the
read-selections-in-one-pass
branch
from
October 2, 2026 13:35
e54f96c to
8996846
Compare
ehennestad
removed this pull request from stack #913
October 2, 2026 13:35
ehennestad
changed the base branch from
fix-get-row-ragged-read-performance
to
perf-find-shapes-contiguous-runs
October 2, 2026 13:35
ehennestad
added this pull request to stack #925
October 2, 2026 13:35
ehennestad
force-pushed
the
read-selections-in-one-pass
branch
from
October 6, 2026 05:38
8996846 to
152a9f5
Compare
ehennestad
force-pushed
the
read-selections-in-one-pass
branch
2 times, most recently
from
October 6, 2026 18:09
873c611 to
bb49070
Compare
getRow reads each contiguous run of a multi-dimensional ragged column as its own DataStub read. Each read opens the file and dataset, builds the selection in MATLAB and converts the result, about 1 ms of fixed cost against about 0.09 ms for the HDF5 read itself. Every other row of a 5,000-row 8 x n column is 2,500 runs and takes 2.3 s. DataStub.loadSelections reads a list of selections through the storage backend. The base LazyArray reads them one by one with load_mat_style. HDF5LazyArray opens the file and dataset once and reads each selection of a numeric dataset that is ':' or one contiguous range per dimension as a plain hyperslab, and passes other selections to load_mat_style. readDataRows uses it for DataStub columns with more than one dimension. The every-other-row case now takes 0.17 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tests check which method read a selection. With one shared count they had to infer it from the total. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nwbRead returns every chunked numeric dataset as a bound DataPipe, so getRow read the runs of such a column with one load_mat_style call each. DataPipe.loadSelections passes the selections to the bound DataStub, or indexes the data of an unbound pipe once per selection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous test expected NotImplemented from the base class, which would also pass if loadSelections threw it itself. LazyArrayFake records each load_mat_style call, so the test checks the calls and the results. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 of 3 tasks
ehennestad
removed this pull request from stack #925
October 6, 2026 20:44
ehennestad
force-pushed
the
read-selections-in-one-pass
branch
from
October 6, 2026 20:45
bb49070 to
76f2d51
Compare
ehennestad
added this pull request to stack #941
October 6, 2026 20:45
This branch has not been deployed
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.
Motivation
Background — With #909,
getRowreads a ragged column with more than one dimension, such aswaveforms, with one read per contiguous run of requested elements.toTableor a contiguous range of rows is one run. Every other row of a table is one run per requested row.Problem — A user who reads many scattered rows of such a column from file pays a fixed cost for every run. In the example below, every other row of a 5,000-row column of 8-sample vectors is 2,500 runs, and
getRowtakes 3.05 s. Each read opens the file and the dataset, builds the selection in MATLAB and converts the result, which costs about 1 ms, while the HDF5 read itself takes about 0.09 ms. The rows are correct; only the time is wrong.Solution — A
DataStubcan now read a list of selections in one pass: it opens the file and the dataset once and reads each contiguous range directly. ADataPipebound to a file passes the list to itsDataStub.nwbReadreturns every chunked numeric dataset as a boundDataPipe, so both kinds of column occur in files read from disk.getRowreads all runs of a column this way, and the example takes 0.38 s.What changed
getRowandtoTableread all runs of a ragged column with more than one dimension in one call to the file. A scattered selection of many short rows is several times faster; a contiguous selection is one run, as before.DataStub.loadSelections(selections)is a new method. It reads several selections, each as indexing theDataStubwith it would, and returns the results in a cell array.DataPipe.loadSelections(selections)is a new method with the same contract. A boundDataPipereads the selections in one pass through itsDataStub; an unboundDataPipeindexes its data once per selection.getRowreturns are unchanged.Implementation notes
io.backend.base.LazyArray.loadSelectionsis a default that callsload_mat_styleonce per selection, so a backend without its own version, such as the Zarr backends in progress, works unchanged.io.backend.hdf5.HDF5LazyArray.loadSelectionsopens the file and the dataset once. A selection of an integer or floating-point dataset with one subscript per dimension, each':'or one contiguous ascending range, is read as a plain hyperslab, withoutio.space.segmentSelection,io.space.findShapesorhdf2mat. Every other selection goes toload_mat_style.readDataRowsin+types/+util/+dynamictable/getRow.muses it forDataStubandDataPipecolumns with more than one dimension. A one-dimensional column is already read in one call.BaseLazyArrayTestlistsloadSelectionsamong the base methods and leaves it out of the not-implemented check, because the base class implements it. A new in-memory test double,tests.unit.io.backend.doubles.LazyArrayFake, inherits the default and records itsload_mat_stylecalls, so the test checks one call per selection, in order, and the returned data.tests.unit.io.backend.doubles.HDF5LazyArraySpycountsload_mat_style,load_h5_styleandloadSelectionscalls separately, andLoadCountis their sum. The new tests check that hyperslab selections are read withoutload_mat_style, that other selections are passed to it with the same results, and that the waveforms of two separate units are read in oneloadSelectionscall.dataPipeTestchecks thatDataPipe.loadSelectionsreturns the same data as indexing theDataPipe, before and after it is bound.DynamicTableRaggedReadTestalready comparesgetRowon a boundDataPipecolumn with the unbound one. The spy cannot be placed inside a boundDataPipe, so no test counts its reads.load_mat_style. The two changes are independent; this PR does not go throughload_mat_stylefor hyperslab selections.Examples
Every other row of a ragged column with 8-sample vectors
The snippet builds a table with a ragged column of 5,000 rows, each holding 1 to 5 vectors of 8 samples, exports it, reads it back, and times
getRowon every other row. The last line compares the rows with those of the table in memory.Before (#909) — The 2,500 rows are 2,500 separate reads from the file, and the call takes 3.05 s.
same as in memory: 1shows the rows are correct.After — The same 2,500 runs are read in one pass over the dataset, and the call takes 0.38 s. The rows are unchanged.
Timings are from one machine and vary with platform and disk.
How to test
Run the snippet above on #909's branch and on this branch and compare the
getRow every other rowline. Then run the new and updated tests:Checklist
fix #XXwhereXXis the issue number?🤖 Generated with Claude Code
Related pull requests
perf-find-shapes-contiguous-runs), stack #925: fix(dynamictable): read ragged columns once per index level in getRow #909 → perf(io): read gapped multi-dimensional selections in one call #923 → perf(io): read several DataStub selections in one pass #912. It was rebased onto perf(io): read gapped multi-dimensional selections in one call #923 on 2 October 2026. perf(io): read gapped multi-dimensional selections in one call #923 makesgetRowread the elements of all requested rows of a file-backed ragged column in oneload_mat_stylecall, so thegetRowchange of this PR, which batched the per-run reads of fix(dynamictable): read ragged columns once per index level in getRow #909 throughloadSelections, was dropped in the rebase together with its testtestGetRowReadsScatteredWaveformsInOneCall.getRow.mis identical in this PR and perf(io): read gapped multi-dimensional selections in one call #923. What remains here is theloadSelectionsAPI onLazyArray,HDF5LazyArray,DataStubandDataPipe, with its tests and the per-method counters ofHDF5LazyArraySpy.