DamiaoCastro commented on issue #26140:
URL: https://github.com/apache/datafusion/issues/26140#issuecomment-6106884055

   A concrete data point for TopK, one of the operators this issue lists as not 
able to use the counter yet.
   
   TopK charges each retained input batch its `get_record_batch_memory_size` 
(`RecordBatchStore::insert`), so N zero-copy slices of one allocation are 
charged N × that allocation. You can hit this without any slicing inside 
DataFusion: `arrow_ipc::reader::StreamDecoder` decodes zero-copy, so every 
record batch of a multi-batch IPC stream points into the one stream buffer.
   
   Repro on 53.1.0: a 128 MiB pool, and 2 000 rows passed as 2 000 one-row 
slices of one batch:
   
   ```rust
   use std::sync::Arc;
   
   use datafusion::arrow::array::{Int64Array, RecordBatch, StringArray};
   use datafusion::arrow::datatypes::{DataType, Field, Schema};
   use datafusion::datasource::MemTable;
   use datafusion::execution::runtime_env::RuntimeEnvBuilder;
   use datafusion::prelude::{SessionConfig, SessionContext};
   
   #[tokio::main(flavor = "current_thread")]
   async fn main() -> datafusion::error::Result<()> {
       // 2 000 rows, passed as 2 000 one-row zero-copy slices of one batch
       let n = 2_000;
       let schema = Arc::new(Schema::new(vec![
           Field::new("k", DataType::Int64, false),
           Field::new("v", DataType::Utf8, false),
       ]));
       let batch = RecordBatch::try_new(
           Arc::clone(&schema),
           vec![
               Arc::new(Int64Array::from_iter_values(0..n as i64)),
               Arc::new(StringArray::from_iter_values((0..n).map(|i| 
format!("{i:0>100}")))),
           ],
       )?;
       let slices: Vec<RecordBatch> = (0..n).map(|i| batch.slice(i, 
1)).collect();
   
       let runtime = RuntimeEnvBuilder::new()
           .with_memory_limit(128 * 1024 * 1024, 1.0)
           .build_arc()?;
       let ctx = SessionContext::new_with_config_rt(SessionConfig::new(), 
runtime);
       ctx.register_table("t", Arc::new(MemTable::try_new(schema, 
vec![slices])?))?;
   
       // k = 100 000 > n: every slice keeps a row, so TopK retains all of them
       ctx.sql("SELECT k, v FROM t ORDER BY k DESC LIMIT 100000")
           .await?
           .collect()
           .await?;
       Ok(())
   }
   ```
   
   ```
   ResourcesExhausted("Additional allocation failed for TopK[0] with top memory 
consumers (across reservations) as:
     TopK[0]#0(can spill: false) consumed 127.8 MB, peak 127.8 MB.
   Error: Failed to allocate additional 279.5 KB for TopK[0] with 127.8 MB 
already allocated for this reservation - 243.8 KB remain available for the 
total pool")
   ```
   
   Each one-row slice is charged the parent's full ~280 KB of buffer capacity. 
I haven't run this on `main`, but the path looks unchanged there:
   - `register_batch` keeps the input batch as-is.
   - `RecordBatchStore::insert` adds `get_record_batch_memory_size` per entry.
   - `maybe_compact` only runs once unused rows reach `20 * batch_size + k`, so 
with a large `k` it never compacts.
   
   We ran into this on stored IPC streams of hundreds of one-row batches. An 
`ORDER BY` over 577 rows with a large `LIMIT` was charged ~0.75 MB per batch 
and failed at 128 MiB. On 53.1 the hash-join build side failed the same way: a 
join of two ~1 000-row tables was charged ~0.6 MB per one-row batch. #22862 
already fixes that on `main`. Our workaround is to `concat_batches` each 
decoded IPC stream before handing it to DataFusion.
   


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