sunchao commented on code in PR #6948:
URL: https://github.com/apache/datafusion-comet/pull/6948#discussion_r4315003915


##########
native/spark-expr/src/array_funcs/sequence.rs:
##########
@@ -184,39 +267,94 @@ where
             None => -1,
         }
     };
+    let scalar_length = if args
+        .iter()
+        .all(|arg| matches!(arg, ColumnarValue::Scalar(_)))
+    {
+        let s = start.value(0);
+        let e = stop.value(0);
+        Some(sequence_length(s, e, row_step(0, s, e))?)
+    } else {
+        None
+    };
+    let scalar_total = scalar_length
+        .map(|len| batch_length(len as u128 * num_rows as u128))
+        .transpose()?;
+    // Columns admit scratch first, then validate and cache each row length 
once before
+    // growing this same reservation for values. Scalars know their complete 
size already.
+    let initial = (|| {
+        let bitmap_count = if null_counts.iter().any(|count| *count != 0) {
+            2
+        } else {
+            0
+        };
+        let bytes = construction_scratch(num_rows, true, bitmap_count)?;
+        let bytes = bytes
+            .checked_add(value_bytes::<T>(scalar_total.unwrap_or(0))?)
+            .ok_or_else(size_error)?;
+        pool.map(|pool| reserve_construction(pool, bytes))
+            .transpose()
+    })();
+    // Keep error-only typed parsing in the cold helper so it need not borrow 
hot input locals.
+    let scratch_error = move |error| {
+        // Only resource exhaustion needs an allocation-free validation pass 
to keep Spark
+        // row errors and the batch element ceiling ahead of it. Propagate 
other pool errors,
+        // such as an interrupted Spark task, immediately without inspecting 
more input rows.
+        if scalar_length.is_none() && matches!(&error, 
DataFusionError::ResourcesExhausted(_)) {
+            validate_before_resource_error::<T>(args, num_rows, error)
+        } else {
+            error
+        }
+    };
+    let reservation = initial.map_err(scratch_error)?;
+    // Reuse validity once per batch instead of checking three inputs and 
rebuilding a
+    // bitmap row by row. Arrow unions sliced bitmaps with bulk word 
operations, allocating
+    // one accumulator and applying the remaining masks in place.
+    let input_nulls = [
+        start.nulls(num_rows),
+        stop.nulls(num_rows),
+        step.as_ref().and_then(|step| step.nulls(num_rows)),
+    ];
+    let nulls = NullBuffer::union_many(input_nulls.iter().map(Option::as_ref))
+        .filter(|nulls| nulls.null_count() != 0);
+    if nulls
+        .as_ref()
+        .is_some_and(|nulls| nulls.null_count() == num_rows)
+    {
+        // Reuse the accumulator so another all-null bitmap cannot exceed its 
capacity allowance.
+        return null_sequence::<T>(child_field, num_rows, nulls);
+    }
 
     // Validate every row before checking the batch total, preserving the 
first Spark error.
     // Retain lengths so short sequences do not repeat per-row arithmetic 
during generation.
-    let mut lengths = Vec::with_capacity(num_rows);
-    let mut total = 0usize;
-    for row in 0..num_rows {
-        if nulls.as_ref().is_some_and(|nulls| nulls.is_null(row)) {
-            lengths.push(0);
-            continue;
-        }
-        let s = start.value(row);
-        let e = stop.value(row);
-        let len = sequence_length(s, e, row_step(row, s, e))?;
-        total += len;
-        lengths.push(len);
-    }
-    // Arrow's List offsets must fit i32; Spark's per-row array limit is 
checked above.
-    if total > i32::MAX as usize {
-        return Err(DataFusionError::External(Box::new(
-            SparkError::SequenceBatchTooLarge {
-                total_elements: total.to_string(),
-            },
-        )));
+    let mut lengths = sequence_vec::<usize>(num_rows).map_err(scratch_error)?;
+    let total = if let Some(len) = scalar_length {
+        lengths.resize(num_rows, len);
+        scalar_total.unwrap()
+    } else {
+        let mut total = 0u128;
+        for row in 0..num_rows {
+            if nulls.as_ref().is_some_and(|nulls| nulls.is_null(row)) {
+                lengths.push(0);
+                continue;
+            }
+            let s = start.value(row);
+            let e = stop.value(row);
+            let len = sequence_length(s, e, row_step(row, s, e))?;
+            total += len as u128;
+            lengths.push(len);
+        }
+        batch_length(total)?
+    };
+    if scalar_length.is_none() {
+        if let Some(reservation) = &reservation {
+            reservation.try_grow(value_bytes::<T>(total)?)?;

Review Comment:
   [P2] Preserve downstream spilling before propagating this refusal. When 
`sequence` feeds a native sort, the sorter can retain enough earlier batches 
that this values reservation fails. `SortExec` polls its input before reaching 
its own reservation/spill logic, so propagating the error here aborts a query 
that could release memory by spilling. With 64 batches of 64 rows, 64-element 
BIGINT sequences, a 220,000-byte pool, and 64 KiB of sort spill reservation, 
the exact base kernel returns all 4,096 rows with 47 spills. The head fails on 
the third batch with `ResourcesExhausted` for `Sequence`, before any spill. 
Each sequence batch fits comfortably in the pool by itself. Please coordinate 
admission with reclamation or protect construction headroom before downstream 
buffering consumes it, and add a spilling integration regression test.
   
   Evidence: Ran a disposable Rust integration test using locked DataFusion 
55.2.0 / Arrow 59.3.0: `cargo test -p datafusion-comet-spark-expr --locked 
--test review_sequence_admission review_sequence_before_spill -- --nocapture`. 
It compiled `sequence.rs` from exact base 
e555dba6f5c8c62ceca8c9c6e311a51b8fbfeb70 alongside the head factory. Fixture: 
one MemTable partition containing 64 batches of 64 rows; for batch n and row i, 
BIGINT id=4095-64*n-i, a=n%2, b=a+63. Query: `SELECT id, spark_sequence(a, b) 
AS vals FROM t ORDER BY vals, id`. Configuration: target_partitions=1, 
batch_size=64, sort_spill_reservation_bytes=65536, GreedyMemoryPool=220000. 
Base: Ok(4096), spill_count=47. Head: only 128 input rows consumed, 
spill_count=0, refusal for 32.0 KB with 772 bytes of sequence scratch reserved 
and 19.1 KB available. At pool limit 480000, base again returns 4096 rows with 
15 spills while head fails before its first spill. Assertions passed for both 
comparisons. Reproducer saved at `/tmp/
 comet-6948-review_sequence_admission.rs`; output at 
`/tmp/comet-6948-repro-final.log`. The disposable checkout test was removed.



-- 
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]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to