use crate::storage::{ObjectInfo, StorageBackend};
use arrow::array::Array;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use tokio::sync::Semaphore;
use super::decode;
use super::parquet_writer;
pub(crate) const ORDER_VERSION: u32 = 1;
pub const SAMPLES_FORMAT_VERSION: u32 = 7;
pub(crate) const DEFAULT_SEGMENT_DURATION_SECS: i64 = 60;
fn order_key(source_key: &str) -> [u8; 16] {
let mut hasher = blake3::Hasher::new();
hasher.update(&ORDER_VERSION.to_le_bytes());
hasher.update(source_key.as_bytes());
let mut id = [0u8; 16];
id.copy_from_slice(&hasher.finalize().as_bytes()[..16]);
id
}
pub(crate) fn part_leaf_of(source_key: &str) -> String {
let hash = blake3::hash(source_key.as_bytes());
hash.to_hex()[..32].to_string()
}
fn versioned_root(output_prefix: &str, source_bucket: &str) -> String {
let p = output_prefix.trim_end_matches('/');
debug_assert!(!p.is_empty(), "output_prefix must not be empty");
let bucket = bucket_segment(source_bucket);
format!("{p}/v{SAMPLES_FORMAT_VERSION}/bucket={bucket}")
}
fn parse_source_bucket(source_key: &str) -> String {
if let Some(rest) = source_key.strip_prefix("s3://") {
rest.split_once('/')
.map(|(b, _)| b)
.unwrap_or(rest)
.to_string()
} else {
"local".to_string()
}
}
fn bucket_segment(source_bucket: &str) -> String {
source_bucket.replace('/', "_")
}
fn samples_part_key(output_prefix: &str, source_key: &str) -> String {
let (date, service, host) = parse_scope_fields(source_key);
format!(
"{root}/samples/service={service}/date={date}/host={host}/{leaf}.parquet",
root = versioned_root(output_prefix, &parse_source_bucket(source_key)),
leaf = part_leaf_of(source_key),
)
}
fn dict_part_key(output_prefix: &str, source_key: &str) -> String {
format!(
"{root}/dict/stacks/{leaf}.parquet",
root = versioned_root(output_prefix, &parse_source_bucket(source_key)),
leaf = part_leaf_of(source_key),
)
}
fn polls_part_key(output_prefix: &str, source_key: &str) -> String {
format!(
"{root}/polls/{leaf}.parquet",
root = versioned_root(output_prefix, &parse_source_bucket(source_key)),
leaf = part_leaf_of(source_key),
)
}
fn spans_part_key(output_prefix: &str, source_key: &str) -> String {
let (date, service, host) = parse_scope_fields(source_key);
format!(
"{root}/spans/service={service}/date={date}/host={host}/{leaf}.parquet",
root = versioned_root(output_prefix, &parse_source_bucket(source_key)),
leaf = part_leaf_of(source_key),
)
}
pub(crate) fn spans_part_key_pub(output_prefix: &str, source_key: &str) -> String {
spans_part_key(output_prefix, source_key)
}
fn samples_prefix(output_prefix: &str, source_bucket: &str) -> String {
format!("{}/samples/", versioned_root(output_prefix, source_bucket))
}
#[derive(Debug, Clone, Default)]
pub(crate) struct Scope {
pub start_ns: Option<i64>,
pub end_ns: Option<i64>,
pub service: Option<String>,
pub hosts: Vec<String>,
}
fn parse_scope_fields(key: &str) -> (String, String, String) {
let path = strip_s3(key);
let parts: Vec<&str> = path.split('/').collect();
if let Some(anchor) = parts.iter().position(|p| is_date(p)) {
let date = parts.get(anchor).copied().unwrap_or("").to_string();
let service = parts.get(anchor + 2).copied().unwrap_or("").to_string();
let host = parts.get(anchor + 3).copied().unwrap_or("").to_string();
(date, service, host)
} else {
let date = parts.first().copied().unwrap_or("").to_string();
let service = parts.get(2).copied().unwrap_or("").to_string();
let host = parts.get(3).copied().unwrap_or("").to_string();
(date, service, host)
}
}
pub(crate) fn host_of(key: &str) -> String {
parse_scope_fields(key).2
}
fn parse_epoch_secs(key: &str) -> Option<i64> {
let file = key.rsplit('/').next()?;
let stem = file.split('.').next()?; let ts = stem.split('-').next()?; ts.parse::<i64>().ok()
}
fn strip_s3(key: &str) -> &str {
if let Some(rest) = key.strip_prefix("s3://") {
rest.split_once('/').map_or(rest, |(_, p)| p)
} else {
key
}
}
fn is_date(s: &str) -> bool {
let b = s.as_bytes();
b.len() == 10
&& b[4] == b'-'
&& b[7] == b'-'
&& b[..4].iter().all(u8::is_ascii_digit)
&& b[5..7].iter().all(u8::is_ascii_digit)
&& b[8..].iter().all(u8::is_ascii_digit)
}
pub(crate) fn is_trace_segment(key: &str) -> bool {
(key.ends_with(".bin.gz") || key.ends_with(".bin"))
&& !key.contains("/samples/")
&& !key.contains("/dict/")
&& !key.contains("/flamegraph-data/")
}
fn matched_and_ordered(
objects: Vec<ObjectInfo>,
scope: &Scope,
segment_duration_secs: i64,
) -> Vec<ObjectInfo> {
let mut matched: Vec<ObjectInfo> = objects
.into_iter()
.filter(|o| is_trace_segment(&o.key))
.filter(|o| scope_matches(&o.key, scope, segment_duration_secs))
.collect();
matched.sort_by_key(|o| order_key(&o.key));
matched
}
fn scope_matches(key: &str, scope: &Scope, segment_duration_secs: i64) -> bool {
let (_date, service, host) = parse_scope_fields(key);
if let Some(want) = &scope.service
&& &service != want
{
return false;
}
if !scope.hosts.is_empty() && !scope.hosts.iter().any(|h| h == &host) {
return false;
}
if scope.start_ns.is_some() || scope.end_ns.is_some() {
let Some(epoch_secs) = parse_epoch_secs(key) else {
return true;
};
let file_start_ns = epoch_secs.saturating_mul(1_000_000_000);
let file_end_ns = (epoch_secs + segment_duration_secs).saturating_mul(1_000_000_000);
if let Some(start) = scope.start_ns
&& file_end_ns <= start
{
return false;
}
if let Some(end) = scope.end_ns
&& file_start_ns >= end
{
return false;
}
}
true
}
fn full_source_key(source_is_local: bool, source_bucket: &str, key: &str) -> String {
if source_is_local {
key.to_string()
} else {
format!("s3://{source_bucket}/{key}")
}
}
#[derive(Clone)]
pub(crate) struct FoldLimits {
pub fetch: Arc<Semaphore>,
pub cpu: Arc<Semaphore>,
pub inflight: Arc<Semaphore>,
}
impl FoldLimits {
pub(crate) fn new(fetch_permits: usize, cpu_permits: usize, inflight_permits: usize) -> Self {
Self {
fetch: Arc::new(Semaphore::new(fetch_permits.max(1))),
cpu: Arc::new(Semaphore::new(cpu_permits.max(1))),
inflight: Arc::new(Semaphore::new(inflight_permits.max(1))),
}
}
pub(crate) fn from_available_parallelism() -> Self {
let par = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4);
let cpu = par.min(MAX_DECODE_CONCURRENCY);
let inflight = cpu * 2;
let fetch = inflight;
Self::new(fetch, cpu, inflight)
}
}
const MAX_DECODE_CONCURRENCY: usize = 6;
impl Default for FoldLimits {
fn default() -> Self {
Self::from_available_parallelism()
}
}
struct EncodedParts {
samples_buf: Vec<u8>,
dict_buf: Vec<u8>,
polls_buf: Vec<u8>,
spans_buf: Vec<u8>,
cpu_stats: CpuStageStats,
}
#[derive(Default)]
struct CpuStageStats {
decompressed_bytes: u64,
gunzip: std::time::Duration,
parquet_encode: std::time::Duration,
decode: crate::ingest::decode::DecodeStats,
}
fn decode_and_encode(bytes: &[u8], full_key: &str) -> anyhow::Result<EncodedParts> {
use std::time::Instant;
let mut cpu_stats = CpuStageStats::default();
let t_gunzip = Instant::now();
let raw = maybe_gunzip(bytes);
cpu_stats.gunzip = t_gunzip.elapsed();
cpu_stats.decompressed_bytes = raw.len() as u64;
let ((samples, stacks, polls, spans), decode_stats) =
decode::decode_samples_with_stats(&raw, full_key)
.map_err(|e| anyhow::anyhow!("decode {full_key}: {e}"))?;
cpu_stats.decode = decode_stats;
let t_encode = Instant::now();
let metadata = HashMap::new();
let mut samples_buf = Vec::new();
parquet_writer::write_samples(&mut samples_buf, &samples, &metadata)?;
let stacks_map: HashMap<[u8; 16], Vec<String>> = stacks.into_iter().collect();
let mut dict_buf = Vec::new();
parquet_writer::write_stacks_dict(&mut dict_buf, &stacks_map)?;
let mut polls_buf = Vec::new();
parquet_writer::write_polls(&mut polls_buf, &polls)?;
let mut spans_buf = Vec::new();
parquet_writer::write_spans(&mut spans_buf, &spans)?;
cpu_stats.parquet_encode = t_encode.elapsed();
Ok(EncodedParts {
samples_buf,
dict_buf,
polls_buf,
spans_buf,
cpu_stats,
})
}
async fn write_parts(
output: &dyn StorageBackend,
output_bucket: &str,
output_prefix: &str,
full_key: &str,
encoded: EncodedParts,
) -> anyhow::Result<()> {
let part_key = samples_part_key(output_prefix, full_key);
let dict_key = dict_part_key(output_prefix, full_key);
let polls_key = polls_part_key(output_prefix, full_key);
let spans_key = spans_part_key(output_prefix, full_key);
let (dict_res, polls_res, spans_res) = tokio::join!(
output.put_object(output_bucket, &dict_key, encoded.dict_buf),
output.put_object(output_bucket, &polls_key, encoded.polls_buf),
output.put_object(output_bucket, &spans_key, encoded.spans_buf),
);
dict_res.map_err(|e| anyhow::anyhow!("write dict {dict_key}: {e}"))?;
polls_res.map_err(|e| anyhow::anyhow!("write polls {polls_key}: {e}"))?;
spans_res.map_err(|e| anyhow::anyhow!("write spans {spans_key}: {e}"))?;
output
.put_object(output_bucket, &part_key, encoded.samples_buf)
.await
.map_err(|e| anyhow::anyhow!("write samples {part_key}: {e}"))?;
Ok(())
}
pub(crate) async fn fold_one(
agg: &AggContext,
raw_key: &str,
limits: &FoldLimits,
) -> anyhow::Result<()> {
use std::time::Instant;
let mut metric = crate::server::metrics::FoldFileMetricsBuilder::new();
let t_total = Instant::now();
let emit = |mut metric: crate::server::metrics::FoldFileMetricsBuilder,
total: std::time::Duration,
failed: bool| {
metric.total(total).failed(failed);
metric.emit();
};
let _inflight = limits
.inflight
.acquire()
.await
.expect("inflight semaphore is never closed");
let t_fetch = Instant::now();
let bytes = {
let _permit = limits
.fetch
.acquire()
.await
.expect("fetch semaphore is never closed");
match agg.source.get_object(&agg.source_bucket, raw_key).await {
Ok(b) => b,
Err(e) => {
emit(metric, t_total.elapsed(), true);
return Err(anyhow::anyhow!("fetch {raw_key}: {e}"));
}
}
};
metric.fetch(t_fetch.elapsed());
metric.source_bytes(bytes.len() as u64);
let full_key = full_source_key(agg.source_is_local, &agg.source_bucket, raw_key);
let decode_key = full_key.clone();
let encoded = {
let _permit = limits
.cpu
.acquire()
.await
.expect("cpu semaphore is never closed");
match tokio::task::spawn_blocking(move || decode_and_encode(&bytes, &decode_key)).await {
Ok(Ok(encoded)) => encoded,
Ok(Err(e)) => {
emit(metric, t_total.elapsed(), true);
return Err(e);
}
Err(e) => {
emit(metric, t_total.elapsed(), true);
return Err(anyhow::anyhow!("decode task panicked: {e}"));
}
}
};
let cpu = &encoded.cpu_stats;
metric
.gunzip(cpu.gunzip)
.decompressed_bytes(cpu.decompressed_bytes)
.decode_phases(&cpu.decode)
.parquet_encode(cpu.parquet_encode);
let t_write = Instant::now();
let write_result = write_parts(
&*agg.output,
&agg.output_bucket,
&agg.output_prefix,
&full_key,
encoded,
)
.await;
metric.write_parts(t_write.elapsed());
let failed = write_result.is_err();
emit(metric, t_total.elapsed(), failed);
write_result
}
pub(crate) async fn list_folded_leaves(
output: &dyn StorageBackend,
output_bucket: &str,
output_prefix: &str,
source_bucket: &str,
service: Option<&str>,
) -> HashSet<String> {
let prefix = folded_set_prefix(output_prefix, source_bucket, service);
let objects = output
.list_objects_all(output_bucket, &prefix)
.await
.unwrap_or_else(|e| {
tracing::warn!(
bucket = %output_bucket,
prefix = %prefix,
error = %e,
"list_folded_leaves: failed to list folded set; treating as empty \
(already-folded files may be re-folded this round)"
);
Vec::new()
});
objects
.iter()
.filter_map(|o| {
let name = o.key.rsplit('/').next()?;
name.strip_suffix(".parquet").map(|s| s.to_string())
})
.collect()
}
fn folded_set_prefix(output_prefix: &str, source_bucket: &str, service: Option<&str>) -> String {
let prefix = samples_prefix(output_prefix, source_bucket);
match service {
Some(service) => format!("{prefix}service={service}/"),
None => prefix,
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub(crate) struct Coverage {
pub files_matched: usize,
pub files_folded: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub folded_set_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_folded_set_id: Option<String>,
pub fold_work_cap: usize,
pub samples_folded: usize,
pub total_bytes: u64,
pub hosts_matched: usize,
pub hosts_folded: usize,
pub fold_errors: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub fold_error_sample: Option<String>,
}
const SOURCE_CPU_PROFILE: u8 = 0;
const SOURCE_SCHED_EVENT: u8 = 1;
#[derive(Clone)]
pub(crate) struct FacetDef {
pub name: &'static str,
pub label: &'static str,
pub kind: FacetKind,
pub default_filter: &'static str,
}
#[derive(Clone)]
pub(crate) enum FacetKind {
MappedU8 {
column: &'static str,
map: &'static [(u8, &'static str)],
absent_value: &'static str,
},
NullDerived {
column: &'static str,
present_label: &'static str,
absent_label: &'static str,
missing_column_label: &'static str,
},
DirectString { column: &'static str },
}
pub(crate) const FACETS: &[FacetDef] = &[
FacetDef {
name: "source",
label: "Source",
kind: FacetKind::MappedU8 {
column: "source",
map: &[(SOURCE_CPU_PROFILE, "cpu"), (SOURCE_SCHED_EVENT, "sched")],
absent_value: "cpu",
},
default_filter: "cpu",
},
FacetDef {
name: "thread_class",
label: "Thread",
kind: FacetKind::NullDerived {
column: "worker_id",
present_label: "worker",
absent_label: "off-worker",
missing_column_label: "worker",
},
default_filter: "",
},
FacetDef {
name: "host",
label: "Host",
kind: FacetKind::DirectString { column: "host" },
default_filter: "",
},
FacetDef {
name: "spawn_location",
label: "Task",
kind: FacetKind::DirectString {
column: "spawn_location",
},
default_filter: "",
},
];
#[derive(Debug, Clone, serde::Serialize)]
pub(crate) struct FacetResult {
pub name: &'static str,
pub label: &'static str,
pub values: Vec<String>,
}
pub(crate) type FacetFilters = HashMap<&'static str, String>;
#[derive(Clone)]
struct FacetAccum {
sets: Vec<HashSet<String>>,
matched_hosts: HashSet<String>,
}
impl FacetAccum {
fn new() -> Self {
Self {
sets: FACETS.iter().map(|_| HashSet::new()).collect(),
matched_hosts: HashSet::new(),
}
}
fn results(&self) -> Vec<FacetResult> {
FACETS
.iter()
.zip(&self.sets)
.map(|(def, set)| {
let mut values: Vec<String> = set.iter().cloned().collect();
values.sort();
FacetResult {
name: def.name,
label: def.label,
values,
}
})
.collect()
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct SampleFilter {
pub start_ns: Option<i64>,
pub end_ns: Option<i64>,
pub min_poll_ns: Option<i64>,
pub max_poll_ns: Option<i64>,
pub facets: FacetFilters,
pub span_type_uid: Option<[u8; 16]>,
pub min_span_ns: Option<i64>,
pub max_span_ns: Option<i64>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub(crate) struct PollDurationBucket {
pub lo_ns: i64,
pub hi_ns: i64,
pub samples: u64,
}
const POLL_HIST_SUBDIV: u32 = 4;
fn poll_bucket(ns: i64) -> Option<u32> {
if ns <= 0 {
return None;
}
let log2 = (ns as f64).log2();
Some((log2 * POLL_HIST_SUBDIV as f64).floor() as u32)
}
fn bucket_edge_ns(k: u32) -> i64 {
2f64.powf(k as f64 / POLL_HIST_SUBDIV as f64).round() as i64
}
type PollHist = HashMap<u32, u64>;
fn poll_hist_bars(hist: &PollHist) -> Vec<PollDurationBucket> {
let mut buckets: Vec<u32> = hist.keys().copied().collect();
buckets.sort_unstable();
buckets
.into_iter()
.map(|k| PollDurationBucket {
lo_ns: bucket_edge_ns(k),
hi_ns: bucket_edge_ns(k + 1),
samples: hist[&k],
})
.collect()
}
pub(crate) struct AggSnapshot<'a> {
pub stack_counts: Vec<(Vec<u8>, u64)>,
pub stacks_dict: &'a HashMap<Vec<u8>, Vec<String>>,
pub total_samples: usize,
pub hosts: usize,
pub min_ts: Option<i64>,
pub max_ts: Option<i64>,
pub facets: Vec<FacetResult>,
pub poll_duration_histogram: Vec<PollDurationBucket>,
}
pub(crate) struct FlamegraphAccum {
filter: SampleFilter,
counts: HashMap<[u8; 16], u64>,
dict: HashMap<Vec<u8>, Vec<String>>,
facets: FacetAccum,
total_samples: usize,
min_ts: Option<i64>,
max_ts: Option<i64>,
poll_hist: PollHist,
}
impl FlamegraphAccum {
pub(crate) fn new(filter: SampleFilter) -> Self {
Self {
filter,
counts: HashMap::new(),
dict: HashMap::new(),
facets: FacetAccum::new(),
total_samples: 0,
min_ts: None,
max_ts: None,
poll_hist: HashMap::new(),
}
}
pub(crate) fn merge(&mut self, samples: Vec<u8>, dict: Option<Vec<u8>>) -> anyhow::Result<()> {
let mut staged_counts = self.counts.clone();
let mut staged_dict = self.dict.clone();
let mut staged_facets = self.facets.clone();
let mut staged_total = self.total_samples;
let mut staged_min_ts = self.min_ts;
let mut staged_max_ts = self.max_ts;
let mut staged_poll_hist = self.poll_hist.clone();
self.read_samples_part_into(
samples,
&mut staged_counts,
&mut staged_dict,
&mut staged_facets,
&mut staged_total,
&mut staged_min_ts,
&mut staged_max_ts,
&mut staged_poll_hist,
)?;
if let Some(dict) = dict {
read_dict_part(dict, &mut staged_dict)?;
}
self.counts = staged_counts;
self.dict = staged_dict;
self.facets = staged_facets;
self.total_samples = staged_total;
self.min_ts = staged_min_ts;
self.max_ts = staged_max_ts;
self.poll_hist = staged_poll_hist;
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn read_samples_part_into(
&self,
data: Vec<u8>,
counts: &mut HashMap<[u8; 16], u64>,
_dict: &mut HashMap<Vec<u8>, Vec<String>>,
facets: &mut FacetAccum,
total_samples: &mut usize,
min_ts: &mut Option<i64>,
max_ts: &mut Option<i64>,
poll_hist: &mut PollHist,
) -> anyhow::Result<()> {
let reader = ::parquet::arrow::arrow_reader::ParquetRecordBatchReader::try_new(
bytes::Bytes::from(data),
4096,
)?;
for batch in reader {
let batch = batch?;
let stack_col = batch.column_by_name("stack_id").and_then(|c| {
c.as_any()
.downcast_ref::<arrow::array::FixedSizeBinaryArray>()
});
let Some(stack_arr) = stack_col else { continue };
let ts_arr = batch
.column_by_name("timestamp_ns")
.and_then(|c| c.as_any().downcast_ref::<arrow::array::Int64Array>());
let poll_arr = batch
.column_by_name("poll_duration_ns")
.and_then(|c| c.as_any().downcast_ref::<arrow::array::Int64Array>());
let poll_band = self.filter.min_poll_ns.is_some() || self.filter.max_poll_ns.is_some();
let facet_cols: Vec<ResolvedFacetCol> = FACETS
.iter()
.map(|def| resolve_facet_col(&batch, def))
.collect();
for i in 0..batch.num_rows() {
if let Some(ts) = ts_arr {
let v = ts.value(i);
if self.filter.start_ns.is_some_and(|start| v < start) {
continue;
}
if self.filter.end_ns.is_some_and(|end| v >= end) {
continue;
}
}
let mut row_values: Vec<Option<String>> = Vec::with_capacity(FACETS.len());
for (fi, col) in facet_cols.iter().enumerate() {
let val = extract_facet_value(col, i);
if let Some(ref v) = val {
facets.sets[fi].insert(v.clone());
}
row_values.push(val);
}
let mut passes = true;
for (fi, def) in FACETS.iter().enumerate() {
if let Some(wanted) = self.filter.facets.get(def.name) {
if wanted.is_empty() {
continue;
}
match &row_values[fi] {
Some(v) if v == wanted => {}
_ => {
passes = false;
break;
}
}
}
}
if !passes {
continue;
}
let poll_dur = poll_arr.and_then(|a| (!a.is_null(i)).then(|| a.value(i)));
if let Some(k) = poll_dur.and_then(poll_bucket) {
*poll_hist.entry(k).or_insert(0) += 1;
}
if poll_band {
match poll_dur {
Some(d) => {
if self.filter.min_poll_ns.is_some_and(|min| d < min) {
continue;
}
if self.filter.max_poll_ns.is_some_and(|max| d > max) {
continue;
}
}
None => continue,
}
}
if let Some(ref wanted_uid) = self.filter.span_type_uid
&& !span_filter_matches(
&batch,
i,
wanted_uid,
self.filter.min_span_ns,
self.filter.max_span_ns,
)
{
continue;
}
let mut id = [0u8; 16];
id.copy_from_slice(stack_arr.value(i));
*counts.entry(id).or_insert(0) += 1;
*total_samples += 1;
if let Some(ts) = ts_arr {
let v = ts.value(i);
*min_ts = Some(min_ts.map_or(v, |m| m.min(v)));
*max_ts = Some(max_ts.map_or(v, |m| m.max(v)));
}
if let Some(ref h) = row_values[host_facet_index()] {
facets.matched_hosts.insert(h.clone());
}
}
}
Ok(())
}
pub(crate) fn snapshot(&self) -> AggSnapshot<'_> {
let stack_counts: Vec<(Vec<u8>, u64)> =
self.counts.iter().map(|(k, v)| (k.to_vec(), *v)).collect();
AggSnapshot {
stack_counts,
stacks_dict: &self.dict,
total_samples: self.total_samples,
hosts: self.facets.matched_hosts.len().max(1),
min_ts: self.min_ts,
max_ts: self.max_ts,
facets: self.facets.results(),
poll_duration_histogram: poll_hist_bars(&self.poll_hist),
}
}
}
const SAMPLES_READ_CONCURRENCY: usize = 24;
pub(crate) async fn fetch_sample_parts(
output: &dyn StorageBackend,
bucket: &str,
output_prefix: &str,
source_key: &str,
) -> Option<(Vec<u8>, Option<Vec<u8>>)> {
let part_key = samples_part_key(output_prefix, source_key);
let dict_key = dict_part_key(output_prefix, source_key);
let (samples, dict_data) = tokio::join!(
output.get_object(bucket, &part_key),
output.get_object(bucket, &dict_key),
);
Some((samples.ok()?, dict_data.ok()))
}
pub(crate) async fn fetch_folded_sample_parts(
output: &dyn StorageBackend,
bucket: &str,
output_prefix: &str,
source_keys: &[String],
) -> Vec<(String, Result<(Vec<u8>, Option<Vec<u8>>), String>)> {
use futures::stream::StreamExt;
futures::stream::iter(source_keys.iter().cloned())
.map(|sk| async move {
let leaf = part_leaf_of(&sk);
match fetch_sample_parts(output, bucket, output_prefix, &sk).await {
Some(parts) => (leaf, Ok(parts)),
None => (
leaf,
Err(format!(
"{}: sample parts GET failed",
sk.rsplit('/').next().unwrap_or(&sk)
)),
),
}
})
.buffer_unordered(SAMPLES_READ_CONCURRENCY)
.collect()
.await
}
const POLLS_READ_CONCURRENCY: usize = 24;
pub(crate) async fn read_polls_parts(
output: &dyn StorageBackend,
bucket: &str,
output_prefix: &str,
source_keys: &[(String, String)],
folded: &HashSet<String>,
) -> Vec<(String, Vec<u8>)> {
use futures::stream::StreamExt;
let fetches: Vec<(String, String)> = source_keys
.iter()
.filter(|(_, full)| folded.contains(&part_leaf_of(full)))
.map(|(raw, full)| (raw.clone(), polls_part_key(output_prefix, full)))
.collect();
futures::stream::iter(fetches)
.map(|(raw_key, polls_key)| async move {
output
.get_object(bucket, &polls_key)
.await
.ok()
.map(|data| (raw_key, data))
})
.buffer_unordered(POLLS_READ_CONCURRENCY)
.filter_map(|x| async { x })
.collect()
.await
}
pub(crate) async fn fetch_polls_part(
output: &dyn StorageBackend,
bucket: &str,
output_prefix: &str,
full_key: &str,
) -> Option<Vec<u8>> {
let polls_key = polls_part_key(output_prefix, full_key);
output.get_object(bucket, &polls_key).await.ok()
}
fn host_facet_index() -> usize {
FACETS
.iter()
.position(|f| f.name == "host")
.expect("FACETS must define a \"host\" facet")
}
enum ResolvedFacetCol<'a> {
MappedU8 {
arr: Option<&'a arrow::array::UInt8Array>,
map: &'static [(u8, &'static str)],
absent_value: &'static str,
},
NullDerived {
arr: Option<&'a arrow::array::UInt32Array>,
present_label: &'static str,
absent_label: &'static str,
missing_column_label: &'static str,
},
DirectString {
arr: Option<&'a arrow::array::StringArray>,
},
}
fn resolve_facet_col<'a>(
batch: &'a arrow::record_batch::RecordBatch,
def: &FacetDef,
) -> ResolvedFacetCol<'a> {
match &def.kind {
FacetKind::MappedU8 {
column,
map,
absent_value,
} => {
let arr = batch
.column_by_name(column)
.and_then(|c| c.as_any().downcast_ref::<arrow::array::UInt8Array>());
ResolvedFacetCol::MappedU8 {
arr,
map,
absent_value,
}
}
FacetKind::NullDerived {
column,
present_label,
absent_label,
missing_column_label,
} => {
let arr = batch
.column_by_name(column)
.and_then(|c| c.as_any().downcast_ref::<arrow::array::UInt32Array>());
ResolvedFacetCol::NullDerived {
arr,
present_label,
absent_label,
missing_column_label,
}
}
FacetKind::DirectString { column } => {
let arr = batch
.column_by_name(column)
.and_then(|c| c.as_any().downcast_ref::<arrow::array::StringArray>());
ResolvedFacetCol::DirectString { arr }
}
}
}
fn extract_facet_value(col: &ResolvedFacetCol, i: usize) -> Option<String> {
match col {
ResolvedFacetCol::MappedU8 {
arr,
map,
absent_value,
} => {
let label = match arr {
Some(a) => {
let v = a.value(i);
map.iter().find(|(k, _)| *k == v).map_or("", |(_, l)| l)
}
None => absent_value,
};
if label.is_empty() {
None
} else {
Some(label.to_string())
}
}
ResolvedFacetCol::NullDerived {
arr,
present_label,
absent_label,
missing_column_label,
} => {
let label = match arr {
Some(a) => {
if a.is_null(i) {
absent_label
} else {
present_label
}
}
None => missing_column_label,
};
Some(label.to_string())
}
ResolvedFacetCol::DirectString { arr } => match arr {
Some(a) if !a.is_null(i) => Some(a.value(i).to_string()),
_ => None,
},
}
}
fn read_dict_part(data: Vec<u8>, dict: &mut HashMap<Vec<u8>, Vec<String>>) -> anyhow::Result<()> {
let reader = ::parquet::arrow::arrow_reader::ParquetRecordBatchReader::try_new(
bytes::Bytes::from(data),
4096,
)?;
for batch in reader {
let batch = batch?;
let stack_arr = batch.column_by_name("stack_id").and_then(|c| {
c.as_any()
.downcast_ref::<arrow::array::FixedSizeBinaryArray>()
});
let frames_arr = batch
.column_by_name("frames")
.and_then(|c| c.as_any().downcast_ref::<arrow::array::ListArray>());
let (Some(stack_arr), Some(frames_arr)) = (stack_arr, frames_arr) else {
continue;
};
for i in 0..batch.num_rows() {
let id = stack_arr.value(i).to_vec();
if dict.contains_key(&id) {
continue;
}
let frame_list = frames_arr.value(i);
if let Some(str_arr) = frame_list
.as_any()
.downcast_ref::<arrow::array::StringArray>()
{
let frames: Vec<String> = (0..str_arr.len())
.map(|j| str_arr.value(j).to_string())
.collect();
dict.insert(id, frames);
}
}
}
Ok(())
}
fn maybe_gunzip(data: &[u8]) -> Vec<u8> {
if data.len() >= 2 && data[0] == 0x1f && data[1] == 0x8b {
use std::io::Read;
let mut decoder = flate2::read::GzDecoder::new(data);
let mut out = Vec::new();
match decoder.read_to_end(&mut out) {
Ok(_) => out,
Err(_) => data.to_vec(),
}
} else {
data.to_vec()
}
}
pub(crate) fn ordered_full_keys_with_size(
objects: Vec<ObjectInfo>,
scope: &Scope,
segment_duration_secs: i64,
source_is_local: bool,
source_bucket: &str,
) -> (Vec<(String, String)>, u64) {
let matched = matched_and_ordered(objects, scope, segment_duration_secs);
let total_bytes: u64 = matched.iter().map(|o| o.size.max(0) as u64).sum();
let keys = matched
.into_iter()
.map(|o| {
let full = full_source_key(source_is_local, source_bucket, &o.key);
(o.key, full)
})
.collect();
(keys, total_bytes)
}
pub(crate) fn span_filter_matches(
batch: &arrow::record_batch::RecordBatch,
row: usize,
wanted_uid: &[u8; 16],
min_span_ns: Option<i64>,
max_span_ns: Option<i64>,
) -> bool {
use arrow::array::{Array, AsArray};
let Some(col) = batch.column_by_name("enclosing_spans") else {
return false;
};
let list_arr = match col.as_list_opt::<i32>() {
Some(a) => a,
None => return false, };
if list_arr.is_null(row) {
return false;
}
let offsets = list_arr.offsets();
let start = offsets[row] as usize;
let end = offsets[row + 1] as usize;
if start == end {
return false; }
let values = list_arr.values();
let struct_arr = match values.as_struct_opt() {
Some(a) => a,
None => return false, };
let type_uid_col = struct_arr.column_by_name("span_type_uid").and_then(|c| {
c.as_any()
.downcast_ref::<arrow::array::FixedSizeBinaryArray>()
});
let elapsed_col = struct_arr
.column_by_name("elapsed_ns")
.and_then(|c| c.as_any().downcast_ref::<arrow::array::Int64Array>());
let Some(type_uid_arr) = type_uid_col else {
return false; };
let has_bounds = min_span_ns.is_some() || max_span_ns.is_some();
if has_bounds && elapsed_col.is_none() {
return false; }
for idx in start..end {
if struct_arr.is_null(idx) {
continue;
}
if type_uid_arr.is_null(idx) {
continue; }
let uid = type_uid_arr.value(idx);
if uid == wanted_uid.as_slice() {
if has_bounds {
let elapsed_arr = elapsed_col.unwrap(); if elapsed_arr.is_null(idx) {
continue;
}
let elapsed = elapsed_arr.value(idx);
if min_span_ns.is_some_and(|min| elapsed < min) {
continue;
}
if max_span_ns.is_some_and(|max| elapsed > max) {
continue;
}
}
return true;
}
}
false
}
#[derive(Clone)]
pub struct AggContext {
pub source: Arc<dyn StorageBackend>,
pub output: Arc<dyn StorageBackend>,
pub source_bucket: String,
pub source_is_local: bool,
pub output_bucket: String,
pub output_prefix: String,
pub source_prefixes: Vec<String>,
pub segment_duration_secs: i64,
}
#[cfg(test)]
mod tests {
use std::future::Future;
use std::pin::Pin;
use super::*;
use crate::storage::{BucketInfo, StorageError};
#[derive(Default)]
struct ConcurrencyProbe {
in_flight: std::sync::atomic::AtomicUsize,
peak: std::sync::atomic::AtomicUsize,
}
impl StorageBackend for ConcurrencyProbe {
fn list_buckets(
&self,
) -> Pin<Box<dyn Future<Output = Result<Vec<BucketInfo>, StorageError>> + Send + '_>>
{
Box::pin(async { Ok(vec![]) })
}
fn list_objects(
&self,
_bucket: &str,
_prefix: &str,
_cap: usize,
) -> Pin<Box<dyn Future<Output = Result<crate::storage::ListPage, StorageError>> + Send + '_>>
{
Box::pin(async { Err(StorageError::NotFound("unused".into())) })
}
fn list_objects_all(
&self,
_bucket: &str,
_prefix: &str,
) -> Pin<Box<dyn Future<Output = Result<Vec<ObjectInfo>, StorageError>> + Send + '_>>
{
Box::pin(async { Ok(vec![]) })
}
fn list_prefixes(
&self,
_bucket: &str,
_prefix: &str,
) -> Pin<Box<dyn Future<Output = Result<Vec<String>, StorageError>> + Send + '_>> {
Box::pin(async { Ok(vec![]) })
}
fn get_object(
&self,
_bucket: &str,
_key: &str,
) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, StorageError>> + Send + '_>> {
use std::sync::atomic::Ordering;
Box::pin(async move {
let cur = self.in_flight.fetch_add(1, Ordering::SeqCst) + 1;
self.peak.fetch_max(cur, Ordering::SeqCst);
for _ in 0..50 {
tokio::task::yield_now().await;
}
self.in_flight.fetch_sub(1, Ordering::SeqCst);
Ok(vec![0u8; 8])
})
}
fn put_object(
&self,
_bucket: &str,
_key: &str,
_data: Vec<u8>,
) -> Pin<Box<dyn Future<Output = Result<(), StorageError>> + Send + '_>> {
Box::pin(async { Ok(()) })
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn inflight_cap_bounds_concurrent_fetches() {
use std::sync::atomic::Ordering;
let probe = Arc::new(ConcurrencyProbe::default());
let agg = AggContext {
source: probe.clone() as Arc<dyn StorageBackend>,
output: probe.clone() as Arc<dyn StorageBackend>,
source_bucket: "src".to_string(),
source_is_local: false,
output_bucket: "out".to_string(),
output_prefix: "flamegraph-data".to_string(),
source_prefixes: vec![],
segment_duration_secs: 60,
};
const INFLIGHT: usize = 3;
let limits = FoldLimits::new(32, 8, INFLIGHT);
let mut tasks = tokio::task::JoinSet::new();
for i in 0..64 {
let agg = agg.clone();
let limits = limits.clone();
tasks.spawn(async move {
let _ = fold_one(&agg, &format!("raw-{i}"), &limits).await;
});
}
while tasks.join_next().await.is_some() {}
let peak = probe.peak.load(Ordering::SeqCst);
assert!(peak > 0, "probe should have observed fetches");
assert!(
peak <= INFLIGHT,
"peak concurrent fetches {peak} exceeded the in-flight cap {INFLIGHT}"
);
}
#[test]
fn default_fold_limits_do_not_scale_decode_with_cores() {
let limits = FoldLimits::from_available_parallelism();
assert!(
limits.cpu.available_permits() <= MAX_DECODE_CONCURRENCY,
"decode concurrency {} must stay within the absolute cap {MAX_DECODE_CONCURRENCY}",
limits.cpu.available_permits()
);
assert!(
limits.inflight.available_permits() <= MAX_DECODE_CONCURRENCY * 2,
"inflight {} should stay a small multiple of the decode cap",
limits.inflight.available_permits()
);
assert!(
limits.fetch.available_permits() <= limits.inflight.available_permits(),
"fetch must not exceed inflight (a fetch needs an inflight permit)"
);
}
#[test]
fn sample_filter_source_and_thread() {
use std::collections::HashMap;
let def = SampleFilter {
facets: HashMap::from([
("source", "cpu".to_string()),
("thread_class", String::new()),
]),
..Default::default()
};
assert_eq!(def.facets.get("source"), Some(&"cpu".to_string()));
assert_eq!(def.facets.get("thread_class"), Some(&String::new()));
let sched = SampleFilter {
facets: HashMap::from([("source", "sched".to_string())]),
..Default::default()
};
assert_eq!(sched.facets.get("source"), Some(&"sched".to_string()));
let names: Vec<&str> = FACETS.iter().map(|f| f.name).collect();
assert!(names.contains(&"source"));
assert!(names.contains(&"thread_class"));
assert!(names.contains(&"host"));
assert!(names.contains(&"spawn_location"));
}
fn samples_parquet(rows: &[(u8, Option<u64>)]) -> Vec<u8> {
use crate::ingest::decode::ResolvedSample;
use crate::ingest::parquet_writer::write_samples;
let samples: Vec<ResolvedSample> = rows
.iter()
.enumerate()
.map(|(i, (stack, poll))| ResolvedSample {
timestamp_ns: 1000 + i as u64,
stack_id: [*stack; 16],
worker_id: Some(1),
source: SOURCE_CPU_PROFILE,
source_key: "2026-06-19/1450/shale/myhost/boot-1/123-0.bin.gz".to_string(),
host: "myhost".to_string(),
service: "shale".to_string(),
date: "2026-06-19".to_string(),
poll_duration_ns: *poll,
spawn_location: Some("src/main.rs:42".to_string()),
enclosing_spans: Vec::new(),
})
.collect();
let mut buf = Vec::new();
write_samples(&mut buf, &samples, &HashMap::new()).unwrap();
buf
}
fn samples_kept(parquet: Vec<u8>, min_poll_ns: Option<i64>, max_poll_ns: Option<i64>) -> usize {
let filter = SampleFilter {
min_poll_ns,
max_poll_ns,
facets: HashMap::from([("source", "cpu".to_string())]),
..Default::default()
};
let mut accum = FlamegraphAccum::new(filter);
accum.merge(parquet, None).unwrap();
accum.snapshot().total_samples
}
#[test]
fn poll_band_filters_samples_by_duration() {
let rows = [
(1u8, Some(500_000)),
(2u8, Some(5_000_000)),
(3u8, Some(50_000_000)),
(4u8, None),
];
let mk = || samples_parquet(&rows);
assert_eq!(samples_kept(mk(), None, None), 4);
assert_eq!(samples_kept(mk(), Some(5_000_000), None), 2);
assert_eq!(samples_kept(mk(), None, Some(1_000_000)), 1);
assert_eq!(samples_kept(mk(), Some(1_000_000), Some(10_000_000)), 1);
assert_eq!(samples_kept(mk(), Some(100_000_000), None), 0);
}
#[test]
fn poll_bucket_is_monotonic_and_subdivides_octaves() {
assert_eq!(poll_bucket(0), None, "0 has no bucket");
assert_eq!(poll_bucket(-5), None, "negative has no bucket");
let b1 = poll_bucket(1_000_000).unwrap();
let b2 = poll_bucket(1_300_000).unwrap();
assert!(
b2 > b1,
"1.0ms and 1.3ms fall in different sub-octave buckets"
);
assert!(b2 - b1 <= POLL_HIST_SUBDIV, "…but within one octave");
assert!(poll_bucket(50_000_000).unwrap() > poll_bucket(500_000).unwrap());
let k = poll_bucket(500_000).unwrap();
assert!(bucket_edge_ns(k) <= 500_000 && 500_000 < bucket_edge_ns(k + 1));
}
#[test]
fn poll_histogram_is_sample_weighted_and_pre_band() {
let rows = [
(1u8, Some(500_000)),
(2u8, Some(500_001)),
(3u8, Some(50_000_000)),
(4u8, None),
];
let hist = |min_poll_ns, max_poll_ns| {
let filter = SampleFilter {
min_poll_ns,
max_poll_ns,
facets: HashMap::from([("source", "cpu".to_string())]),
..Default::default()
};
let mut accum = FlamegraphAccum::new(filter);
accum.merge(samples_parquet(&rows), None).unwrap();
accum.snapshot().poll_duration_histogram
};
let bars = hist(None, None);
assert_eq!(bars.len(), 2, "two occupied buckets");
assert_eq!(bars[0].samples, 2, "0.5ms bucket is sample-weighted (2)");
assert_eq!(bars[1].samples, 1, "50ms bucket holds the one slow sample");
assert!(
bars[0].lo_ns <= 500_000 && 500_000 < bars[0].hi_ns,
"fast bar brackets 0.5ms"
);
assert!(
bars[1].lo_ns <= 50_000_000 && 50_000_000 < bars[1].hi_ns,
"slow bar brackets 50ms"
);
assert!(
bars[0].hi_ns <= bars[1].lo_ns,
"bars are disjoint and ascending"
);
assert_eq!(
hist(Some(10_000_000), None).len(),
2,
"band does not shrink the histogram"
);
}
#[test]
fn order_key_is_deterministic_and_versioned() {
let a = order_key("2026-06-19/1300/shale/host-a/boot/1-0.bin.gz");
let b = order_key("2026-06-19/1300/shale/host-a/boot/1-0.bin.gz");
assert_eq!(a, b, "same key → same order");
let c = order_key("2026-06-19/1300/shale/host-b/boot/1-0.bin.gz");
assert_ne!(a, c, "different key → different order (almost surely)");
}
#[test]
fn parse_scope_fields_handles_prefix() {
let (d, s, h) = parse_scope_fields(
"traces/2026-04-09/1910/checkout-api/us-east-1/abcd/1744224000-3.bin.gz",
);
assert_eq!(d, "2026-04-09");
assert_eq!(s, "checkout-api");
assert_eq!(h, "us-east-1");
}
#[test]
fn parse_scope_fields_handles_s3_uri() {
let (d, s, h) =
parse_scope_fields("s3://bkt/2026-06-19/1300/shale/host-a/boot-1/1-0.bin.gz");
assert_eq!(d, "2026-06-19");
assert_eq!(s, "shale");
assert_eq!(h, "host-a");
}
#[test]
fn parse_epoch_from_filename() {
assert_eq!(
parse_epoch_secs("2026-04-09/1910/svc/host/boot/1744224000-3.bin.gz"),
Some(1744224000)
);
}
#[test]
fn part_keys_are_partitioned_with_hash_leaf() {
let sk = "s3://bkt/2026-06-19/1300/shale/host-a/boot-1/1-0.bin.gz";
let pk = samples_part_key("flamegraph-data", sk);
let expected_prefix = format!(
"flamegraph-data/v{SAMPLES_FORMAT_VERSION}/bucket=bkt/samples/service=shale/date=2026-06-19/host=host-a/"
);
assert!(pk.starts_with(&expected_prefix));
assert!(pk.ends_with(".parquet"));
assert_eq!(pk, samples_part_key("flamegraph-data", sk));
}
#[test]
fn folded_set_prefix_is_pruned_to_service() {
let prefix = folded_set_prefix("flamegraph-data", "bkt", Some("shale"));
assert_eq!(
prefix,
format!("flamegraph-data/v{SAMPLES_FORMAT_VERSION}/bucket=bkt/samples/service=shale/")
);
}
#[test]
fn parse_source_bucket_from_key() {
assert_eq!(
parse_source_bucket("s3://my-bucket/2026-06-19/1300/svc/host/boot/1-0.bin.gz"),
"my-bucket"
);
assert_eq!(
parse_source_bucket("2026-06-19/1300/svc/host/boot/1-0.bin.gz"),
"local"
);
}
#[test]
fn output_namespaced_by_source_bucket_isolates_buckets() {
let a = samples_part_key("out", "s3://bucket-a/2026-06-19/1300/svc/h/b/1-0.bin.gz");
let b = samples_part_key("out", "s3://bucket-b/2026-06-19/1300/svc/h/b/1-0.bin.gz");
assert!(a.contains("/bucket=bucket-a/"));
assert!(b.contains("/bucket=bucket-b/"));
assert_ne!(a, b);
assert_ne!(
samples_prefix("out", "bucket-a"),
samples_prefix("out", "bucket-b")
);
}
#[test]
fn scope_filters_by_service_host_and_time() {
let scope = Scope {
start_ns: Some(1_744_224_000_000_000_000),
end_ns: Some(1_744_224_100_000_000_000),
service: Some("shale".to_string()),
hosts: vec!["host-a".to_string()],
};
assert!(scope_matches(
"2026-04-09/1910/shale/host-a/boot/1744224050-0.bin.gz",
&scope,
60
));
assert!(!scope_matches(
"2026-04-09/1910/other/host-a/boot/1744224050-0.bin.gz",
&scope,
60
));
assert!(!scope_matches(
"2026-04-09/1910/shale/host-z/boot/1744224050-0.bin.gz",
&scope,
60
));
assert!(!scope_matches(
"2026-04-09/1910/shale/host-a/boot/1744999999-0.bin.gz",
&scope,
60
));
}
#[test]
fn scope_host_set_matches_union_exactly() {
let scope = Scope {
hosts: vec!["host-a".to_string(), "host-c".to_string()],
..Default::default()
};
assert!(scope_matches("d/h/svc/host-a/boot/1-0.bin.gz", &scope, 60));
assert!(scope_matches("d/h/svc/host-c/boot/1-0.bin.gz", &scope, 60));
assert!(!scope_matches("d/h/svc/host-b/boot/1-0.bin.gz", &scope, 60));
assert!(!scope_matches(
"d/h/svc/host-aa/boot/1-0.bin.gz",
&scope,
60
));
let all = Scope::default();
assert!(scope_matches("d/h/svc/any-host/boot/1-0.bin.gz", &all, 60));
}
#[test]
fn scope_time_overlap_keeps_boundary_file() {
let scope = Scope {
start_ns: Some(1_744_224_000_000_000_000),
end_ns: Some(1_744_224_100_000_000_000),
..Default::default()
};
assert!(
scope_matches("d/h/svc/host/boot/1744223970-0.bin.gz", &scope, 60),
"file [t-30, t+30) overlaps window opening at t"
);
}
#[test]
fn matched_set_is_ordered_by_order_key() {
let objs: Vec<ObjectInfo> = (0..20)
.map(|i| ObjectInfo {
key: format!("2026-06-19/1300/shale/host-{i}/boot/1-0.bin.gz"),
size: 1,
last_modified: None,
})
.collect();
let ordered = matched_and_ordered(objs.clone(), &Scope::default(), 60);
assert_eq!(ordered.len(), 20);
let mut by_key = ordered.clone();
by_key.sort_by_key(|o| order_key(&o.key));
assert_eq!(
ordered.iter().map(|o| &o.key).collect::<Vec<_>>(),
by_key.iter().map(|o| &o.key).collect::<Vec<_>>()
);
}
#[test]
fn span_filter_old_schema_fail_closed() {
use crate::ingest::decode::ResolvedSample;
use crate::ingest::parquet_writer::write_samples;
let samples = vec![ResolvedSample {
timestamp_ns: 1000,
stack_id: [1u8; 16],
worker_id: Some(1),
source: SOURCE_CPU_PROFILE,
source_key: "2026-06-19/1450/shale/myhost/boot-1/123-0.bin.gz".to_string(),
host: "myhost".to_string(),
service: "shale".to_string(),
date: "2026-06-19".to_string(),
poll_duration_ns: Some(5_000_000),
spawn_location: Some("src/main.rs:42".to_string()),
enclosing_spans: Vec::new(), }];
let mut buf = Vec::new();
write_samples(&mut buf, &samples, &HashMap::new()).unwrap();
let filter = SampleFilter {
span_type_uid: Some([42u8; 16]),
facets: HashMap::from([("source", "cpu".to_string())]),
..Default::default()
};
let mut accum = FlamegraphAccum::new(filter);
accum.merge(buf, None).unwrap();
assert_eq!(
accum.snapshot().total_samples,
0,
"span filter must fail closed: sample with no membership data must NOT pass"
);
}
#[test]
fn span_filter_matches_valid_membership() {
use crate::ingest::decode::{EnclosingSpanSummary, ResolvedSample};
use crate::ingest::parquet_writer::write_samples;
let target_uid = [42u8; 16];
let samples = vec![ResolvedSample {
timestamp_ns: 1000,
stack_id: [1u8; 16],
worker_id: Some(1),
source: SOURCE_CPU_PROFILE,
source_key: "2026-06-19/1450/shale/myhost/boot-1/123-0.bin.gz".to_string(),
host: "myhost".to_string(),
service: "shale".to_string(),
date: "2026-06-19".to_string(),
poll_duration_ns: Some(5_000_000),
spawn_location: Some("src/main.rs:42".to_string()),
enclosing_spans: vec![EnclosingSpanSummary {
span_uid: [1u8; 16],
span_type_uid: target_uid,
elapsed_ns: 10_000_000,
details_complete: true,
}],
}];
let mut buf = Vec::new();
write_samples(&mut buf, &samples, &HashMap::new()).unwrap();
let filter = SampleFilter {
span_type_uid: Some(target_uid),
facets: HashMap::from([("source", "cpu".to_string())]),
..Default::default()
};
let mut accum = FlamegraphAccum::new(filter);
accum.merge(buf, None).unwrap();
assert_eq!(
accum.snapshot().total_samples,
1,
"span filter must match when membership is present"
);
}
#[test]
fn span_filter_exact_window_boundaries() {
use crate::ingest::decode::{EnclosingSpanSummary, ResolvedSample};
use crate::ingest::parquet_writer::write_samples;
let target_uid = [42u8; 16];
let make_sample = |elapsed_ns: u64| -> ResolvedSample {
ResolvedSample {
timestamp_ns: 1000,
stack_id: [1u8; 16],
worker_id: Some(1),
source: SOURCE_CPU_PROFILE,
source_key: "2026-06-19/1450/shale/myhost/boot-1/123-0.bin.gz".to_string(),
host: "myhost".to_string(),
service: "shale".to_string(),
date: "2026-06-19".to_string(),
poll_duration_ns: None,
spawn_location: None,
enclosing_spans: vec![EnclosingSpanSummary {
span_uid: [1u8; 16],
span_type_uid: target_uid,
elapsed_ns,
details_complete: true,
}],
}
};
let samples = vec![
make_sample(1_000_000), make_sample(5_000_000), make_sample(10_000_000), make_sample(10_000_001), make_sample(999_999), ];
let mut buf = Vec::new();
write_samples(&mut buf, &samples, &HashMap::new()).unwrap();
let filter = SampleFilter {
span_type_uid: Some(target_uid),
min_span_ns: Some(1_000_000),
max_span_ns: Some(10_000_000),
facets: HashMap::from([("source", "cpu".to_string())]),
..Default::default()
};
let mut accum = FlamegraphAccum::new(filter);
accum.merge(buf, None).unwrap();
assert_eq!(
accum.snapshot().total_samples,
3,
"span filter boundaries must be inclusive: [min, max]"
);
}
#[test]
fn span_filter_keeps_only_matching_span_types_frames() {
use crate::ingest::decode::{EnclosingSpanSummary, ResolvedSample};
use crate::ingest::parquet_writer::{write_samples, write_stacks_dict};
let type_a = [0xAAu8; 16];
let type_b = [0xBBu8; 16];
let stack_a = [0x0Au8; 16];
let stack_b = [0x0Bu8; 16];
let frames_a = vec!["frame_A_only".to_string(), "shared_root".to_string()];
let frames_b = vec!["frame_B_only".to_string(), "shared_root".to_string()];
let sample = |stack_id: [u8; 16], type_uid: [u8; 16], ts: u64| ResolvedSample {
timestamp_ns: ts,
stack_id,
worker_id: Some(1),
source: SOURCE_CPU_PROFILE,
source_key: "2026-06-19/1450/shale/myhost/boot-1/123-0.bin.gz".to_string(),
host: "myhost".to_string(),
service: "shale".to_string(),
date: "2026-06-19".to_string(),
poll_duration_ns: None,
spawn_location: None,
enclosing_spans: vec![EnclosingSpanSummary {
span_uid: [1u8; 16],
span_type_uid: type_uid,
elapsed_ns: 5_000_000,
details_complete: true,
}],
};
let samples = vec![
sample(stack_a, type_a, 1000),
sample(stack_a, type_a, 1001),
sample(stack_a, type_a, 1002),
sample(stack_b, type_b, 1003),
sample(stack_b, type_b, 1004),
];
let mut samples_buf = Vec::new();
write_samples(&mut samples_buf, &samples, &HashMap::new()).unwrap();
let mut dict = HashMap::new();
dict.insert(stack_a, frames_a.clone());
dict.insert(stack_b, frames_b.clone());
let mut dict_buf = Vec::new();
write_stacks_dict(&mut dict_buf, &dict).unwrap();
let surviving_frames =
|span_type_uid: Option<[u8; 16]>| -> std::collections::HashSet<String> {
let filter = SampleFilter {
span_type_uid,
facets: HashMap::from([("source", "cpu".to_string())]),
..Default::default()
};
let mut accum = FlamegraphAccum::new(filter);
accum
.merge(samples_buf.clone(), Some(dict_buf.clone()))
.unwrap();
let snap = accum.snapshot();
let mut frames = std::collections::HashSet::new();
for (stack_id, count) in &snap.stack_counts {
assert!(*count > 0, "a stack with zero count should not be present");
if let Some(fs) = snap.stacks_dict.get(stack_id) {
for f in fs {
frames.insert(f.clone());
}
}
}
frames
};
let a = surviving_frames(Some(type_a));
assert!(
a.contains("frame_A_only"),
"type-A filter must keep frame_A_only, got {a:?}"
);
assert!(
!a.contains("frame_B_only"),
"type-A filter must NOT keep frame_B_only (broken filter leaks B), got {a:?}"
);
let b = surviving_frames(Some(type_b));
assert!(
b.contains("frame_B_only"),
"type-B filter must keep frame_B_only, got {b:?}"
);
assert!(
!b.contains("frame_A_only"),
"type-B filter must NOT keep frame_A_only (broken filter leaks A), got {b:?}"
);
let both = surviving_frames(None);
assert!(
both.contains("frame_A_only") && both.contains("frame_B_only"),
"no span filter must keep both span types' frames, got {both:?}"
);
}
#[test]
fn flamegraph_merge_dict_failure_leaves_accum_unchanged() {
use crate::ingest::decode::ResolvedSample;
use crate::ingest::parquet_writer::write_samples;
let sample = ResolvedSample {
timestamp_ns: 5000,
stack_id: [42u8; 16],
worker_id: Some(0),
source: SOURCE_CPU_PROFILE,
source_key: "2026-06-19/1450/shale/myhost/boot-1/123-0.bin.gz".to_string(),
host: "myhost".to_string(),
service: "shale".to_string(),
date: "2026-06-19".to_string(),
poll_duration_ns: None,
spawn_location: None,
enclosing_spans: Vec::new(),
};
let mut samples_buf = Vec::new();
write_samples(&mut samples_buf, &[sample], &HashMap::new()).unwrap();
let filter = SampleFilter {
facets: HashMap::from([("source", "cpu".to_string())]),
..Default::default()
};
let mut accum = FlamegraphAccum::new(filter.clone());
accum.merge(samples_buf.clone(), None).unwrap();
assert_eq!(accum.snapshot().total_samples, 1);
let mut accum2 = FlamegraphAccum::new(filter);
let result = accum2.merge(samples_buf, Some(b"not valid parquet".to_vec()));
assert!(result.is_err(), "garbage dict must fail");
assert_eq!(
accum2.snapshot().total_samples,
0,
"transactional merge must leave accum unchanged on dict failure"
);
}
#[test]
fn flamegraph_merge_samples_failure_leaves_accum_unchanged() {
let filter = SampleFilter {
facets: HashMap::from([("source", "cpu".to_string())]),
..Default::default()
};
let mut accum = FlamegraphAccum::new(filter);
let result = accum.merge(b"not valid parquet".to_vec(), None);
assert!(result.is_err(), "garbage samples must fail");
assert_eq!(
accum.snapshot().total_samples,
0,
"transactional merge must leave accum unchanged on samples failure"
);
}
}