1use super::*;
2use crate::layout::TextEngine;
3use draw::Renderer;
4use onestore::page::Page;
5use onestore::page::text::Paragraph;
6use parley::fontique::Blob;
7use std::{
8 sync::Arc,
9 time::{Duration, Instant},
10};
11
12#[test]
13#[ignore = "manual release GPU timing and cache occupancy probe"]
14fn renderer_cost() {
15 let mut engine = TextEngine::default();
16 if let Some(font) = std::env::var_os("CANVAS_TEST_SUBSTITUTE") {
17 engine
18 .register_substitute(Blob::new(Arc::new(std::fs::read(font).unwrap())))
19 .unwrap();
20 }
21 let source = std::env::var_os("CANVAS_TEST_SECTION").map(|path| std::fs::read(path).unwrap());
22 let title = std::env::var("CANVAS_TEST_PAGE").unwrap_or_default();
23 let mut scene = source
24 .as_ref()
25 .map(|bytes| load_scene(bytes, &title, &mut engine));
26 let layout = source.is_none().then(|| {
27 engine
28 .layout(
29 &Paragraph::new(
30 "A paragraph with café, trees 🌳 and a few words.\n".repeat(2000),
31 Default::default(),
32 ),
33 360.0,
34 )
35 .unwrap()
36 });
37 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
38 let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap();
39 eprintln!("canvas_gpu_adapter\t{:?}", adapter.get_info());
40 let (device, queue) = pollster::block_on(adapter.request_device(&Default::default())).unwrap();
41 let size = [1386, 759];
42 let format = wgpu::TextureFormat::Rgba8UnormSrgb;
43 let target = device.create_texture(&wgpu::TextureDescriptor {
44 label: Some("Canvas profile target"),
45 size: wgpu::Extent3d {
46 width: size[0],
47 height: size[1],
48 depth_or_array_layers: 1,
49 },
50 mip_level_count: 1,
51 sample_count: 1,
52 dimension: wgpu::TextureDimension::D2,
53 format,
54 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
55 view_formats: &[],
56 });
57 let view: draw::Target = target.create_view(&Default::default()).into();
58 let mut renderer = Renderer::new(device, queue, format);
59 let occupancy = renderer.occupancy();
60 eprintln!(
61 "canvas_gpu_allocated_bytes\tatlas\t{}\tvertex_buffer\t{}\ttarget\t{}",
62 occupancy.atlas_bytes,
63 occupancy.vertex_buffer_bytes,
64 u64::from(size[0]) * u64::from(size[1]) * 4
65 );
66 for phase in [
67 "warm",
68 "pan",
69 "zoom",
70 "evict",
71 "offscreen_x",
72 "offscreen_y",
73 "reopen",
74 ] {
75 if phase == "reopen" && source.is_none() {
76 continue;
77 }
78 if phase.starts_with("offscreen") {
79 renderer.clear_glyph_cache();
80 renderer.clear_images();
81 }
82 for sample in 0..32 {
83 if phase == "evict" {
84 renderer.clear_glyph_cache();
85 renderer.clear_images();
86 }
87 let mut viewport = Viewport {
88 size,
89 scale: 96.0 / 72.0,
90 origin: [0.0; 2],
91 };
92 match phase {
93 "pan" => viewport.origin[1] = -(sample as f32 * 63.25),
94 "zoom" => viewport.scale *= [0.5, 0.75, 1.0, 1.25, 1.5, 2.0, 3.0, 4.0][sample % 8],
95 "offscreen_x" => viewport.origin[0] = 1_000_000.0,
96 "offscreen_y" => viewport.origin[1] = 1_000_000.0,
97 "reopen" => {
98 scene = Some(load_scene(source.as_ref().unwrap(), &title, &mut engine));
99 }
100 _ => {}
101 }
102 let mut primitives = Vec::new();
103 if let Some(scene) = &scene {
104 scene
105 .append_primitives(&mut primitives, [0.0; 2], Paper::WHITE)
106 .unwrap();
107 } else {
108 primitives.push(draw::Primitive::Text {
109 clip: None,
110 text: layout.as_ref().unwrap(),
111 origin: [36.0, 90.0],
112 ink: Paper::WHITE.ink,
113 });
114 }
115 let start = Instant::now();
116 renderer
117 .draw(&view, size, [1.0; 4], &[viewport.layer(&primitives)])
118 .unwrap();
119 let submitted = start.elapsed().as_nanos();
120 renderer
121 .device()
122 .poll(wgpu::PollType::Wait {
123 submission_index: None,
124 timeout: Some(Duration::from_secs(10)),
125 })
126 .unwrap();
127 let completed = start.elapsed().as_nanos();
128 let occupancy = renderer.occupancy();
129 assert!(occupancy.within_budget);
130 eprintln!(
131 "canvas_gpu_sample\t{phase}\t{sample}\t{submitted}\t{completed}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}",
132 occupancy.glyphs,
133 occupancy.atlas_texels,
134 occupancy.images,
135 occupancy.image_bytes,
136 occupancy.vertices,
137 occupancy.batches,
138 occupancy.vertex_capacity_bytes,
139 occupancy.batch_capacity_bytes,
140 occupancy.glyph_capacity,
141 occupancy.image_capacity
142 );
143 }
144 }
145}
146
147fn load_scene(bytes: &[u8], title: &str, engine: &mut TextEngine) -> page::PageScene {
148 let store = onestore::Store::parse(bytes).unwrap();
149 let index = onestore::RevisionIndex::parse(&store).unwrap();
150 page::PageScene::new(
151 Page::from_document(&onestore::document::Document::parse(&index).unwrap(), title).unwrap(),
152 engine,
153 )
154 .unwrap()
155}
156
157/// Frame cost of a large page as the app draws it: the view's primitives and the renderer's
158/// submission, resting, scrolling, typing and pressing Enter. `CANVAS_TEST_SECTION` and
159/// `CANVAS_TEST_PAGE` choose a stored page; otherwise one outline of 4000 paragraphs, which then
160/// gains a picture and a table in its middle to type beside and in.
161#[cfg(feature = "interaction")]
162#[test]
163#[ignore = "manual release frame timing probe"]
164fn frame_cost() {
165 use crate::document::{TextDocument, TextPosition};
166 use crate::editor::CanvasEditor;
167 use crate::interaction::{PageView, TextColors};
168 use draw::edit::{Key, NamedKey};
169 use onestore::page::{
170 Image, ParagraphContent, Table, TableCell, TableColumn, TableRow, text::new_id,
171 };
172 const LINES: usize = 4000;
173 let lines = || {
174 (0..LINES).map(|line| {
175 Paragraph::new(
176 format!("Line {line}: a paragraph of notes with café and a few more words to wrap across the outline's width."),
177 Default::default(),
178 )
179 })
180 };
181 let synthetic = std::env::var_os("CANVAS_TEST_SECTION").is_none();
182 let mut engine = TextEngine::default();
183 let (editor, scene) = match std::env::var_os("CANVAS_TEST_SECTION") {
184 Some(path) => {
185 let bytes = std::fs::read(path).unwrap();
186 let store = onestore::Store::parse(&bytes).unwrap();
187 let index = onestore::RevisionIndex::parse(&store).unwrap();
188 let document = onestore::document::Document::parse(&index).unwrap();
189 let title = std::env::var("CANVAS_TEST_PAGE").unwrap_or_default();
190 let page = Page::from_document(&document, &title).unwrap();
191 let (scene, editor) = page::PageScene::from_page(page, &mut engine).unwrap();
192 (editor, Some((scene, [0.0; 2])))
193 }
194 None => {
195 let document = TextDocument::new(lines().collect()).unwrap();
196 let editor = CanvasEditor::new(&mut engine, document, 480.0).unwrap();
197 (editor, None)
198 }
199 };
200 let size = [2360, 1300];
201 let mut view = PageView::new(editor, engine, scene, size, 2.0, Duration::from_millis(500));
202 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
203 let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap();
204 let (device, queue) = pollster::block_on(adapter.request_device(&Default::default())).unwrap();
205 let format = wgpu::TextureFormat::Rgba8UnormSrgb;
206 let target = device.create_texture(&wgpu::TextureDescriptor {
207 label: Some("Frame profile target"),
208 size: wgpu::Extent3d {
209 width: size[0],
210 height: size[1],
211 depth_or_array_layers: 1,
212 },
213 mip_level_count: 1,
214 sample_count: 1,
215 dimension: wgpu::TextureDimension::D2,
216 format,
217 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
218 view_formats: &[],
219 });
220 let target: draw::Target = target.create_view(&Default::default()).into();
221 let mut renderer = Renderer::new(device, queue, format);
222 let colors = TextColors {
223 caret: [0.0, 0.0, 0.0, 1.0],
224 selection: [0.7, 0.8, 1.0, 1.0],
225 paper: Paper::WHITE,
226 };
227 let _ = view.pointer_moved([400.0, 400.0]).unwrap();
228 for phase in ["rest", "scroll", "type", "enter", "picture", "cell"] {
229 match phase {
230 "picture" if synthetic => {
231 let mut nodes = TextDocument::new(lines().collect())
232 .unwrap()
233 .nodes()
234 .to_vec();
235 let mut picture = nodes[0].clone();
236 picture.id = new_id().unwrap();
237 picture.content = ParagraphContent::Image(Image {
238 id: new_id().unwrap(),
239 layout: onestore::document::Layout {
240 max_width: Some(120.0),
241 max_height: Some(80.0),
242 ..Default::default()
243 },
244 size: None,
245 bytes: None,
246 display: None,
247 alt: None,
248 background: false,
249 printout: None,
250 tags: Vec::new(),
251 link: None,
252 text: None,
253 });
254 let cell = || TableCell {
255 id: new_id().unwrap(),
256 layout: Default::default(),
257 indents: vec![18.0, 0.0, 27.0],
258 shading: None,
259 paragraphs: vec![
260 crate::document::node(
261 Paragraph::new("Cell".into(), Default::default()),
262 Default::default(),
263 )
264 .unwrap(),
265 ],
266 unsupported: Vec::new(),
267 };
268 let mut table = nodes[0].clone();
269 table.id = new_id().unwrap();
270 table.content = ParagraphContent::Table(Table {
271 id: new_id().unwrap(),
272 columns: vec![
273 TableColumn {
274 width: 72.0,
275 locked: true,
276 };
277 2
278 ],
279 rows: (0..2)
280 .map(|_| TableRow {
281 id: new_id().unwrap(),
282 cells: vec![cell(), cell()],
283 })
284 .collect(),
285 borders: Some(true),
286 layout: Default::default(),
287 tags: Vec::new(),
288 });
289 // The picture precedes paragraph LINES / 2, which the table follows.
290 nodes.insert(LINES / 2, picture);
291 nodes.insert(LINES / 2 + 2, table);
292 let document = TextDocument::from_nodes(nodes).unwrap();
293 view.editor = CanvasEditor::new(&mut view.engine, document, 480.0).unwrap();
294 }
295 "picture" | "cell" if !synthetic => continue,
296 _ => {}
297 }
298 let caret = match phase {
299 "picture" => Some(LINES / 2),
300 "cell" => Some(LINES / 2 + 1),
301 _ => None,
302 };
303 if let Some(paragraph) = caret {
304 view.editor
305 .select(
306 [TextPosition {
307 paragraph,
308 offset: 0,
309 }; 2]
310 .into(),
311 )
312 .unwrap();
313 }
314 let mut times = [Vec::new(), Vec::new(), Vec::new()];
315 for _ in 0..64 {
316 let start = Instant::now();
317 match phase {
318 "scroll" => {
319 let _ = view.wheel([0.0, -120.0]).unwrap();
320 }
321 "enter" => {
322 let _ = view.key(&Key::Named(NamedKey::Enter), None).unwrap();
323 }
324 "type" | "picture" | "cell" => {
325 let _ = view.key(&Key::Named(NamedKey::Other), Some("a")).unwrap();
326 }
327 _ => {}
328 }
329 let input = start.elapsed();
330 let start = Instant::now();
331 view.update_pictures(colors.paper, std::task::Waker::noop());
332 let primitives = view.primitives(colors).unwrap();
333 let built = start.elapsed();
334 let start = Instant::now();
335 renderer
336 .draw(&target, size, [1.0; 4], &[view.viewport.layer(&primitives)])
337 .unwrap();
338 let drawn = start.elapsed();
339 drop(primitives);
340 renderer
341 .device()
342 .poll(wgpu::PollType::Wait {
343 submission_index: None,
344 timeout: Some(Duration::from_secs(10)),
345 })
346 .unwrap();
347 for (list, time) in times.iter_mut().zip([input, built, drawn]) {
348 list.push(time);
349 }
350 }
351 for (name, mut list) in ["input", "primitives", "draw"].into_iter().zip(times) {
352 list.sort();
353 eprintln!(
354 "frame_cost\t{phase}\t{name}\tp50 {:.2} ms\tp99 {:.2} ms",
355 list[list.len() / 2].as_secs_f64() * 1e3,
356 list[list.len() * 99 / 100].as_secs_f64() * 1e3
357 );
358 }
359 }
360}