Skip to content

Conversation

@haohuaijin
Copy link
Collaborator

@haohuaijin haohuaijin commented Oct 24, 2025

the signature is take 14% percent of time for calculate 45w series histogram query.

histogram_quantile(
  0.9,
  sum by(le, path) (
    rate(codelab_api_request_duration_seconds_bucket[5m])
  )
)
  • change signature hash to ahash
  • add split-debuginfo = "packed" to generate symbol table for profiling in macos
image

@github-actions
Copy link
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Determinism

Switching from DefaultHasher to ahash::AHasher changes hash determinism and potential cross-platform/stability characteristics. Confirm that signatures are intended to be consistent only within a single runtime/environment and that no persisted or cross-process comparisons rely on the old hashing behavior.

    let mut hasher = ahash::AHasher::default();
    labels
        .iter()
        .filter(|item| !exclude_names.contains(&item.name.as_str()))
        .for_each(|item| {
            hasher.write(item.name.as_bytes());
            hasher.write(item.value.as_bytes());
        });
    hasher.finish()
}

#[cfg(test)]
mod tests {
    use std::f64;

    use float_cmp::approx_eq;

    use super::*;

    fn generate_test_labels() -> Labels {
        let labels: Labels = vec![
            Arc::new(Label {
                name: "a".to_owned(),
                value: "1".to_owned(),
            }),
            Arc::new(Label {
                name: "b".to_owned(),
                value: "2".to_owned(),
            }),
            Arc::new(Label {
                name: "c".to_owned(),
                value: "3".to_owned(),
            }),
            Arc::new(Label {
                name: "d".to_owned(),
                value: "4".to_owned(),
            }),
        ];
        labels
    }
    #[test]
    fn test_signature_without_labels() {
        let labels: Labels = generate_test_labels();

        let sig_all = signature(&labels);
        let sig_without_ac = signature_without_labels(&labels, &["a", "c"]);

        // Signatures should be different when excluding labels
        assert_ne!(sig_all, sig_without_ac);

        // Signature with empty exclusion list should match full signature
        assert_eq!(signature(&labels), signature_without_labels(&labels, &[]));

        // Same labels should produce same signature (deterministic)
        assert_eq!(sig_all, signature(&labels));
        assert_eq!(sig_without_ac, signature_without_labels(&labels, &["a", "c"]));
    }
Build Reproducibility

Added split-debuginfo="packed" and feature changes; verify CI/build environments (Linux/macOS/Windows) handle packed debuginfo and that new features (aws-lc-rs, grpc-tonic, vrl split features) don’t alter optional dependency trees unexpectedly.

inherits = "release"
codegen-units = 1
lto = "thin"

[profile.release-profiling]
inherits = "release"
debug = true
strip = false
codegen-units = 4
split-debuginfo = "packed"

[dependencies]
actix-cors = "0.7"
actix-http = "3.9"
actix-multipart = { version = "0.7", features = ["derive"] }
actix-web.workspace = true
actix-web-httpauth = "0.8"
actix-web-opentelemetry = { version = "0.22", features = ["metrics"] }
actix-web-rust-embed-responder = { version = "2.2", default-features = false, features = [
    "support-rust-embed-for-web",
    "base64",
] }
actix-tls.workspace = true
ahash.workspace = true
anyhow.workspace = true
argon2.workspace = true
async-trait.workspace = true
async-recursion.workspace = true
async-walkdir.workspace = true
awc = { version = "3.5", features = ["rustls-0_23"] }
aws-sdk-sns.workspace = true
base64.workspace = true
bitflags = "2.9"
bitvec.workspace = true
blake3 = { version = "1.6", features = ["rayon"] }
bytes.workspace = true
byteorder.workspace = true
chrono.workspace = true
clap = { version = "4.1", default-features = false, features = [
    "std",
    "help",
    "usage",
    "suggestions",
    "cargo",
] }
cloudevents-sdk = { version = "0.8.0", features = ["actix"] }
cron.workspace = true
csv = "1.3"
dashmap.workspace = true
datafusion.workspace = true
datafusion-proto.workspace = true
datafusion-functions-aggregate-common.workspace = true
datafusion-functions-json.workspace = true
arrow.workspace = true
arrow-flight.workspace = true
arrow-json.workspace = true
arrow-schema.workspace = true
parquet.workspace = true
object_store.workspace = true
env_logger.workspace = true
faststr.workspace = true
flate2.workspace = true
futures.workspace = true
hashlink.workspace = true
hashbrown.workspace = true
hex.workspace = true
http-auth-basic = "0.3"
itertools.workspace = true
jsonwebtoken = "9.3"
log.workspace = true
maxminddb = "0.26"
md5.workspace = true
memchr.workspace = true
mimalloc = { version = "0.1.43", default-features = false, optional = true }
once_cell.workspace = true
opentelemetry.workspace = true
opentelemetry_sdk.workspace = true
opentelemetry-otlp.workspace = true
opentelemetry-proto.workspace = true
parking_lot.workspace = true
prometheus.workspace = true
promql-parser = "0.6"
prost.workspace = true
prost-wkt-types.workspace = true
proto.workspace = true
prettytable-rs = "0.10.0"
pyroscope = { version = "0.5.8", optional = true }
pyroscope_pprofrs = { version = "0.2.8", optional = true }
rand.workspace = true
rayon.workspace = true
regex.workspace = true
regex-syntax.workspace = true
reqwest.workspace = true
rust-embed-for-web = "11.2.1"
rustls.workspace = true
rustls-pemfile.workspace = true
sea-orm.workspace = true
segment.workspace = true
serde.workspace = true
serde_json.workspace = true
sha256.workspace = true
snafu.workspace = true
snap.workspace = true
sqlparser.workspace = true
strum.workspace = true
svix-ksuid.workspace = true
thiserror.workspace = true
time.workspace = true
tikv-jemallocator = { version = "0.6", optional = true }
tokio.workspace = true
console-subscriber = { version = "0.4", optional = true }
tonic.workspace = true
tracing.workspace = true
tracing-appender.workspace = true
tracing-opentelemetry.workspace = true
tracing-subscriber.workspace = true
tempfile.workspace = true
uaparser = "0.6.4"
url.workspace = true
utoipa.workspace = true
utoipa-swagger-ui.workspace = true
version-compare = "0.2.0"
vector-enrichment.workspace = true
x509-parser = "0.18.0"
vrl.workspace = true
zstd.workspace = true
config.workspace = true
infra.workspace = true
ingester.workspace = true
wal.workspace = true
report_server.workspace = true
chromiumoxide.workspace = true
lettre.workspace = true
tantivy.workspace = true
tantivy-fst.workspace = true
zip.workspace = true
futures-util = "0.3.31"
tokio-util = "0.7.13"
pprof = { version = "0.15.0", features = [
    "flamegraph",
    "prost-codec",
], optional = true }
tokio-stream.workspace = true
mime = "0.3.17"
pin-project-lite = "0.2.16"
futures-core = "0.3.31"
actix-service = "2.0.3"
actix-utils = "3.0.1"
derive_more = { version = "2.0.1", features = ["full"] }
brotli = "8.0.1"
tokio-rustls.workspace = true
roaring.workspace = true
flight.workspace = true
async-stream.workspace = true

[dev-dependencies]
async-walkdir.workspace = true
expect-test.workspace = true
base64 = "0.22"
float-cmp = "0.10"
rcgen = "0.14.4"

[workspace]
members = [
    "src/config",
    "src/infra",
    "src/ingester",
    "src/wal",
    "src/proto",
    "src/report_server",
    "src/flight",
]
resolver = "2"

[workspace.package]
version = "0.1.0"
edition = "2024"
license = "AGPL-3.0"

[workspace.dependencies]
config = { path = "src/config" }
infra = { path = "src/infra" }
ingester = { path = "src/ingester" }
wal = { path = "src/wal" }
proto = { path = "src/proto" }
report_server = { path = "src/report_server" }
flight = { path = "src/flight" }
aes-siv = "0.7.0"
ahash = { version = "0.8", features = ["serde"] }
actix-web = { version = "4.11", features = ["rustls-0_23"] }
actix-tls = { version = "3.4", features = [
    "connect",
    "uri",
    "rustls-0_23-native-roots",
    "rustls-0_23-webpki-roots",
] }
anyhow = "1.0"
arc-swap = "1.7.1"
argon2 = { version = "0.5", features = ["alloc", "password-hash"] }
async-nats = "0.42"
async-stream = "0.3.6"
async-trait = "0.1"
async-recursion = "1.0"
async-walkdir = "2.1.0"
aws-config = "1.5.17"
aws-sdk-sns = "1.61.0"
base64 = "0.22"
bitvec = "1.0"
bytes = "1.10"
byteorder = "1.5"
chromiumoxide = { git = "https://github.com/mattsse/chromiumoxide", features = [
    "tokio-runtime",
    "_fetcher-rusttls-tokio",
], default-features = false, rev = "6f2392f78ae851e2acf33df8e9764cc299d837db" }
chrono = { version = "0.4", default-features = false, features = ["clock"] }
cityhasher = { version = "0.1", default-features = false }
collapse = "0.1.2"
cron = "0.15"
dashmap = { version = "6.1", features = ["serde"] }
datafusion = "50.2.0"
datafusion-proto = "50.2.0"
datafusion-functions-aggregate-common = "50.2.0"
datafusion-functions-json = "0.50.0"
expect-test = "1.4"
arrow = { version = "56.0.0", features = ["ipc_compression", "prettyprint"] }
arrow-flight = "56.0.0"
arrow-json = "56.0.0"
arrow-schema = { version = "56.0.0", features = ["serde"] }
parquet = { version = "56.0.0", features = ["arrow", "async", "object_store"] }
object_store = { version = "0.12.3", features = ["aws", "azure", "gcp"] }
sqlparser = { version = "0.58.0", features = ["serde", "visitor"] }
dotenv_config = "0.2.2"
dotenvy = "0.15.7"
env_logger = "0.11.8"
faststr = { version = "0.2", features = ["serde"] }
flate2 = { version = "1.0", features = ["zlib"] }
futures = "0.3"
get_if_addrs = "0.5"
hashlink = "0.10"
hashbrown = { version = "0.16.0", features = ["serde"] }
hex = "0.4"
indexmap = { version = "2.7", features = ["serde"] }
itertools = "0.14"
lettre = { version = "0.11", default-features = false, features = [
    "builder",
    "hostname",
    "smtp-transport",
    "pool",
    "tokio1",
    "tokio1-rustls-tls",
    "aws-lc-rs",
] }
log = "0.4"
md5 = "0.8.0"
memchr = "2.7"
murmur3 = "0.5"
once_cell = "1.20"
ordered-float = { version = "5.0.0", features = ["serde"] }
opentelemetry = "0.30"
opentelemetry_sdk = { version = "0.30", features = [
    "experimental_trace_batch_span_processor_with_async_runtime",
    "rt-tokio",
    "trace",
    "metrics",
    "spec_unstable_metrics_views",
] }
opentelemetry-otlp = { version = "0.30", features = [
    "http-proto",
    "serialize",
    "serde",
    "trace",
    "reqwest-client",
    "grpc-tonic",
] }
opentelemetry-proto = { version = "0.30", features = [
    "gen-tonic",
    "with-serde",
    "logs",
    "metrics",
    "trace",
] }
parking_lot = "0.12"
prometheus = { version = "0.14", features = ["process"] }
prost = "0.13.1"
prost-wkt-types = "0.6"
rand = "0.9.2"
rayon = "1.10"
regex = "1.11"
regex-syntax = "0.8"
reqwest = { version = "0.12", default-features = false, features = [
    "rustls-tls-native-roots",
    "stream",
] }
roaring = "0.11.2"
rustls-pemfile = "2"
rustls = { version = "0.23.20", default-features = false, features = [
    "std",
    "tls12",
] }
sea-orm = { version = "1.1.8", features = [
    "sqlx-all",
    "runtime-tokio-rustls",
    "macros",
] }
sea-orm-migration = { version = "1.1.8", features = [
    "sqlx-mysql",
    "sqlx-postgres",
    "sqlx-sqlite",
    "runtime-tokio-rustls",
] }
segment = "~0.2.4"
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["arbitrary_precision"] }
sha1 = "0.10.6"
sha256 = "1.4.0"
snafu = "0.8.9"
snap = "1"
sqlx = { version = "0.8.3", features = [
    "runtime-tokio-rustls",
    "postgres",
    "mysql",
    "sqlite",
    "chrono",
] }
strum = { version = "0.27", features = ["derive"] }
svix-ksuid = { version = "0.8", features = ["serde"] }
sysinfo = "0.37"
tantivy = { version = "0.25", features = ["quickwit"] }
tantivy-fst = "0.5"
tempfile = "3"
thiserror = "2.0.17"
time = "0.3"
tokio = { version = "1", features = ["full"] }
tokio-rustls = { version = "0.26", default-features = false, features = [
    "logging",
    "tls12",
] }
tokio-util = { version = "0.7.12", features = ["compat"] }
tokio-stream = "0.1"
tonic = { version = "0.13", features = ["gzip", "prost", "tls-webpki-roots"] }
tracing = "0.1.40"
tracing-appender = "0.2.3"
tracing-log = "0.2"
tracing-opentelemetry = "0.31"
tracing-subscriber = { version = "0.3.20", features = [
    "env-filter",
    "json",
    "registry",
] }
url = "2.5"
urlencoding = "2.1"
utoipa = { version = "5", features = ["actix_extras", "openapi_extensions"] }
utoipa-swagger-ui = { version = "9", features = ["actix-web"] }
uuid = { version = "1.18.1", features = ["v7"] }
vector-enrichment = { version = "0.1.0", package = "enrichment", git = "https://github.com/openobserve/vector", rev = "2003d617c7befdc7aa7f73a37263d272cd4a3718" }
vrl = { version = "0.27.0", git = "https://github.com/vectordotdev/vrl.git", branch = "main", features = [
    "value",
    "compiler",
    "test",
] }
zip = "6.0.0"

@github-actions
Copy link
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Make hashing deterministic

AHasher::default() seeds from a random key and is non-deterministic across runs,
breaking stable signatures and tests over process restarts. Use a fixed-seeded
hasher (e.g., AHasher::new_with_keys) to ensure deterministic signatures while
keeping performance.

src/service/promql/value.rs [793]

-let mut hasher = ahash::AHasher::default();
+// Use fixed keys to keep signatures deterministic across runs
+let mut hasher = ahash::AHasher::new_with_keys(0x9E3779B185EBCA87, 0xC2B2AE3D27D4EB4F);
Suggestion importance[1-10]: 8

__

Why: Using AHasher::default() can yield nondeterministic hashes across runs, which undermines stable signatures and tests; switching to fixed keys preserves determinism with similar performance. The suggestion directly targets the changed line and the improved code accurately reflects the proposed fix.

Medium
General
Avoid flaky difference assertion

This assertion can be flaky if excluded labels are absent or filtered equivalently;
it will spuriously fail. Guard the assertion by ensuring the exclusion actually
removes at least one label from the set before asserting difference.

src/service/promql/value.rs [840-842]

-// Signatures should be different when excluding labels
-assert_ne!(sig_all, sig_without_ac);
+// Only assert difference if exclusion actually removes at least one label
+if labels.iter().any(|l| ["a", "c"].contains(&l.name.as_str())) {
+    assert_ne!(sig_all, sig_without_ac);
+}
Suggestion importance[1-10]: 6

__

Why: Guarding assert_ne! avoids potential flakiness if excluded labels aren't present; while current test labels include "a" and "c", the guard increases robustness without harming intent. The improved code matches the existing lines with a precise conditional around the assertion.

Low

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Greptile Overview

Greptile Summary

This PR optimizes the performance-critical signature_without_labels function by replacing std::hash::DefaultHasher with ahash::AHasher, addressing a 14% performance bottleneck in histogram queries processing 450,000 series.

Key Changes

  • Performance Optimization: Switched from DefaultHasher to AHasher in signature_without_labels() function (src/service/promql/value.rs:793)
  • Test Improvements: Updated tests to verify deterministic behavior instead of relying on hardcoded hash values, making tests more robust across hash algorithm changes
  • Profiling Support: Added split-debuginfo = "packed" to profliing profile for better debugging capabilities
  • Code Formatting: Minor formatting improvements to dependency lists in Cargo.toml

Performance Impact

The signature function is heavily used throughout the PromQL engine:

  • Called in aggregation operations (sum, avg, count, etc.) to group metrics by label sets
  • Used in binary vector operations for matching time series
  • Critical for duplicate label set detection

AHasher from the ahash crate provides significantly faster hashing than DefaultHasher, especially for small keys like metric labels, while maintaining good hash distribution.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • This is a well-isolated performance optimization with proper test coverage. The change from DefaultHasher to AHasher is a straightforward swap that maintains the same interface. The ahash dependency was already in use in the codebase, and tests were updated to verify deterministic behavior rather than specific hash values, making them more resilient to future changes.
  • No files require special attention

Important Files Changed

File Analysis

Filename Score Overview
src/service/promql/value.rs 5/5 Replaced std::hash::DefaultHasher with ahash::AHasher in signature_without_labels function for better performance and updated tests to verify deterministic behavior instead of hardcoded hash values
Cargo.toml 5/5 Added profiling configuration (split-debuginfo = "packed") and reformatted dependencies for consistency

Sequence Diagram

sequenceDiagram
    participant Query as PromQL Query
    participant Agg as Aggregation Module
    participant Sig as signature_without_labels()
    participant Hash as AHasher
    participant Map as HashMap<u64, T>

    Query->>Agg: Execute aggregation (e.g., sum, avg)
    loop For each metric series
        Agg->>Sig: Calculate signature(labels)
        Sig->>Hash: Create AHasher::default()
        Note over Hash: Previously: DefaultHasher::new()
        loop For each label
            Sig->>Hash: write(name.as_bytes())
            Sig->>Hash: write(value.as_bytes())
        end
        Sig->>Hash: finish()
        Hash-->>Sig: Return u64 hash
        Sig-->>Agg: Return signature
        Agg->>Map: Group by signature
    end
    Map-->>Query: Aggregated results
Loading

2 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

@hengfeiyang
Copy link
Contributor

hengfeiyang commented Oct 24, 2025

This is my benchmark result:

https://github.com/hengfeiyang/hello_world_rust/tree/main/hash

# cargo bench

idgen/ahash-sum64       time:   [12.979 ns 13.190 ns 13.434 ns]
                        change: [+44.751% +47.294% +49.936%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 11 outliers among 100 measurements (11.00%)
  1 (1.00%) high mild
  10 (10.00%) high severe
idgen/defaultHash-sum64 time:   [25.574 ns 25.683 ns 25.798 ns]
                        change: [+47.032% +47.758% +48.492%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 10 outliers among 100 measurements (10.00%)
  10 (10.00%) high mild
idgen/cityhash-sum64    time:   [35.515 ns 35.599 ns 35.694 ns]
                        change: [+46.841% +47.495% +48.100%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 4 outliers among 100 measurements (4.00%)
  1 (1.00%) low severe
  2 (2.00%) high mild
  1 (1.00%) high severe
idgen/gxhash-sum64      time:   [11.130 ns 11.146 ns 11.162 ns]
                        change: [+32.826% +33.158% +33.505%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 7 outliers among 100 measurements (7.00%)
  2 (2.00%) low mild
  5 (5.00%) high mild

@testdino-playwright-reporter
Copy link

⚠️ Test Run Unstable


Author: haohuaijin | Branch: improve-metrics | Commit: de1401d

Testdino Test Results

Status Total Passed Failed Skipped Flaky Pass Rate Duration
All tests passed 365 343 0 19 3 94% 4m 39s

View Detailed Results

@testdino-playwright-reporter
Copy link

⚠️ Test Run Unstable


Author: haohuaijin | Branch: improve-metrics | Commit: de1401d

Testdino Test Results

Status Total Passed Failed Skipped Flaky Pass Rate Duration
All tests passed 365 343 0 19 3 94% 4m 39s

View Detailed Results

@testdino-playwright-reporter
Copy link

⚠️ Test Run Unstable


Author: haohuaijin | Branch: improve-metrics | Commit: 813dfa3

Testdino Test Results

Status Total Passed Failed Skipped Flaky Pass Rate Duration
All tests passed 365 341 0 19 5 93% 5m 1s

View Detailed Results

@testdino-playwright-reporter
Copy link

⚠️ Test Run Unstable


Author: hengfeiyang | Branch: improve-metrics | Commit: 655820b

Testdino Test Results

Status Total Passed Failed Skipped Flaky Pass Rate Duration
All tests passed 365 345 0 19 1 95% 4m 40s

View Detailed Results

@testdino-playwright-reporter
Copy link

⚠️ Test Run Unstable


Author: hengfeiyang | Branch: improve-metrics | Commit: 655820b

Testdino Test Results

Status Total Passed Failed Skipped Flaky Pass Rate Duration
All tests passed 365 342 0 19 4 94% 4m 38s

View Detailed Results

@testdino-playwright-reporter
Copy link

⚠️ Test Run Unstable


Author: haohuaijin | Branch: improve-metrics | Commit: beb7f8d

Testdino Test Results

Status Total Passed Failed Skipped Flaky Pass Rate Duration
All tests passed 365 342 0 19 4 94% 4m 42s

View Detailed Results

@haohuaijin haohuaijin merged commit e62c212 into main Oct 24, 2025
32 of 33 checks passed
@haohuaijin haohuaijin deleted the improve-metrics branch October 24, 2025 12:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants