Skip to content

Commit 30fdd01

Browse files
authored
block_on_stream_thread_safe: increase channel size to number of cores (#9116)
Duckdb uses CurrentThreadRuntime which in turn uses smol executor. `block_on_stream_thread_safe` is what allows it to drive scan from multiple threads. However, this method currently blocks on a channel of size 1 which becomes a bottleneck if we run on a system with many cores, say 128-core AWS c6id.metal. Changing it to an item-per-core allows to save some time on large core systems. Signed-off-by: Mikhail Kot <mikhail@spiraldb.com>
1 parent 49f98c3 commit 30fdd01

1 file changed

Lines changed: 24 additions & 12 deletions

File tree

‎vortex-io/src/runtime/current.rs‎

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use futures::StreamExt;
99
use futures::stream::BoxStream;
1010
use parking_lot::Mutex;
1111
use smol::block_on;
12+
use vortex_utils::parallelism::get_available_parallelism;
1213

1314
use crate::runtime::BlockingRuntime;
1415
use crate::runtime::Executor;
@@ -67,8 +68,10 @@ impl CurrentThreadRuntime {
6768

6869
// We create an MPMC result channel and spawn a task to drive the stream and send results.
6970
// This allows multiple worker threads to drive the execution while all waiting for results
70-
// on the channel.
71-
let (result_tx, result_rx) = kanal::bounded_async(1);
71+
// on the channel. Channel buffers up to one item per core so calling threads can get a
72+
// ready item without every one of them having to become an executor.
73+
let capacity = get_available_parallelism().unwrap_or(1).max(1);
74+
let (result_tx, result_rx) = kanal::bounded_async(capacity);
7275
let driver = self.executor.spawn(async move {
7376
futures::pin_mut!(stream);
7477
while let Some(item) = stream.next().await {
@@ -154,19 +157,28 @@ impl<T> Iterator for ThreadSafeIterator<T> {
154157
type Item = T;
155158

156159
fn next(&mut self) -> Option<Self::Item> {
160+
// If driver already has an item, take it without driving the executor
161+
match self.results.try_recv() {
162+
Ok(Some(item)) => return Some(item),
163+
Ok(None) => {}
164+
Err(_) => return self.get_task_error(),
165+
}
166+
157167
match block_on(self.executor.run(self.results.recv())) {
158168
Ok(item) => Some(item),
159-
// The result channel closes when the driver task finishes. Join the task so a panic
160-
// raised while driving the stream is re-raised here instead of being lost. The first
161-
// consumer to observe closure joins it; any later consumer just sees the stream end.
162-
Err(_) => {
163-
let task = self.driver.lock().take();
164-
if let Some(task) = task {
165-
block_on(self.executor.run(task));
166-
}
167-
None
168-
}
169+
Err(_) => self.get_task_error(),
170+
}
171+
}
172+
}
173+
174+
impl<T> ThreadSafeIterator<T> {
175+
// Join current task so panics are re-raised
176+
fn get_task_error(&self) -> Option<T> {
177+
let task = self.driver.lock().take();
178+
if let Some(task) = task {
179+
block_on(self.executor.run(task));
169180
}
181+
None
170182
}
171183
}
172184

0 commit comments

Comments
 (0)