0017. Copy-on-Write Document Snapshots with Arc & Arc::make_mut
Wrapping BoardModel collections in Arc<Vec<T>> and Arc<HashMap<K, V>> (serde rc feature) so taking a snapshot is O(1), mutating a review pin clones only the touched HashMap, and proptest verifies structural pointer sharing (Arc::ptr_eq).
Why O(N) Deep Clones Break 60 FPS Immediate-Mode CAD Apps
Suppose a production PCB layout holds 50,000 TraceSegments, 20,000 SmdPads, and 2,000 ComponentSymbols, while an engineer adds or edits a single ReviewNote pin. If BoardModel stores plain Vec<T> and HashMap<K, V> fields, calling let snapshot = self.board.clone() before an edit allocates and copies every trace, pad, and component on the heap (O(N) memory and CPU time).Structural Sharing (Arc Copy-on-Write): By wrapping each collection in Arc<...> (Arc<Vec<TraceSegment>>, Arc<Vec<SmdPad>>, Arc<Vec<ComponentSymbol>>, Arc<HashMap<PinId, ReviewNote>>), BoardModel::clone() copies only four 8-byte pointers and increments four atomic reference counts (O(1)).
When we need to mutate a collection—such as inserting or updating a review pin—we call Arc::make_mut(&mut self.review_pins):
- If
strong_count == 1(no snapshot shares this buffer):Arc::make_mutreturns a&mut HashMap<PinId, ReviewNote>in-place with zero allocation. - If
strong_count > 1(a snapshot or undo entry shares this buffer):Arc::make_mutclones onlyreview_pinsinto a newArcallocation and mutates the new copy, whiletraces,pads, andcomponentsremain 100% pointer-shared (Arc::ptr_eq == true) with the snapshot!
flowchart TD
subgraph Baseline["baseline_snapshot: BoardModel"]
B1["traces: Arc"]
B2["pads: Arc"]
B3["components: Arc"]
B4["review_pins: Arc"]
end
subgraph Working["working: BoardModel (after Arc::make_mut(&mut working.review_pins))"]
W1["traces: Arc"]
W2["pads: Arc"]
W3["components: Arc"]
W4["review_pins: Arc (Cloned)"]
end
HeapT["Heap: Vec<TraceSegment> (Shared)"]
HeapP["Heap: Vec<SmdPad> (Shared)"]
HeapC["Heap: Vec<ComponentSymbol> (Shared)"]
HeapROld["Heap: HashMap (2 Pins — Baseline)"]
HeapRNew["Heap: HashMap (3 Pins — Working)"]
B1 & W1 --> HeapT
B2 & W2 --> HeapP
B3 & W3 --> HeapC
B4 --> HeapROld
W4 --> HeapRNew
style HeapT fill:#14231c,stroke:#3dd68c,color:#e6edf3
style HeapP fill:#14231c,stroke:#3dd68c,color:#e6edf3
style HeapC fill:#14231c,stroke:#3dd68c,color:#e6edf3
style HeapROld fill:#141a21,stroke:#3b8eea,color:#e6edf3
style HeapRNew fill:#2a1b14,stroke:#ff8b6b,color:#e6edf3
Serializing Arc<T> with serde's "rc" Feature & Property Testing with proptest
To keep BoardModel's #[derive(Serialize, Deserialize)] working seamlessly across Arc<...> fields, we enable serde's "rc" feature and add proptest under [dev-dependencies] using cargo add:Tier 2 Property-Based Testing (proptest): Unit tests check fixed examples; proptest! generates hundreds of randomized (x, y, comment) inputs and shrinks any failure to its minimal counterexample.
cargo add --package pcb-doc serde --features derive,rc
cargo add --package pcb-doc --dev proptest
Here is the resulting crates/pcb-doc/Cargo.toml:
[package]
name = "pcb-doc"
version = "0.1.0"
edition = "2024"
[lints]
workspace = true
[dependencies]
emath = { version = "0.37", features = ["serde"] }
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0"
thiserror = "2.0"
[dev-dependencies]
proptest = "1.11"
We also add shared_arc_buffers(&self, other: &Self) -> usize using Arc::ptr_eq so both our proptest! suite and the Panel::right UI ("Shared Arc Buffers: 3 / 4") can inspect structural sharing directly. Let's walk through all the new Copy-on-Write blocks across crates/pcb-doc/src/lib.rs and src/main.rs, then view the assembled listing in context.
Block 1: Wrapping BoardModel Collections in Arc<...> & Measuring Pointer Sharing (crates/pcb-doc/src/lib.rs)
use std::{
collections::{HashMap, hash_map::Entry},
sync::Arc,
};
/// Copy-on-Write (`Arc`) domain model: cloning `BoardModel` copies only four `Arc`
/// pointers (`O(1)`), and mutating a collection via `Arc::make_mut` clones only the
/// touched collection while sharing all untouched buffers across snapshots.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct BoardModel {
pub traces: Arc<Vec<TraceSegment>>,
pub pads: Arc<Vec<SmdPad>>,
pub components: Arc<Vec<ComponentSymbol>>,
pub review_pins: Arc<HashMap<PinId, ReviewNote>>,
pub next_pin_id: u32,
}
impl Default for BoardModel {
fn default() -> Self {
Self {
traces: Arc::new(sample_traces()),
pads: Arc::new(sample_pads()),
components: Arc::new(sample_components()),
review_pins: Arc::new(sample_review_pins()),
next_pin_id: 3,
}
}
}
impl BoardModel {
/// Counts how many of the 4 `Arc` collection buffers (`0..=4`) share the exact
/// same heap allocation (`Arc::ptr_eq`) with `other`.
pub fn shared_arc_buffers(&self, other: &Self) -> usize {
usize::from(Arc::ptr_eq(&self.traces, &other.traces))
+ usize::from(Arc::ptr_eq(&self.pads, &other.pads))
+ usize::from(Arc::ptr_eq(&self.components, &other.components))
+ usize::from(Arc::ptr_eq(&self.review_pins, &other.review_pins))
}
}
Arc<Vec<...>>&Arc<HashMap<...>>: Wrapping each collection instd::sync::Arcturns#[derive(Clone)]onBoardModelfrom anO(N)deep heap allocation into fourO(1)atomic reference-count increments. BecauseArc<T>implementsDeref<Target = T>, read-only callers (self.pads.iter(),self.review_pins.get(&id)) continue to work transparently!Arc::ptr_eq(&self.traces, &other.traces): Compares the raw heap data pointers of twoArchandles inO(1)time without inspecting the elements inside the vector or map.usize::from(bool): Convertstrueinto1_usizeandfalseinto0_usizecleanly withoutascasts, summing how many of the four collection buffers (0..=4) still point to the exact same heap allocation.
Block 2: Copy-on-Write Mutations with Arc::make_mut (crates/pcb-doc/src/lib.rs)
impl BoardModel {
/// Appends a copper pad using copy-on-write (`Arc::make_mut`).
pub fn push_smd_pad(&mut self, pad: SmdPad) {
Arc::make_mut(&mut self.pads).push(pad);
}
/// Clears all review pins using copy-on-write (`Arc::make_mut`).
pub fn clear_review_pins(&mut self) {
Arc::make_mut(&mut self.review_pins).clear();
}
pub fn insert_review_pin(
&mut self,
id: PinId,
position_mm: Pos2,
author: &str,
comment: &str,
severity: PinSeverity,
) -> bool {
match Arc::make_mut(&mut self.review_pins).entry(id) {
Entry::Vacant(slot) => {
slot.insert(ReviewNote {
id,
position_mm,
author: author.to_owned(),
comment: comment.to_owned(),
severity,
status: ReviewStatus::Open,
});
true
}
Entry::Occupied(_) => false,
}
}
pub fn transition_pin_status(&mut self, id: PinId, next: ReviewStatus) -> bool {
let Some(pin) = Arc::make_mut(&mut self.review_pins).get_mut(&id) else {
return false;
};
if pin.status.can_transition_to(next) {
pin.status = next;
true
} else {
false
}
}
}
Arc::make_mut(&mut self.review_pins): The workhorse of copy-on-write in Rust. BecauseArc<T>normally provides only shared&Taccess,Arc::make_mutchecks the strong reference count of the allocation:- If
strong_count == 1, no snapshot shares this buffer, soArc::make_muthands back&mut HashMap<PinId, ReviewNote>in-place with zero heap allocation. - If
strong_count > 1(e.g.,baseline_snapshotalso points to thisHashMap),Arc::make_mutclonesHashMap<PinId, ReviewNote>into a brand-newArc, decrements the oldArc's count, and returns&mutto the new copy—leavingtraces,pads, andcomponents100% shared (3 / 4).
- If
push_smd_pad&clear_review_pins: Dedicated mutation helpers onBoardModelthat encapsulateArc::make_mut(&mut self.pads)andArc::make_mut(&mut self.review_pins).
Block 3: O(1) Baseline Snapshots & Telemetry UI in PcbReviewerApp (src/main.rs)
pub struct PcbReviewerApp {
pub board: BoardModel,
pub baseline_snapshot: BoardModel,
pub view: ViewState,
pub saved_session_json: String,
}
impl Default for PcbReviewerApp {
fn default() -> Self {
let board = BoardModel::default();
let baseline_snapshot = board.clone();
Self {
board,
baseline_snapshot,
view: ViewState::default(),
saved_session_json: String::new(),
}
}
}
impl PcbReviewerApp {
/// Records an `O(1)` copy-on-write snapshot by cloning the 4 `Arc` pointers in `self.board`.
pub fn take_baseline_snapshot(&mut self) {
self.baseline_snapshot = self.board.clone();
}
/// Restores `self.board` from `self.baseline_snapshot` in `O(1)` pointer copies.
pub fn restore_baseline_snapshot(&mut self) {
self.board = self.baseline_snapshot.clone();
}
/// Returns how many of the 4 `Arc` buffers (`0..=4`) are pointer-shared with `baseline_snapshot`.
pub fn shared_buffers_with_baseline(&self) -> usize {
self.board.shared_arc_buffers(&self.baseline_snapshot)
}
pub fn render_session_panel(&mut self, ui: &mut egui::Ui) {
ui.heading("CoW Snapshots (Arc)");
ui.label(format!(
"Shared Arc Buffers: {} / 4",
self.shared_buffers_with_baseline()
));
if ui.button("Take Snapshot").clicked() {
self.take_baseline_snapshot();
}
if ui.button("Restore Snapshot").clicked() {
self.restore_baseline_snapshot();
}
ui.separator();
ui.heading("Session Persistence (JSON)");
// ... existing Board ID, Export JSON, Reload JSON, Test Corrupt JSON, and I/O Error banner ...
}
}
- Custom
impl Default for PcbReviewerApp: Replaces#[derive(Default)]sobaseline_snapshotis initialized vialet baseline_snapshot = board.clone();. If we used#[derive(Default)], Rust would callBoardModel::default()twice, allocating two separate sets ofArcbuffers (0 / 4shared) instead of sharing the initial allocations (4 / 4shared)! take_baseline_snapshot&restore_baseline_snapshot: Both operations execute inO(1)time by cloning only the fourArcpointers insideBoardModel.- Live
"Shared Arc Buffers: {} / 4"Readout: Displaysself.shared_buffers_with_baseline()insidePanel::right("session_panel")so the user can watch the count drop from4 / 4to3 / 4when a review pin is edited, and jump back to4 / 4when clicking"Take Snapshot"or"Restore Snapshot".
Block 4: Pointer-Sharing Unit Tests, proptest! Suite & egui_kittest Verification (crates/pcb-doc/src/lib.rs & src/main.rs)
// In crates/pcb-doc/src/lib.rs (#[cfg(test)] mod tests):
use proptest::prelude::*;
#[test]
fn shared_arc_buffers_tracks_copy_on_write_pointer_sharing_across_all_four_collections() {
let baseline = BoardModel::default();
let mut working = baseline.clone();
assert_eq!(working.shared_arc_buffers(&baseline), 4);
working.clear_review_pins();
assert!(working.review_pins.is_empty());
assert_eq!(baseline.review_pins.len(), 2);
assert_eq!(working.shared_arc_buffers(&baseline), 3);
working.push_smd_pad(SmdPad {
center_mm: pos2(10.0, 10.0),
size_mm: vec2(2.0, 2.0),
layer: PcbLayer::TopCopper,
net: NetId(1),
});
assert_eq!(working.pads.len(), 4);
assert_eq!(baseline.pads.len(), 3);
assert_eq!(working.shared_arc_buffers(&baseline), 2);
Arc::make_mut(&mut working.components).pop();
assert_eq!(working.shared_arc_buffers(&baseline), 1);
Arc::make_mut(&mut working.traces).pop();
assert_eq!(working.shared_arc_buffers(&baseline), 0);
}
proptest! {
#[test]
fn prop_cow_pin_mutation_preserves_baseline_snapshot_and_shares_untouched_arcs(
x in 5.0_f32..95.0_f32,
y in 5.0_f32..65.0_f32,
comment in "[a-zA-Z0-9 _-]{1,24}",
) {
let baseline = BoardModel::default();
let mut working = baseline.clone();
prop_assert_eq!(working.shared_arc_buffers(&baseline), 4);
let added_id = working.add_review_pin_at(
pos2(x, y),
"proptest",
&comment,
PinSeverity::Warning,
);
prop_assert_eq!(baseline.review_pins.len(), 2);
prop_assert!(!baseline.review_pins.contains_key(&added_id));
prop_assert_eq!(working.review_pins.len(), 3);
prop_assert_eq!(&working.review_pins[&added_id].comment, &comment);
prop_assert!(Arc::ptr_eq(&baseline.traces, &working.traces));
prop_assert!(Arc::ptr_eq(&baseline.pads, &working.pads));
prop_assert!(Arc::ptr_eq(&baseline.components, &working.components));
prop_assert!(!Arc::ptr_eq(&baseline.review_pins, &working.review_pins));
prop_assert_eq!(working.shared_arc_buffers(&baseline), 3);
}
}
- Deterministic
4 -> 3 -> 2 -> 1 -> 0Sharing Progression:shared_arc_buffers_tracks_copy_on_write_pointer_sharing_across_all_four_collectionsmutatesreview_pins,pads,components, andtracesone by one, proving that eachArc::make_mutdetaches only its own collection whilebaselineretains its original elements. proptest! { ... }: Generates randomized floating-point coordinates (x in 5.0_f32..95.0_f32,y in 5.0_f32..65.0_f32) and regex-matched comment strings ("[a-zA-Z0-9 _-]{1,24}"), usingprop_assert!andprop_assert_eq!to verify that adding any review pin leavesbaselineuntouched and keepstraces,pads, andcomponentspointer-equal (Arc::ptr_eq).
Complete Lesson 0017 Copy-on-Write Snapshot Listing (crates/pcb-doc/src/lib.rs & src/main.rs)
Here are all the new and updated Lesson 0017 sections across crates/pcb-doc/src/lib.rs and src/main.rs in full context:
// === crates/pcb-doc/src/lib.rs ===
use std::{
collections::{HashMap, hash_map::Entry},
sync::Arc,
};
/// Copy-on-Write (`Arc`) domain model: cloning `BoardModel` copies only four `Arc`
/// pointers (`O(1)`), and mutating a collection via `Arc::make_mut` clones only the
/// touched collection while sharing all untouched buffers across snapshots.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct BoardModel {
pub traces: Arc<Vec<TraceSegment>>,
pub pads: Arc<Vec<SmdPad>>,
pub components: Arc<Vec<ComponentSymbol>>,
pub review_pins: Arc<HashMap<PinId, ReviewNote>>,
pub next_pin_id: u32,
}
impl Default for BoardModel {
fn default() -> Self {
Self {
traces: Arc::new(sample_traces()),
pads: Arc::new(sample_pads()),
components: Arc::new(sample_components()),
review_pins: Arc::new(sample_review_pins()),
next_pin_id: 3,
}
}
}
impl BoardModel {
/// Counts how many of the 4 `Arc` collection buffers (`0..=4`) share the exact
/// same heap allocation (`Arc::ptr_eq`) with `other`.
pub fn shared_arc_buffers(&self, other: &Self) -> usize {
usize::from(Arc::ptr_eq(&self.traces, &other.traces))
+ usize::from(Arc::ptr_eq(&self.pads, &other.pads))
+ usize::from(Arc::ptr_eq(&self.components, &other.components))
+ usize::from(Arc::ptr_eq(&self.review_pins, &other.review_pins))
}
/// Appends a copper pad using copy-on-write (`Arc::make_mut`).
pub fn push_smd_pad(&mut self, pad: SmdPad) {
Arc::make_mut(&mut self.pads).push(pad);
}
/// Clears all review pins using copy-on-write (`Arc::make_mut`).
pub fn clear_review_pins(&mut self) {
Arc::make_mut(&mut self.review_pins).clear();
}
pub fn insert_review_pin(
&mut self,
id: PinId,
position_mm: Pos2,
author: &str,
comment: &str,
severity: PinSeverity,
) -> bool {
match Arc::make_mut(&mut self.review_pins).entry(id) {
Entry::Vacant(slot) => {
slot.insert(ReviewNote {
id,
position_mm,
author: author.to_owned(),
comment: comment.to_owned(),
severity,
status: ReviewStatus::Open,
});
true
}
Entry::Occupied(_) => false,
}
}
pub fn transition_pin_status(&mut self, id: PinId, next: ReviewStatus) -> bool {
let Some(pin) = Arc::make_mut(&mut self.review_pins).get_mut(&id) else {
return false;
};
if pin.status.can_transition_to(next) {
pin.status = next;
true
} else {
false
}
}
}
// === src/main.rs ===
pub struct PcbReviewerApp {
pub board: BoardModel,
pub baseline_snapshot: BoardModel,
pub view: ViewState,
pub saved_session_json: String,
}
impl Default for PcbReviewerApp {
fn default() -> Self {
let board = BoardModel::default();
let baseline_snapshot = board.clone();
Self {
board,
baseline_snapshot,
view: ViewState::default(),
saved_session_json: String::new(),
}
}
}
impl PcbReviewerApp {
/// Records an `O(1)` copy-on-write snapshot by cloning the 4 `Arc` pointers in `self.board`.
pub fn take_baseline_snapshot(&mut self) {
self.baseline_snapshot = self.board.clone();
}
/// Restores `self.board` from `self.baseline_snapshot` in `O(1)` pointer copies.
pub fn restore_baseline_snapshot(&mut self) {
self.board = self.baseline_snapshot.clone();
}
/// Returns how many of the 4 `Arc` buffers (`0..=4`) are pointer-shared with `baseline_snapshot`.
pub fn shared_buffers_with_baseline(&self) -> usize {
self.board.shared_arc_buffers(&self.baseline_snapshot)
}
pub fn render_session_panel(&mut self, ui: &mut egui::Ui) {
ui.heading("CoW Snapshots (Arc)");
ui.label(format!(
"Shared Arc Buffers: {} / 4",
self.shared_buffers_with_baseline()
));
if ui.button("Take Snapshot").clicked() {
self.take_baseline_snapshot();
}
if ui.button("Restore Snapshot").clicked() {
self.restore_baseline_snapshot();
}
ui.separator();
ui.heading("Session Persistence (JSON)");
ui.label(format!("Board ID: {}", self.view.active_board_id));
if ui.button("Export JSON").clicked() {
self.export_session_snapshot();
}
if ui.button("Reload JSON").clicked() {
let raw = self.saved_session_json.clone();
self.import_session_from_json(&raw);
}
if ui.button("Test Corrupt JSON").clicked() {
self.import_session_from_json("{corrupt");
}
if let Some(err) = self.view.last_io_error.clone() {
ui.label(format!("I/O Error: {err}"));
if ui.button("Dismiss Error").clicked() {
self.view.dismiss_io_error();
}
}
}
}
Arc::make_mut & Arc::ptr_eq Visualizer
Arc<Vec<T>> • Arc::make_mut • shared_arc_buffers
Mutate individual collections on working: BoardModel or take/restore a baseline snapshot. Watch how Arc::make_mut clones only the touched collection when strong_count > 1, leaving untouched buffers pointer-shared (Arc::ptr_eq == true).
1. Document Operations
2. Pointer Sharing Telemetry
Arc Buffers with Arc::make_mut & Measure Sharing with Arc::ptr_eq
Skill Practice • Copy-on-Write Arc
Complete shared_arc_buffers, clear_review_pins, and transition_pin_status on BoardModel so mutations clone only the touched Arc buffer when shared.
pub fn shared_arc_buffers(&self, other: &Self) -> usize {
usize::from(Arc::)
+ usize::from(Arc::ptr_eq(&self.pads, &other.pads))
+ usize::from(Arc::ptr_eq(&self.components, &other.components))
+ usize::from(Arc::ptr_eq(&self.review_pins, &other.review_pins))
}
pub fn clear_review_pins(&mut self) {
Arc::.clear();
}
pub fn transition_pin_status(&mut self, id: PinId, next: ReviewStatus) -> bool {
let Some(pin) = Arc::.get_mut(&id) else {
return false;
};
if pin.status.can_transition_to(next) {
pin.status = next;
true
} else {
false
}
}
proptest & Mutation Gate (0017)
cargo test --workspace + proptest + cargo mutants
Run the workspace tests (including prop_cow_pin_mutation_preserves_baseline_snapshot_and_shares_untouched_arcs) and the mutation suite for the new CoW methods:
cargo test --manifest-path ./apps/0017-copy-on-write-arc-document-snapshots/Cargo.toml --workspace
cargo mutants --manifest-path ./apps/0017-copy-on-write-arc-document-snapshots/Cargo.toml --workspace --in-place -F "shared_arc_buffers|push_smd_pad|clear_review_pins|take_baseline_snapshot|restore_baseline_snapshot|shared_buffers_with_baseline|render_session_panel"
Recommended Primary Source
std::sync::Arc::make_mut Official Rust Standard Library Docs and Proptest Book — "Getting Started".
VecDeque) & Continuous Slider Drag Coalescing, where we build BoardHistory on top of our O(1) Arc snapshots and coalesce 60-FPS slider drags into a single undo step!