Repository navigation
Tracking: large data (layers over 2 million features, slow hillshade and GeoTIFF writing) #1111
Description
On real-sized data RustGIS either refuses or is much slower than it needs to be. Measured on one US state (3,212,306 building footprints, 4,562 census block groups, a 9667 × 11947 DEM; rustgis-cli at d58cb8a, i7-6700, single runs, ±15%), with the time an established tool takes on the same files in brackets. This issue tracks fixing that, and has one decision to make first.
What's wrong now
- Layers over 2,000,000 features can't be exported, reprojected or used in tools.
MAX_LIMIT(crates/data/src/source/mod.rs) refuses them: all 3.2M buildings can't be converted, and [Bug]: A source over the 2,000,000-feature limit is silently replaced by a sample when read without streaming: exports and tools run on half the data #1108 is worse, a silent half sample. It's a count, not memory: it refuses 3.2M small polygons that fit in about 1.2 GB, but doesn't stop 2M large ones from exhausting memory. - Exports hold the whole layer, sometimes twice. Converting 1.79M polygons to FlatGeobuf takes 22.7 s and 1.2 GB (8.3 s, 0.26 GB). Reprojection clones the
Dataset, and the FlatGeobuf writer builds the whole file in aVec<u8>. - Hillshade is slow. It takes 59 s (7 s).
gradient()incrates/analysis/src/rasterops.rslooks up the coordinate system for every cell, 115M times here, and runs five trig functions per cell. Its output is written as Float32 although the values are whole numbers 0–255. - Writing GeoTIFFs is slow. The COG export takes 43 s (16–22 s), nearly all of it Deflate at level 6 through
miniz_oxide, about 18 MB/s (crates/data/src/raster/cogwrite.rs). - Summarize Within compares every point with every polygon ([Bug]: Summarize Within (countPoints) tests every point against every polygon: 1.8 million points in 4,562 polygons run for over 28 minutes #1105): over 28 minutes for 1.79M points (15 s).
Needs a decision: tools on layers bigger than memory
Steps 2 and 3 below happen either way: exports stream, and the count becomes a memory budget. What should tools do past the budget?
- A. Streaming tools (large). Per-feature tools (reproject, buffer, calculate geometry) work a chunk at a time. Tools that compare two layers (count points, spatial join, clip, select by location) index the smaller layer and stream the larger. Outputs go to temporary files added as streamed layers. That means tools on any size, but every tool gets a second execution path, and temporary-file outputs touch undo, project saving and the web app, which has no temporary files.
- B. Stay in memory (small to very large). Tools keep working on a whole layer under the budget, and past it the answer is "clip or filter first". There is one execution path, but a ceiling, lower in the web app. Small changes (boxing the common one-ring polygon, reserving from the known count) might take a feature from about 355 to 250 bytes. A flat, columnar geometry model would roughly halve memory, but touches every crate.
Which fits where RustGIS is heading?
The steps also assume these defaults. Say so if any is wrong:
- hillshade writes Byte;
- threads on the desktop only, with the web app single-threaded as the renderer already is (
threads_for); - a budget of 4 GiB on the desktop and 1 GiB in the web app, adjustable;
- COGs use the floating-point predictor (TIFF Predictor 3), which GDAL, QGIS and the
tiffcrate read.
Plan
1. Correctness (small, independent)
- Refuse instead of sampling ([Bug]: A source over the 2,000,000-feature limit is silently replaced by a sample when read without streaming: exports and tools run on half the data #1108): an incomplete read in
source::materializebecomes an error, checked from the known count before reading. - Index Summarize Within ([Bug]: Summarize Within (countPoints) tests every point against every polygon: 1.8 million points in 4,562 polygons run for over 28 minutes #1105): the polygons in
ExtentIndex, each point tested against its candidates only.
2. Stream exports and reprojection (medium)
-
visit_chunksfor GeoPackage, FlatGeobuf and GeoParquet. Shapefile already has it (feat(render): stream complete Shapefile layers in ordered chunks #681). - A writer per format that takes chunks, and
share.exportLayeron a streamed layer reading chunk → reproject → writer. Memory goes from 1.2 GB to about 0.3 GB, and the limit no longer applies to exports. - FlatGeobuf in the web app: its index needs a temporary file, so write it unindexed there.
3. A memory budget instead of MAX_LIMIT (small, after 2)
-
load_allcounts bytes as it reads chunks and refuses past the budget, withtry_reservefor the large vectors.
4. Terrain kernels (small, independent)
- Hillshade, slope and aspect as row passes: constants computed once, three row slices walked with
windows(3)andzip, with no-data and edges on the existing path. A standalone test gave identical output on all 115M cells, 21 s → 4 s on one thread, in the web app too. - Hillshade writes Byte.
5. GeoTIFF writing (small to medium, independent)
- Deflate level 1 with Predictor 3 in
cogwrite.rs: 157 MB/s and 25% smaller on the DEM's tiles; the COG export goes to about 12 s. - The untiled export through the in-house writer:
tiff0.11.3 can't write Predictor 3, and its speed today depends onzipturning onzlib-rs.
6. Threads on the desktop (medium, after 4 and 5)
- Raster kernels in row bands, COG tiles compressed in parallel: about 3× on 4 cores.
7. Tools past the budget (large or skipped, after the decision)
- A: streaming tools, Summarize Within first. B: the small memory savings.
Related
#1105 and #1108 are filed separately because they're bugs whatever is decided here. Step 2 builds on #681's chunked reading. #1065 (layers read in place are read twice on every render) is about drawing, not tools, so it stays separate.
Activity
choupeanut commentedon Oct 29, 2026
I will take a small, independent part of step 4: hoist the invariant cell-size calculation for slope, aspect and hillshade, and the invariant light angles for hillshade, while retaining the existing formulas, output type and no-data behavior. I will compare results against the current implementation and measure the speed on a representative raster. I see #1085 separately corrects axis orientation; this change will not alter that behavior or take over its fix.
choupeanut commentedon Oct 29, 2026
I will take the remaining terrain row-pass item in step 4, separate from the merged constant-hoisting change. I will retain the existing edge, no-data and axis-orientation behavior, compare slope, aspect and hillshade against the current implementation, and measure the kernels before and after. This adds no dependency or output-format change.
choupeanut commentedon Oct 29, 2026
I will take the GeoPackage portion of step 2: implement complete, ordered, byte-bounded visit_chunks traversal for the native and in-memory GeoPackage readers, with extent, invalid-row and cancellation coverage. This is the reader groundwork only; streaming writers and export wiring remain separate. No dependency, format or command changes.
Mavrick-F commentedon Oct 29, 2026
I'm taking step 2 (stream exports and reprojection), as a stack of three pull requests:
visit_chunksfor GeoPackage, FlatGeobuf and GeoParquet, behind a newvisits_chunks()capability.streams_chunks()also makes the map draw a source by walking all of it on every render ([Feature]: Layers read in place are read and projected in full twice on every render #1065), so it stays off for these formats and drawing is unchanged.share.exportLayeron a streamed layer reading chunk → reproject → writer, so exports no longer hold the whole layer or stop atMAX_LIMIT.The first is up now; it adds no dependency and changes no command or file format.
choupeanut commentedon Oct 29, 2026
I see #1131 now covers the GeoPackage traversal as well as the other step 2 readers, with the separate traversal capability and an independent SQLite connection. I am stepping back from my overlapping GeoPackage work so there is no duplicate PR. Thanks!
Mavrick-F commentedon Oct 29, 2026
@choupeanut Thanks, sorry for missing your note about starting the GeoPackage work!
Mavrick-F commentedon Oct 29, 2026
Starting the second pull request of step 2 now, on top of #1131:
share.exportLayeron a streamed layer reads it a chunk at a time, reprojects each chunk in place and writes it straight to the file, for GeoJSON, CSV, FlatGeobuf, GeoPackage and GeoParquet on the desktop. Those formats get one writer each, used by both the in-memory and the streamed export, so the two give the same file. Exports in these formats then no longer stop atMAX_LIMIT.Shapefile, KML, KMZ and GPX keep the whole-layer path for now: a Shapefile's DBF field widths come from a pass over the data, so streaming it needs two passes, which I'd rather do as its own change. The web app's export is step 2's third item.
Mavrick-F commentedon Oct 29, 2026
Two updates from a full run of the measurements behind this issue (four sizes, from 19,000 to 3.2 million buildings, upstream main at 13aee13 plus #1119):
path,raster.projectwrites its output throughcogwrite, so it pays the same Deflate cost: projecting the 9667 × 11947 DEM took 48 s, of which writing is about 35 s (the COG export of the same DEM alone took 35 s). The warp itself is about 13 s, on par withgdalwarp(15 s including its own write).