1//! Publishes typed keystrokes to a Samba lab one at a time and reports each publication's
2//! wall time: `smb_publish_probe ADDRESS KEYSTROKES [CONTROL]`. With `CONTROL`, the
3//! `tools/smb-proxy.py` control file, each publication is marked in the proxy's trace.
4
5use notebook::{
6 EditStatus, Replica, SmbRemote,
7 smb::{Client, Credentials},
8};
9use onestore::op::{Edit, Op, PageOp};
10use std::{
11 env, fs,
12 time::{Duration, Instant, SystemTime, UNIX_EPOCH},
13};
14
15fn main() -> Result<(), Box<dyn std::error::Error>> {
16 let args: Vec<_> = env::args().skip(1).collect();
17 let [address, count, control @ ..] = &args[..] else {
18 return Err("smb_publish_probe ADDRESS KEYSTROKES [CONTROL]".into());
19 };
20 let count: u32 = count.parse()?;
21 let mark = |phase: String| -> std::io::Result<()> {
22 if let [control] = control {
23 fs::write(
24 format!("{control}.tmp"),
25 format!("{{\"phase\":\"{phase}\"}}"),
26 )?;
27 fs::rename(format!("{control}.tmp"), control)?;
28 // The proxy polls its control file every 50 ms.
29 std::thread::sleep(Duration::from_millis(120));
30 }
31 Ok(())
32 };
33 let connect = || {
34 Client::connect(
35 address,
36 "agent",
37 Credentials::default(),
38 Duration::from_secs(5),
39 )
40 };
41 let path = format!(
42 "probe-{}.one",
43 SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos()
44 );
45 let source = onestore::create_section(&path, "Body", "Probe")?;
46 connect()?.create(&path, &source)?;
47 let directory = tempfile::tempdir()?;
48 let replica = Replica::create(directory.path().join("cache.sqlite"), &source)?;
49 let space = replica.pages()?[0].0;
50 let text = replica
51 .page(space)?
52 .objects
53 .iter()
54 .find_map(|object| match object {
55 onestore::page::PageObject::Outline(outline) => outline
56 .paragraphs
57 .iter()
58 .find_map(|p| p.text().map(|t| t.id)),
59 _ => None,
60 })
61 .ok_or("no text")?;
62 let mut remote = SmbRemote::new(connect()?, path.clone(), 1 << 24);
63 mark("idle".into())?;
64 let mut idle = Vec::new();
65 for _ in 0..count {
66 let started = Instant::now();
67 assert_eq!(replica.sync_once(&mut remote)?.edit, None);
68 idle.push(started.elapsed());
69 }
70 let mut published = Vec::new();
71 for n in 0..count {
72 replica.apply(
73 "Probe",
74 Edit {
75 at: 133_000_000_000_000_000,
76 ops: vec![Op::Page {
77 space,
78 op: PageOp::Text {
79 text,
80 range: 4 + n..4 + n,
81 with: "x".into(),
82 },
83 }],
84 },
85 )?;
86 mark(format!("publish-{n}"))?;
87 let started = Instant::now();
88 let synced = replica.sync_once(&mut remote)?;
89 published.push(started.elapsed());
90 assert!(matches!(
91 synced.edit,
92 Some((_, EditStatus::Published { .. }))
93 ));
94 }
95 mark("done".into())?;
96 let quantiles = |mut times: Vec<Duration>| {
97 times.sort();
98 let at = |q: f64| times[((times.len() - 1) as f64 * q) as usize].as_secs_f64() * 1000.0;
99 format!(
100 "p50 {:.2} ms, p90 {:.2} ms, max {:.2} ms",
101 at(0.5),
102 at(0.9),
103 at(1.0)
104 )
105 };
106 println!("idle poll: {}", quantiles(idle));
107 println!("publish: {}", quantiles(published));
108 connect()?.delete(&path)?;
109 Ok(())
110}