@@ -330,45 +330,48 @@ def test_from_map_batched_integration_with_datafusion_via_read_xarray():
330330
331331
332332def test_read_xarray_loads_one_chunk_at_a_time (large_ds ):
333+ tracemalloc .stop () # reset any state left by a previously-failed test
333334 tracemalloc .start ()
334- iterable = read_xarray (large_ds )
335- first_size , first_peak = tracemalloc .get_traced_memory ()
336- tracemalloc .reset_peak ()
335+ try :
336+ iterable = read_xarray (large_ds )
337+ first_size , first_peak = tracemalloc .get_traced_memory ()
338+ tracemalloc .reset_peak ()
337339
338- sizes , peaks = [], []
340+ sizes , peaks = [], []
339341
340- first_chunk = large_ds .isel (next (block_slices (large_ds )))
341- chunk_size = first_chunk .nbytes
342+ first_chunk = large_ds .isel (next (block_slices (large_ds )))
343+ chunk_size = first_chunk .nbytes
342344
343- # Creating the iterator should be inexpensive -- less than one chunk.
344- # We multiply by constant factors because chunks have additional overhead
345- assert first_size < chunk_size * 3
346- assert first_peak < chunk_size * 6
347-
348- for it in iterable :
349- _ = it
350- cur_size , cur_peak = tracemalloc .get_traced_memory ()
351- tracemalloc .reset_peak ()
352- sizes .append (cur_size )
353- peaks .append (cur_peak )
345+ # Creating the iterator should be inexpensive -- less than one chunk.
346+ # We multiply by constant factors because chunks have additional overhead
347+ assert first_size < chunk_size * 3
348+ assert first_peak < chunk_size * 6
354349
355- for size in sizes :
356- # Observed range: 1.59–1.83× chunk_size.
357- # iter_record_batches holds data-variable arrays (≈1× chunk) while
358- # yielding sub-batches, plus the current Arrow batch (≈0.65× chunk).
359- assert chunk_size * 1.3 < size , f"size { size } unexpectedly low"
360- assert chunk_size * 2.2 > size , f"size { size } unexpectedly high"
350+ for it in iterable :
351+ _ = it
352+ cur_size , cur_peak = tracemalloc . get_traced_memory ()
353+ tracemalloc . reset_peak ()
354+ sizes . append ( cur_size )
355+ peaks . append ( cur_peak )
361356
362- for peak in peaks :
363- # Observed range: 1.84–3.28× chunk_size.
364- # Peak includes data arrays + Arrow batch + temporary coordinate index
365- # arrays; the first batch of each chunk is highest (Dask compute overhead).
366- assert chunk_size * 1.5 < peak , f"peak { peak } unexpectedly low"
367- assert chunk_size * 4.0 > peak , f"peak { peak } unexpectedly high"
357+ for size in sizes :
358+ # Observed range: 1.59–1.83× on macOS, up to ~2.7× on Linux
359+ # (glibc + Arrow allocate more intermediate buffers).
360+ # iter_record_batches holds data-variable arrays (≈1× chunk) while
361+ # yielding sub-batches, plus the current Arrow batch (≈0.65× chunk).
362+ assert chunk_size * 1.3 < size , f"size { size } unexpectedly low"
363+ assert chunk_size * 3.5 > size , f"size { size } unexpectedly high"
368364
369- assert max (peaks ) < large_ds .nbytes
365+ for peak in peaks :
366+ # Observed range: 1.84–3.28× chunk_size.
367+ # Peak includes data arrays + Arrow batch + temporary coordinate index
368+ # arrays; the first batch of each chunk is highest (Dask compute overhead).
369+ assert chunk_size * 1.5 < peak , f"peak { peak } unexpectedly low"
370+ assert chunk_size * 4.0 > peak , f"peak { peak } unexpectedly high"
370371
371- tracemalloc .stop ()
372+ assert max (peaks ) < large_ds .nbytes
373+ finally :
374+ tracemalloc .stop ()
372375
373376
374377def test_read_xarray_table_memory_bounds (large_ds ):
@@ -384,37 +387,41 @@ def test_read_xarray_table_memory_bounds(large_ds):
384387 first_chunk = large_ds .isel (next (block_slices (large_ds )))
385388 chunk_size = first_chunk .nbytes
386389
390+ tracemalloc .stop () # reset any state left by a previously-failed test
387391 # --- Registration phase ---
388392 tracemalloc .start ()
389- table = read_xarray_table (large_ds )
390- reg_size , reg_peak = tracemalloc .get_traced_memory ()
391- tracemalloc .reset_peak ()
392-
393- # The lazy generator only materialises coord arrays (~O(dim sizes)) and
394- # factory closure objects — no data arrays. Both metrics should be well
395- # below one chunk of data.
396- assert reg_size < chunk_size , (
397- f"Registration held { reg_size } bytes >= chunk_size { chunk_size } : "
398- "data may have been loaded eagerly"
399- )
400- assert (
401- reg_peak < chunk_size * 2
402- ), f"Registration peak { reg_peak } too high (expected < 2× chunk_size { chunk_size } )"
403-
404- # --- Query phase ---
405- ctx = SessionContext ()
406- ctx .register_table ("weather" , table )
407- ctx .sql ("SELECT AVG(temperature), AVG(precipitation) FROM weather" ).collect ()
408- _ , query_peak = tracemalloc .get_traced_memory ()
409-
410- # tracemalloc measures Python-heap allocations, which include Arrow
411- # buffer copies and object overhead on top of the raw data. The
412- # observed peak is typically 1.1–1.5× the raw dataset size; we use
413- # 2× as a generous bound that would still catch catastrophic regressions
414- # (e.g. loading all partitions twice simultaneously).
415- assert query_peak < large_ds .nbytes * 2 , (
416- f"Query peak { query_peak } >= 2× dataset { large_ds .nbytes } : "
417- "may be holding excessive data in memory"
418- )
393+ try :
394+ table = read_xarray_table (large_ds )
395+ reg_size , reg_peak = tracemalloc .get_traced_memory ()
396+ tracemalloc .reset_peak ()
419397
420- tracemalloc .stop ()
398+ # The lazy generator only materialises coord arrays (~O(dim sizes)) and
399+ # factory closure objects — no data arrays. Both metrics should be well
400+ # below one chunk of data.
401+ assert reg_size < chunk_size , (
402+ f"Registration held { reg_size } bytes >= chunk_size { chunk_size } : "
403+ "data may have been loaded eagerly"
404+ )
405+ assert (
406+ reg_peak < chunk_size * 2
407+ ), f"Registration peak { reg_peak } too high (expected < 2× chunk_size { chunk_size } )"
408+
409+ # --- Query phase ---
410+ ctx = SessionContext ()
411+ ctx .register_table ("weather" , table )
412+ ctx .sql (
413+ "SELECT AVG(temperature), AVG(precipitation) FROM weather"
414+ ).collect ()
415+ _ , query_peak = tracemalloc .get_traced_memory ()
416+
417+ # tracemalloc measures Python-heap allocations, which include Arrow
418+ # buffer copies and object overhead on top of the raw data. The
419+ # observed peak is typically 1.1–1.5× the raw dataset size; we use
420+ # 2× as a generous bound that would still catch catastrophic regressions
421+ # (e.g. loading all partitions twice simultaneously).
422+ assert query_peak < large_ds .nbytes * 2 , (
423+ f"Query peak { query_peak } >= 2× dataset { large_ds .nbytes } : "
424+ "may be holding excessive data in memory"
425+ )
426+ finally :
427+ tracemalloc .stop ()
0 commit comments