Repository navigation
asap-executor: a columnar physical operator library (state columns, per-sample updaters) for runtimes executing the DAG #762
Description
Problem, by example
ASAPQuery-backend executes ASAPPlanner's installed DAG with asap-executor's operators. The test case is the planner-layering PromQL examples on 2,000 series (90 minutes, 15 s samples), on a release build with perf sampling. The data plane spent 53 CPU core-seconds, and the sketch kernels were only 3–7% of that. The rest comes from the executor's row layout: a Batch is Vec<Vec<Value>>, one Vec<Value> per row.
| Where the time goes (share of data-plane CPU) | |
|---|---|
| malloc / free | 32% |
| row clones | 11% |
grouping keys built per row (Vec<Vec<u8>>) |
9% |
| memory reservation recomputed per row | 9% |
| row drops | 5% |
| batch validation | 2% |
| Row layout total | 68% (75% without raw data retained) |
The same work in ASAPQuery's engine, single core, same data:
| asap-executor (rows) | ASAPQuery | |
|---|---|---|
| Pane construction | 10.5 µs/sample | 2.3 µs/sample |
sum by (job) (rate(...)) over stored panes |
15.4 ms | 4.1 ms |
| 60-minute quantile over stored panes | 62.2 ms | 33.5 ms |
The data plane must execute the DAG node by node for every query shape. No shape-matched fast path beside it is allowed. So the operators themselves must get cheaper.
Proposal: asap-executor becomes a columnar physical operator library
ASAPPlanner provides the physical operator library: one implementation per plan-node kind. The runtime's execution engine walks the DAG and calls those operators. The library also keeps a minimal single-threaded DAG runner, for the Planner's own tests and for InstallablePlan::query_time_reference.
Batch format. A batch is a set of equal-length columns plus a selection vector; a filter marks rows instead of copying them.
| Column kind | Layout |
|---|---|
| Numeric | Vec<f64> / Vec<i64> plus a validity bitmap |
| Series id | Vec<u32> into a per-run series table of interned Arc<LabelSet>. Labels are decoded only when results leave the engine. |
| Group id | Vec<u32> into the operator's group table. The group of a series is computed once and cached by series id. |
| Label / string | Dictionary-encoded |
| Summary state | A state column (below) |
Batches hold up to a fixed row limit (e.g. 8,192), so memory accounting and validation are paid per batch.
State columns: how sketches are columnar. One state per group, contiguous. All states in a column share one family and one parameter set, taken from the plan.
- Exact accumulators (sum, count, min, max, last) are plain numeric columns.
- Fixed-size sketches (Count-Min, Count Sketch, HLL registers) can share one flat
groups × sizebuffer. Merge is element-wise. - Variable-size sketches (KLL, DDSketch, top-k heaps, UnivMon) keep one asap_sketchlib object per group, without the per-row
Valuewrapper. - Stored panes are immutable
Arc<StateBatch>, read by reference.
Operators. Every physical operator kind gets a columnar implementation:
| Operator | Columnar implementation |
|---|---|
| Scan | Matchers evaluated once per series id |
| TimeRange / TimeShift | Selection on the timestamp column; instant lookback keeps the latest row per series |
| Filter, Project | Vectorized expressions |
| Aggregate | Cached series-to-group ids |
| SummaryAgg / SummaryMerge / SummaryEstimate / FinalizeExactAccumulator | Whole state columns per call |
| Join / SemiJoin | Hash on ids |
| Sort / Limit / TopK | Permutation vector / heap |
A node's output is computed once per evaluation and shared through Arc.
Per-sample updaters. A public API updates one open pane's state with one sample: (family, params) → updater, with update(group, value), merge and seal. It is the same code SummaryAgg uses on a batch, so a runtime's per-sample ingestion builds byte-identical state.
Thread safety. Batches and state columns are Send + Sync, so a runtime can share stored panes across query threads.
Scope and acceptance
- Land it operator by operator. All existing executor tests, and the integration tests that execute plans, pass unchanged.
- A parity test checks each per-sample updater against the batch
SummaryAggfor every summary family. - Measured on the ASAPQuery-backend benefit harness, release build, absolute numbers:
- pane construction approaches 2.3 µs/sample;
- stored-state queries approach 4.1 / 24.8 / 33.5 ms on the same data;
- the row-layout share of data-plane CPU drops well below the measured 68%.
- Out of scope: the runtime's scheduling, threads and SDS. Those live in ASAPQuery-backend.
Related
ASAPQuery-backend design-doc PR #826, sections "Execution engine: a columnar DAG executor" and "Overhead of the data plane, compared with ASAPQuery". #761 (Stage 3 cost terms for scans, per-row work and merges), #755 (installable plan and RawSampleSelector).
Activity
fix(executor): review findings on the columnar batch
feat(executor): Arrow columnar batches; columnar Scan, TimeRange sele…
fix(executor): review findings on state columns and the per-sample up…
perf(executor): decode dictionary texts lazily; keep own group labels…
feat(executor): state columns, columnar summary operators and the per…
fix(executor): review findings on the columnar Aggregate, try_from_co…
feat(executor): Batch::try_from_columns, columnar exact Aggregate and…
fix(executor): review findings on the columnar operators
feat(executor): columnar Sort, Limit, joins, windows and PromQL serie…
feat(executor): remove the row form of Batch, Send + Sync compiled pl…
feat(executor): HydraCms in StateColumnUpdater, text MIN/MAX and merg…
zzylol commentedon Oct 19, 2026
Done in #763, #765, #766, #767, #768 and #769:
Batchis removed;StateColumnUpdater(HydraCms included) are in;Batch::try_from_columnsbuilds a pane from columns;RawSampleSelectorareSend + Sync;be-micro, same machine, executor before the first PR vs main now:
sum by (job) (rate)topk by (job)The remaining follow-up is a Low:
StateColumnUpdater::add_group_with_keyrefuses an empty key, while the batch build accepts a group with no group columns. Flat sketch buffers are tracked in #764.