1use std::time::Duration;
2
3use crate::annotations::*;
4use crate::display_params::*;
5use crate::draw::DrawingState;
6use crate::model::Model;
7use nu_ansi_term::AnsiString;
8use nu_ansi_term::Color::*;
9#[allow(unused_imports)]
10use std::convert::TryFrom;
11use tract_core::internal::*;
12use tract_itertools::Itertools;
13
14pub fn render(
15 model: &dyn Model,
16 annotations: &Annotations,
17 options: &DisplayParams,
18) -> TractResult<()> {
19 if options.quiet {
20 return Ok(());
21 }
22 render_prefixed(model, "", &[], annotations, options)?;
23 if !model.properties().is_empty() {
24 println!("{}", White.bold().paint("# Properties"));
25 }
26 for (k, v) in model.properties().iter().sorted_by_key(|(k, _)| k.to_string()) {
27 println!("* {}: {:?}", White.paint(k), v)
28 }
29 let symbols = model.symbols();
30 if !symbols.all_assertions().is_empty() {
31 println!("{}", White.bold().paint("# Assertions"));
32 for a in symbols.all_assertions() {
33 println!(" * {a}");
34 }
35 }
36 for (ix, scenario) in symbols.all_scenarios().into_iter().enumerate() {
37 if ix == 0 {
38 println!("{}", White.bold().paint("# Scenarios"));
39 }
40 for a in scenario.1 {
41 println!(" * {}: {}", scenario.0, a);
42 }
43 }
44 Ok(())
45}
46
47pub fn render_node(
48 model: &dyn Model,
49 node_id: usize,
50 annotations: &Annotations,
51 options: &DisplayParams,
52) -> TractResult<()> {
53 render_node_prefixed(model, "", &[], node_id, None, annotations, options)
54}
55
56fn render_prefixed(
57 model: &dyn Model,
58 prefix: &str,
59 scope: &[(usize, String)],
60 annotations: &Annotations,
61 options: &DisplayParams,
62) -> TractResult<()> {
63 let mut drawing_state =
64 if options.should_draw() { Some(DrawingState::default()) } else { None };
65 let node_ids = options.order(model)?;
66 for node in node_ids {
67 if options.filter(model, scope, node)? {
68 render_node_prefixed(
69 model,
70 prefix,
71 scope,
72 node,
73 drawing_state.as_mut(),
74 annotations,
75 options,
76 )?
77 } else if let Some(ref mut ds) = drawing_state {
78 let _prefix = ds.draw_node_vprefix(model, node, options)?;
79 let _body = ds.draw_node_body(model, node, options)?;
80 let _suffix = ds.draw_node_vsuffix(model, node, options)?;
81 }
82 }
83 Ok(())
84}
85
86fn render_node_prefixed(
87 model: &dyn Model,
88 prefix: &str,
89 scope: &[(usize, String)],
90 node_id: usize,
91 mut drawing_state: Option<&mut DrawingState>,
92 annotations: &Annotations,
93 options: &DisplayParams,
94) -> TractResult<()> {
95 let qid = NodeQId(scope.into(), node_id);
96 let tags = annotations.tags.get(&qid).cloned().unwrap_or_default();
97 let name_color = tags.style.unwrap_or_else(|| White.into());
98 let node_name = model.node_name(node_id);
99 let node_op_name = model.node_op_name(node_id);
100 let profile_column_pad = format!("{:>1$}", "", options.profile as usize * 20);
101 let cost_column_pad = format!("{:>1$}", "", options.cost as usize * 25);
102 let mem_padding = if annotations.memory_summary.is_some() { 15 } else { 30 };
103 let tmp_mem_usage_column_pad =
104 format!("{:>1$}", "", options.tmp_mem_usage as usize * mem_padding);
105 let flops_column_pad = format!("{:>1$}", "", (options.profile && options.cost) as usize * 20);
106
107 if let Some(ref mut ds) = &mut drawing_state {
108 for l in ds.draw_node_vprefix(model, node_id, options)? {
109 println!("{cost_column_pad}{profile_column_pad}{flops_column_pad}{tmp_mem_usage_column_pad}{prefix}{l} ");
110 }
111 }
112
113 let mut profile_column = tags.profile.map(|measure| {
115 let profile_summary = annotations.profile_summary.as_ref().unwrap();
116 let use_micros = profile_summary.sum < Duration::from_millis(1);
117 let ratio = measure.as_secs_f64() / profile_summary.sum.as_secs_f64();
118 let ratio_for_color = measure.as_secs_f64() / profile_summary.max.as_secs_f64();
119 let color = colorous::RED_YELLOW_GREEN.eval_continuous(1.0 - ratio_for_color);
120 let color = nu_ansi_term::Color::Rgb(color.r, color.g, color.b);
121 let label = format!(
122 "{:7.3} {}s/i {} ",
123 measure.as_secs_f64() * if use_micros { 1e6 } else { 1e3 },
124 if use_micros { "ยต" } else { "m" },
125 color.bold().paint(format!("{:>4.1}%", ratio * 100.0))
126 );
127 std::iter::once(label)
128 });
129
130 #[allow(clippy::manual_repeat_n)]
132 let mut cost_column = if options.cost {
133 Some(
134 tags.cost
135 .iter()
136 .map(|c| {
137 let key = format!("{:?}", c.0);
138 let value = render_tdim(&c.1);
139 let value_visible_len = c.1.to_string().len();
140 let padding = 24usize.saturating_sub(value_visible_len + key.len());
141 key + &*std::iter::repeat(' ').take(padding).join("") + &value.to_string() + " "
142 })
143 .peekable(),
144 )
145 } else {
146 None
147 };
148
149 let mut flops_column = if options.profile && options.cost {
151 let timing: f64 = tags.profile.as_ref().map(|d| d.as_secs_f64()).unwrap_or(0.0);
152 let flops_column_pad = flops_column_pad.clone();
153 let it = tags.cost.iter().map(move |c| {
154 if c.0.is_compute() {
155 let flops = c.1.to_usize().unwrap_or(0) as f64 / timing;
156 let unpadded = if flops > 1e9 {
157 format!("{:.3} GF/s", flops / 1e9)
158 } else if flops > 1e6 {
159 format!("{:.3} MF/s", flops / 1e6)
160 } else if flops > 1e3 {
161 format!("{:.3} kF/s", flops / 1e3)
162 } else {
163 format!("{flops:.3} F/s")
164 };
165 format!("{:>1$} ", unpadded, 19)
166 } else {
167 flops_column_pad.clone()
168 }
169 });
170 Some(it)
171 } else {
172 None
173 };
174
175 let mut tmp_mem_usage_column = if options.tmp_mem_usage {
177 let it = tags.tmp_mem_usage.iter().map(move |mem| {
178 let unpadded = if let Ok(mem_size) = mem.to_usize() {
179 render_memory(mem_size)
180 } else {
181 format!("{mem:.3} B")
182 };
183 format!("{:>1$} ", unpadded, mem_padding - 1)
184 });
185 Some(it)
186 } else {
187 None
188 };
189
190 let mut drawing_lines: Box<dyn Iterator<Item = String>> =
192 if let Some(ds) = drawing_state.as_mut() {
193 let body = ds.draw_node_body(model, node_id, options)?;
194 let suffix = ds.draw_node_vsuffix(model, node_id, options)?;
195 let filler = ds.draw_node_vfiller(model, node_id)?;
196 Box::new(body.into_iter().chain(suffix).chain(std::iter::repeat(filler)))
197 } else {
198 Box::new(std::iter::repeat(cost_column_pad.clone()))
199 };
200
201 macro_rules! prefix {
202 () => {
203 let cost = cost_column
204 .as_mut()
205 .map(|it| it.next().unwrap_or_else(|| cost_column_pad.to_string()))
206 .unwrap_or("".to_string());
207 let profile = profile_column
208 .as_mut()
209 .map(|it| it.next().unwrap_or_else(|| profile_column_pad.to_string()))
210 .unwrap_or("".to_string());
211 let flops = flops_column
212 .as_mut()
213 .map(|it| it.next().unwrap_or_else(|| flops_column_pad.to_string()))
214 .unwrap_or("".to_string());
215 let tmp_mem_usage = tmp_mem_usage_column
216 .as_mut()
217 .map(|it| it.next().unwrap_or_else(|| tmp_mem_usage_column_pad.to_string()))
218 .unwrap_or("".to_string());
219 print!(
220 "{}{}{}{}{}{} ",
221 profile,
222 cost,
223 flops,
224 tmp_mem_usage,
225 prefix,
226 drawing_lines.next().unwrap(),
227 )
228 };
229 }
230
231 prefix!();
232 println!(
233 "{} {} {}",
234 White.bold().paint(format!("{node_id}")),
235 (if node_name == "UnimplementedOp" { Red.bold() } else { Blue.bold() }).paint(node_op_name),
236 name_color.italic().paint(node_name)
237 );
238 for label in tags.labels.iter() {
239 prefix!();
240 println!(" * {label}");
241 }
242 if let Io::Long = options.io {
243 for (ix, i) in model.node_inputs(node_id).iter().enumerate() {
244 let star = if ix == 0 { '*' } else { ' ' };
245 prefix!();
246 println!(
247 " {} input fact #{}: {} {}",
248 star,
249 ix,
250 White.bold().paint(format!("{i:?}")),
251 model.outlet_fact_format(*i),
252 );
253 }
254 for slot in 0..model.node_output_count(node_id) {
255 let star = if slot == 0 { '*' } else { ' ' };
256 let outlet = OutletId::new(node_id, slot);
257 let mut model_io = vec![];
258 for (ix, _) in model.input_outlets().iter().enumerate().filter(|(_, o)| **o == outlet) {
259 model_io.push(Cyan.bold().paint(format!("MODEL INPUT #{ix}")).to_string());
260 }
261 if let Some(t) = &tags.model_input {
262 model_io.push(t.to_string());
263 }
264 for (ix, _) in model.output_outlets().iter().enumerate().filter(|(_, o)| **o == outlet)
265 {
266 model_io.push(Yellow.bold().paint(format!("MODEL OUTPUT #{ix}")).to_string());
267 }
268 if let Some(t) = &tags.model_output {
269 model_io.push(t.to_string());
270 }
271 let successors = model.outlet_successors(outlet);
272 prefix!();
273 let mut axes =
274 tags.outlet_axes.get(slot).map(|s| s.join(",")).unwrap_or_else(|| "".to_string());
275 if !axes.is_empty() {
276 axes.push(' ')
277 }
278 println!(
279 " {} output fact #{}: {}{} {} {} {}",
280 star,
281 slot,
282 Green.bold().italic().paint(axes),
283 model.outlet_fact_format(outlet),
284 White.bold().paint(successors.iter().map(|s| format!("{s:?}")).join(" ")),
285 model_io.join(", "),
286 Blue.bold().italic().paint(
287 tags.outlet_labels
288 .get(slot)
289 .map(|s| s.join(","))
290 .unwrap_or_else(|| "".to_string())
291 )
292 );
293 if options.outlet_labels {
294 if let Some(label) = model.outlet_label(OutletId::new(node_id, slot)) {
295 prefix!();
296 println!(" {} ", White.italic().paint(label));
297 }
298 }
299 }
300 }
301 if options.info {
302 for info in model.node_op(node_id).info()? {
303 prefix!();
304 println!(" * {info}");
305 }
306 }
307 if options.invariants {
308 if let Some(typed) = model.downcast_ref::<TypedModel>() {
309 let node = typed.node(node_id);
310 let (inputs, outputs) = typed.node_facts(node.id)?;
311 let axes_mapping = node.op().as_typed().unwrap().axes_mapping(&inputs, &outputs)?;
312 prefix!();
313 println!(" * {axes_mapping}");
314 }
315 }
316 if options.debug_op {
317 prefix!();
318 println!(" * {:?}", model.node_op(node_id));
319 }
320 for section in tags.sections {
321 if section.is_empty() {
322 continue;
323 }
324 prefix!();
325 println!(" * {}", section[0]);
326 for s in §ion[1..] {
327 prefix!();
328 println!(" {s}");
329 }
330 }
331
332 if !options.folded {
333 for (label, sub) in model.nested_models(node_id) {
334 let prefix = drawing_lines.next().unwrap();
335 let mut scope: TVec<_> = scope.into();
336 scope.push((node_id, label));
337 let scope_prefix = scope.iter().map(|(_, p)| p).join("|");
338 render_prefixed(
339 sub,
340 &format!("{prefix} [{scope_prefix}] "),
341 &scope,
342 annotations,
343 options,
344 )?
345 }
346 }
347 if let Io::Short = options.io {
348 let same = !model.node_inputs(node_id).is_empty()
349 && model.node_output_count(node_id) == 1
350 && model.outlet_fact_format(node_id.into())
351 == model.outlet_fact_format(model.node_inputs(node_id)[0]);
352 if !same || model.output_outlets().iter().any(|o| o.node == node_id) {
353 let style = drawing_state
354 .map(|s| s.wires.last().and_then(|w| w.color).unwrap_or(s.latest_node_color))
355 .unwrap_or_else(|| White.into());
356 for ix in 0..model.node_output_count(node_id) {
357 prefix!();
358 println!(
359 " {}{}{} {}",
360 style.paint(box_drawing::heavy::HORIZONTAL),
361 style.paint(box_drawing::heavy::HORIZONTAL),
362 style.paint(box_drawing::heavy::HORIZONTAL),
363 model.outlet_fact_format((node_id, ix).into())
364 );
365 }
366 }
367 }
368
369 while cost_column.as_mut().map(|cost| cost.peek().is_some()).unwrap_or(false) {
370 prefix!();
371 println!();
372 }
373
374 Ok(())
375}
376
377pub fn render_summaries(
378 model: &dyn Model,
379 annotations: &Annotations,
380 options: &DisplayParams,
381) -> TractResult<()> {
382 let total = annotations.tags.values().sum::<NodeTags>();
383
384 if options.tmp_mem_usage {
385 if let Some(summary) = &annotations.memory_summary {
386 println!("{}", White.bold().paint("Memory summary"));
387 println!(" * Peak flushable memory: {}", render_memory(summary.max));
388 }
389 }
390 if options.cost {
391 println!("{}", White.bold().paint("Cost summary"));
392 for (c, i) in &total.cost {
393 println!(" * {:?}: {}", c, render_tdim(i));
394 }
395 }
396
397 if options.profile {
398 let summary = annotations.profile_summary.as_ref().unwrap();
399
400 let have_accel_profiling = annotations.tags.iter().any(|(_, tag)| tag.accelerator_profile.is_some());
401 println!(
402 "{}{}{}",
403 White.bold().paint(format!("{:<43}", "Most time consuming operations")),
404 White.bold().paint(format!("{:<17}", "CPU")),
405 White.bold().paint(if have_accel_profiling { "Accelerator" } else { "" }),
406 );
407
408 for (op, (cpu_dur, accel_dur, n)) in annotations
409 .tags
410 .iter()
411 .map(|(k, v)| {
412 (
413 k.model(model).unwrap().node_op_name(k.1),
414 (v.profile.unwrap_or_default(), v.accelerator_profile.unwrap_or_default()),
415 )
416 })
417 .sorted_by_key(|a| a.0.to_string())
418 .group_by(|(n, _)| n.clone())
419 .into_iter()
420 .map(|(a, group)| {
421 (
422 a,
423 group.into_iter().fold(
424 (Duration::default(), Duration::default(), 0),
425 |(accu, accel_accu, n), d| (accu + d.1 .0, accel_accu + d.1 .1, n + 1),
426 ),
427 )
428 })
429 .sorted_by_key(|(_, d)| if have_accel_profiling { d.1 } else { d.0 })
430 .rev()
431 {
432 println!(
433 " * {} {:3} nodes: {} {}",
434 Blue.bold().paint(format!("{op:22}")),
435 n,
436 dur_avg_ratio(cpu_dur, summary.sum),
437 if have_accel_profiling {
438 dur_avg_ratio(accel_dur, summary.accel_sum)
439 } else {
440 "".to_string()
441 }
442 );
443 }
444
445 println!("{}", White.bold().paint("By prefix"));
446 fn prefixes_for(s: &str) -> impl Iterator<Item = String> + '_ {
447 use tract_itertools::*;
448 let split = s.split('.').count();
449 (0..split).map(move |n| s.split('.').take(n).join("."))
450 }
451 let all_prefixes = annotations
452 .tags
453 .keys()
454 .flat_map(|id| prefixes_for(id.model(model).unwrap().node_name(id.1)))
455 .filter(|s| !s.is_empty())
456 .sorted()
457 .unique()
458 .collect::<Vec<String>>();
459
460 for prefix in &all_prefixes {
461 let sum = annotations
462 .tags
463 .iter()
464 .filter(|(k, _v)| k.model(model).unwrap().node_name(k.1).starts_with(prefix))
465 .map(|(_k, v)| v)
466 .sum::<NodeTags>();
467
468 let profiler =
469 if !have_accel_profiling { sum.profile } else { sum.accelerator_profile };
470 if profiler.unwrap_or_default().as_secs_f64() / summary.entire.as_secs_f64() < 0.01 {
471 continue;
472 }
473 print!("{} ", dur_avg_ratio(profiler.unwrap_or_default(), summary.sum));
474
475 for _ in prefix.chars().filter(|c| *c == '.') {
476 print!(" ");
477 }
478 println!("{prefix}");
479 }
480
481 println!(
482 "Not accounted by ops: {}",
483 dur_avg_ratio(summary.entire - summary.sum.min(summary.entire), summary.entire)
484 );
485
486 if have_accel_profiling {
487 println!(
488 "(Total CPU Op time - Total Accelerator Op time): {}",
489 dur_avg_ratio(summary.sum - summary.accel_sum.min(summary.sum), summary.entire)
490 );
491 }
492 println!("Entire network performance: {}", dur_avg(summary.entire));
493 }
494
495 Ok(())
496}
497
498pub fn dur_avg(measure: Duration) -> String {
500 White.bold().paint(format!("{:.3} ms/i", measure.as_secs_f64() * 1e3)).to_string()
501}
502
503pub fn dur_avg_ratio(measure: Duration, global: Duration) -> String {
506 format!(
507 "{} {}",
508 White.bold().paint(format!("{:7.3} ms/i", measure.as_secs_f64() * 1e3)),
509 Yellow
510 .bold()
511 .paint(format!("{:>4.1}%", measure.as_secs_f64() / global.as_secs_f64() * 100.)),
512 )
513}
514
515fn render_memory(mem_size: usize) -> String {
516 let kb = 1024.0;
517 let mb = kb * 1024.0;
518 let gb = mb * 1024.0;
519 let mem_size = mem_size as f32;
520 if mem_size > gb {
521 format!("{:.3} GB", mem_size / gb)
522 } else if mem_size > mb {
523 format!("{:.3} MB", mem_size / mb)
524 } else if mem_size > kb {
525 format!("{:.3} KB", mem_size / kb)
526 } else {
527 format!("{mem_size:.3} B")
528 }
529}
530
531fn render_tdim(d: &TDim) -> AnsiString<'static> {
532 if let Ok(i) = d.to_i64() {
533 render_big_integer(i)
534 } else {
535 d.to_string().into()
536 }
537}
538
539fn render_big_integer(i: i64) -> nu_ansi_term::AnsiString<'static> {
540 let raw = i.to_string();
541 let mut blocks = raw
542 .chars()
543 .rev()
544 .chunks(3)
545 .into_iter()
546 .map(|mut c| c.join("").chars().rev().join(""))
547 .enumerate()
548 .map(|(ix, s)| if ix % 2 == 1 { White.bold().paint(s).to_string() } else { s })
549 .collect::<Vec<_>>();
550 blocks.reverse();
551 blocks.into_iter().join("").into()
552}