omnigraph/vendor/lance-table/benches/manifest_intern.rs
aaltshuler b5c0c6238b fix(deps): vendor lance-table 7.0.0 + lance#7480 so merge-updated tables survive filtered reads after deletes
iss-merge-rowid-overlap-corrupts-filtered-reads / lance#7444: an
update-style merge_insert over a merge-written fragment legally reuses the
updated rows' stable row ids (row-id-lineage spec: updates preserve
_rowid) while the superseded fragment keeps its full sequence plus a
deletion vector. A later delete leaves the overlapping id range sparsely
tiled, and lance-table 7.0.0's RowIdIndex::new asserted dense tiling —
failing every filtered read that builds the id→address map ("Wrong range"
debug assert; "all columns in a record batch must have the same length"
or a silently-wrong batch in release).

The upstream fix (lance#7480, merged 2026-07-01) landed hours AFTER
v8.0.0 was cut, so no release ≤ 8.0.0 carries it. Consume it now as a
vendored pin: vendor/lance-table is the pristine published 7.0.0 source
plus ONLY the #7480 rowids/index.rs hunk (drop the false tiling assert;
hard-error on the true invariant — one live id claimed by two fragments)
and upstream's regression unit test, wired via [patch.crates-io]. The fix
is read-side only, so already-written graphs become readable as-is — no
data repair.

Removal condition (see vendor/lance-table/README.omnigraph.md): drop the
vendor dir + patch entry at the first Lance bump whose lance-table ships
lance#7480 (9.0.0, or a backported 8.0.1). The surface guard
filtered_scan_tolerates_merge_update_row_id_overlap keeps that honest in
both directions.

Turns the previous commit's red tests green. Full workspace gate passes
(cargo test --workspace --locked --no-fail-fast, 68 suites).
2026-07-02 23:23:39 +03:00

261 lines
8.7 KiB
Rust

// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The Lance Authors
// Benchmarks use eprintln! to report memory stats alongside criterion output.
#![allow(clippy::print_stderr)]
//! Benchmark for manifest fragment interning.
//!
//! Measures memory savings and deserialization throughput when interning
//! `DataFile.fields`, `DataFile.column_indices`, and
//! `RowDatasetVersionMeta::Inline` bytes across many fragments.
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
use deepsize::DeepSizeOf;
use prost::Message;
use lance_table::format::pb;
use lance_table::format::{DataFileFieldInterner, Fragment};
fn num_fragments() -> u64 {
std::env::var("BENCH_NUM_FRAGMENTS")
.map(|s| s.parse().unwrap())
.unwrap_or(100_000)
}
/// Build a vector of protobuf DataFragment messages that simulate a
/// homogeneous, post-compaction table: every fragment has the same field
/// list, column indices, and version metadata bytes.
fn make_uniform_pb_fragments(n: u64, num_fields: usize) -> Vec<pb::DataFragment> {
let fields: Vec<i32> = (0..num_fields as i32).collect();
let column_indices: Vec<i32> = (0..num_fields as i32).collect();
// Simulate version metadata: a small protobuf-encoded payload
// (identical across all fragments post-compaction)
let version_bytes: Vec<u8> = {
let seq = pb::RowDatasetVersionSequence {
runs: vec![pb::RowDatasetVersionRun {
span: Some(pb::U64Segment {
segment: Some(pb::u64_segment::Segment::Range(pb::u64_segment::Range {
start: 0,
end: 1000,
})),
}),
version: 42,
}],
};
seq.encode_to_vec()
};
(0..n)
.map(|i| pb::DataFragment {
id: i,
files: vec![pb::DataFile {
path: format!("data/{i}.lance"),
fields: fields.clone(),
column_indices: column_indices.clone(),
file_major_version: 2,
file_minor_version: 0,
file_size_bytes: 0,
base_id: None,
}],
deletion_file: None,
row_id_sequence: None,
physical_rows: 1000,
last_updated_at_version_sequence: Some(
pb::data_fragment::LastUpdatedAtVersionSequence::InlineLastUpdatedAtVersions(
version_bytes.clone(),
),
),
created_at_version_sequence: Some(
pb::data_fragment::CreatedAtVersionSequence::InlineCreatedAtVersions(
version_bytes.clone(),
),
),
})
.collect()
}
/// Deserialize protobuf fragments WITHOUT interning (baseline).
fn deserialize_without_interning(protos: &[pb::DataFragment]) -> Vec<Fragment> {
protos
.iter()
.map(|p| Fragment::try_from(p.clone()).unwrap())
.collect()
}
/// Deserialize protobuf fragments WITH interning.
fn deserialize_with_interning(protos: &[pb::DataFragment]) -> Vec<Fragment> {
let mut interner = DataFileFieldInterner::default();
protos
.iter()
.map(|p| interner.intern_fragment(p.clone()).unwrap())
.collect()
}
/// Build fragments where each group shares the same version metadata,
/// simulating many small appends without compaction.
fn make_diverse_pb_fragments(
n: u64,
num_fields: usize,
unique_versions: u64,
) -> Vec<pb::DataFragment> {
let fields: Vec<i32> = (0..num_fields as i32).collect();
let column_indices: Vec<i32> = (0..num_fields as i32).collect();
let group_size = n / unique_versions;
let version_payloads: Vec<Vec<u8>> = (0..unique_versions)
.map(|v| {
let seq = pb::RowDatasetVersionSequence {
runs: vec![pb::RowDatasetVersionRun {
span: Some(pb::U64Segment {
segment: Some(pb::u64_segment::Segment::Range(pb::u64_segment::Range {
start: 0,
end: 1000,
})),
}),
version: v,
}],
};
seq.encode_to_vec()
})
.collect();
(0..n)
.map(|i| {
let version_idx = (i / group_size).min(unique_versions - 1) as usize;
pb::DataFragment {
id: i,
files: vec![pb::DataFile {
path: format!("data/{i}.lance"),
fields: fields.clone(),
column_indices: column_indices.clone(),
file_major_version: 2,
file_minor_version: 0,
file_size_bytes: 0,
base_id: None,
}],
deletion_file: None,
row_id_sequence: None,
physical_rows: 1000,
last_updated_at_version_sequence: Some(
pb::data_fragment::LastUpdatedAtVersionSequence::InlineLastUpdatedAtVersions(
version_payloads[version_idx].clone(),
),
),
created_at_version_sequence: Some(
pb::data_fragment::CreatedAtVersionSequence::InlineCreatedAtVersions(
version_payloads[version_idx].clone(),
),
),
}
})
.collect()
}
fn bench_deserialization(c: &mut Criterion) {
let mut group = c.benchmark_group("manifest_intern");
let n = num_fragments();
for num_fields in [10, 50] {
let protos = make_uniform_pb_fragments(n, num_fields);
group.bench_with_input(
BenchmarkId::new("deserialize_no_intern", num_fields),
&num_fields,
|b, _| {
b.iter(|| deserialize_without_interning(&protos));
},
);
group.bench_with_input(
BenchmarkId::new("deserialize_with_intern", num_fields),
&num_fields,
|b, _| {
b.iter(|| deserialize_with_interning(&protos));
},
);
}
// Benchmark with many unique version payloads
for unique_versions in [10, 100, 500] {
let protos = make_diverse_pb_fragments(n, 10, unique_versions);
group.bench_with_input(
BenchmarkId::new("deserialize_no_intern_diverse", unique_versions),
&unique_versions,
|b, _| {
b.iter(|| deserialize_without_interning(&protos));
},
);
group.bench_with_input(
BenchmarkId::new("deserialize_with_intern_diverse", unique_versions),
&unique_versions,
|b, _| {
b.iter(|| deserialize_with_interning(&protos));
},
);
}
group.finish();
}
fn bench_memory(c: &mut Criterion) {
let mut group = c.benchmark_group("manifest_memory");
let n = num_fragments();
for num_fields in [10, 50] {
let protos = make_uniform_pb_fragments(n, num_fields);
let no_intern = deserialize_without_interning(&protos);
let with_intern = deserialize_with_interning(&protos);
let size_no_intern = no_intern.deep_size_of();
let size_with_intern = with_intern.deep_size_of();
eprintln!(
"\n[{} fragments, {} fields] Memory without interning: {:.2} MB",
n,
num_fields,
size_no_intern as f64 / 1_048_576.0
);
eprintln!(
"[{} fragments, {} fields] Memory with interning: {:.2} MB",
n,
num_fields,
size_with_intern as f64 / 1_048_576.0
);
eprintln!(
"[{} fragments, {} fields] Savings: {:.2} MB ({:.1}%)",
n,
num_fields,
(size_no_intern - size_with_intern) as f64 / 1_048_576.0,
(1.0 - size_with_intern as f64 / size_no_intern as f64) * 100.0
);
// Benchmark deep_size_of measurement itself (sanity check)
group.bench_with_input(
BenchmarkId::new("deep_size_of_interned", num_fields),
&num_fields,
|b, _| {
b.iter(|| with_intern.deep_size_of());
},
);
drop(no_intern);
drop(with_intern);
}
group.finish();
}
#[cfg(target_os = "linux")]
criterion_group!(
name = benches;
config = Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(100, pprof::criterion::Output::Flamegraph(None)));
targets = bench_deserialization, bench_memory
);
#[cfg(not(target_os = "linux"))]
criterion_group!(benches, bench_deserialization, bench_memory);
criterion_main!(benches);