From b16cffd5c618f0a0aa2eb900109e4e6183154227 Mon Sep 17 00:00:00 2001 From: Dennis Zhuang Date: Wed, 21 Oct 2026 12:08:42 +0800 Subject: [PATCH] feat(promql): add max_of, min_of and query context functions to native kernels Signed-off-by: Dennis Zhuang --- src/promql/src/map.rs | 5 +- src/promql/src/map/broadcast.rs | 52 +++++ src/promql/src/map/max_min_cases.txt | 65 ++++++ src/promql/src/map/tests.rs | 56 +++++ src/promql/src/map_time.rs | 31 +++ .../src/map_time/duration_seconds_cases.txt | 211 ++++++++++++++++++ src/promql/src/map_time/tests.rs | 32 ++- 7 files changed, 450 insertions(+), 2 deletions(-) create mode 100644 src/promql/src/map/max_min_cases.txt create mode 100644 src/promql/src/map_time/duration_seconds_cases.txt diff --git a/src/promql/src/map.rs b/src/promql/src/map.rs index ffc7383f37e..2d2c4b80a7b 100644 --- a/src/promql/src/map.rs +++ b/src/promql/src/map.rs @@ -23,7 +23,10 @@ mod go_math; #[cfg(test)] mod tests; -pub use broadcast::{ScalarSeriesOperand, map_arithmetic_series, map_compare_series}; +pub use broadcast::{ + ScalarFunction, ScalarSeriesOperand, map_arithmetic_series, map_compare_series, + map_scalar_function, +}; pub use comparison::{Comparison, ComparisonOp, map_compare, scalar_arithmetic, scalar_compare}; use std::f64::consts::PI; diff --git a/src/promql/src/map/broadcast.rs b/src/promql/src/map/broadcast.rs index b94605a7c28..b6b1a4c161b 100644 --- a/src/promql/src/map/broadcast.rs +++ b/src/promql/src/map/broadcast.rs @@ -74,6 +74,58 @@ pub fn map_compare_series( }) } +/// `max_of(a, b)` and `min_of(a, b)`, which Prometheus evaluates with Go's `math.Max` and +/// `math.Min`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ScalarFunction { + MaxOf, + MinOf, +} + +impl ScalarFunction { + /// Go's `math.Max` / `math.Min`: an infinity of the function's own sign wins over NaN, NaN + /// over any other value, and `+0` is greater than `-0`. + pub fn apply(self, a: f64, b: f64) -> f64 { + let (wins, signed_zero_wins) = match self { + Self::MaxOf => (f64::INFINITY, false), + Self::MinOf => (f64::NEG_INFINITY, true), + }; + if a == wins || b == wins { + wins + } else if a.is_nan() || b.is_nan() { + f64::NAN + } else if a == 0.0 && a == b { + if a.is_sign_negative() == signed_zero_wins { + a + } else { + b + } + } else if (a > b) == (self == Self::MaxOf) { + a + } else { + b + } + } +} + +/// Applies `function` to two time-varying scalars, aligned by absolute grid index; a point where +/// either has no value has no result. Both operands must be unlabelled single-field series. +pub fn map_scalar_function( + schema: &Arc, + function: ScalarFunction, + left: &Series, + right: &Series, +) -> Result> { + validate_scalar(schema, left)?; + map_grid_scalar( + schema, + left, + ScalarSeriesOperand::Right(right), + left.metric.clone(), + |left, right, _| Some(function.apply(left, right)), + ) +} + /// Calls `calculate(left, right, vector_sample)` at each index where both operands exist. fn map_grid_scalar( schema: &Arc, diff --git a/src/promql/src/map/max_min_cases.txt b/src/promql/src/map/max_min_cases.txt new file mode 100644 index 00000000000..ecd6c0dc738 --- /dev/null +++ b/src/promql/src/map/max_min_cases.txt @@ -0,0 +1,65 @@ +# Generated by promql-compat cmd/narthex-golden-scalarfn built for amd64: `a b math.Max(a,b) math.Min(a,b)` as hex bits. +0 0 0 0 +0 8000000000000000 0 8000000000000000 +0 3ff0000000000000 3ff0000000000000 0 +0 bff0000000000000 0 bff0000000000000 +0 4004000000000000 4004000000000000 0 +0 7ff0000000000000 7ff0000000000000 0 +0 fff0000000000000 0 fff0000000000000 +0 7ff8000000000001 7ff8000000000001 7ff8000000000001 +8000000000000000 0 0 8000000000000000 +8000000000000000 8000000000000000 8000000000000000 8000000000000000 +8000000000000000 3ff0000000000000 3ff0000000000000 8000000000000000 +8000000000000000 bff0000000000000 8000000000000000 bff0000000000000 +8000000000000000 4004000000000000 4004000000000000 8000000000000000 +8000000000000000 7ff0000000000000 7ff0000000000000 8000000000000000 +8000000000000000 fff0000000000000 8000000000000000 fff0000000000000 +8000000000000000 7ff8000000000001 7ff8000000000001 7ff8000000000001 +3ff0000000000000 0 3ff0000000000000 0 +3ff0000000000000 8000000000000000 3ff0000000000000 8000000000000000 +3ff0000000000000 3ff0000000000000 3ff0000000000000 3ff0000000000000 +3ff0000000000000 bff0000000000000 3ff0000000000000 bff0000000000000 +3ff0000000000000 4004000000000000 4004000000000000 3ff0000000000000 +3ff0000000000000 7ff0000000000000 7ff0000000000000 3ff0000000000000 +3ff0000000000000 fff0000000000000 3ff0000000000000 fff0000000000000 +3ff0000000000000 7ff8000000000001 7ff8000000000001 7ff8000000000001 +bff0000000000000 0 0 bff0000000000000 +bff0000000000000 8000000000000000 8000000000000000 bff0000000000000 +bff0000000000000 3ff0000000000000 3ff0000000000000 bff0000000000000 +bff0000000000000 bff0000000000000 bff0000000000000 bff0000000000000 +bff0000000000000 4004000000000000 4004000000000000 bff0000000000000 +bff0000000000000 7ff0000000000000 7ff0000000000000 bff0000000000000 +bff0000000000000 fff0000000000000 bff0000000000000 fff0000000000000 +bff0000000000000 7ff8000000000001 7ff8000000000001 7ff8000000000001 +4004000000000000 0 4004000000000000 0 +4004000000000000 8000000000000000 4004000000000000 8000000000000000 +4004000000000000 3ff0000000000000 4004000000000000 3ff0000000000000 +4004000000000000 bff0000000000000 4004000000000000 bff0000000000000 +4004000000000000 4004000000000000 4004000000000000 4004000000000000 +4004000000000000 7ff0000000000000 7ff0000000000000 4004000000000000 +4004000000000000 fff0000000000000 4004000000000000 fff0000000000000 +4004000000000000 7ff8000000000001 7ff8000000000001 7ff8000000000001 +7ff0000000000000 0 7ff0000000000000 0 +7ff0000000000000 8000000000000000 7ff0000000000000 8000000000000000 +7ff0000000000000 3ff0000000000000 7ff0000000000000 3ff0000000000000 +7ff0000000000000 bff0000000000000 7ff0000000000000 bff0000000000000 +7ff0000000000000 4004000000000000 7ff0000000000000 4004000000000000 +7ff0000000000000 7ff0000000000000 7ff0000000000000 7ff0000000000000 +7ff0000000000000 fff0000000000000 7ff0000000000000 fff0000000000000 +7ff0000000000000 7ff8000000000001 7ff0000000000000 7ff8000000000001 +fff0000000000000 0 0 fff0000000000000 +fff0000000000000 8000000000000000 8000000000000000 fff0000000000000 +fff0000000000000 3ff0000000000000 3ff0000000000000 fff0000000000000 +fff0000000000000 bff0000000000000 bff0000000000000 fff0000000000000 +fff0000000000000 4004000000000000 4004000000000000 fff0000000000000 +fff0000000000000 7ff0000000000000 7ff0000000000000 fff0000000000000 +fff0000000000000 fff0000000000000 fff0000000000000 fff0000000000000 +fff0000000000000 7ff8000000000001 7ff8000000000001 fff0000000000000 +7ff8000000000001 0 7ff8000000000001 7ff8000000000001 +7ff8000000000001 8000000000000000 7ff8000000000001 7ff8000000000001 +7ff8000000000001 3ff0000000000000 7ff8000000000001 7ff8000000000001 +7ff8000000000001 bff0000000000000 7ff8000000000001 7ff8000000000001 +7ff8000000000001 4004000000000000 7ff8000000000001 7ff8000000000001 +7ff8000000000001 7ff0000000000000 7ff0000000000000 7ff8000000000001 +7ff8000000000001 fff0000000000000 7ff8000000000001 fff0000000000000 +7ff8000000000001 7ff8000000000001 7ff8000000000001 7ff8000000000001 diff --git a/src/promql/src/map/tests.rs b/src/promql/src/map/tests.rs index e0eea771af1..1cc936287fd 100644 --- a/src/promql/src/map/tests.rs +++ b/src/promql/src/map/tests.rs @@ -551,3 +551,59 @@ fn math_drops_reserved_metadata_and_empty_labels_with_the_name() { assert_eq!(output.metric.name(), None); assert_eq!(names, ["__other__", "host"]); } + +/// `max_of` / `min_of` against Go's `math.Max` / `math.Min`, whose signed-zero, NaN and infinity +/// rules differ from `f64::max` / `f64::min`. +#[test] +fn max_of_and_min_of_follow_go() { + let bits = |text: &str| f64::from_bits(u64::from_str_radix(text, 16).unwrap()); + let same = |a: f64, b: f64| a.to_bits() == b.to_bits() || (a.is_nan() && b.is_nan()); + let mut checked = 0; + for line in include_str!("max_min_cases.txt") + .lines() + .filter(|line| !line.starts_with('#')) + { + let values: Vec = line.split_whitespace().map(bits).collect(); + let (a, b) = (values[0], values[1]); + assert!( + same(ScalarFunction::MaxOf.apply(a, b), values[2]), + "max_of({a}, {b})" + ); + assert!( + same(ScalarFunction::MinOf.apply(a, b), values[3]), + "min_of({a}, {b})" + ); + checked += 1; + } + assert_eq!(checked, 64); +} + +/// Time-varying scalars pair by absolute position; a point either one lacks has no result. +#[test] +fn scalar_functions_pair_scalars_by_position() { + let schema = Arc::new(schema(&["scalar"])); + let scalar = |start: usize, values: &[Option]| { + Series::try_new( + &schema, + MetricName::try_new(None, Vec::new()).unwrap(), + TimeAxis::Grid( + GridRange::try_new(schema.eval_grid.clone().unwrap(), start, values.len()).unwrap(), + ), + SeriesFields::try_from_vec(vec![floats(values)]).unwrap(), + ) + .unwrap() + }; + let left = scalar(2, &[Some(1.0), Some(5.0), None, Some(-0.0)]); + let right = scalar(3, &[Some(2.0), Some(7.0), Some(0.0), Some(9.0)]); + let result = map_scalar_function(&schema, ScalarFunction::MaxOf, &left, &right) + .unwrap() + .unwrap(); + assert_eq!( + run_floats(&result, 0), + vec![((3, 1), vec![Some(5.0)]), ((5, 1), vec![Some(0.0)])] + ); + assert!( + run_floats(&result, 0)[1].1[0].unwrap().is_sign_positive(), + "max_of(-0, +0) is +0" + ); +} diff --git a/src/promql/src/map_time.rs b/src/promql/src/map_time.rs index 5101cb7fbfa..afb9fd419aa 100644 --- a/src/promql/src/map_time.rs +++ b/src/promql/src/map_time.rs @@ -43,6 +43,37 @@ pub enum DateFunction { Year, } +/// `start()`, `end()`, `range()` and `step()`: constants of a query, which Prometheus folds into +/// number literals before evaluating it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum QueryContextFunction { + Start, + End, + Range, + Step, +} + +impl QueryContextFunction { + /// The value for a query from `start_ms` to `end_ms` every `step_ms`. `step()` is 0 when the + /// start and end are equal, as for an instant query. + pub fn value(self, start_ms: i64, end_ms: i64, step_ms: i64) -> f64 { + match self { + Self::Start => start_ms as f64 / 1000.0, + Self::End => end_ms as f64 / 1000.0, + Self::Range => duration_seconds(end_ms - start_ms), + Self::Step if start_ms == end_ms => 0.0, + Self::Step => duration_seconds(step_ms), + } + } +} + +/// Go's `time.Duration.Seconds` of `ms` milliseconds: whole seconds plus the remainder over 1e9, +/// two roundings rather than one division. The nanosecond count saturates as `time.Time.Sub` does. +fn duration_seconds(ms: i64) -> f64 { + let nanos = ms.saturating_mul(1_000_000); + (nanos / 1_000_000_000) as f64 + (nanos % 1_000_000_000) as f64 / 1e9 +} + /// `time()`: the evaluation time in seconds at every point of the schema's grid, unlabelled. pub fn time(schema: &SeriesSchema) -> Result { eval_time_series(schema, |ms| ms as f64 / 1000.0) diff --git a/src/promql/src/map_time/duration_seconds_cases.txt b/src/promql/src/map_time/duration_seconds_cases.txt new file mode 100644 index 00000000000..851c93c2196 --- /dev/null +++ b/src/promql/src/map_time/duration_seconds_cases.txt @@ -0,0 +1,211 @@ +# Generated by promql-compat cmd/narthex-golden-scalarfn built for amd64: `ms time.Duration.Seconds()` for a millisecond difference, as hex bits. +0 0 +1 3f50624dd2f1a9fc +999 3feff7ced916872b +1000 3ff0000000000000 +1001 3ff004189374bc6a +4097 40106353f7ced917 +86400000 40f5180000000000 +9007199254740993 42012e0be826d695 +9223372036854 42012e0be826d4fe +9223372036855 42012e0be826d695 +2340891196754 41e170e5479820c5 +28653324801615 42012e0be826d695 +15150070723101 42012e0be826d695 +5203274071043 41f3623a9570b021 +6052846691025 41f68c71c6306666 +19267443321048 42012e0be826d695 +12341472526366 42012e0be826d695 +20530526999188 42012e0be826d695 +34395057096608 42012e0be826d695 +8401369033625 41ff4c2b7c9a0000 +31640814836996 42012e0be826d695 +29341436692007 42012e0be826d695 +34783771064630 42012e0be826d695 +35122158268884 42012e0be826d695 +1479560899851 41d60c13b0f676c9 +23204167308761 42012e0be826d695 +24788341754728 42012e0be826d695 +31429837266203 42012e0be826d695 +34244401504646 42012e0be826d695 +13068139990387 42012e0be826d695 +7761385230625 41fce9d570ea0000 +9735534642756 42012e0be826d695 +20240948685375 42012e0be826d695 +33614386955765 42012e0be826d695 +4944260193162 41f26b36c612978d +14479808172613 42012e0be826d695 +17797368697448 42012e0be826d695 +14261095223522 42012e0be826d695 +13428055133044 42012e0be826d695 +26352584022491 42012e0be826d695 +12325408233432 42012e0be826d695 +17010777739835 42012e0be826d695 +28430422903295 42012e0be826d695 +29753883088875 42012e0be826d695 +25998949474507 42012e0be826d695 +10226187168843 42012e0be826d695 +17769199632283 42012e0be826d695 +4363496320571 41f0415ab80922d1 +3287173645812 41e87dc941b9fbe7 +12725827928836 42012e0be826d695 +24795910633675 42012e0be826d695 +2828521278844 41e512f9e7db020c +4170359431716 41ef125450f6e979 +3589276087583 41eabe0036f2a7f0 +28524985356220 42012e0be826d695 +30320656727859 42012e0be826d695 +4256148642671 41efb5f5945578d5 +29566978976822 42012e0be826d695 +1648135601414 41d88f236c5a7efa +4780677690357 41f1cf35a3a5b646 +3390646811377 41e94325436c1062 +6912080800569 41f9bfdfba091aa0 +19744540834685 42012e0be826d695 +5048451373608 41f2ce9412d9ba5e +31511077099905 42012e0be826d695 +33060246075211 42012e0be826d695 +29446920826861 42012e0be826d695 +20868689814545 42012e0be826d695 +25732868269994 42012e0be826d695 +26023816056118 42012e0be826d695 +3312873794438 41e8aece284e0419 +8928671130029 4200a185acd03b64 +27201078145192 42012e0be826d695 +28161007422010 42012e0be826d695 +23548861302960 42012e0be826d695 +8403898590605 41ff4e950de9ae14 +2101319756957 41df4fe6133d3f7d +18153789976583 42012e0be826d695 +16078846218429 42012e0be826d695 +14439764018086 42012e0be826d695 +15717223852073 42012e0be826d695 +7594418227955 41fc4a9a033f47ae +1007547111184 41ce06f973978d50 +7653748532646 41fc832ef34a5604 +1542530612607 41d6fc498d26d917 +8959567520889 4200b04135071cac +1963454100123 41dd41fba507df3b +15423611227189 42012e0be826d695 +26921592371531 42012e0be826d695 +31560677483706 42012e0be826d695 +21024346859206 42012e0be826d695 +21411506917513 42012e0be826d695 +14299718455620 42012e0be826d695 +25966354044973 42012e0be826d695 +21480240207871 42012e0be826d695 +13383017194856 42012e0be826d695 +19694757267735 42012e0be826d695 +8329763775671 41ff07e1bbfabc6a +24204352256265 42012e0be826d695 +5166480575653 41f33f23cbfa72b0 +17311660727623 42012e0be826d695 +26768379971433 42012e0be826d695 +21452255664239 42012e0be826d695 +32930407231978 42012e0be826d695 +15551692227912 42012e0be826d695 +23123035545994 42012e0be826d695 +5875644708440 41f5e37392470a3d +18292969058316 42012e0be826d695 +1707825217332 41d972d610553f7d +23633727413655 42012e0be826d695 +24472694155897 42012e0be826d695 +32056439689975 42012e0be826d695 +25133237617897 42012e0be826d695 +10984619967605 42012e0be826d695 +12399994332458 42012e0be826d695 +17017307690557 42012e0be826d695 +24405803592380 42012e0be826d695 +34039839184523 42012e0be826d695 +12086900862154 42012e0be826d695 +4880216990563 41f22e2339e9020c +8291016285084 41fee2ede5d15810 +34263874447617 42012e0be826d695 +5219901505275 41f3721604146666 +3325751404254 41e8c75e0d8820c5 +10069265674283 42012e0be826d695 +2205986208125 41e06f95b4040000 +17331608106692 42012e0be826d695 +13164910887275 42012e0be826d695 +20165189440141 42012e0be826d695 +18100958213119 42012e0be826d695 +33252881487365 42012e0be826d695 +28493023658516 42012e0be826d695 +8853845580432 42007dd7b26374bc +34838873892001 42012e0be826d695 +20214329624262 42012e0be826d695 +31705600540751 42012e0be826d695 +31189001046116 42012e0be826d695 +29476027927210 42012e0be826d695 +26781011366842 42012e0be826d695 +22699855557780 42012e0be826d695 +17749079048291 42012e0be826d695 +30750407110207 42012e0be826d695 +24417907650311 42012e0be826d695 +10509573177644 42012e0be826d695 +1079378703273 41d01580c3d178d5 +34862833947828 42012e0be826d695 +24980588397686 42012e0be826d695 +11209991956240 42012e0be826d695 +8937655194575 4200a5ce5cd4999a +23978885618615 42012e0be826d695 +30065528340854 42012e0be826d695 +2628030736846 41e39492221b126f +2663938618407 41e3d90f474d0625 +34895264552379 42012e0be826d695 +15670995231869 42012e0be826d695 +4191047303785 41ef39c9d0f91eb8 +8437263482680 41ff6e66c7aae148 +3960463773594 41ed81fc33b3020c +4309293785322 41f00da9ad9526e9 +26649392599963 42012e0be826d695 +4157860955829 41eefa7d8b7a872b +22296599249089 42012e0be826d695 +32695131508279 42012e0be826d695 +30635397919632 42012e0be826d695 +15331538117365 42012e0be826d695 +11063348740606 42012e0be826d695 +32371941731295 42012e0be826d695 +13719859399284 42012e0be826d695 +7293417131828 41fb2b8b6abd3f7d +33407534042909 42012e0be826d695 +31297857606586 42012e0be826d695 +1097102363540 41d0591d06e28f5c +25332039849587 42012e0be826d695 +20041642732253 42012e0be826d695 +18752081044817 42012e0be826d695 +29442119239956 42012e0be826d695 +28855454932055 42012e0be826d695 +31958604071351 42012e0be826d695 +18688407702704 42012e0be826d695 +17871169582207 42012e0be826d695 +16003600945447 42012e0be826d695 +5284509633417 41f3afb37c16ac08 +11575618223404 42012e0be826d695 +23263846003261 42012e0be826d695 +14260376149051 42012e0be826d695 +20109100243330 42012e0be826d695 +16359587412236 42012e0be826d695 +34836697130263 42012e0be826d695 +23618221322728 42012e0be826d695 +16214535784284 42012e0be826d695 +8272436198602 41fed135be69a1cb +5401941921460 41f41fb17a175c29 +7235498080071 41faf44f060122d1 +14773466706669 42012e0be826d695 +34843449378732 42012e0be826d695 +23707829047074 42012e0be826d695 +4081628494438 41ee6916a9ce0419 +13573087199008 42012e0be826d695 +21542290482225 42012e0be826d695 +16883004474069 42012e0be826d695 +16172951872856 42012e0be826d695 +5849767460850 41f5cac5e24d999a +12771076879764 42012e0be826d695 +7080148168638 41fa6027cc8a353f +13469889734230 42012e0be826d695 +9296342191848 42012e0be826d695 +753418709293 41c67420eaa58106 +23411833150514 42012e0be826d695 +29448477422232 42012e0be826d695 +29413433948272 42012e0be826d695 diff --git a/src/promql/src/map_time/tests.rs b/src/promql/src/map_time/tests.rs index 05128523a65..3dba9472832 100644 --- a/src/promql/src/map_time/tests.rs +++ b/src/promql/src/map_time/tests.rs @@ -24,7 +24,7 @@ use datatypes::series::samples::{ }; use datatypes::series::time::{EvalGrid, GridRange, GridSpan, GridSpec, TimeAxis}; -use super::{DateFunction, date_of_eval_time, map_date, time}; +use super::{DateFunction, QueryContextFunction, date_of_eval_time, map_date, time}; use crate::grid_series::merge_same_labelset; use crate::grid_series::reference::{ Labels, Points, canonical_bits, golden_cases, labels_of, metric_of, points_by_labels, @@ -289,3 +289,33 @@ fn map_date_reads_a_sparse_axis_by_its_positions() { ]) ); } + +/// `range()` and `step()` are Go `Duration.Seconds` of a millisecond count, which rounds twice +/// and so differs from `ms / 1000` in the last bit for most values. +#[test] +fn query_context_durations_match_go() { + let mut checked = 0; + for line in include_str!("duration_seconds_cases.txt") + .lines() + .filter(|line| !line.starts_with('#')) + { + let (ms, bits) = line.split_once(' ').unwrap(); + let ms: i64 = ms.parse().unwrap(); + let expected = u64::from_str_radix(bits, 16).unwrap(); + assert_eq!( + QueryContextFunction::Range.value(0, ms, 1).to_bits(), + expected, + "range() over {ms} ms" + ); + assert_eq!( + QueryContextFunction::Step.value(0, 1, ms).to_bits(), + expected, + "step() of {ms} ms" + ); + checked += 1; + } + assert!(checked > 200, "{checked} durations checked"); + assert_eq!(QueryContextFunction::Step.value(5_000, 5_000, 1_000), 0.0); + assert_eq!(QueryContextFunction::Start.value(-1_500, 0, 1), -1.5); + assert_eq!(QueryContextFunction::End.value(0, 2_250, 1), 2.25); +}