diff --git a/changelog.d/changed/1335-plan-cache.md b/changelog.d/changed/1335-plan-cache.md new file mode 100644 index 00000000..06b14d19 --- /dev/null +++ b/changelog.d/changed/1335-plan-cache.md @@ -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. diff --git a/docs/maintaining.md b/docs/maintaining.md index c966e423..446ec0a8 100644 --- a/docs/maintaining.md +++ b/docs/maintaining.md @@ -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 diff --git a/probes/examples/probe_1335_projected_graphs.rs b/probes/examples/probe_1335_projected_graphs.rs index 69f02ada..bf42eb62 100644 --- a/probes/examples/probe_1335_projected_graphs.rs +++ b/probes/examples/probe_1335_projected_graphs.rs @@ -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 @@ -28,8 +36,18 @@ //! binding-context family-categories symbol-families same //! ingest-refusals same-refusals //! wall-facts compared same +//! plan-cache compared mismatches +//! (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 mismatches +//! (the same through a cache that holds at most 48 of cost) +//! plan-cache size peak-entries peak-cost clears +//! (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 (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; @@ -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; @@ -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. @@ -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 { @@ -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, ®istry)); + let catalog_verdict = global_catalog(&mut rf, ®istry); + println!("global-catalog {catalog_verdict}"); let catalog = native_document::read_single(&mut rf, "Global/Latest") .and_then(|bytes| native_es_catalog::decode(&bytes, ®istry)) .ok() @@ -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; @@ -247,11 +319,49 @@ fn main() -> Result<()> { *refused.entry(class.clone()).or_default() += 1; } } + let full_cached = + decode_graph_cached(body, ®istry, &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, + ®istry, + &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, ®istry, &limits, catalog.as_ref(), keep); decodes += 1; per_keep[index].0 += 1; + let projected_cached = decode_graph_cached( + body, + ®istry, + &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, + ®istry, + &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 { @@ -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(()) } diff --git a/src/research/native_document.rs b/src/research/native_document.rs index 256fa559..14928faa 100644 --- a/src/research/native_document.rs +++ b/src/research/native_document.rs @@ -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 = BTreeMap::new(); @@ -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 diff --git a/src/research/native_parameters.rs b/src/research/native_parameters.rs index 08488924..5b84b6a1 100644 --- a/src/research/native_parameters.rs +++ b/src/research/native_parameters.rs @@ -156,7 +156,14 @@ pub fn decode_graph_with_catalog_usage( limits: &GraphLimits, catalog: Option<&crate::native_extensible_storage::Catalog>, ) -> Result<(ObjectGraph, GraphUsage)> { - decode_graph_in(body, registry, limits, catalog, None) + decode_graph_in( + body, + registry, + limits, + catalog, + None, + &mut PlanCache::default(), + ) } /// [`decode_graph_with_catalog_usage`] keeping only the fields of every object /// whose key is in `keep` (a bare field name, or `Declaring::name` where the @@ -176,7 +183,101 @@ pub fn decode_graph_projected( catalog: Option<&crate::native_extensible_storage::Catalog>, keep: &[&str], ) -> Result<(ObjectGraph, GraphUsage)> { - decode_graph_in(body, registry, limits, catalog, Some(keep)) + decode_graph_in( + body, + registry, + limits, + catalog, + Some(keep), + &mut PlanCache::default(), + ) +} +/// The class plans of the decodes that share it, kept so that each class's is +/// made once for a pass over a file's records, not once for each record +/// (#1335: building them was 3.1% of the export time of the 33 samples of +/// Revit 2015 to 2023). A plan depends on the schema, the class and the fields +/// a projected decode keeps, and nothing else, so a decode that finds a plan +/// made by an earlier one reads exactly as it would have with its own. A cache +/// is for one schema: [`decode_graph_cached`] empties it when it is given +/// another. +/// +/// What a cache holds is bounded: a plan costs [`PLAN_ENTRY_COST`] and one for +/// each field of every class of its chain and each field kept, the cache holds +/// at most [`PLAN_CACHE_COST`] of them (or the limit given to +/// [`Self::with_limit`]), and one that would pass it empties the cache first. A +/// plan dearer than the whole limit is not kept. Either way a decode that +/// finds no plan makes its own, which is what every decode did before the cache, +/// so the bound cannot change a graph: a schema made to hold plans of tens of +/// thousands of fields, or a caller that passes a new list of fields each call, +/// gets the decode it always got, not an allocation that grows with the file. +#[derive(Debug)] +pub struct PlanCache { + /// The checksum of the schema the plans were made for. + registry: String, + /// By class tag and the address and length of the fields kept; the plan + /// says which fields, so that the same address holding other fields is + /// not taken for the same list. + plans: HashMap>, + /// The cost of the plans held. + cost: usize, + /// The most cost the cache holds. + limit: usize, + /// How many times the cache was emptied for passing its limit. + clears: usize, +} +/// What a [`PlanCache`] holds by default, as a sum of plan costs: an estimate +/// of about 60 bytes for each unit. Measured over 88 files of Revit 2015 to +/// 2027 with one cache shared by seven field lists at once, more than any pass +/// shares, the most a file made it hold was 174,316 (8,127 plans, Snowdon +/// Towers architectural), 17.4% of it, and no file emptied it (the probe of +/// #1335 prints the figure). +pub const PLAN_CACHE_COST: usize = 1_000_000; +/// What each plan costs besides its fields: its entry in the table, its +/// vectors and its share of the cache's overhead. +pub const PLAN_ENTRY_COST: usize = 8; +impl Default for PlanCache { + fn default() -> Self { + Self::with_limit(PLAN_CACHE_COST) + } +} +impl PlanCache { + /// An empty cache that holds at most `limit` of cost. + pub fn with_limit(limit: usize) -> Self { + Self { + registry: String::new(), + plans: HashMap::new(), + cost: 0, + limit, + clears: 0, + } + } + /// The number of plans held. + pub fn entries(&self) -> usize { + self.plans.len() + } + /// The cost of the plans held. + pub fn cost(&self) -> usize { + self.cost + } + /// How many times the cache was emptied for passing its limit. + pub fn clears(&self) -> usize { + self.clears + } +} +/// A class's tag and the address and length of the fields a decode keeps. +type PlanKey = (u16, usize, usize); +/// [`decode_graph_with_catalog_usage`], or [`decode_graph_projected`] where +/// `keep` is given, taking the class plans from `plans` and leaving there those +/// it makes. The graph is the one the decode without a cache gives. +pub fn decode_graph_cached( + body: &[u8], + registry: &Registry, + limits: &GraphLimits, + catalog: Option<&crate::native_extensible_storage::Catalog>, + keep: Option<&[&str]>, + plans: &mut PlanCache, +) -> Result<(ObjectGraph, GraphUsage)> { + decode_graph_in(body, registry, limits, catalog, keep, plans) } fn decode_graph_in( body: &[u8], @@ -184,6 +285,7 @@ fn decode_graph_in( limits: &GraphLimits, catalog: Option<&crate::native_extensible_storage::Catalog>, keep: Option<&[&str]>, + plans: &mut PlanCache, ) -> Result<(ObjectGraph, GraphUsage)> { ensure!( limits.max_values > 0 && limits.max_objects > 0 && limits.max_depth > 0, @@ -191,7 +293,13 @@ fn decode_graph_in( ); let tag = le_bytes::u16_at(body, 0).ok_or_else(|| anyhow::anyhow!("truncated root object tag"))?; - let mut cursor = Cursor::new(body, registry, catalog, 2, *limits, Spans::None).with_keep(keep); + if plans.registry != registry.source_sha256 { + plans.plans.clear(); + plans.cost = 0; + plans.registry.clone_from(®istry.source_sha256); + } + let mut cursor = + Cursor::new(body, registry, catalog, 2, *limits, Spans::None, plans).with_keep(keep); let mut objects = Vec::new(); let mut edges = Vec::new(); let mut shared = BTreeMap::::new(); @@ -338,7 +446,8 @@ pub fn decode_object_fields_spanning( .ok_or_else(|| anyhow::anyhow!("native object class absent"))? .name .clone(); - let mut cursor = Cursor::new(body, registry, None, start, *limits, spans); + let mut plans = PlanCache::default(); + let mut cursor = Cursor::new(body, registry, None, start, *limits, spans, &mut plans); let fields = cursor.class::(class_tag, 0)?; Ok(ObjectFields { class_tag, @@ -360,8 +469,10 @@ struct PendingPointer { /// where the chain declares that name more than once. It depends on the /// schema alone, so a decode makes it once for each class it meets (#1237: /// most of an object's allocations were this, made again for every one). -struct ClassPlan<'a> { - lineage: Vec<&'a Class>, +#[derive(Debug)] +struct ClassPlan { + /// The tags of the chain's classes, as the schema registry finds them. + lineage: Vec, keys: Vec>, /// Whether each field is one a projected decode keeps /// ([`decode_graph_projected`]); empty for a decode that is not one. @@ -372,6 +483,28 @@ struct ClassPlan<'a> { /// built one meets there. Found only for a projected decode; `None` /// otherwise. duplicate: Option<(usize, usize)>, + /// The fields the plan was made for, where it was made for a projected + /// decode. + keep: Option>, +} +impl ClassPlan { + /// What the plan costs a [`PlanCache`]: its entry, a unit for each field + /// of every class of its chain, and one for each field kept. + fn cost(&self) -> usize { + PLAN_ENTRY_COST + + self.keys.iter().map(Vec::len).sum::() + + self.keep.as_ref().map_or(0, Vec::len) + } + /// Whether the plan was made for the fields `keep` names. + fn is_for(&self, keep: Option<&[&str]>) -> bool { + match (&self.keep, keep) { + (None, None) => true, + (Some(own), Some(wanted)) => { + own.len() == wanted.len() && own.iter().zip(wanted).all(|(a, b)| a.as_str() == *b) + } + _ => false, + } + } } struct Cursor<'a> { body: &'a [u8], @@ -382,7 +515,7 @@ struct Cursor<'a> { pending: Vec, spans: Vec, keep_spans: Spans, - plans: HashMap>>, + plans: &'a mut PlanCache, limits: GraphLimits, /// The fields a projected decode keeps ([`decode_graph_projected`]). keep: Option<&'a [&'a str]>, @@ -395,6 +528,7 @@ impl<'a> Cursor<'a> { pos: usize, limits: GraphLimits, keep_spans: Spans, + plans: &'a mut PlanCache, ) -> Self { Self { body, @@ -405,7 +539,7 @@ impl<'a> Cursor<'a> { pending: Vec::new(), spans: Vec::new(), keep_spans, - plans: HashMap::new(), + plans, limits, keep: None, } @@ -590,9 +724,13 @@ impl<'a> Cursor<'a> { "native class recursion budget exceeded" ); let plan = self.plan(tag, depth)?; + let registry = self.registry; let project = BUILD && top && self.keep.is_some(); let mut values = Map::new(); - for (distance, c) in plan.lineage.iter().enumerate().rev() { + for (distance, class_tag) in plan.lineage.iter().enumerate().rev() { + let c = registry + .class(*class_tag) + .ok_or_else(|| anyhow::anyhow!("unknown inline native class {class_tag}"))?; for (field_index, f) in c.fields.iter().enumerate() { // Measured VWall transient flags; other flags remain unsupported. if f.uninterpreted_flags & 2 != 0 { @@ -692,16 +830,25 @@ impl<'a> Cursor<'a> { Spans::Class(only) => only == tag, } } - /// The class `tag`'s [`ClassPlan`], made on first use. - fn plan(&mut self, tag: u16, depth: usize) -> Result>> { - if let Some(plan) = self.plans.get(&tag) { - // The chain was made under a budget that held at its own depth: - // the check that matters is the one before its last class. - ensure!( - depth + plan.lineage.len() - 1 < self.limits.max_depth, - "native class recursion budget exceeded" - ); - return Ok(Rc::clone(plan)); + /// The class `tag`'s [`ClassPlan`], made on first use by any decode that + /// shares this cursor's [`PlanCache`]. + fn plan(&mut self, tag: u16, depth: usize) -> Result> { + let key = ( + tag, + self.keep.map_or(0, |keep| keep.as_ptr() as usize), + self.keep.map_or(0, |keep| keep.len()), + ); + if let Some(plan) = self.plans.plans.get(&key) { + if plan.is_for(self.keep) { + // The chain was made under a budget that held at its own + // depth: the check that matters is the one before its last + // class. + ensure!( + depth + plan.lineage.len() - 1 < self.limits.max_depth, + "native class recursion budget exceeded" + ); + return Ok(Rc::clone(plan)); + } } // The lineage borrows the registry's classes: cloning them, every // field name with them, for each object decoded was most of a @@ -789,12 +936,29 @@ impl<'a> Cursor<'a> { None }; let plan = Rc::new(ClassPlan { - lineage, + lineage: lineage.iter().map(|c| c.tag).collect(), keys, kept, duplicate, + keep: self + .keep + .map(|keep| keep.iter().map(|field| (*field).to_string()).collect()), }); - self.plans.insert(tag, Rc::clone(&plan)); + // A plan is kept only while the cache's cost stays within its limit: + // one that would pass it empties the cache, and one dearer than the + // limit alone is not kept. The decode has the plan either way. + let cost = plan.cost(); + if cost <= self.plans.limit { + if self.plans.cost + cost > self.plans.limit { + self.plans.plans.clear(); + self.plans.cost = 0; + self.plans.clears += 1; + } + if let Some(replaced) = self.plans.plans.insert(key, Rc::clone(&plan)) { + self.plans.cost = self.plans.cost.saturating_sub(replaced.cost()); + } + self.plans.cost += cost; + } Ok(plan) } fn field(&mut self, f: &Field, depth: usize) -> Result { @@ -934,7 +1098,16 @@ fn identifier(value: &Value) -> Result { /// remain unconsumed. Callers must separately establish current record ownership. pub fn decode(body: &[u8], registry: &Registry) -> Result { let base = crate::native_element::decode_base(body, registry)?; - let mut cursor = Cursor::new(body, registry, None, 2, GraphLimits::default(), Spans::None); + let mut plans = PlanCache::default(); + let mut cursor = Cursor::new( + body, + registry, + None, + 2, + GraphLimits::default(), + Spans::None, + &mut plans, + ); let fields = cursor.class::(base.class_tag, 0)?; let derived_fields_end = cursor.pos; let mut parameters = Vec::new(); diff --git a/tools/ci/scrub-holdout-results.sh b/tools/ci/scrub-holdout-results.sh index 95ddf744..9b7fd6d5 100755 --- a/tools/ci/scrub-holdout-results.sh +++ b/tools/ci/scrub-holdout-results.sh @@ -75,10 +75,14 @@ # fine tessellation decides some bodies and the line that counts them (RE-331 # section 8, #1365: the same), the counts of the IFC4 schema's findings on # the export (`schema validate:` and `schema rules:` lines, #1430: a number, and -# for a rule finding the entity and rule that name it, never its message), and -# the `wall rules:` count lines of tools/re/wall_rule_counts.py (RE-348, RE-349: -# how many walls take the wall base and end rules and why the others do not, as -# numbers beside fixed words and reason names, never an ElementId), and +# for a rule finding the entity and rule that name it, never its message), the +# `projected graphs:` count lines of probe_1335_projected_graphs (#1335: counts +# of records decoded and of differences, a verdict word for the catalog, schema +# class names and the field names of the readers' lists, never a file name or an +# ElementId) with its `probe: exit N` line, the `wall rules:` count lines of +# tools/re/wall_rule_counts.py (RE-348, RE-349: how many walls take the wall base +# and end rules and why the others do not, as numbers beside fixed words and +# reason names, never an ElementId), and # ifc.sha256, the hash of rvt-rs's own export; every other file is deleted. set -euo pipefail @@ -110,6 +114,13 @@ for dir in "$RESULTS"/holdout-*/ "$RESULTS"/bimcollab-*/ "$RESULTS"/racadv-*/; d grep -E '^native readers: [A-Za-z0-9 _:.,;()=#<>/+-]+$' "$f" || true grep -E '^saved graphics pre2023: [A-Za-z0-9 _:.,;()=#<>/+-]+$' "$f" || true grep -E '^census stages: [A-Za-z0-9 _:.,;()=#<>/+-]+$' "$f" || true + # probe_1335_projected_graphs (#1335): counts, verdict words, schema class + # names (a class name is the schema's, never the model's) and the field + # names of the readers' lists, as numbers beside fixed words. Its `file` + # line names the model's file and its `native:` line may carry an error + # with an ElementId; both are dropped, and `exit N` stays. + grep -E '^(global-catalog [a-z-]+|compared records [0-9]+ decodes [0-9]+ full-ok [0-9]+ full-refused [0-9]+ mismatches [0-9]+ budget-stopped (true|false)|keep [A-Za-z0-9_,]* compared [0-9]+ mismatches [0-9]+|refused [A-Za-z0-9_]* [0-9]+|binding-context family-categories [0-9]+ symbol-families [0-9]+ same (true|false) ingest-refusals [0-9]+ same-refusals (true|false)|wall-facts compared [0-9]+ same (true|false)|plan-cache compared [0-9]+ mismatches [0-9]+|plan-cache limited compared [0-9]+ mismatches [0-9]+|plan-cache size peak-entries [0-9]+ peak-cost [0-9]+ clears [0-9]+|mismatch [a-z-]+ [A-Za-z0-9_]*)$' "$f" | sed 's/^/projected graphs: /' || true + grep -E '^exit [0-9]+$' "$f" | sed 's/^/probe: /' || true done for f in "$dir"wall_rules.txt; do [ -f "$f" ] || continue