viiccwen commented on code in PR #1462:
URL: https://github.com/apache/mahout/pull/1462#discussion_r3790952946


##########
qdp/qdp-core/src/pipeline_runner.rs:
##########
@@ -1796,6 +1812,117 @@ mod tests {
             );
         }
 
+        /// #1436: the `VecDeque` ring buffer must not grow across a long 
stream. A deliberately
+        /// small `read_chunk_scratch` forces `produce()` to refill the ring 
on nearly every batch,
+        /// so the test exercises the real hot-path pattern — front-drain the 
consumed prefix while
+        /// new chunks are appended to the back. A correct ring reuses the 
freed front slots for the
+        /// appended tail, so capacity settles to a constant: front-advance is 
O(1) amortized with no
+        /// reallocation and no per-batch shift of the retained tail. The 
`refills_observed` assert
+        /// guards against a regression where the whole stream is slurped up 
front (which would make
+        /// the capacity check vacuous).
+        ///
+        /// The same loop also pins down the wrap-around batch copy: once the 
ring wraps, a batch
+        /// can straddle the physical end of the allocation, so `produce()` 
must stitch it together
+        /// from both halves of `as_slices()`. Reaching that case needs a read 
chunk that is *not*
+        /// a multiple of the batch stride: if every batch drained the ring 
empty, `VecDeque` would
+        /// reset the head to 0 on each refill and the ring would never wrap 
(`split_observed`
+        /// asserts the straddling case is actually reached).
+        #[test]
+        fn test_streaming_producer_buffer_capacity_constant_over_100_batches() 
{
+            const BATCH_SIZE: usize = 4;
+            const SAMPLE_LEN: usize = 4; // 2 qubits, amplitude encoding
+            const BATCHES: usize = 100;
+            // Elements consumed per batch.
+            const STRIDE: usize = BATCH_SIZE * SAMPLE_LEN;
+            // One sample more than a batch: produce() refills on nearly every 
batch and always
+            // leaves a remainder behind, so the head walks around the ring 
instead of resetting.
+            const READ_CHUNK: usize = STRIDE + SAMPLE_LEN;
+            // Each sample written by write_f32_parquet_n, repeated across the 
batch.
+            const SAMPLE: [f32; SAMPLE_LEN] = [0.25, 0.5, 0.75, 1.0];

Review Comment:
   Every sample in this fixture has the same values, the test cannot detect 
samples being reordered, duplicated, or skipped across the wrap boundary. 
Reversing the `front` and `back` slices still passes this test.



##########
qdp/qdp-core/src/pipeline_runner.rs:
##########
@@ -1796,6 +1812,117 @@ mod tests {
             );
         }
 
+        /// #1436: the `VecDeque` ring buffer must not grow across a long 
stream. A deliberately
+        /// small `read_chunk_scratch` forces `produce()` to refill the ring 
on nearly every batch,
+        /// so the test exercises the real hot-path pattern — front-drain the 
consumed prefix while
+        /// new chunks are appended to the back. A correct ring reuses the 
freed front slots for the
+        /// appended tail, so capacity settles to a constant: front-advance is 
O(1) amortized with no
+        /// reallocation and no per-batch shift of the retained tail. The 
`refills_observed` assert
+        /// guards against a regression where the whole stream is slurped up 
front (which would make
+        /// the capacity check vacuous).
+        ///
+        /// The same loop also pins down the wrap-around batch copy: once the 
ring wraps, a batch
+        /// can straddle the physical end of the allocation, so `produce()` 
must stitch it together
+        /// from both halves of `as_slices()`. Reaching that case needs a read 
chunk that is *not*
+        /// a multiple of the batch stride: if every batch drained the ring 
empty, `VecDeque` would
+        /// reset the head to 0 on each refill and the ring would never wrap 
(`split_observed`
+        /// asserts the straddling case is actually reached).
+        #[test]
+        fn test_streaming_producer_buffer_capacity_constant_over_100_batches() 
{
+            const BATCH_SIZE: usize = 4;
+            const SAMPLE_LEN: usize = 4; // 2 qubits, amplitude encoding
+            const BATCHES: usize = 100;
+            // Elements consumed per batch.
+            const STRIDE: usize = BATCH_SIZE * SAMPLE_LEN;
+            // One sample more than a batch: produce() refills on nearly every 
batch and always
+            // leaves a remainder behind, so the head walks around the ring 
instead of resetting.
+            const READ_CHUNK: usize = STRIDE + SAMPLE_LEN;
+            // Each sample written by write_f32_parquet_n, repeated across the 
batch.
+            const SAMPLE: [f32; SAMPLE_LEN] = [0.25, 0.5, 0.75, 1.0];
+            let path = temp_parquet_path("streaming_cap");
+            // A few extra samples so the 100th produce() is a full batch, not 
EOF.
+            write_f32_parquet_n(&path, BATCH_SIZE * (BATCHES + 5));
+
+            let run = (|| -> Result<()> {
+                let reader = ParquetStreamingReader::<f32>::new(
+                    &path,
+                    Some(DEFAULT_PARQUET_ROW_GROUP_SIZE),
+                    NullHandling::FillZero,
+                )?;
+                // Exactly the steady-state peak the production build path 
reserves: one batch
+                // plus one refill chunk. Sized this tightly, the head wraps 
within the first few
+                // batches, so straddling batch copies are exercised early and 
often.
+                let mut producer = StreamingProducer::<f32> {
+                    reader,
+                    buffer: VecDeque::with_capacity(STRIDE + READ_CHUNK),
+                    read_chunk_scratch: vec![0.0_f32; READ_CHUNK],
+                    sample_size: SAMPLE_LEN,
+                    batch_size: BATCH_SIZE,
+                    num_qubits: 2,
+                    batches_yielded: 0,
+                    batch_limit: usize::MAX,
+                };

Review Comment:
   IMO, This test constructs the deque with the expected steady-state capacity 
directly, so it does not verify the `reserve` logic in 
`build_streaming_producer`, right? The production reservation could be removed 
or broken while this test still passes.
   
   Could the test create the producer through the prod initialization path and 
then verify that capacity remains stable for 100 batches?



##########
qdp/qdp-core/src/pipeline_runner.rs:
##########
@@ -631,10 +636,17 @@ where
 
     buffer.truncate(written);
     let read_chunk_scratch = vec![T::default(); initial_cap];
+    // Pre-reserve the streaming steady state so the ring never reallocates 
mid-run. Each batch
+    // drains up to `required` elements from the front while a refill appends 
up to one
+    // `initial_cap` chunk to the back, so live length peaks at `required + 
initial_cap`.
+    // `VecDeque::from` reuses the Vec's existing `initial_cap` capacity 
(O(1), no realloc), so
+    // this only tops it up by `required` — making front-advance realloc-free 
from batch 0.
+    let required = config.batch_size * sample_size;
+    let mut buffer = VecDeque::from(buffer);
+    buffer.reserve((required + initial_cap).saturating_sub(buffer.len()));

Review Comment:
   This eagerly reserves space for a full batch plus one refill chunk, even 
when the file only contains a partial batch. For large samples, this can 
substantially increase memory usage; for example, 20-qubit amplitude data with 
`batch_size=64` and `f64` grows from roughly 8 MiB to 520 MiB.
   
   Could we avoid reserving the theoretical peak before EOF is known, and use 
checked arithmetic for the capacity calculation?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to