Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions changelog.d/changed/1335-plan-cache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
- **A pass over a file's records makes the plan of each class once, not once
for each record (#1335).** Decoding a record works out, for each class it
meets, the chain of classes and the key of each field. A pass over a Revit
2015 to 2023 project's records did that again for every record, which was
3.1% of the time of the 33 samples' exports. The passes now keep the plans
between records (`native_parameters::PlanCache`), in a cache that holds at
most a million units of plan cost and empties itself past that, so that a
schema with huge classes cannot make it grow without bound. The graphs are
the same. No export changes. The figures are in the pull request.
18 changes: 16 additions & 2 deletions docs/maintaining.md
Original file line number Diff line number Diff line change
Expand Up @@ -748,8 +748,22 @@ beside the one function that reads the projected graph
`native_es_catalog::from_graph`), and a field that function comes to read must be
added to its list, because a field left out reads as absent without an error.
`probes/examples/probe_1335_projected_graphs.rs` imports the constants, decodes every
record both ways and runs those functions on both graphs; run it
(`-f probe=probe_1335_projected_graphs`) after any change to a list or a reader.
record both ways and runs those functions on both graphs.

A pass keeps the class plans of its decodes in a `native_parameters::PlanCache`, made
once for each class instead of once for each record. The cache holds at most
`PLAN_CACHE_COST` of plan cost (a unit for each field of a plan's chain and each field
kept, and 8 for each entry) and empties itself or keeps nothing past it, so what a
schema or a caller's field lists can make it hold is bounded, and since a decode with
no plan makes its own, what it holds cannot change a graph. The same probe decodes
every record through a shared cache and through one that holds almost nothing, and
compares each with the decode that makes its own plans; it also prints the most a file
makes the shared cache hold. That shows a shared cache equals a fresh one; that a decode
without a cache equals the one before the cache existed rests on the exports' byte
identity in Measure.

Run the probe (`-f probe=probe_1335_projected_graphs`) after any change to a list, to a
reader or to the plan cache: it exits 1 where anything it compares differs.

## Dependencies and the MSRV

Expand Down
142 changes: 140 additions & 2 deletions probes/examples/probe_1335_projected_graphs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@
//! `from_graph` (`CATALOG_FIELDS`). The lists are the readers' own constants,
//! imported, not copied.
//!
//! And a decode through a `PlanCache` shared by every decode of the file gives
//! exactly the graph, usage and refusal of the same decode that makes its own
//! plans, whatever the lists and the order, and also through a cache whose limit
//! is so small that it empties itself or keeps nothing. What this proves is that
//! a shared cache equals a fresh one. That a decode without a cache equals the
//! one before the cache existed rests on the byte identity of the exports,
//! which Measure shows; this probe does not run the earlier code.
//!
//! Every record of the file's native partitions is decoded in full and
//! projected under each of the readers' lists, whatever its class, and under
//! the identity alone and the empty list (every field left out). The graph the
Expand All @@ -28,8 +36,18 @@
//! binding-context family-categories <n> symbol-families <n> same <bool>
//! ingest-refusals <n> same-refusals <bool>
//! wall-facts compared <n> same <bool>
//! plan-cache compared <n> mismatches <n>
//! (every decode through one cache shared by the whole file and every
//! list, against the same decode with its own plans)
//! plan-cache limited compared <n> mismatches <n>
//! (the same through a cache that holds at most 48 of cost)
//! plan-cache size peak-entries <n> peak-cost <n> clears <n>
//! (the most the shared cache held after any decode, across the seven
//! decodes of each record; a pass shares a cache among at most three)
//! mismatch <kind> <Class> (the first few)
//!
//! The exit status is 1 where anything above differs.
//!
//! Usage:
//! cargo run --profile ci -p rvt-probes --example probe_1335_projected_graphs -- FILE
use anyhow::Result;
Expand All @@ -40,7 +58,8 @@ use rvt::research::native_parameter_definitions::{
FAMILY_BINDING_FIELDS, Registry as Definitions, SYMBOL_BINDING_FIELDS,
};
use rvt::research::native_parameters::{
GraphLimits, ObjectGraph, decode_graph_projected, decode_graph_with_catalog_usage,
GraphLimits, GraphUsage, ObjectGraph, PlanCache, decode_graph_cached, decode_graph_projected,
decode_graph_with_catalog_usage,
};
use rvt::research::native_walls::{WALL_READ_FIELDS, wall_facts};
use rvt::research::schema_registry;
Expand All @@ -50,6 +69,9 @@ use std::time::{Duration, Instant};

const BUDGET: Duration = Duration::from_secs(12 * 60);
const SHOWN: usize = 12;
/// The limit of the cache that is made to empty itself (a plan of a class with
/// no field of its own costs 8).
const LIMITED_COST: usize = 48;

/// The readers' lists, then two that stress the mechanism: the identity alone
/// and no field at all.
Expand Down Expand Up @@ -99,6 +121,42 @@ fn ingest(definitions: &mut Definitions, graph: &ObjectGraph, id: u64) -> Result
.map_err(|error| error.to_string())
}

/// Whether two decodes of one body gave the same graph in every field.
fn same_graph(a: &ObjectGraph, b: &ObjectGraph) -> bool {
a.consumed_bytes == b.consumed_bytes
&& a.objects.len() == b.objects.len()
&& a.objects.iter().zip(&b.objects).all(|(x, y)| {
x.class_tag == y.class_tag
&& x.class_name == y.class_name
&& x.token == y.token
&& x.start == y.start
&& x.fields_end == y.fields_end
&& x.fields == y.fields
})
&& a.edges.len() == b.edges.len()
&& a.edges.iter().zip(&b.edges).all(|(x, y)| {
x.source_object_index == y.source_object_index
&& x.pointer_offset == y.pointer_offset
&& x.pointer_token == y.pointer_token
&& x.target_object_index == y.target_object_index
&& x.target_class_tag == y.target_class_tag
})
}

type Decoded = Result<(ObjectGraph, GraphUsage)>;

/// Whether two decodes of one body agree: the same refusal, or the same graph
/// and usage.
fn same_outcome(a: &Decoded, b: &Decoded) -> bool {
match (a, b) {
(Ok((x, u)), Ok((y, v))) => {
u.values == v.values && u.objects == v.objects && same_graph(x, y)
}
(Err(x), Err(y)) => x.to_string() == y.to_string(),
_ => false,
}
}

fn differences(full: &ObjectGraph, projected: &ObjectGraph, keep: &[&str]) -> Vec<&'static str> {
let mut found = Vec::new();
if full.consumed_bytes != projected.consumed_bytes {
Expand Down Expand Up @@ -195,7 +253,8 @@ fn main() -> Result<()> {
}
let formats = native_document::read_single(&mut rf, "Formats/Latest")?;
let registry = schema_registry::parse(&formats)?;
println!("global-catalog {}", global_catalog(&mut rf, &registry));
let catalog_verdict = global_catalog(&mut rf, &registry);
println!("global-catalog {catalog_verdict}");
let catalog = native_document::read_single(&mut rf, "Global/Latest")
.and_then(|bytes| native_es_catalog::decode(&bytes, &registry))
.ok()
Expand Down Expand Up @@ -229,6 +288,19 @@ fn main() -> Result<()> {
let mut walls = 0usize;
let mut same_walls = true;
let mut shown = 0usize;
// One cache for every decode of the file, under every list, as a pass
// shares one: each decode is compared with the same decode that makes its
// own plans.
let mut cache = PlanCache::default();
let mut cached = 0usize;
let mut cached_wrong = 0usize;
// And one that empties itself or keeps nothing, so that the bound is
// exercised on every file.
let mut limited = PlanCache::with_limit(LIMITED_COST);
let mut limited_compared = 0usize;
let mut limited_wrong = 0usize;
let mut peak_entries = 0usize;
let mut peak_cost = 0usize;
let result = native_document::extract_graphs(&mut rf, &options, |record| {
if started.elapsed() > BUDGET {
stopped = true;
Expand All @@ -247,11 +319,49 @@ fn main() -> Result<()> {
*refused.entry(class.clone()).or_default() += 1;
}
}
let full_cached =
decode_graph_cached(body, &registry, &limits, catalog.as_ref(), None, &mut cache);
cached += 1;
cached_wrong += usize::from(!same_outcome(&full, &full_cached));
let full_limited = decode_graph_cached(
body,
&registry,
&limits,
catalog.as_ref(),
None,
&mut limited,
);
limited_compared += 1;
limited_wrong += usize::from(!same_outcome(&full, &full_limited));
peak_entries = peak_entries.max(cache.entries());
peak_cost = peak_cost.max(cache.cost());
for (index, keep) in KEEPS.iter().enumerate() {
let projected =
decode_graph_projected(body, &registry, &limits, catalog.as_ref(), keep);
decodes += 1;
per_keep[index].0 += 1;
let projected_cached = decode_graph_cached(
body,
&registry,
&limits,
catalog.as_ref(),
Some(keep),
&mut cache,
);
cached += 1;
cached_wrong += usize::from(!same_outcome(&projected, &projected_cached));
let projected_limited = decode_graph_cached(
body,
&registry,
&limits,
catalog.as_ref(),
Some(keep),
&mut limited,
);
limited_compared += 1;
limited_wrong += usize::from(!same_outcome(&projected, &projected_limited));
peak_entries = peak_entries.max(cache.entries());
peak_cost = peak_cost.max(cache.cost());
let kind = match (&full, &projected) {
(Ok((graph, usage)), Ok((other, other_usage))) => {
if usage.values != other_usage.values || usage.objects != other_usage.objects {
Expand Down Expand Up @@ -336,5 +446,33 @@ fn main() -> Result<()> {
full_definitions.symbol_families.len(),
);
println!("wall-facts compared {walls} same {same_walls}");
println!("plan-cache compared {cached} mismatches {cached_wrong}");
println!("plan-cache limited compared {limited_compared} mismatches {limited_wrong}");
println!(
"plan-cache size peak-entries {peak_entries} peak-cost {peak_cost} clears {}",
cache.clears()
);
let mut failures = Vec::new();
if mismatches > 0 {
failures.push("a projected decode differs from the full one");
}
if cached_wrong > 0 {
failures.push("a decode through the shared cache differs from one with its own plans");
}
if limited_wrong > 0 {
failures.push("a decode through the limited cache differs from one with its own plans");
}
if !same || !same_refusals {
failures.push("the binding context or its refusals differ");
}
if !same_walls {
failures.push("wall_facts differs between the full and the projected graph");
}
if !matches!(catalog_verdict.as_str(), "same" | "same-refusal" | "absent") {
failures.push("the catalog differs between the full and the projected document");
}
if !failures.is_empty() {
anyhow::bail!("{}", failures.join("; "));
}
Ok(())
}
25 changes: 10 additions & 15 deletions src/research/native_document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -978,6 +978,8 @@ fn extract_records(
if let Some(records) = indexed {
file.remember_native_record_index(RecordIndex { records });
}
// The class plans of the pass's decodes, made once for each class (#1335).
let mut plans = native_parameters::PlanCache::default();
let mut seen = BTreeSet::new();
let mut seen_ids = BTreeSet::new();
let mut definition_owners: BTreeMap<u16, bool> = BTreeMap::new();
Expand Down Expand Up @@ -1154,21 +1156,14 @@ fn extract_records(
let projected = options
.graph_fields
.and_then(|fields| fields(class_name.as_deref()?));
let decoded = match projected {
Some(keep) => native_parameters::decode_graph_projected(
body,
registry,
&limits,
summary.extensible_storage_catalog.as_ref(),
keep,
),
None => native_parameters::decode_graph_with_catalog_usage(
body,
registry,
&limits,
summary.extensible_storage_catalog.as_ref(),
),
};
let decoded = native_parameters::decode_graph_cached(
body,
registry,
&limits,
summary.extensible_storage_catalog.as_ref(),
projected,
&mut plans,
);
let decoded = decoded.and_then(|(graph, usage)| {
if source.channel == 102 {
let root = graph
Expand Down
Loading