Skip to content

Commit 13470a2

Browse files
authored
Improve data density visualization by sampling dense chunks (#11766)
1 parent ccd9864 commit 13470a2

2 files changed

Lines changed: 76 additions & 3 deletions

File tree

crates/viewer/re_time_panel/benches/bench_density_graph.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,30 @@ fn bench_many_chunks(c: &mut Criterion) {
215215
}
216216
}
217217

218+
/// Benchmark that specifically tests the `uniform_sample_events` path.
219+
///
220+
/// This uses large chunks that exceed the individual event rendering threshold,
221+
/// forcing the sampling path to be taken.
222+
fn bench_sampling(c: &mut Criterion) {
223+
let mut group = c.benchmark_group("sampling");
224+
225+
let sizes = [5000, 10000, 20000, 50000, 100000];
226+
for size in sizes {
227+
for max_sampled_events_per_chunk in [0, 4000, 8000] {
228+
let id = format!("{size}/sample_{max_sampled_events_per_chunk}");
229+
230+
let config = DensityGraphBuilderConfig {
231+
max_sampled_events_per_chunk,
232+
..Default::default()
233+
};
234+
235+
group.bench_with_input(id, &single_chunk(size, true), |b, &entry| {
236+
run(b, config, entry);
237+
});
238+
}
239+
}
240+
}
241+
218242
fn main() {
219243
// More noisy results, but benchmark ends a lot sooner.
220244
let mut criterion = Criterion::default()
@@ -226,6 +250,7 @@ fn main() {
226250

227251
bench_single_chunks(&mut criterion);
228252
bench_many_chunks(&mut criterion);
253+
bench_sampling(&mut criterion);
229254

230255
criterion.final_summary();
231256
}

crates/viewer/re_time_panel/src/data_density_graph.rs

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -582,10 +582,27 @@ pub fn build_density_graph<'a>(
582582
};
583583

584584
if should_render_individual_events {
585+
// Render all individual events
585586
for (time, num_events) in chunk.num_events_cumulative_per_unique_time(timeline) {
586-
data.add_chunk_point(time, num_events as usize);
587+
data.add_chunk_point(time, num_events as f32);
588+
}
589+
} else if config.max_sampled_events_per_chunk > 0 {
590+
let events = chunk.num_events_cumulative_per_unique_time(timeline);
591+
592+
if events.len() > config.max_sampled_events_per_chunk {
593+
// If there's more rows than the configured max, we sample events to get a fast, good enough density estimate.
594+
data.add_uniform_sample_from_chunk(
595+
&events,
596+
config.max_sampled_events_per_chunk,
597+
);
598+
} else {
599+
// No need to sample, we can use all events.
600+
for (time, num_events) in events {
601+
data.add_chunk_point(time, num_events as f32);
602+
}
587603
}
588604
} else {
605+
// Fall back to uniform distribution across the entire time range
589606
data.add_chunk_range(time_range, num_events_in_chunk);
590607
}
591608
}
@@ -604,6 +621,10 @@ pub struct DensityGraphBuilderConfig {
604621

605622
/// If an unsorted chunk has fewer events than this we show its individual events.
606623
pub max_events_in_unsorted_chunk: u64,
624+
625+
/// When a chunk is too large to render all events, uniformly sample this many events
626+
/// to create a good enough density estimate instead.
627+
pub max_sampled_events_per_chunk: usize,
607628
}
608629

609630
impl DensityGraphBuilderConfig {
@@ -612,6 +633,7 @@ impl DensityGraphBuilderConfig {
612633
max_total_chunk_events: 0,
613634
max_events_in_unsorted_chunk: 0,
614635
max_events_in_sorted_chunk: 0,
636+
max_sampled_events_per_chunk: 0,
615637
};
616638

617639
/// All sorted chunks will be rendered as individual events,
@@ -620,13 +642,15 @@ impl DensityGraphBuilderConfig {
620642
max_total_chunk_events: u64::MAX,
621643
max_events_in_unsorted_chunk: 0,
622644
max_events_in_sorted_chunk: u64::MAX,
645+
max_sampled_events_per_chunk: 0,
623646
};
624647

625648
/// All chunks will be rendered as individual events.
626649
pub const ALWAYS_SPLIT_ALL_CHUNKS: Self = Self {
627650
max_total_chunk_events: u64::MAX,
628651
max_events_in_unsorted_chunk: u64::MAX,
629652
max_events_in_sorted_chunk: u64::MAX,
653+
max_sampled_events_per_chunk: 0,
630654
};
631655
}
632656

@@ -649,6 +673,10 @@ impl Default for DensityGraphBuilderConfig {
649673

650674
// Processing unsorted events is about 20% slower than sorted events.
651675
max_events_in_unsorted_chunk: 8_000,
676+
677+
// When chunks are too large to render all events, sample this many events uniformly
678+
// to create a good enough density estimate.
679+
max_sampled_events_per_chunk: 8_000,
652680
}
653681
}
654682
}
@@ -709,12 +737,32 @@ impl<'a> DensityGraphBuilder<'a> {
709737
}
710738
}
711739

712-
fn add_chunk_point(&mut self, time: TimeInt, num_events: usize) {
740+
/// Uniformly sample events using the given sample size.
741+
///
742+
/// Each sampled event's count is reweighted to preserve the total density.
743+
fn add_uniform_sample_from_chunk(&mut self, events: &[(TimeInt, u64)], sample_size: usize) {
744+
re_tracing::profile_function!();
745+
746+
let step = events.len() as f32 / sample_size as f32;
747+
748+
for i in 0..sample_size {
749+
let idx = (i as f32 * step) as usize;
750+
// This means we might miss the last event if rounding down, but that's acceptable.
751+
if let Some(&(time, count)) = events.get(idx) {
752+
// Reweight the count to preserve total density
753+
let weighted_count = count as f32 * step;
754+
755+
self.add_chunk_point(time, weighted_count);
756+
}
757+
}
758+
}
759+
760+
fn add_chunk_point(&mut self, time: TimeInt, weight: f32) {
713761
let Some(x) = self.time_ranges_ui.x_from_time_f32(time.into()) else {
714762
return;
715763
};
716764

717-
self.density_graph.add_point(x, num_events as _);
765+
self.density_graph.add_point(x, weight);
718766

719767
if let Some(pointer_pos) = self.pointer_pos {
720768
let is_hovered = {

0 commit comments

Comments
 (0)