Skip to content

Commit a08f756

Browse files
committed
compute: let the last linear-join stage write the output edge
With no finalization closure, a linear join's last stage output is the node's output, so `vec_to_columnar` encoded it after the fact. Handing that stage a `ConsolidatingColumnBuilder` instead makes it write the edge directly, dropping the operator hop and the `Vec` of owned `Row`s that used to sit on the edge in between. The choice is per stage rather than global, because a `Column` is the wrong intermediate for a consumer that re-encodes what it reads. Both a following stage's arrangement and a finalization closure do exactly that, and a `Vec` hands them moved `Row` allocations where a `Column` would copy row bytes first. So every non-terminal stage keeps the `Vec` accumulator, and `render` takes the container builder as a parameter to let the two coexist. An error-capable closure keeps the accumulator even when terminal. Its output has to pass through `ok_err` before the ok side can be encoded, and the demux materializes that side as a `Vec` regardless. `ConsolidatingColumnBuilder` rather than a plain `ColumnBuilder`: the join gives owned `(Row, T, Diff)` tuples and `Rows` implements `Push<&Row>`, not `Push<Row>`. It also consolidates across work chunks, which `mz_join_core`'s per-chunk `consolidate_updates` cannot. The `DifferentialDataflow` implementation builds its own `Vec` output and takes no container builder, so that arm re-encodes through `encode_updates`.
1 parent 175ed67 commit a08f756

1 file changed

Lines changed: 160 additions & 75 deletions

File tree

src/compute/src/render/join/linear_join.rs

Lines changed: 160 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,11 @@ use mz_timely_util::columnar::builder::ColumnBuilder;
3636
use mz_timely_util::columnar::consolidate::ConsolidatingColumnBuilder;
3737
use mz_timely_util::columnar::{Col2ValBatcher, Col2ValPagedBatcher, columnar_exchange};
3838
use mz_timely_util::operator::{CollectionExt, StreamExt};
39-
use timely::container::CapacityContainerBuilder;
39+
use timely::ContainerBuilder;
40+
use timely::container::{CapacityContainerBuilder, PushInto};
4041
use timely::dataflow::channels::pact::{ExchangeCore, Pipeline};
4142
use timely::dataflow::operators::OkErr;
43+
use timely::dataflow::operators::generic::Operator;
4244
use timely::dataflow::{Scope, Stream};
4345

4446
use crate::extensions::arrange::MzArrangeCore;
@@ -101,15 +103,21 @@ impl LinearJoinSpec {
101103
}
102104
}
103105

104-
/// Render a join operator according to this specification.
105-
fn render<'s, T, Tr1, Tr2, L, I>(
106+
/// Render a join operator according to this specification, assembling its
107+
/// output through `CB`.
108+
///
109+
/// The `DifferentialDataflow` implementation builds its own `Vec` output and
110+
/// cannot be handed a container builder, so that arm re-encodes through `CB`.
111+
/// The `Materialize` implementation writes `CB` directly.
112+
fn render<'s, T, Tr1, Tr2, L, I, CB>(
106113
&self,
107114
arranged1: Arranged<'s, Tr1>,
108115
arranged2: Arranged<'s, Tr2>,
109116
result: L,
110-
) -> VecCollection<'s, T, I::Item, Diff>
117+
) -> Stream<'s, T, CB::Container>
111118
where
112119
T: Lattice + timely::progress::Timestamp,
120+
CB: ContainerBuilder + PushInto<(I::Item, T, Diff)> + 'static,
113121
Tr1: TraceReader<Batch: Navigable, Time = T> + Clone + 'static,
114122
Tr2: TraceReader<Batch: Navigable, Time = T> + Clone + 'static,
115123
BatchCursor<Tr1>: Cursor<Time = T, Diff = Diff>,
@@ -119,44 +127,30 @@ impl LinearJoinSpec {
119127
{
120128
use LinearJoinImpl::*;
121129

122-
// `mz_join_core` builds its output through a container builder. The
123-
// `Vec` accumulator this method returns needs the capacity builder.
124-
type VecCB<D, T> = CapacityContainerBuilder<Vec<(D, T, Diff)>>;
125-
126130
match (
127131
self.implementation,
128132
self.yielding.after_work,
129133
self.yielding.after_time,
130134
) {
131-
(DifferentialDataflow, _, _) => arranged1.join_core(arranged2, result),
135+
(DifferentialDataflow, _, _) => {
136+
encode_updates::<_, _, CB>(arranged1.join_core(arranged2, result), "JoinCoreEncode")
137+
}
132138
(Materialize, Some(work_limit), Some(time_limit)) => {
133139
let yield_fn =
134140
move |start: Instant, work| work >= work_limit || start.elapsed() >= time_limit;
135-
mz_join_core::<_, _, _, _, _, _, VecCB<I::Item, T>>(
136-
arranged1, arranged2, result, yield_fn,
137-
)
138-
.as_collection()
141+
mz_join_core::<_, _, _, _, _, _, CB>(arranged1, arranged2, result, yield_fn)
139142
}
140143
(Materialize, Some(work_limit), None) => {
141144
let yield_fn = move |_start, work| work >= work_limit;
142-
mz_join_core::<_, _, _, _, _, _, VecCB<I::Item, T>>(
143-
arranged1, arranged2, result, yield_fn,
144-
)
145-
.as_collection()
145+
mz_join_core::<_, _, _, _, _, _, CB>(arranged1, arranged2, result, yield_fn)
146146
}
147147
(Materialize, None, Some(time_limit)) => {
148148
let yield_fn = move |start: Instant, _work| start.elapsed() >= time_limit;
149-
mz_join_core::<_, _, _, _, _, _, VecCB<I::Item, T>>(
150-
arranged1, arranged2, result, yield_fn,
151-
)
152-
.as_collection()
149+
mz_join_core::<_, _, _, _, _, _, CB>(arranged1, arranged2, result, yield_fn)
153150
}
154151
(Materialize, None, None) => {
155152
let yield_fn = |_start, _work| false;
156-
mz_join_core::<_, _, _, _, _, _, VecCB<I::Item, T>>(
157-
arranged1, arranged2, result, yield_fn,
158-
)
159-
.as_collection()
153+
mz_join_core::<_, _, _, _, _, _, CB>(arranged1, arranged2, result, yield_fn)
160154
}
161155
}
162156
}
@@ -215,9 +209,13 @@ enum JoinedFlavor<'scope, T: RenderTimestamp> {
215209
/// stage. `differential_join` forms its arrangement key off the edge, so a
216210
/// columnar source flows in without a `ColumnarToVec` decode.
217211
Edge(CollectionEdge<'scope, T>),
218-
/// The intra-operator multi-stage accumulator. `mz_join_core` is
219-
/// `Vec`-internal, so the accumulator is a bare `VecCollection`, not a
220-
/// collection edge.
212+
/// The intra-operator multi-stage accumulator.
213+
///
214+
/// A stage whose output is consumed by another stage's arrangement or by a
215+
/// finalization closure writes this. Both of those re-encode what they read,
216+
/// and a `Vec` hands them moved `Row` allocations where a `Column` would
217+
/// copy row bytes, so the accumulator stays `Vec`. Only a stage whose output
218+
/// *is* the node's output writes [`JoinedFlavor::Edge`].
221219
Collection(VecCollection<'scope, T, Row, Diff>),
222220
/// A dataflow-local arrangement.
223221
Local(Arranged<'scope, RowRowAgent<T, Diff>>),
@@ -326,18 +324,23 @@ where
326324
};
327325

328326
// progress through stages, updating partial results and errors.
329-
for stage_plan in linear_plan.stage_plans.into_iter() {
327+
//
328+
// The last stage writes the node's output edge directly, but only when
329+
// no finalization closure follows it. With a closure, the closure's
330+
// builder writes the edge and the stage feeds it the `Vec` accumulator.
331+
let stage_count = linear_plan.stage_plans.len();
332+
let terminal_stage_writes_edge = linear_plan.final_closure.is_none();
333+
for (index, stage_plan) in linear_plan.stage_plans.into_iter().enumerate() {
334+
let terminal = index + 1 == stage_count && terminal_stage_writes_edge;
330335
// Different variants of `joined` implement this differently,
331336
// and the logic is centralized there.
332-
let stream = self.differential_join(
337+
joined = self.differential_join(
333338
joined,
334339
inputs[stage_plan.lookup_relation].enter_region(inner),
335340
stage_plan,
341+
terminal,
336342
&mut errors,
337343
);
338-
// Update joined results and capture any errors. `mz_join_core`
339-
// produces a `Vec` collection, the intra-operator accumulator.
340-
joined = JoinedFlavor::Collection(stream);
341344
}
342345

343346
// We have completed the join building, but may have work remaining.
@@ -377,10 +380,14 @@ where
377380
errors.push(errs);
378381
updates
379382
} else {
380-
// Identity finalization: the raw output is the result. The source edge
381-
// (single-input join) is already columnar and passes through; the `Vec`
382-
// accumulator encodes via `vec_to_columnar`, non-consolidating to match
383-
// the raw output.
383+
// Identity finalization: the raw output is the result. A
384+
// single-input join passes its source edge through, and with stages
385+
// the last one wrote the edge itself, because
386+
// `terminal_stage_writes_edge` holds exactly here.
387+
//
388+
// The accumulator arm is reachable only through an initial closure
389+
// on a stage-less join, which current lowering never emits. It
390+
// encodes rather than panics, so a lowering change stays correct.
384391
match joined {
385392
JoinedFlavor::Edge(edge) => edge,
386393
JoinedFlavor::Collection(collection) => vec_to_columnar(collection),
@@ -398,6 +405,9 @@ where
398405

399406
/// Looks up the arrangement for the next input and joins it to the arranged
400407
/// version of the join of previous inputs.
408+
///
409+
/// `terminal` marks a stage whose output is the node's output, which makes
410+
/// it write the output edge rather than the `Vec` accumulator.
401411
fn differential_join<'s>(
402412
&self,
403413
mut joined: JoinedFlavor<'s, T>,
@@ -409,8 +419,9 @@ where
409419
closure,
410420
lookup_relation: _,
411421
}: LinearStagePlan,
422+
terminal: bool,
412423
errors: &mut Vec<VecCollection<'s, T, DataflowErrorSer, Diff>>,
413-
) -> VecCollection<'s, T, Row, Diff> {
424+
) -> JoinedFlavor<'s, T> {
414425
// If we have a streamed input, we must first form an arrangement. The
415426
// source edge keys off the `CollectionEdge` (a columnar source has no
416427
// `ColumnarToVec` hop); the intra-operator accumulator is a bare
@@ -449,7 +460,7 @@ where
449460
ArrangementFlavor::Local(oks, errs1) => {
450461
let (oks, errs2) = self
451462
.differential_join_inner::<RowRowAgent<_, _>, RowRowAgent<_, _>>(
452-
local, oks, closure,
463+
local, oks, closure, terminal,
453464
);
454465

455466
errors.push(errs1.as_collection(|k, _v| k.clone()));
@@ -459,7 +470,7 @@ where
459470
ArrangementFlavor::Trace(_gid, oks, errs1) => {
460471
let (oks, errs2) = self
461472
.differential_join_inner::<RowRowAgent<_, _>, RowRowEnter<_, _, _>>(
462-
local, oks, closure,
473+
local, oks, closure, terminal,
463474
);
464475

465476
errors.push(errs1.as_collection(|k, _v| k.clone()));
@@ -471,7 +482,7 @@ where
471482
ArrangementFlavor::Local(oks, errs1) => {
472483
let (oks, errs2) = self
473484
.differential_join_inner::<RowRowEnter<_, _, _>, RowRowAgent<_, _>>(
474-
trace, oks, closure,
485+
trace, oks, closure, terminal,
475486
);
476487

477488
errors.push(errs1.as_collection(|k, _v| k.clone()));
@@ -481,7 +492,7 @@ where
481492
ArrangementFlavor::Trace(_gid, oks, errs1) => {
482493
let (oks, errs2) = self
483494
.differential_join_inner::<RowRowEnter<_, _, _>, RowRowEnter<_, _, _>>(
484-
trace, oks, closure,
495+
trace, oks, closure, terminal,
485496
);
486497

487498
errors.push(errs1.as_collection(|k, _v| k.clone()));
@@ -498,13 +509,19 @@ where
498509
///
499510
/// The return type includes an optional error collection, which may be
500511
/// `None` if we can determine that `closure` cannot error.
512+
/// `terminal` marks a stage whose output is the node's output, which makes
513+
/// the ok side write a [`ColumnBuilder`] instead of the `Vec` accumulator,
514+
/// so the node needs no leaf encode. An error-capable closure writes the
515+
/// accumulator either way, because its output has to be demuxed by
516+
/// `ok_err` before the ok side can be encoded.
501517
fn differential_join_inner<'s, Tr1, Tr2>(
502518
&self,
503519
prev_keyed: Arranged<'s, Tr1>,
504520
next_input: Arranged<'s, Tr2>,
505521
closure: JoinClosure,
522+
terminal: bool,
506523
) -> (
507-
VecCollection<'s, T, Row, Diff>,
524+
JoinedFlavor<'s, T>,
508525
Option<VecCollection<'s, T, DataflowErrorSer, Diff>>,
509526
)
510527
where
@@ -518,25 +535,22 @@ where
518535
// Reuseable allocation for unpacking.
519536
let mut datums = DatumVec::new();
520537

538+
// The `Vec` accumulator's builder. Named because the ok side picks
539+
// between it and a `ColumnBuilder` on `terminal`.
540+
type VecCB<D, T> = CapacityContainerBuilder<Vec<(D, T, Diff)>>;
541+
521542
if closure.could_error() {
522543
let (oks, err) = self
523544
.linear_join_spec
524-
.render(prev_keyed, next_input, move |key, old, new| {
525-
let mut row_builder = SharedRow::get();
526-
let temp_storage = RowArena::new();
527-
528-
let mut datums_local = datums.borrow();
529-
key.extend_datums(&temp_storage, &mut datums_local, None);
530-
old.extend_datums(&temp_storage, &mut datums_local, None);
531-
new.extend_datums(&temp_storage, &mut datums_local, None);
532-
533-
closure
534-
.apply(&mut datums_local, &temp_storage, &mut row_builder)
535-
.map(|row| row.cloned())
536-
.map_err(DataflowErrorSer::from)
537-
.transpose()
538-
})
539-
.inner
545+
.render::<T, _, _, _, _, VecCB<Result<Row, DataflowErrorSer>, T>>(
546+
prev_keyed,
547+
next_input,
548+
move |key, old, new| {
549+
apply_join_closure(&closure, &mut datums, key, old, new)
550+
.map_err(DataflowErrorSer::from)
551+
.transpose()
552+
},
553+
)
540554
.ok_err(|(x, t, d)| {
541555
// TODO(mcsherry): consider `ok_err()` for `Collection`.
542556
match x {
@@ -545,30 +559,101 @@ where
545559
}
546560
});
547561

548-
(oks.as_collection(), Some(err.as_collection()))
549-
} else {
562+
let oks = oks.as_collection();
563+
let oks = if terminal {
564+
// The demux already materialized the ok side as a `Vec`, so the
565+
// leaf encode stands here.
566+
JoinedFlavor::Edge(vec_to_columnar(oks))
567+
} else {
568+
JoinedFlavor::Collection(oks)
569+
};
570+
(oks, Some(err.as_collection()))
571+
} else if terminal {
550572
let oks = self
551573
.linear_join_spec
552-
.render(prev_keyed, next_input, move |key, old, new| {
553-
let mut row_builder = SharedRow::get();
554-
let temp_storage = RowArena::new();
555-
556-
let mut datums_local = datums.borrow();
557-
key.extend_datums(&temp_storage, &mut datums_local, None);
558-
old.extend_datums(&temp_storage, &mut datums_local, None);
559-
new.extend_datums(&temp_storage, &mut datums_local, None);
574+
.render::<T, _, _, _, _, ConsolidatingColumnBuilder<Row, T, Diff>>(
575+
prev_keyed,
576+
next_input,
577+
move |key, old, new| {
578+
apply_join_closure(&closure, &mut datums, key, old, new)
579+
.expect("Closure claimed to never error")
580+
},
581+
);
560582

561-
closure
562-
.apply(&mut datums_local, &temp_storage, &mut row_builder)
563-
.expect("Closure claimed to never error")
564-
.cloned()
565-
});
583+
(JoinedFlavor::Edge(oks.as_collection()), None)
584+
} else {
585+
let oks = self
586+
.linear_join_spec
587+
.render::<T, _, _, _, _, VecCB<Row, T>>(
588+
prev_keyed,
589+
next_input,
590+
move |key, old, new| {
591+
apply_join_closure(&closure, &mut datums, key, old, new)
592+
.expect("Closure claimed to never error")
593+
},
594+
);
566595

567-
(oks, None)
596+
(JoinedFlavor::Collection(oks.as_collection()), None)
568597
}
569598
}
570599
}
571600

601+
/// Unpacks one join match into datums and applies `closure` to it.
602+
///
603+
/// `None` means the closure filtered the match out. The output row is owned
604+
/// because the row `closure` writes borrows the shared row builder, which the
605+
/// caller must not hold past this call.
606+
fn apply_join_closure<K, V1, V2>(
607+
closure: &JoinClosure,
608+
datums: &mut DatumVec,
609+
key: K,
610+
old: V1,
611+
new: V2,
612+
) -> Result<Option<Row>, mz_expr::EvalError>
613+
where
614+
K: ExtendDatums,
615+
V1: ExtendDatums,
616+
V2: ExtendDatums,
617+
{
618+
let mut row_builder = SharedRow::get();
619+
let temp_storage = RowArena::new();
620+
621+
let mut datums_local = datums.borrow();
622+
key.extend_datums(&temp_storage, &mut datums_local, None);
623+
old.extend_datums(&temp_storage, &mut datums_local, None);
624+
new.extend_datums(&temp_storage, &mut datums_local, None);
625+
626+
closure
627+
.apply(&mut datums_local, &temp_storage, &mut row_builder)
628+
.map(|row| row.cloned())
629+
}
630+
631+
/// Re-encodes a `Vec` collection through `CB`.
632+
///
633+
/// For a join implementation that builds its own `Vec` output and so cannot be
634+
/// handed a container builder.
635+
fn encode_updates<'s, T, D, CB>(
636+
collection: VecCollection<'s, T, D, Diff>,
637+
name: &str,
638+
) -> Stream<'s, T, CB::Container>
639+
where
640+
T: timely::progress::Timestamp,
641+
D: Data,
642+
CB: ContainerBuilder + PushInto<(D, T, Diff)> + 'static,
643+
{
644+
collection
645+
.inner
646+
.unary::<CB, _, _, _>(Pipeline, name, |_, _| {
647+
move |input, output| {
648+
input.for_each(|time, data| {
649+
output
650+
.session_with_builder(&time)
651+
.give_iterator(data.drain(..));
652+
});
653+
}
654+
})
655+
}
656+
572657
/// Keys a row-formatted join input stream into columnar `((key, value), t, d)`
573658
/// updates, splitting off key-evaluation errors into a separate stream.
574659
///

0 commit comments

Comments
 (0)