1//! Measures Live Share's typing latency: how long after a keystroke lands in one guest's
2//! replica the other guest's section shows it, and the same from the host to a guest.
3//!
4//! `cargo run -p notebook --features live --example live_latency -- lan|relay [URL]`: `lan`
5//! meets by mDNS on this computer's loopback; `relay` through `URL`, or a relay this run
6//! starts on loopback.
7
8#[path = "../tests/support/server.rs"]
9mod server;
10
11use notebook::{
12 Replica,
13 live::{
14 Hello, Reach,
15 share::{self, Guest, Host, Sharing},
16 },
17 session::{Background, Notebook, Section},
18};
19use onestore::op::{Edit, Op, PageOp};
20use std::{
21 path::Path,
22 sync::Arc,
23 thread,
24 time::{Duration, Instant},
25};
26
27const KEYS: usize = 20;
28
29fn until(what: &str, done: impl Fn() -> bool) {
30 let deadline = Instant::now() + Duration::from_secs(60);
31 while !done() {
32 assert!(Instant::now() < deadline, "{what}");
33 thread::sleep(Duration::from_millis(1));
34 }
35}
36
37fn hello(name: &str) -> Hello {
38 Hello::new(name.into(), None).unwrap()
39}
40
41/// A guest as the app holds one: the notebook, its background, and the section open.
42struct Open {
43 _background: Background,
44 section: Section,
45}
46
47fn guest(
48 name: &str,
49 code: &str,
50 reach: Option<Reach>,
51 relay: Option<&str>,
52 cache: &Path,
53) -> (Arc<Guest>, Open) {
54 let welcome = share::join(hello(name), code, "", reach, relay).unwrap();
55 let guest = Guest::start(
56 hello(name),
57 welcome.share,
58 welcome.secret,
59 reach,
60 relay,
61 || {},
62 )
63 .unwrap();
64 until("no host", || guest.host().is_some());
65 let mut notebook = Notebook::open_hosted(Arc::clone(&guest), cache).unwrap();
66 let background = Background::hosted(Arc::clone(&guest), || {}).unwrap();
67 background.watch(notebook.replicas());
68 let replica = notebook.replica_path("Garden.one").unwrap();
69 std::fs::create_dir_all(replica.parent().unwrap()).unwrap();
70 let replica =
71 Replica::open_or_create(&replica, None, || notebook.read_section("Garden.one")).unwrap();
72 let section =
73 Section::resume_hosted("Garden.one".into(), replica, Arc::clone(&guest), || {}).unwrap();
74 background.hold("Garden.one", &section);
75 (
76 guest,
77 Open {
78 _background: background,
79 section,
80 },
81 )
82}
83
84/// Types `key` at the end of the page's text in `section`; returns the text it then reads.
85fn type_key(section: &Section, key: char) -> String {
86 let (space, text, before) = text_of(section);
87 let at = before.encode_utf16().count() as u32;
88 let op = PageOp::Text {
89 text,
90 range: at..at,
91 with: key.to_string(),
92 };
93 let edit = Edit {
94 at: 134_000_000_000_000_000,
95 ops: vec![Op::Page { space, op }],
96 };
97 section.replica().apply("Typist", edit).unwrap();
98 format!("{before}{key}")
99}
100
101fn text_of(section: &Section) -> (onestore::ExGuid, onestore::ExGuid, String) {
102 let (space, ..) = section.pages().unwrap()[0];
103 let page = section.page(space).unwrap();
104 page.objects
105 .iter()
106 .find_map(|object| match object {
107 onestore::page::PageObject::Outline(outline) => outline
108 .paragraphs
109 .iter()
110 .find_map(|p| p.text().map(|t| (space, t.id, t.text.text().to_owned()))),
111 _ => None,
112 })
113 .unwrap()
114}
115
116fn report(what: &str, mut times: Vec<Duration>) {
117 times.sort();
118 let ms = |at: usize| times[at].as_secs_f64() * 1000.0;
119 println!(
120 "{what}: median {:.0} ms, p90 {:.0} ms, worst {:.0} ms over {} keys",
121 ms(times.len() / 2),
122 ms(times.len() * 9 / 10),
123 ms(times.len() - 1),
124 times.len()
125 );
126}
127
128fn main() {
129 let args: Vec<String> = std::env::args().skip(1).collect();
130 let (reach, url) = match args.first().map(String::as_str) {
131 Some("lan") => (Some(Reach::Loopback), None),
132 Some("relay") => (
133 None,
134 Some(args.get(1).cloned().unwrap_or_else(|| {
135 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
136 let url = format!("ws://{}", listener.local_addr().unwrap());
137 thread::spawn(move || relay::server::serve(listener, Default::default()));
138 url
139 })),
140 ),
141 _ => panic!("lan or relay [URL]"),
142 };
143 let relay = url.as_deref();
144 let directory = tempfile::tempdir().unwrap();
145 let folder = directory.path().join("Garden");
146 std::fs::create_dir_all(&folder).unwrap();
147 std::fs::write(
148 folder.join("Garden.one"),
149 onestore::create_section("Garden.one", "Typed:", "Fixture").unwrap(),
150 )
151 .unwrap();
152 let storage = Notebook::open(&folder, directory.path().join("host"))
153 .unwrap()
154 .into_storage();
155 let host = Host::start(
156 storage,
157 hello("Ada"),
158 Sharing::new("").unwrap(),
159 "Garden",
160 reach,
161 relay,
162 || {},
163 |_| Ok(()),
164 )
165 .unwrap();
166 until("no code", || {
167 host.code().is_some_and(|code| share::code(&code).is_some())
168 });
169 let code = host.code().unwrap();
170 let (_grace, grace) = guest("Grace", &code, reach, relay, &directory.path().join("g"));
171 let (_alan, alan) = guest("Alan", &code, reach, relay, &directory.path().join("a"));
172
173 let mut times = Vec::new();
174 for key in "abcdefghijklmnopqrstuvwxyz".chars().take(KEYS) {
175 let typed = Instant::now();
176 let expected = type_key(&grace.section, key);
177 until("the other guest never saw a key", || {
178 alan.section.events();
179 text_of(&alan.section).2 == expected
180 });
181 times.push(typed.elapsed());
182 thread::sleep(Duration::from_millis(150));
183 }
184 report("guest to guest", times);
185
186 // The host's own keystrokes, published to its file as its app does, then reported to
187 // the share as its app reports them.
188 let file = folder.join("Garden.one");
189 let mut times = Vec::new();
190 for key in "ABCDEFGHIJKLMNOPQRSTUVWXYZ".chars().take(KEYS) {
191 let image = std::fs::read(&file).unwrap();
192 let (space, text, before) = server::text(&image);
193 let at = before.encode_utf16().count() as u32;
194 let typed = Instant::now();
195 let next = server::typed(&image, space, text, at..at, &key.to_string());
196 std::fs::write(&file, &next).unwrap();
197 host.touched(&["Garden.one".into()]);
198 let expected = format!("{before}{key}");
199 until("the guest never saw the host's key", || {
200 alan.section.events();
201 text_of(&alan.section).2 == expected
202 });
203 times.push(typed.elapsed());
204 thread::sleep(Duration::from_millis(150));
205 }
206 report("host to guest", times);
207}