From 098cdda9bf05fedac6f4a820319f86e14817fa42 Mon Sep 17 00:00:00 2001 From: Moritz Hoffmann Date: Sat, 19 Sep 2026 01:42:55 +0200 Subject: [PATCH 1/2] Suspend the cursor join within a key The cursor join tactic played a key's whole cross product into its container builder before checking whether a container had completed, so a key large enough to outrun the driver's fuel buffered its entire output before the iterator returned anything. A single-key join of 10,000 by 10,000 values peaked at 3.2 GB of resident memory for its 100,000,000 matches. The driver's budget never got a chance to act, because `next` did not return until the key was done. Keys now take one of two paths, chosen once the key's edits are loaded. A key whose cross product fits `KEY_WORK_LIMIT`, half the driver's fuel, runs to completion in one call over histories of borrowed values, as before. A larger key is reloaded into histories of value indices, which the iterator owns and which therefore survive a suspension, and is then replayed a container at a time. The indices are what make the state storable: a history of `Cursor::Val<'a>` cannot outlive the storage the iterator owns, so it cannot be held across calls. The same key now peaks at 97 MB. The benchmark added here shows the shapes that still take the uninterrupted path are unaffected: one value per key and a handful of values per key both land within noise of the previous code. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/operators/cursor/history.rs | 39 ++ .../src/operators/cursor/join.rs | 409 +++++++++++++++++- .../src/operators/history.rs | 19 +- differential-dataflow/src/operators/join.rs | 25 +- differential-dataflow/tests/join_bench.rs | 84 ++++ 5 files changed, 555 insertions(+), 21 deletions(-) create mode 100644 differential-dataflow/tests/join_bench.rs diff --git a/differential-dataflow/src/operators/cursor/history.rs b/differential-dataflow/src/operators/cursor/history.rs index f4592b7de..b489858b5 100644 --- a/differential-dataflow/src/operators/cursor/history.rs +++ b/differential-dataflow/src/operators/cursor/history.rs @@ -3,6 +3,7 @@ use crate::lattice::Lattice; use crate::operators::history::{EditList, HistoryReplay, ValueHistory}; use crate::trace::Cursor; +use crate::trace::implementations::containers::BatchContainer; /// Walks the cursor's values at the current key into `target`, advancing times by `meet` if supplied. fn load_values<'a, V, T, D, C>( @@ -47,6 +48,44 @@ where load_values(history.edits_mut(), cursor, storage, meet); } +/// Loads the cursor's values at its current key into `values`, and their edits into `history` +/// keyed by each value's index in `values`. +/// +/// The indices are what let the history outlive the borrow of `storage`: a history of +/// `Cursor::Val<'a>` can only live as long as the storage it reads from, and so cannot be held +/// across calls by an iterator that owns that storage. A history of indices can, at the cost of +/// copying the key's values into a container the iterator owns too. Callers that replay a key in +/// one uninterrupted pass should prefer `load_current`, which copies nothing. +/// +/// The cursor is left with its values exhausted, as `load_current` leaves it. +pub(super) fn load_current_indexed( + values: &mut C::ValContainer, + history: &mut ValueHistory, + cursor: &mut C, + storage: &C::Storage, + meet: Option<&T>, +) +where + T: Ord + Clone + Lattice, + D: crate::difference::Semigroup, + C: Cursor