Skip to content

Commit 12fe696

Browse files
AdamGSjoseph-isaacsclauderobert3005
authored
Exectuion and optimization tracing harness for testing (#7814)
## Summary Introduces an internal tracing API that enables testing execution and optimization flows with snapshot testing! Just wrap your code into a callaback and call `trace_op`, and you get back both the result and a snapshot you can assert. For example: ```rust #[test] fn trace_optimize_reduce_fixpoint() -> VortexResult<()> { let values = PrimitiveArray::from_iter([0i32, 1, 2, 3]).into_array(); let filter = FilterArray::try_new(values.clone(), Mask::new_true(values.len()))?.into_array(); let traced = trace_op(|| filter.optimize())?; assert!(traced.output.is::<Primitive>()); assert_arrays_eq!(traced.output, values); insta::assert_snapshot!(traced.trace.to_string(), @r" optimize root=vortex.filter(i32, len=4) session=false reduce TrivialFilterRule: vortex.filter(i32, len=4) -> vortex.primitive(i32, len=4) done output=vortex.primitive(i32, len=4) "); Ok(()) } ``` The API for tests is pretty nice, internally there are still improvements that can be made here, but this is a good step towards building more confidence and making the execution model clearer. --------- Signed-off-by: Adam Gutglick <adam@spiraldb.com> Signed-off-by: Robert Kruszewski <github@robertk.io> Co-authored-by: Joe Isaacs <joe.isaacs@live.co.uk> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Robert Kruszewski <github@robertk.io>
1 parent 08c336f commit 12fe696

16 files changed

Lines changed: 2875 additions & 66 deletions

File tree

‎Cargo.lock‎

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎encodings/runend/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ workspace = true
2929

3030
[dev-dependencies]
3131
divan = { workspace = true }
32+
insta = { workspace = true }
3233
itertools = { workspace = true }
3334
mimalloc = { workspace = true }
3435
rand = { workspace = true }

‎encodings/runend/src/lib.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ mod iter;
1616
mod kernel;
1717
pub mod ops;
1818
mod rules;
19+
#[cfg(test)]
20+
#[cfg(not(codspeed))]
21+
mod trace_tests;
1922

2023
#[doc(hidden)]
2124
pub mod _benchmarking {
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! Snapshot tests tracing how reductions and executions flow through run-end arrays.
5+
6+
use vortex_array::ArrayRef;
7+
use vortex_array::Canonical;
8+
use vortex_array::ExecutionCtx;
9+
use vortex_array::IntoArray;
10+
use vortex_array::arrays::BoolArray;
11+
use vortex_array::arrays::ConstantArray;
12+
use vortex_array::arrays::DictArray;
13+
use vortex_array::arrays::FilterArray;
14+
use vortex_array::arrays::PrimitiveArray;
15+
use vortex_array::arrays::scalar_fn::ScalarFnFactoryExt;
16+
use vortex_array::assert_arrays_eq;
17+
use vortex_array::optimizer::ArrayOptimizer;
18+
use vortex_array::scalar::Scalar;
19+
use vortex_array::scalar_fn::fns::binary::Binary;
20+
use vortex_array::scalar_fn::fns::operators::Operator;
21+
use vortex_array::session::ArraySession;
22+
use vortex_array::test_harness::trace::trace_op;
23+
use vortex_error::VortexResult;
24+
use vortex_mask::Mask;
25+
use vortex_session::VortexSession;
26+
27+
use crate::RunEnd;
28+
29+
fn execution_ctx() -> ExecutionCtx {
30+
let session = VortexSession::empty().with::<ArraySession>();
31+
// Execute-parent kernels live in the session registry; without registering RunEnd's own
32+
// kernels the traces below would show plain canonicalization instead of the pushdowns.
33+
crate::initialize(&session);
34+
ExecutionCtx::new(session)
35+
}
36+
37+
/// Run-end encoding of `[1, 1, 1, 2, 2, 3, 3, 3, 3]`: ends `[3, 5, 9]`, values `[1, 2, 3]`.
38+
fn runend_array() -> VortexResult<ArrayRef> {
39+
Ok(RunEnd::encode(
40+
PrimitiveArray::from_iter([1i32, 1, 1, 2, 2, 3, 3, 3, 3]).into_array(),
41+
&mut execution_ctx(),
42+
)?
43+
.into_array())
44+
}
45+
46+
#[test]
47+
fn trace_compare_on_runend() -> VortexResult<()> {
48+
let runend = runend_array()?;
49+
let rhs = ConstantArray::new(Scalar::from(2i32), runend.len()).into_array();
50+
let compared = Binary.try_new_array(runend.len(), Operator::Eq, [runend, rhs])?;
51+
52+
let traced = trace_op(|| compared.optimize())?;
53+
insta::assert_snapshot!(traced.trace.to_string(), @"
54+
optimize root=vortex.binary(bool, len=9) session=false
55+
reduce_parent static:RunEndScalarFnRule slot=0 parent=vortex.binary(bool, len=9) child=vortex.runend(i32, len=9) -> vortex.runend(bool, len=9)
56+
done output=vortex.runend(bool, len=9)
57+
");
58+
59+
let optimized = traced.output;
60+
let traced = trace_op(|| {
61+
optimized
62+
.execute::<Canonical>(&mut execution_ctx())
63+
.map(IntoArray::into_array)
64+
})?;
65+
66+
assert_arrays_eq!(
67+
traced.output,
68+
BoolArray::from_iter([false, false, false, true, true, false, false, false, false]),
69+
&mut execution_ctx()
70+
);
71+
insta::assert_snapshot!(traced.trace.to_string(), @"
72+
execute_until target=AnyCanonical root=vortex.runend(bool, len=9)
73+
iter 0 current=vortex.runend(bool, len=9) builder_active=false
74+
execute_until target=AnyCanonical root=vortex.binary(bool, len=3)
75+
iter 0 current=vortex.binary(bool, len=3) builder_active=false
76+
Done array=vortex.bool(bool, len=3)
77+
iter 1 current=vortex.bool(bool, len=3) builder_active=false
78+
return output=vortex.bool(bool, len=3)
79+
Done array=vortex.bool(bool, len=9)
80+
iter 1 current=vortex.bool(bool, len=9) builder_active=false
81+
return output=vortex.bool(bool, len=9)
82+
");
83+
84+
Ok(())
85+
}
86+
87+
#[test]
88+
fn trace_filter_on_runend() -> VortexResult<()> {
89+
let runend = runend_array()?;
90+
let filtered = FilterArray::try_new(
91+
runend,
92+
Mask::from_iter([true, false, false, true, true, false, false, true, false]),
93+
)?
94+
.into_array();
95+
96+
let traced = trace_op(|| {
97+
filtered
98+
.execute::<Canonical>(&mut execution_ctx())
99+
.map(IntoArray::into_array)
100+
})?;
101+
102+
assert_arrays_eq!(
103+
traced.output,
104+
PrimitiveArray::from_iter([1i32, 2, 2, 3]),
105+
&mut execution_ctx()
106+
);
107+
insta::assert_snapshot!(traced.trace.to_string(), @"
108+
execute_until target=AnyCanonical root=vortex.filter(i32, len=4)
109+
iter 0 current=vortex.filter(i32, len=4) builder_active=false
110+
child_execute_parent session[0]:execute_parent_fn slot=0 parent=vortex.filter(i32, len=4) child=vortex.runend(i32, len=9) -> vortex.dict(i32, len=4)
111+
iter 1 current=vortex.dict(i32, len=4) builder_active=false
112+
child_execute_parent session[0]:execute_parent_fn slot=1 parent=vortex.dict(i32, len=4) child=vortex.primitive(i32, len=3) -> vortex.primitive(i32, len=4)
113+
iter 2 current=vortex.primitive(i32, len=4) builder_active=false
114+
return output=vortex.primitive(i32, len=4)
115+
");
116+
117+
Ok(())
118+
}
119+
120+
#[test]
121+
fn trace_take_on_runend() -> VortexResult<()> {
122+
let runend = runend_array()?;
123+
// A take is expressed as a `DictArray` whose codes are the take indices.
124+
let indices = PrimitiveArray::from_iter([8u64, 0, 4, 4]).into_array();
125+
let take = DictArray::try_new(indices, runend)?.into_array();
126+
127+
let traced = trace_op(|| {
128+
take.execute::<Canonical>(&mut execution_ctx())
129+
.map(IntoArray::into_array)
130+
})?;
131+
132+
assert_arrays_eq!(
133+
traced.output,
134+
PrimitiveArray::from_iter([3i32, 1, 2, 2]),
135+
&mut execution_ctx()
136+
);
137+
insta::assert_snapshot!(traced.trace.to_string(), @"
138+
execute_until target=AnyCanonical root=vortex.dict(i32, len=4)
139+
iter 0 current=vortex.dict(i32, len=4) builder_active=false
140+
child_execute_parent session[0]:execute_parent_fn slot=1 parent=vortex.dict(i32, len=4) child=vortex.runend(i32, len=9) -> vortex.dict(i32, len=4)
141+
iter 1 current=vortex.dict(i32, len=4) builder_active=false
142+
child_execute_parent session[0]:execute_parent_fn slot=1 parent=vortex.dict(i32, len=4) child=vortex.primitive(i32, len=3) -> vortex.primitive(i32, len=4)
143+
iter 2 current=vortex.primitive(i32, len=4) builder_active=false
144+
return output=vortex.primitive(i32, len=4)
145+
");
146+
147+
Ok(())
148+
}

‎vortex-array/src/arrays/scalar_fn/vtable/mod.rs‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,6 @@ impl VTable for ScalarFn {
155155
}
156156

157157
fn execute(array: Array<Self>, ctx: &mut ExecutionCtx) -> VortexResult<ExecutionResult> {
158-
ctx.log(format_args!("scalar_fn({}): executing", array.scalar_fn()));
159158
let args = VecExecutionArgs::new(array.children(), array.len());
160159
array
161160
.scalar_fn()

0 commit comments

Comments
 (0)