Repository navigation
feat: price every operator from the measured cost table, with calibration and predicted usage - #789
Merged
Merged
Conversation
…tion and predicted usage The measured table keys each entry by operator kind, phase and family (sketch, exact accumulator or aggregate measure), with ns per unit, ns per invocation and optional bytes per state. Sketch entries interpolate in log state size between the two nearest measured configurations. Nodes without an entry take the formulas scaled by one calibration factor fitted from the measured nodes of all candidates. Query-time scans price raw reads; each request pays ns_per_evaluation. The earlier JSON array still loads. InstallablePlan's report carries Stage 3's predicted usage per node and per query.
…iction; document the global factor's limit
zzylol
force-pushed
the
feat/operator-cost-table
branch
from
October 21, 2026 01:06
a09f8d0 to
9589af3
Compare
GordonYuanyc
added a commit
that referenced
this pull request
Oct 21, 2026
Planner main fails clippy (-D warnings) and one runtime_install test since #787, #789, #795 and the joint search commit: - summary_coverage.rs: a Window alias for the relative time window (type_complexity). - workload_candidates.rs: an array instead of vec! (useless_vec). - query_candidates.rs: OperatorNode::reachable instead of a closure. - stage1_to_stage3.rs: allow too_many_arguments on search_stage2_and_stage3, as lib.rs already does once. - joint_search_equivalence.rs: struct initializer for the settings, and slice::from_ref instead of a clone. - runtime_install.rs: the merged-pane DDSketch candidate of the p99 avg workload is now P3-m1 (was P5-m1); every other assertion of that test passes unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zzylol
pushed a commit
that referenced
this pull request
Oct 21, 2026
refactor: follow asap_sketchlib's UnivMon, Hydra and HLL renames UnivMon fields rows/cols/layers/total_weight/layer_heaps, new/clear/ update_weighted/estimate_*, OwnedDataInput, UNIVMON_MAX_LAYERS; Hydra rows/cols and HydraCounter::CountMinSketch; HLL update_with_hash. refactor!: HLL, KLL, DDSketch, Count-Min and KMV kernels on native asap_sketchlib types The portable HllSketch, KllSketch, DdSketch and CountMinSketch are gone from asap_sketchlib. HLL is a classic HyperLogLogImpl at each precision 4..=18 (register types from impl_hll_registers!); KLL is Kll::with_k; DDSketch the native DdSketch (get_value_at_quantile, get_count); plain Count-Min a regular-path CountMinF64 that adds every weight, zero and negative ones included; KMV the native Kmv. Memory charges come from Kll::memory_bytes, Kmv::memory_bytes and CountMinSketch::memory_bytes. The DDSketch ratio tests interpolate PromQL quantiles from the sketch's order statistics. refactor!: weighted frequency on asap_sketchlib's f64 Count-Min and Count Sketch asap_sketchlib drops WeightedFrequency. The Planner kernel keeps its API over CountMinSketchWithHeap / CountSketchWithHeap (capacity > 0) or a plain CountMinSketch / CountSketch (capacity 0), all Vector2D<f64> on the regular path. An identity tuple is one TupleKey, hashed and retained as bytes. Planner checks what sketchlib does not: finite weights (and nonnegative for Count-Min), finite Float64 identities, an odd Count Sketch depth, and a total weight magnitude within f64. Top-k rows are the heap's keys at their current estimate, descending, ties by key bytes. refactor!: HydraKLL on asap_sketchlib's Hydra with KLL cells (#644) HydraKllSketch is gone from asap_sketchlib. The kernel is a one-column Hydra over HydraCounter::Kll, read by Hydra's median of the cells' KLL quantiles, and charged the cells' Kll::memory_bytes. refactor: box the weighted frequency state held as a template Its heap makes it far larger than the other group-state kinds. test: fixtures that still exercise native KLL memory and DDSketch quantiles Kll allocates its item buffer as items arrive, so each stored-pane state takes 1,100 samples to hold its full buffer (about 11 kB) again; with one sample the merge fit the 4 MiB limit. DdSketch reads its minimum and maximum exactly, so the cross-zero ratio fixture interpolates interior order statistics: [-2, -1, 1.011, 2] instead of [-1, 1.011]. chore!: pin asap_sketchlib to main (de15d66) asap_sketchlib main, with ProjectASAP/asap_sketchlib#242 merged. UnivMon builds any row count its 128-bit key hash covers, so the UnivMon shape tests probe that budget instead of a 20-row limit. Merge remote-tracking branch 'origin/main' into chore/asap-sketchlib-main fix: cap UnivMon at the 20 rows its ASAPv1 state encodes sketchlib main's UnivMon::check_shape accepts any row count within the 128-bit key hash, but its ASAPv1 encoder still refuses more than MATRIX_MAX_ROWS rows. Keep the executor's builds and the Planner's hash_independent at 20 rows, as on the old pin, so every UnivMon state the executor builds can be stored. fix: count the KLL and weighted frequency structs once in memory charges Kll::memory_bytes and the heap sketches' memory_bytes already include their own struct; adding size_of::<Self>() on top counted it twice. chore: pin asap_sketchlib to release 0.4.0 (eb04e7f) eb04e7f is the 0.4.0 release commit on top of de15d66; the library code is the same, only the version, CHANGELOG and README change. Merge remote-tracking branch 'origin/main' into chore/asap-sketchlib-main # Conflicts: # crates/plan-selection/src/lib.rs fix: clippy on main's new code and the renumbered P99 pane candidate Planner main fails clippy (-D warnings) and one runtime_install test since #787, #789, #795 and the joint search commit: - summary_coverage.rs: a Window alias for the relative time window (type_complexity). - workload_candidates.rs: an array instead of vec! (useless_vec). - query_candidates.rs: OperatorNode::reachable instead of a closure. - stage1_to_stage3.rs: allow too_many_arguments on search_stage2_and_stage3, as lib.rs already does once. - joint_search_equivalence.rs: struct initializer for the settings, and slice::from_ref instead of a clone. - runtime_install.rs: the merged-pane DDSketch candidate of the p99 avg workload is now P3-m1 (was P5-m1); every other assertion of that test passes unchanged.
This was referenced Oct 21, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A runtime that measures its own costs could only state sketch update, merge and estimate times, and only for exact parameter matches. Exact accumulators, scans (raw reads), aggregates, per-request overhead, phase differences and memory had no slot. Measuring the KLL but not the exact plan's operators made the exact rebuild look cheap, so Stage 3 flipped quantile queries to it (#771).
InstallablePlanalso had no prediction to compare with what the runtime measured.What
PlanSelectionSettings.measured_operation_costsis now aMeasuredOperationCoststable. EachOperatorCostentry is keyed by operator kind (every physical operator), phase (ingestion_time,query_time, or both), and family (sketch params, exact kind, or aggregate measure). Its value isns_per_unit + ns_per_invocation, with optionalmemory_bytes_per_state. The table also hasns_per_evaluationper query request andprovenance(executor version, machine).Scanentry prices raw samples fetched and decoded from the external database. That cost counts in the candidate's total.SelectedPlanReport.predicted_usagecarries Stage 3's predicted usage for the selected plan: the totals, plus per-node and per-query figures. Each node names the installed DAG's nodes that run it: a pane chain lists its expanded panes, every other node its own id.MeasuredOperationCoststill loads. Each entry becomes its operator's entry at both phases, and an update becomes both aSummaryAggand aPaneChainentry.How
priceis nowprice_at_scale(..., formula_scale). It computes each node's formula estimate and then looks the node up in the table:units × ns_per_unit + ns_per_invocation, scanning included. Units are samples for a scan, states for a merge, and input rows otherwise.formula_scale; its scanned bytes stay as the formula gives them.price_at_scalealso returns the measured and formula time of its measured nodes.select_planruns the pre-pricing checks, prices every checked candidate once at scale 1 to fit the factor, then prices again at that factor for the latency and capacity checks and the ranking. Rejections stay in candidate order. A table with no operators skips the first pass, so results with an empty table are unchanged.The compaction merge uses the state's query-time
SummaryMergeentry, else one formula operation × the factor. Per-request cost is added to the CPU of the node the query reads first, and to the query's latency.Key code interfaces added / modified
crates/plan-selection/src/measured_operation_costs.rs
crates/plan-selection/src/lib.rs
crates/plan-selection/src/cost/analytical_cost.rs
crates/planner/src/installable.rs
crates/types/src/ir/operator/pane_chain.rs
asap_planneralso re-exportsMeasuredOperationCosts,OperatorCost,OperatorKind,CostTableProvenance,NodePriceSource,ExecutionTimingand thePredicted*types.Usage
The end-to-end flows are checked in
calibration_keeps_unmeasured_nodes_on_the_measured_scale,external_raw_reads_make_the_query_time_raw_plan_costlier(plan-selection) andthe_plan_carries_stage3s_predicted_usage(plannerruntime_install).Before this PR
The test fixture is a per-series p99 over 5 min, queried every minute, 10,000 rows/s, priced on CPU only. The candidates are EXACT (rebuild from raw samples at query time), B2 (
P1-m0, KLL panes rebuilt at query time) and B1 (P1-m1, KLL panes maintained at ingestion time). The table measures the KLL as expensive (150 ns per row, 2,000 ns per merge, 5,000 ns per estimate) and the time range as cheap, but not the scan or the aggregate.Without calibration, which is how the table used to behave, the predicted CPU in core-seconds over 90 minutes is:
The exact rebuild looks 23x cheaper than it really is, so it is selected. A KLL sized k = 469 could not use a k = 200 or k = 1024 entry. A query-time raw read had no price. The plan file carried no prediction.
After this PR
With the same partial table, the calibration factor scales the unmeasured scan and aggregate:
B1 is selected, which is also the choice under the full table.
The calibration factor can still mis-rank plans. Suppose the unmeasured scan and quantile really cost 40 and 20 ns. The full table then selects EXACT, but the partial table still prices them at the KLL's ratio and selects B1, which costs 1.42× more than EXACT under the full table. The gap grows as those costs fall; review measured up to 2.6×.
a_partial_table_can_still_mis_rank_planspins this down, and #791 tracks the per-operator-kind factor. With a full table where the exact plan really is cheaper (1 ns per sample and per row), EXACT is selected. Pricing query-time raw reads at 200 ns per sample turns the choice from EXACT (when raw reads are free) to B1. The plan file carriespredicted_usage. For Example 4b, a pane-only plan, its per-node prices add up to Stage 3's selection cost.These numbers come from the plan-selection fixture. The ASAPQuery-backend harness numbers in #771 need its
measure_costsextended to every operator kind, which is the runtime-side follow-up.Verification
cargo test --workspace --lockedpasses.calibration_keeps_unmeasured_nodes_on_the_measured_scalea_partial_table_can_still_mis_rank_plansa_calibration_factor_below_one_never_makes_cpu_negative(regression test for negative CPU; failed before the fix withcpu_ops_per_second: -466180.0)a_kll_between_two_measured_sizes_is_interpolatedexternal_raw_reads_make_the_query_time_raw_plan_costlierthe_plan_carries_stage3s_predicted_usagea_measured_entry_prices_its_node_and_calibrates_the_othersa_phase_entry_wins_over_one_for_both_phaseseach_request_pays_the_per_evaluation_costmeasured_state_bytes_size_the_kept_statethe_earlier_table_format_still_loadsinvalid_measured_tables_are_rejectedcompaction_prices_merges_from_the_measured_merge(updated)Limitations: the calibration factor is a single global heuristic (#791). A node that the expansion rebuilds once per pane (the chain's input window) has no installed id.
convert_rqe_atomic_costsstill writes the earlier format, which loads.Closes #771