mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-10-12 23:12:28 +00:00
test(perf): model bounded mixed integer and fractional metric series
Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
This commit is contained in:
@@ -204,6 +204,7 @@ pub(super) enum ValuePattern {
|
||||
QuantizedSignal,
|
||||
SignalWithSporadicStalls,
|
||||
MixedSignalRepeated,
|
||||
BoundedMixed,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ValuePattern {
|
||||
|
||||
@@ -197,8 +197,35 @@ fn deterministic_prom_value(args: &PromRemoteWriteArgs, series_idx: u64, sample_
|
||||
args.value_base + (series_idx % 97) as f64 + local as f64 * args.value_step
|
||||
}
|
||||
}
|
||||
ValuePattern::BoundedMixed => bounded_mixed_value(args, series_idx, local),
|
||||
}
|
||||
}
|
||||
|
||||
fn bounded_mixed_value(args: &PromRemoteWriteArgs, series_idx: u64, local: u64) -> f64 {
|
||||
const SPANS: [i64; 4] = [10, 1_000, 100_000, 10_000_000];
|
||||
let series = splitmix64(series_idx ^ args.value_seed);
|
||||
let span = SPANS[(series >> 62) as usize] * (1 + ((series >> 32) % 10) as i64);
|
||||
let baseline = (series % args.value_cardinality.max(1)) as f64 * span as f64 / 10.0;
|
||||
let run_length = args.value_run_length.max(1);
|
||||
let group = local / run_length;
|
||||
let phase = local % run_length;
|
||||
let anchor = bounded_mixed_anchor(series, group, span);
|
||||
let next_anchor = bounded_mixed_anchor(series, group + 1, span);
|
||||
let signal = baseline + anchor + (next_anchor - anchor) * phase as f64 / run_length as f64;
|
||||
let value = (args.value_base + args.value_step * signal).round();
|
||||
if series_idx % args.value_mixed_every.max(1) == args.value_mixed_every.max(1) - 1 {
|
||||
let numerator = splitmix64(series ^ local) % 999 + 1;
|
||||
value + numerator as f64 / 1_000.0
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
fn bounded_mixed_anchor(series: u64, group: u64, span: i64) -> f64 {
|
||||
let limit = span - 1;
|
||||
(splitmix64(series ^ group) % (2 * limit as u64 + 1)) as i64 as f64 - limit as f64
|
||||
}
|
||||
|
||||
fn splitmix64(mut value: u64) -> u64 {
|
||||
value = value.wrapping_add(0x9e3779b97f4a7c15);
|
||||
let mut mixed = value;
|
||||
@@ -307,7 +334,7 @@ mod tests {
|
||||
for chunk_idx in 0..10 {
|
||||
let mut chunk = args(ValuePattern::SeededRandom);
|
||||
chunk.samples_per_series = 1_440;
|
||||
chunk.value_sample_offset = chunk_idx * 1_440;
|
||||
chunk.value_sample_offset = chunk_idx * 1_439;
|
||||
chunk.value_total_samples_per_series = Some(14_400);
|
||||
for sample_idx in 0..chunk.samples_per_series {
|
||||
assert_eq!(
|
||||
@@ -319,6 +346,118 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bounded_mixed_has_bounded_95_5_per_series_values() {
|
||||
let mut args = args(ValuePattern::BoundedMixed);
|
||||
args.series_count = 1_000;
|
||||
args.samples_per_series = 4_320;
|
||||
args.value_cardinality = 1_000;
|
||||
args.value_seed = 42;
|
||||
args.value_run_length = 60;
|
||||
args.value_mixed_every = 20;
|
||||
|
||||
let mut integral_series = 0;
|
||||
let mut fractional_series = 0;
|
||||
let mut spans = HashSet::new();
|
||||
let mut ranges = HashSet::new();
|
||||
for series_idx in 0..args.series_count {
|
||||
let series = splitmix64(series_idx ^ args.value_seed);
|
||||
let span = [10_i64, 1_000, 100_000, 10_000_000][(series >> 62) as usize]
|
||||
* (1 + ((series >> 32) % 10) as i64);
|
||||
let baseline = (series % args.value_cardinality.max(1)) as f64 * span as f64 / 10.0;
|
||||
let values = (0..args.samples_per_series)
|
||||
.map(|sample_idx| deterministic_prom_value(&args, series_idx, sample_idx))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert!(values.iter().all(|value| value.is_finite()));
|
||||
assert!(
|
||||
values
|
||||
.iter()
|
||||
.all(|value| *value >= baseline - span as f64 - 1.0
|
||||
&& *value <= baseline + span as f64 + 1.0)
|
||||
);
|
||||
assert!(
|
||||
values.windows(2).any(|pair| pair[1] > pair[0]),
|
||||
"series {series_idx} never rises"
|
||||
);
|
||||
assert!(
|
||||
values.windows(2).any(|pair| pair[1] < pair[0]),
|
||||
"series {series_idx} never falls"
|
||||
);
|
||||
ranges.insert((
|
||||
values
|
||||
.iter()
|
||||
.copied()
|
||||
.fold(f64::INFINITY, f64::min)
|
||||
.to_bits(),
|
||||
values
|
||||
.iter()
|
||||
.copied()
|
||||
.fold(f64::NEG_INFINITY, f64::max)
|
||||
.to_bits(),
|
||||
));
|
||||
spans.insert(span);
|
||||
if series_idx % args.value_mixed_every == args.value_mixed_every - 1 {
|
||||
fractional_series += 1;
|
||||
assert!(values.iter().all(|value| value.fract() != 0.0));
|
||||
} else {
|
||||
integral_series += 1;
|
||||
assert!(values.iter().all(|value| value.fract() == 0.0));
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!((integral_series, fractional_series), (950, 50));
|
||||
assert!(spans.len() > 4);
|
||||
assert!(ranges.len() > 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bounded_mixed_is_seeded_and_chunk_bit_identical() {
|
||||
let mut monolithic = args(ValuePattern::BoundedMixed);
|
||||
monolithic.samples_per_series = 14_400;
|
||||
monolithic.value_total_samples_per_series = Some(14_400);
|
||||
monolithic.value_cardinality = 1_000;
|
||||
monolithic.value_seed = 42;
|
||||
monolithic.value_run_length = 60;
|
||||
monolithic.value_mixed_every = 20;
|
||||
|
||||
let original = (0..600)
|
||||
.map(|sample_idx| deterministic_prom_value(&monolithic, 19, sample_idx))
|
||||
.collect::<Vec<_>>();
|
||||
monolithic.value_seed = 43;
|
||||
assert_ne!(
|
||||
original,
|
||||
(0..600)
|
||||
.map(|sample_idx| deterministic_prom_value(&monolithic, 19, sample_idx))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
monolithic.value_seed = 42;
|
||||
for series_idx in [0, 19] {
|
||||
for chunk_idx in 0..10 {
|
||||
let mut chunk = args(ValuePattern::BoundedMixed);
|
||||
chunk.samples_per_series = 1_440;
|
||||
chunk.value_sample_offset = chunk_idx * 1_439;
|
||||
chunk.value_total_samples_per_series = Some(14_400);
|
||||
chunk.value_cardinality = 1_000;
|
||||
chunk.value_seed = 42;
|
||||
chunk.value_run_length = 60;
|
||||
chunk.value_mixed_every = 20;
|
||||
for sample_idx in 0..chunk.samples_per_series {
|
||||
assert_eq!(
|
||||
deterministic_prom_value(&chunk, series_idx, sample_idx).to_bits(),
|
||||
deterministic_prom_value(
|
||||
&monolithic,
|
||||
series_idx,
|
||||
chunk.value_sample_offset + sample_idx
|
||||
)
|
||||
.to_bits()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn values_continue_across_series_boundaries() {
|
||||
let mut args = args(ValuePattern::Unique);
|
||||
|
||||
+40
-13
@@ -86,15 +86,19 @@ sample values without changing label cardinality or the runner lifecycle:
|
||||
[scenario.remote_write.value]
|
||||
pattern = "quantized_signal" # linear, constant, modulo, unique, seeded_random,
|
||||
# run_length, quantized_signal,
|
||||
# signal_with_sporadic_stalls, mixed_signal_repeated
|
||||
# signal_with_sporadic_stalls, mixed_signal_repeated,
|
||||
# bounded_mixed
|
||||
base = 0.0
|
||||
step = 0.125
|
||||
cardinality = 4096 # buckets for modulo/seeded_random/quantized_signal/run_length
|
||||
seed = 12345 # deterministic seeded_random input
|
||||
run_length = 8 # adjacent samples per bucket for run_length/quantized_signal
|
||||
cardinality = 4096 # buckets for modulo/seeded_random/quantized_signal/run_length;
|
||||
# baseline buckets for bounded_mixed
|
||||
seed = 12345 # deterministic seeded_random input and bounded_mixed series hash
|
||||
run_length = 8 # adjacent samples per bucket for run_length/quantized_signal;
|
||||
# interpolation interval for bounded_mixed
|
||||
stall_every = 100 # interval for signal_with_sporadic_stalls
|
||||
stall_length = 16 # held samples inside each stall interval
|
||||
mixed_every = 5 # every Nth sample becomes the repeated base value
|
||||
mixed_every = 5 # every Nth sample becomes the repeated base value;
|
||||
# one fractional bounded_mixed series per N series
|
||||
```
|
||||
|
||||
The default `linear` pattern preserves the helper's historical formula. Use
|
||||
@@ -103,12 +107,29 @@ data shapes, `run_length` for run-heavy low-cardinality series, `quantized_signa
|
||||
for signal-like values collapsed into a finite bucket set, `signal_with_sporadic_stalls`
|
||||
for mostly continuous signals with periodic flat spots, and `mixed_signal_repeated`
|
||||
for signal-plus-periodic-default mixtures. `unique` or high-cardinality buckets
|
||||
still work for broad sample-value distributions. This is a generic sample-value
|
||||
control for query/ingestion cases; it does not inspect or assert storage
|
||||
encoding, Parquet footers, or storage policy choices. For chunked remote-write
|
||||
ingestion, the runner passes the sample offset and total sample count to the
|
||||
helper so non-linear value patterns use a stable global/per-series ordinal across
|
||||
chunks.
|
||||
still work for broad sample-value distributions.
|
||||
|
||||
`bounded_mixed` is synthetic rather than an empirical workload. For series `s`,
|
||||
`h = splitmix64(s ^ seed)` selects a span from `[10, 1_000, 100_000, 10_000_000]`
|
||||
using its top two bits, multiplied by `1 + ((h >> 32) % 10)`. Its baseline is
|
||||
`(h % max(cardinality, 1)) * span / 10`. Ranges can overlap. At local sample
|
||||
`l = sample_offset + sample_idx`, it linearly interpolates hash-derived anchors
|
||||
in `[-span + 1, span - 1]`, changing anchors every `max(run_length, 1)` samples.
|
||||
It applies `base + step * signal`, then rounds to an integer. Every
|
||||
`max(mixed_every, 1)`-th series adds a hash-derived fraction from `1/1000` to
|
||||
`999/1000`. The 95/5 finite, nonintegral guarantee applies to the case's bounded
|
||||
parameters, not arbitrary huge or nonfinite base/step inputs. With `base = 0`
|
||||
and `step = 1`, output lies within `baseline ± span`, allowing one unit for
|
||||
rounding and the fractional addition. The explicit case uses 1,000 equal-length
|
||||
series and `mixed_every = 20`, giving exactly 95% integer-valued and 5%
|
||||
fractional Float64 samples. Its ranges and temporal shape are synthetic design
|
||||
parameters, not measured properties from the survey.
|
||||
|
||||
This is a generic sample-value control for query/ingestion cases; it does not
|
||||
inspect or assert storage encoding, Parquet footers, or storage policy choices.
|
||||
For chunked remote-write ingestion, the runner passes the sample offset and total
|
||||
sample count to the helper so non-linear value patterns use a stable global/per-series
|
||||
ordinal across chunks.
|
||||
|
||||
Case schema, value distribution defaults, storage defaults, and read-bench
|
||||
defaults are owned by Rust. The outer CI driver calls
|
||||
@@ -373,8 +394,14 @@ uv run --no-project python .github/scripts/query-regression-run.py \
|
||||
|
||||
`tests/perf/query_cases/sst_float_bss/case.toml` is an explicit-only case for
|
||||
comparing a default empty physical metric table with a candidate
|
||||
byte-stream-split (BSS) physical table. Current local-case findings are recorded
|
||||
in [`query_cases/sst_float_bss/RESULTS.md`](query_cases/sst_float_bss/RESULTS.md).
|
||||
byte-stream-split (BSS) physical table. It writes 1,000 series × 4,320 samples
|
||||
(4,320,000 rows) using synthetic `bounded_mixed` values: 95% integral series and
|
||||
5% nonintegral series, with bounded per-series fluctuation rather than globally
|
||||
unique values. This deliberately matches a 95/5 design; it is not an empirical
|
||||
claim about any production population. A separate survey sample found 94.79%
|
||||
integral values, but that observation does not make its values globally unique.
|
||||
Historical unique-value findings are preserved, but superseded pending a mixed
|
||||
workload rerun, in [`query_cases/sst_float_bss/RESULTS.md`](query_cases/sst_float_bss/RESULTS.md).
|
||||
Both targets must use the exact same release `greptime` binary; only the
|
||||
per-target table setup SQL differs. Run the existing driver from a checkout
|
||||
containing that binary, with absolute paths and fresh data directories for every
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
# SST float BSS: current-case results
|
||||
# SST float BSS: historical unique-integer results
|
||||
|
||||
> Superseded workload: the current case now generates bounded, fluctuating
|
||||
> 95% integer-valued / 5% fractional series. The numbers below belong only to
|
||||
> the earlier global-unique-integer experiment, not the current case. Mixed
|
||||
> workload measurements are pending.
|
||||
|
||||
## Decision context
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
[case]
|
||||
name = "sst_float_bss"
|
||||
description = "Compare default and byte-stream-split floating-point SST bytes and warmed value-scanning query latency"
|
||||
description = "Compare default and byte-stream-split floating-point SST bytes and warmed value-scanning query latency with bounded 95/5 per-series DOUBLE values"
|
||||
|
||||
[scenario]
|
||||
kind = "prom_remote_write_then_query"
|
||||
@@ -12,7 +12,7 @@ kind = "prom_remote_write_then_query"
|
||||
database = "public"
|
||||
metric = "sst_float_bss"
|
||||
physical_table = "sst_float_bss_physical"
|
||||
series_count = 1024
|
||||
series_count = 1000
|
||||
samples_per_series = 4320
|
||||
sample_chunk_size = 1440
|
||||
flush_every_sample_chunks = 1
|
||||
@@ -32,9 +32,15 @@ candidate_setup_sql = [
|
||||
]
|
||||
|
||||
[scenario.remote_write.value]
|
||||
pattern = "unique"
|
||||
# Synthetic, not empirical: 95% integral and 5% nonintegral series vary
|
||||
# smoothly within deterministic per-series ranges; series are not a global monotonic counter.
|
||||
pattern = "bounded_mixed"
|
||||
base = 0
|
||||
step = 1
|
||||
cardinality = 1000
|
||||
seed = 42
|
||||
mixed_every = 20
|
||||
run_length = 60
|
||||
|
||||
[scenario.remote_write.prom_store]
|
||||
pending_rows_flush_interval = "1s"
|
||||
|
||||
Reference in New Issue
Block a user