Run wire arena regressions in CI and shorten cache documentation

This commit is contained in:
Hakan Seven committed 2026-09-05 12:33:20 +03:00
1 parent ff3262e39d
commit 3253965194
4 files changed
+11 -67

No files matched your search

+1
View File
@@ -19,6 +19,7 @@ jobs:
run: |
cargo test --lib app::control::tests
cargo test --lib io::update_check::tests
cargo test --lib scene::pipeline::wire_arena::tests
- name: Validate MCP smoke script
if: runner.os != 'Windows'
run: python3 -m py_compile docs/automation/mcp_smoke.py
+7 -51
View File
@@ -432,24 +432,10 @@ struct ResidentWireLayout {
pub struct PreparedOpenGeometry {
pub wires: Arc<Vec<WireModel>>,
pub interaction_index: Option<Arc<crate::scene::pick::interaction_index::InteractionIndex>>,
/// The per-entity tessellation memo the loader thread filled while building
/// `wires`, with the guard hash it was valid under.
///
/// `prepare_open_geometry` works on a throwaway `Scene`, so this used to be
/// dropped with it and the first geometry edit re-tessellated all 186 455
/// entities from cold — 1.3 s of work the loader had already done. The
/// guard travels with it because the memo is only usable under the state it
/// was built for; a mismatch on the receiving side clears it, which is
/// exactly the old behaviour.
/// Loader tessellations and their guard; a state mismatch clears the memo.
pub tess_memo: HashMap<Handle, Arc<Vec<WireModel>>>,
pub tess_guard: u64,
/// Block definitions the loader thread built while tessellating, keyed as
/// `block_defn_cache` keys them but without an epoch — the receiving scene
/// stamps its own.
///
/// Dropped with the throwaway scene before, which is why the first edit
/// spent 1147 ms rebuilding definitions the loader had just built from the
/// same, unmodified document.
/// Loader block definitions; the receiving scene supplies its own epoch.
pub block_defns: Vec<(u64, Arc<cache::block_cache::BlockCache>)>,
}
@@ -1859,14 +1845,8 @@ pub struct Scene {
/// Boundary source → associative hatch handles. Source edits reuse this
/// index instead of scanning the document during every drag step.
associative_hatch_source_cache: RefCell<Option<HashMap<Handle, Vec<Handle>>>>,
/// Whether the drawing contains any associative centerline or center mark.
///
/// Refreshing those means scanning every entity and parsing its extended
/// data — 1.17 M entities and ~30 ms each on the reproducer, run twice on
/// every edit, to find none at all. `None` means not yet determined; the
/// first scan records the answer, and later edits only re-examine the
/// handles they actually changed. Staying `true` after the last one is
/// deleted is merely slower, never wrong.
/// Conservative association hint: unknown until scanned, then updated from
/// changed entities. Retaining `true` after deletion only costs an extra scan.
has_associative_centers: std::cell::Cell<Option<bool>>,
/// Tessellated block definitions in block-local coords, keyed by render
/// background and block epoch. Model and Paper adapt black/white colours
@@ -2589,12 +2569,7 @@ impl Scene {
changes
}
/// Whether the associative centerline / center-mark scans can be skipped.
///
/// Answers from the cached hint when it is known, and otherwise from the
/// changed entities alone: if any of them carries an association the hint
/// flips to `true` and the scans run from then on. Only an undetermined
/// hint costs a full pass, and that happens once.
/// Check changed entities and scan the document only when the hint is unknown.
fn associative_centers_possible(&self, changes: &[(Handle, ChangeKind)]) -> bool {
if changes.iter().any(|(handle, _)| {
self.document.get_entity(*handle).is_some_and(|entity| {
@@ -7943,23 +7918,8 @@ impl Scene {
}
}
/// Build the interaction handle index and the insert-hatch map now, while
/// the drawing is still opening.
///
/// Both are built lazily on first use, and first use is the user's first
/// hover over the canvas — where the cold build cost 992 ms mid-gesture
/// (`hover-detail ... handles=986.1`). They cannot move to the loader
/// thread: they read `hatches` / `meshes` / `block_meshes`, which are
/// installed on the real `Scene` after the loader is done.
///
/// This does not make the work cheaper, it moves it to where the user is
/// already waiting. A second on an open that is already seconds long reads
/// very differently from a second-long freeze on a mouse move.
/// Prepare hover indexes after derived geometry is installed during open.
pub(crate) fn warm_interaction_caches(&self) {
// Both indexes on the hover path, not just one. The handle index alone
// left `hover-detail ... handles=384.6` on the first hover: the same
// stage also builds the entity quadtree, which is keyed on
// `geometry_epoch` and equally cold at that point.
let _ = self.entity_index();
let _ = self.interaction_handle_index();
}
@@ -9179,11 +9139,7 @@ impl Scene {
if let Some((_, ref idx)) = *cache {
if let Some(sort_map) = idx.get(&block_handle) {
sorted = true;
// `sort_by_cached_key`, not `sort_by_key`: the key parses
// the wire's name out of a `String`, and `sort_by_key` calls
// it O(n log n) times. On the resident set that is ~198 k
// wires parsed eighteen times over — 295 ms on every rebuild,
// including rebuilds where nothing was re-tessellated.
// Parse each handle once while preserving stable draw order.
wires.sort_by_cached_key(|w| {
let key = Self::handle_from_wire_name(&w.name)
.map(|h| h.value())
+2 -7
View File
@@ -8,13 +8,8 @@ fn device_ceiling(device: &wgpu::Device) -> usize {
(usize::try_from(device.limits().max_buffer_size).unwrap_or(usize::MAX) / 10) * 9
}
/// The wire arena keeps one persistent instance buffer for the whole resident
/// set and patches it in place. Capping that at the per-chunk budget pushes any
/// drawing past ~524 k instances onto the batched fallback, which re-uploads
/// everything on every edit — 200 ms a click on a drawing with 821 k. A single
/// long-lived buffer can afford more than a transient chunk, and 128 MiB is
/// still four times smaller than the 505 MiB allocation that crashed a 2 GB
/// card in the first place.
/// Persistent arenas need a larger cap than transient chunks to keep large
/// drawings on the incremental upload path.
const ARENA_CAP_BYTES: usize = 128 * 1024 * 1024;
/// `OCS_GPU_CHUNK_MIB`, when set. Overrides both caps so a test can force
+1 -9
View File
@@ -2419,15 +2419,7 @@ impl Pipeline {
Some(&mut self.block_geometry),
);
// Index handle → wire slots once, here, so the per-hover selection
// overlay can gather just the highlighted wires instead of scanning +
// string-parsing the whole set every time the hovered entity changes.
//
// Parsing 198 k names is the bulk of this and each one is independent,
// so it runs across cores; only the map insertion stays serial, and
// that is cheap once the parse is done. Emission order is preserved --
// `par_iter().enumerate()` collects in index order -- which the slot
// lists rely on.
// Parse handles in parallel, preserving wire order for hover slot lists.
let t_index = crate::perf::enabled().then(iced::time::Instant::now);
let parsed: Vec<(u64, u32)> = {
use crate::par::prelude::*;