Repository navigation
fix(dynamictable): read ragged columns once per index level in getRow - #909
Open
ehennestad wants to merge 7 commits into
Open
ehennestad wants to merge 7 commits into
ehennestad wants to merge 7 commits into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## rename-getrow-variables #909 +/- ##
===========================================================
+ Coverage 95.44% 95.52% +0.07%
===========================================================
Files 240 240
Lines 8870 8955 +85
===========================================================
+ Hits 8466 8554 +88
+ Misses 404 401 -3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This was referenced Sep 30, 2026
ehennestad
added this pull request to stack #913
September 30, 2026 19:29
ehennestad
removed this pull request from stack #913
October 2, 2026 13:35
ehennestad
added this pull request to stack #925
October 2, 2026 13:35
ehennestad
force-pushed
the
fix-get-row-ragged-read-performance
branch
2 times, most recently
from
October 6, 2026 18:00
df66561 to
7a49608
Compare
getRow read a ragged column by recursing once per row, and at the data level once per element. Each call read from file on its own, so a doubly ragged column with S spikes cost about S dataset reads. For the units table in #787 (172 units, 211 722 spikes), toTable took 181 s after reading the file on one machine and 1792 s on the reporter's. Each level of a ragged column is now read once, over the span of elements the requested rows cover, and the rows are sliced from those reads in memory. The same table now takes 1.7 s after read. Empty rows are read once per column with an empty selection rather than sliced from the window, so they keep the type and shape a direct read gives them. The output of getRow is unchanged for in-memory, file-backed and DataPipe columns. The spy that counts the reads subclasses HDF5LazyArray because DataStub is sealed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reading each level over the span that covers the requested rows also loads every row in between. For getRow([1 172]) on the units table in #787 that is the whole 108 MB waveforms dataset, to return two units. Each index level is now read in one call for exactly the index elements the requested rows need, and one-dimensional data is read the same way. Multi-dimensional data is read in one call per contiguous run of requested elements. A multi-dimensional selection with gaps becomes one hyperslab per run, and building it grows faster than linearly with the number of irregular runs: 1,000 runs took 5.7 s. toTable and any contiguous range of rows still cost one read per level. A scattered selection of many short rows in a multi-dimensional column now costs one read per run, which matches main (every other row of a 5,000-row 8 x n column: 3.2 s here, 3.1 s on main). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…elements getRow reads an empty row as an empty selection from the column. For a file-backed column read with one subscript, load_mat_style reads the first element to learn the type of an empty selection, so a dataset with no elements fails with NWB:DataStub:Load:InvalidSelection. A ragged column whose rows are all empty, such as the spike times of units without spikes written to an extendable dataset, could not be read with getRow or toTable. When the dataset has no elements, the empty row is now built from the column's data type, the way io.parseCompound builds the columns of a compound dataset without rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
readDataRows read every column into a block of the requested elements and then sliced each row out of the block. For a column in memory that copies every element twice, and toTable briefly holds a second copy of the whole column: 108 MB for the waveforms column in #787. A column in memory is now indexed once per row with the row's element range, as before this branch. Columns in a file and DataPipes keep the block reads, which are what make reading from file fast. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014hPv8PGs633qAZfx7p6CTD
A DataPipe is read by indexing it, through its subsref, not through its load method, and before export it holds its data in memory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add two tests for the compound paths of getRow: an empty row of a file-backed compound column with no elements, which is read as a table without rows that keeps the member names and types, and a ragged compound column built in memory as a scalar struct of columns. A compound dataset without rows cannot be exported, so the test drops the rows from the exported dataset, as another NWB writer would write it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Say why readElementRanges reads the index elements sorted and without duplicates and why element 0 is dropped, define a run in readDataRows before the variables that use it, and note what the block positions and the clamped row lengths compute. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 of 3 tasks
ehennestad
removed this pull request from stack #925
October 6, 2026 20:44
ehennestad
force-pushed
the
fix-get-row-ragged-read-performance
branch
from
October 6, 2026 20:45
7a49608 to
2fef956
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.
fix #787
Motivation
Makes
toTableandgetRowfast on ragged columns read from a file, which were read one row at a time: a 20-unit Units table with waveforms takes 0.25 s instead of 29.6 s.Background — A ragged column stores rows of different lengths as one flat data array plus an index of cumulative row ends. In a Units table,
waveformsis doubly ragged: one index has a row per unit that points at the unit's spikes, and a second index has a row per spike that points at its waveform. Units with 3 and 2 spikes give the unit index[3 5].Problem — A user reads an NWB file and calls
toTableorgetRowon a Units table with waveforms. The call takes minutes, while the same call on the table before export takes about a second. In the 20-unit example below (26,583 spikes),toTabletakes 0.94 s before export and 29.6 s after reading the file. For the full table in #787 (172 units, 211,722 spikes) it takes 181 s here, and the reporter measured 1792 s. The cause is that each unit's index entries and each spike's waveform are read from the file one at a time, so 26,583 spikes cost more than 26,000 separate file reads. The output is correct, so the only symptom is the wait.Solution —
getRownow reads only the elements of the requested rows, and for contiguous rows, as intoTable, it reads each level of the column once. The rows are then taken from those reads in memory. The example takes 0.25 s after reading the file, and the output is unchanged.Additional fix: rows of a column whose dataset has no elements
Problem — A user reads a file in which every row of a ragged column is empty, so the column's dataset holds no elements, and
getRowortoTablefails. In the second example below,getRow(1:2)on a table whoseamplitudesdataset was written as an extendable dataset with nothing appended stops withDataStub indices for dimension 1 must be less than or equal to dimension size 0instead of returning two empty rows. The cause is that an empty row is read as an empty selection from the column, and the reader reads the first element of the dataset to learn the type of an empty selection; a dataset with no elements has no first element. This also fails onmain.Solution — When the dataset has no elements, the empty row is built from the column's data type, so
getRow(1:2)returns two0x0rows.Why in this PR — This PR reads the empty rows of a column through one new helper, and the fix changes only that helper.
What changed
getRowandtoTableread only the elements of the requested rows of a ragged column.toTableand any contiguous range of rows cost one read per level, so a doubly ragged column such aswaveformscosts 3 reads.getRow([1 172]), each index level and any one-dimensional data are still read in one call. Multi-dimensional data such aswaveformsis read in one call per contiguous run of requested elements, so a scattered selection never makes more reads than before this PR.DataPipe, including empty rows.Implementation notes
selectin+types/+util/+dynamictable/getRow.mlooks up the column'sVectorDataandVectorIndexobjects once. For a ragged column,getRaggedRowsreads each index level in one call for exactly the index elements the requested rows need (readElementRanges), expands those rows' element ranges into the rows requested from the level below (expandRanges), and reads the data withreadDataRows. It then nests the rows level by level withmat2cell.readDataRowsreads one-dimensional data in one call, as a point selection. It reads multi-dimensional data in one call per contiguous run, because a multi-dimensional selection with gaps goes throughio.space.findShapes, whose cost grows faster than linearly with the number of irregular runs: 1,000 runs took 5.7 s on the machine used for this PR.8 x ncolumn takes 3.2 s, the same asmain. Reading the span that covers the rows takes 0.07 s but also loads the rows that were not requested. A setting to choose between time and memory could be added later.0x0from file and0x1in memory, and this PR leaves that as it is.readEmptyRowbuilds the empty row from the column'sdataTypeinstead of reading. The types followio.parseCompound, which builds the columns of a compound dataset without rows the same way: text and non-boolean enums as cell arrays, booleans as logical arrays, numeric and reference types from<class>.empty, and a compound type as a table with one variable per member.getRaggedRowstakes the chain of column objects rather than the table, so it can move ontoVectorIndexBase.getRowsfrom Refactor plan: consolidate DynamicTable helpers around column behaviour, one column accessor, declared structure and pure validation #907 without changes.readRows,orientRows,getRowDimensionandindexRowsare the previous data branch split into parts, so reading from the column and slicing from a window use the same rule for which axis holds the rows.tests.unit.io.backend.doubles.HDF5LazyArraySpy, that subclassesHDF5LazyArrayand is passed to theDataStubconstructor, becauseDataStubis sealed. It counts the reads and records the elements each read selects.Examples
toTableon a Units table with waveforms, read from fileThe snippet builds a Units table with 20 units and one 64-sample waveform per spike, exports it, reads it back, and times
toTableon both copies. It is the script from #787 with fewer units.Before — Reading the table from the file takes about 30 times as long as building it from memory.
tables equal: 1shows that the result was already correct; only the time is wrong.After — Reading from the file takes no longer than building the table in memory, and the two tables are still equal.
Timings are from one machine and vary with platform and disk.
A ragged column whose dataset has no elements
The snippet writes a table with two rows and a ragged
amplitudescolumn. The column's data is an extendable dataset with nothing appended, so both rows are empty and the dataset holds no elements. It then reads the file and gets both rows.Before —
getRowfails while reading the empty rows, so no rows are returned.After — Both rows are returned, each with an empty
doublearray, which is how an empty row of a file-backed numeric column reads.How to test
Run the snippet above on
mainand on this branch and compare thetoTable after readline. Then run the new tests.testGetRowReadsEachLevelOncefails onmainand passes here.testGetRowReadsOnlyRequestedElementschecks thatgetRow([1 4])does not read the waveforms of unit 3, which lies between the two.testGetRowWhenColumnHasNoElementscovers the additional fix; it fails onmainwith the error above. The other tests check that file-backed, in-memory andDataPipecolumns return the same rows as before.Checklist
fix #XXwhereXXis the issue number?🤖 Generated with Claude Code