1//! `keystroke_probe PARAGRAPHS KEYSTROKES`: types into the middle paragraph of a page
2//! of PARAGRAPHS paragraphs and reports, per keystroke, the time, the bytes the cache
3//! writes to disk and the bytes the section file sees: queued alone, then each one
4//! published at once, then idle polls. The section file stays in memory so the
5//! process's disk writes (macOS `proc_pid_rusage`) are the cache's.
6
7use notebook::{Remote, Replica};
8use onestore::{
9 CommitError, CommitIo, ExGuid, Stamp, Transaction,
10 document::{Format, Layout},
11 op::{Edit, Op, PageOp, SectionOp},
12 page::{
13 Outline, Page, PageObject, PageParagraph, Paragraph, ParagraphContent, TextObject,
14 text::new_id,
15 },
16};
17use std::{
18 io,
19 time::{Duration, Instant},
20};
21
22fn disk_written() -> u64 {
23 // SAFETY: proc_pid_rusage fills the zeroed structure for this process.
24 unsafe {
25 let mut info: libc::rusage_info_v2 = std::mem::zeroed();
26 libc::proc_pid_rusage(
27 libc::getpid(),
28 libc::RUSAGE_INFO_V2,
29 &mut info as *mut _ as *mut _,
30 );
31 info.ri_diskio_byteswritten
32 }
33}
34
35/// The section file in memory, counting what synchronization reads and writes, so the
36/// process's disk writes are the cache's alone.
37struct Memory {
38 bytes: Vec<u8>,
39 read: u64,
40 written: u64,
41}
42
43impl CommitIo for Memory {
44 fn read_at(&mut self, offset: u64, output: &mut [u8]) -> io::Result<usize> {
45 let offset = offset as usize;
46 let size = output.len().min(self.bytes.len().saturating_sub(offset));
47 output[..size].copy_from_slice(&self.bytes[offset..offset + size]);
48 self.read += size as u64;
49 Ok(size)
50 }
51 fn write_at(&mut self, offset: u64, bytes: &[u8]) -> io::Result<usize> {
52 let offset = offset as usize;
53 self.bytes
54 .resize(self.bytes.len().max(offset + bytes.len()), 0);
55 self.bytes[offset..offset + bytes.len()].copy_from_slice(bytes);
56 self.written += bytes.len() as u64;
57 Ok(bytes.len())
58 }
59 fn flush(&mut self) -> io::Result<()> {
60 Ok(())
61 }
62}
63
64impl Remote for Memory {
65 fn read(&mut self) -> io::Result<Vec<u8>> {
66 self.read += self.bytes.len() as u64;
67 Ok(self.bytes.clone())
68 }
69 fn stamp(&mut self) -> io::Result<Stamp> {
70 self.read += 1024;
71 Stamp::of(&self.bytes).map_err(io::Error::other)
72 }
73 fn publish(&mut self, transaction: &Transaction) -> Result<(), CommitError> {
74 transaction.commit(self)
75 }
76 fn confirm(&mut self, base: &Stamp) -> Result<(), CommitError> {
77 onestore::confirm(self, base)
78 }
79}
80
81fn paragraph(i: usize) -> PageParagraph {
82 let words = [
83 "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog",
84 ];
85 let text: Vec<&str> = (0..1 + i % 17).map(|j| words[(i * 7 + j) % 8]).collect();
86 PageParagraph {
87 id: new_id().unwrap(),
88 parent: None,
89 level: 1,
90 style: None,
91 format: Format::default(),
92 content: ParagraphContent::Text(TextObject {
93 id: new_id().unwrap(),
94 date_field: None,
95 text: Paragraph::new(text.join(" "), Format::default()),
96 tags: Vec::new(),
97 }),
98 lists: Vec::new(),
99 tags: Vec::new(),
100 media: Default::default(),
101 collapsed: false,
102 }
103}
104
105fn percentiles(label: &str, mut times: Vec<Duration>, bytes: &[u64]) {
106 times.sort();
107 let at = |q: f64| times[((times.len() - 1) as f64 * q) as usize];
108 let mean = bytes.iter().sum::<u64>() as f64 / bytes.len() as f64;
109 let mut sorted = bytes.to_vec();
110 sorted.sort();
111 println!(
112 "{label}: time p50 {:.2?} p90 {:.2?} max {:.2?}; bytes mean {:.0} p50 {} max {}",
113 at(0.5),
114 at(0.9),
115 at(1.0),
116 mean,
117 sorted[sorted.len() / 2],
118 sorted[sorted.len() - 1]
119 );
120}
121
122fn main() -> Result<(), Box<dyn std::error::Error>> {
123 let mut args = std::env::args().skip(1);
124 let count: usize = args.next().ok_or("PARAGRAPHS")?.parse()?;
125 let keystrokes: usize = args.next().map_or(Ok(200), |n| n.parse())?;
126 let directory = tempfile::tempdir()?;
127 // The probe page, stored in the section file before the cache opens.
128 let source = {
129 let arena = onestore::Arena::default();
130 let mut section =
131 onestore::Section::open(&arena, onestore::create_section("probe.one", "", "Probe")?)?;
132 let creation = onestore::PageCreation::new(None, Some("Probe"), "Probe")?;
133 let page = Page {
134 title: "Probe".into(),
135 identity: None,
136 created: None,
137 margin_origin: [0.0; 2],
138 rtl: false,
139 color: None,
140 rule_lines: None,
141 objects: vec![PageObject::Outline(Outline {
142 id: new_id()?,
143 title: false,
144 min_width: None,
145 layout: Layout {
146 x: Some(36.0),
147 y: Some(86.4),
148 ..Layout::default()
149 },
150 indents: Vec::new(),
151 paragraphs: (0..count).map(paragraph).collect(),
152 unsupported: Vec::new(),
153 })],
154 definitions: Default::default(),
155 };
156 section.apply(
157 "Probe",
158 &Edit {
159 at: 133_000_000_000_000_000,
160 ops: vec![Op::Section(SectionOp::Import { creation, page })],
161 },
162 )?;
163 section.seal()?;
164 section.image()
165 };
166 println!("section {} bytes, {count} paragraphs", source.len());
167 let cache = Replica::create(directory.path().join("cache.sqlite"), &source)?;
168 let (space, _, _) = cache
169 .pages()?
170 .into_iter()
171 .find(|(_, title, _)| title == "Probe")
172 .ok_or("the probe page")?;
173 let target: ExGuid = cache
174 .page(space)?
175 .objects
176 .iter()
177 .find_map(|object| match object {
178 PageObject::Outline(outline) if outline.paragraphs.len() == count => {
179 outline.paragraphs[count / 2].text().map(|text| text.id)
180 }
181 _ => None,
182 })
183 .ok_or("the probe paragraph")?;
184 let mut remote = Memory {
185 bytes: source.clone(),
186 read: 0,
187 written: 0,
188 };
189 let mut at = 0_u32;
190 let mut keystroke = |cache: &Replica| -> Result<(), notebook::Error> {
191 cache.apply(
192 "Probe",
193 Edit {
194 at: 133_000_000_000_000_000,
195 ops: vec![Op::Page {
196 space,
197 op: PageOp::Text {
198 text: target,
199 range: at..at,
200 with: "x".into(),
201 },
202 }],
203 },
204 )?;
205 at += 1;
206 Ok(())
207 };
208
209 let (mut times, mut bytes) = (Vec::new(), Vec::new());
210 for _ in 0..keystrokes {
211 let (disk, start) = (disk_written(), Instant::now());
212 keystroke(&cache)?;
213 times.push(start.elapsed());
214 bytes.push(disk_written() - disk);
215 }
216 percentiles("queued keystroke (apply + SQLite commit)", times, &bytes);
217
218 let (disk, read, written, start) =
219 (disk_written(), remote.read, remote.written, Instant::now());
220 cache.sync_once(&mut remote)?;
221 println!(
222 "publish the {keystrokes} queued keystrokes as one batch: {:.2?}; cache {} B, file writes {} B, file reads {} B",
223 start.elapsed(),
224 disk_written() - disk,
225 remote.written - written,
226 remote.read - read
227 );
228
229 let (mut times, mut cache_bytes, mut file_bytes, mut reads) =
230 (Vec::new(), Vec::new(), Vec::new(), Vec::new());
231 for _ in 0..keystrokes {
232 let (disk, read, written, start) =
233 (disk_written(), remote.read, remote.written, Instant::now());
234 keystroke(&cache)?;
235 cache.sync_once(&mut remote)?;
236 times.push(start.elapsed());
237 file_bytes.push(remote.written - written);
238 cache_bytes.push(disk_written() - disk);
239 reads.push(remote.read - read);
240 }
241 percentiles(
242 "keystroke published at once: cache",
243 times.clone(),
244 &cache_bytes,
245 );
246 percentiles(
247 "keystroke published at once: file writes",
248 times.clone(),
249 &file_bytes,
250 );
251 percentiles("keystroke published at once: file reads", times, &reads);
252
253 let (disk, read) = (disk_written(), remote.read);
254 for _ in 0..100 {
255 assert!(cache.sync_once(&mut remote)?.edit.is_none());
256 }
257 println!(
258 "100 idle polls: cache {} B, file reads {} B",
259 disk_written() - disk,
260 remote.read - read
261 );
262 Ok(())
263}