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 = Section::resume_hosted(
73 "Garden.one".into(),
74 replica,
75 Arc::clone(&guest),
76 false,
77 || {},
78 )
79 .unwrap();
80 background.hold("Garden.one", &section);
81 (
82 guest,
83 Open {
84 _background: background,
85 section,
86 },
87 )
88}
89
90/// Types `key` at the end of the page's text in `section`; returns the text it then reads.
91fn type_key(section: &Section, key: char) -> String {
92 let (space, text, before) = text_of(section);
93 let at = before.encode_utf16().count() as u32;
94 let op = PageOp::Text {
95 text,
96 range: at..at,
97 with: key.to_string(),
98 };
99 let edit = Edit {
100 at: 134_000_000_000_000_000,
101 ops: vec![Op::Page { space, op }],
102 };
103 section.replica().apply("Typist", edit).unwrap();
104 format!("{before}{key}")
105}
106
107fn text_of(section: &Section) -> (onestore::ExGuid, onestore::ExGuid, String) {
108 let (space, ..) = section.pages().unwrap()[0];
109 let page = section.page(space).unwrap();
110 page.objects
111 .iter()
112 .find_map(|object| match object {
113 onestore::page::PageObject::Outline(outline) => outline
114 .paragraphs
115 .iter()
116 .find_map(|p| p.text().map(|t| (space, t.id, t.text.text().to_owned()))),
117 _ => None,
118 })
119 .unwrap()
120}
121
122fn report(what: &str, mut times: Vec<Duration>) {
123 times.sort();
124 let ms = |at: usize| times[at].as_secs_f64() * 1000.0;
125 println!(
126 "{what}: median {:.0} ms, p90 {:.0} ms, worst {:.0} ms over {} keys",
127 ms(times.len() / 2),
128 ms(times.len() * 9 / 10),
129 ms(times.len() - 1),
130 times.len()
131 );
132}
133
134fn main() {
135 let args: Vec<String> = std::env::args().skip(1).collect();
136 let (reach, url) = match args.first().map(String::as_str) {
137 Some("lan") => (Some(Reach::Loopback), None),
138 Some("relay") => (
139 None,
140 Some(args.get(1).cloned().unwrap_or_else(|| {
141 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
142 let url = format!("ws://{}", listener.local_addr().unwrap());
143 thread::spawn(move || relay::server::serve(listener, Default::default()));
144 url
145 })),
146 ),
147 _ => panic!("lan or relay [URL]"),
148 };
149 let relay = url.as_deref();
150 let directory = tempfile::tempdir().unwrap();
151 let folder = directory.path().join("Garden");
152 std::fs::create_dir_all(&folder).unwrap();
153 std::fs::write(
154 folder.join("Garden.one"),
155 onestore::create_section("Garden.one", "Typed:", "Fixture").unwrap(),
156 )
157 .unwrap();
158 let storage = Notebook::open(&folder, directory.path().join("host"))
159 .unwrap()
160 .into_storage();
161 let host = Host::start(
162 storage,
163 hello("Ada"),
164 Sharing::new("").unwrap(),
165 "Garden",
166 reach,
167 relay,
168 || {},
169 |_| Ok(()),
170 )
171 .unwrap();
172 until("no code", || {
173 host.code().is_some_and(|code| share::code(&code).is_some())
174 });
175 let code = host.code().unwrap();
176 let (_grace, grace) = guest("Grace", &code, reach, relay, &directory.path().join("g"));
177 let (_alan, alan) = guest("Alan", &code, reach, relay, &directory.path().join("a"));
178
179 let mut times = Vec::new();
180 for key in "abcdefghijklmnopqrstuvwxyz".chars().take(KEYS) {
181 let typed = Instant::now();
182 let expected = type_key(&grace.section, key);
183 until("the other guest never saw a key", || {
184 alan.section.events();
185 text_of(&alan.section).2 == expected
186 });
187 times.push(typed.elapsed());
188 thread::sleep(Duration::from_millis(150));
189 }
190 report("guest to guest", times);
191
192 // The host's own keystrokes, published to its file as its app does, then reported to
193 // the share as its app reports them.
194 let file = folder.join("Garden.one");
195 let mut times = Vec::new();
196 for key in "ABCDEFGHIJKLMNOPQRSTUVWXYZ".chars().take(KEYS) {
197 let image = std::fs::read(&file).unwrap();
198 let (space, text, before) = server::text(&image);
199 let at = before.encode_utf16().count() as u32;
200 let typed = Instant::now();
201 let next = server::typed(&image, space, text, at..at, &key.to_string());
202 std::fs::write(&file, &next).unwrap();
203 host.touched(&["Garden.one".into()]);
204 let expected = format!("{before}{key}");
205 until("the guest never saw the host's key", || {
206 alan.section.events();
207 text_of(&alan.section).2 == expected
208 });
209 times.push(typed.elapsed());
210 thread::sleep(Duration::from_millis(150));
211 }
212 report("host to guest", times);
213}