aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-08 18:35:27 +0000
committertv <tv@krebsco.de>2026-11-08 19:36:58 +0100
commit9b58d2e870fddb5f04e846b0566dd6a613d2f699 (patch)
treed94d8faa5fee1135128e05a090ed8cd66a28df94
parente9813db05f2cc8590b785639a3c41ba60812909a (diff)
bench: what screening costs while catching up
Review 2, N7, measured first: a node whose document has no policy (a joiner's empty one) applies what it receives unjudged. Screening it too would cost finding every change's paths and judging it. bench_screening takes 1 000, 10 000 and 100 000 changes into an empty document, as one delta brings them: unjudged, as Automerge applies them, against screened (access::screen::apply), with no policy in them (each passes, read at its dependencies, after its paths were found) and with the policy their first change wrote (each judged by the rules).
-rw-r--r--docs/development.md4
-rw-r--r--src/document/bench.rs108
2 files changed, 110 insertions, 2 deletions
diff --git a/docs/development.md b/docs/development.md
index 2fb7ab5..85f6fce 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -19,8 +19,8 @@ nor arti; among Mycelium's tests is a swarm simulated on its envelope).
The benchmarks are ignored tests named `bench_*`: the document's (writes and sync
messages, what signing and checking every change would cost: `bench_change_signatures`,
-and knowing every change's paths: `bench_change_paths`;
-`document/bench.rs`), the remakes' replay (`remake/bench.rs`), and what
+knowing every change's paths: `bench_change_paths`, and judging every change while
+catching up: `bench_screening`; `document/bench.rs`), the remakes' replay (`remake/bench.rs`), and what
passing changes on costs over the simulated network (`simtest/bench.rs`: messages and
bytes per write by kind, propagation delays, for a full mesh, a writer behind NAT (in a full mesh, and in a partial one), two
transports, partial meshes of 50 and 100 nodes, lossy links (with delays at random,
diff --git a/src/document/bench.rs b/src/document/bench.rs
index bc1f6fa..467dc2f 100644
--- a/src/document/bench.rs
+++ b/src/document/bench.rs
@@ -29,6 +29,13 @@
//! rebuilds them, and from the bytes they came as; and inspected before they are
//! applied (`inspect::Inspector`), their objects made among them or looked up in the
//! document, each once.
+//!
+//! `screening`: what judging every change received costs while catching up
+//! (`access::screen`), against applying them unjudged, as a node whose document has no
+//! policy does now: many changes into an empty document, as one delta brings them;
+//! judged with no policy in them (each passes, read at its dependencies, after its
+//! paths were found), and with the policy their first change wrote (each judged by the
+//! rules).
use std::time::{Duration, Instant};
@@ -607,3 +614,104 @@ fn bench_change_paths() {
);
}
}
+
+/// `changes` changes on `doc`, each writing `servers/<region>/<n>/cpu`, by `actors`
+/// actors in turn, as `nested` does.
+fn nested_on(mut doc: Document, changes: usize, actors: usize) -> Document {
+ let per = changes.div_ceil(actors.max(1));
+ for i in 0..changes {
+ if i % per == 0 {
+ doc.set_actor(automerge::ActorId::random());
+ }
+ let path = [
+ "servers".to_owned(),
+ format!("region{}", i % 4),
+ format!("{}", (i / 4) % 50),
+ "cpu".to_owned(),
+ ];
+ transact(&mut doc, UnixMillis(1), &WRITER, |doc| {
+ store::write_path(doc, &path, &serde_json::json!(i))
+ })
+ .expect("write");
+ }
+ doc
+}
+
+/// The time to take `doc`'s changes into an empty document: unjudged, as Automerge
+/// applies them; and screened (`access::screen::apply`).
+fn screening_costs(workload: &str, doc: &Document) {
+ let raw: Vec<Vec<u8>> = doc
+ .get_changes(&[])
+ .iter()
+ .map(|change| change.raw_bytes().to_vec())
+ .collect();
+ let changes = || -> Vec<automerge::Change> {
+ raw.iter()
+ .map(|bytes| automerge::Change::from_bytes(bytes.clone()).expect("change"))
+ .collect()
+ };
+ let unjudged = median(3, || {
+ let changes = changes();
+ let mut empty = Document::new();
+ let started = Instant::now();
+ empty.apply_changes(changes).expect("apply");
+ started.elapsed()
+ });
+ let screened = median(3, || {
+ let changes = changes();
+ let mut empty = Document::new();
+ let started = Instant::now();
+ let screened = crate::access::screen::apply(
+ &mut empty,
+ Epoch::GENESIS,
+ changes,
+ &mut crate::access::screen::Waiting::default(),
+ &mut crate::access::screen::Refusals::default(),
+ None,
+ &mut crate::document::PatchLog::inactive(),
+ )
+ .expect("screen");
+ let elapsed = started.elapsed();
+ assert_eq!(screened.applied, raw.len(), "every change taken");
+ elapsed
+ });
+ report_signatures(
+ workload,
+ unjudged,
+ &[("screened", screened.saturating_sub(unjudged))],
+ );
+}
+
+#[test]
+#[ignore = "a benchmark: cargo benchmarks"]
+fn bench_screening() {
+ println!();
+ for changes in [1_000, 10_000, 100_000] {
+ screening_costs(
+ &format!("{changes} changes, no policy"),
+ &nested(changes, changes / 100),
+ );
+ // The swarm, its policy and the writer's member entry first: every change after
+ // is judged by the rules (no registry: the writer's node, a member, may).
+ let mut start = Document::new();
+ transact(&mut start, UnixMillis(1), &WRITER, |doc| {
+ let path = |text: &str| -> Vec<String> { text.split('/').map(str::to_owned).collect() };
+ store::write_path(doc, &path("_continuum/swarm"), &serde_json::json!("bench"))?;
+ store::write_path(
+ doc,
+ &path("_continuum/access/policy"),
+ &serde_json::json!({"version": 1}),
+ )?;
+ store::write_path(
+ doc,
+ &crate::swarm::members::member_path(WRITER.node),
+ &serde_json::json!({}),
+ )
+ })
+ .expect("start");
+ screening_costs(
+ &format!("{changes} changes, the policy first"),
+ &nested_on(start, changes, changes / 100),
+ );
+ }
+}