tract_libcli/
terminal.rs

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    // profile column
114    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    // cost column
131    #[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    // flops column
150    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    // tmp_mem_usage column
176    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    // drawing column
191    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 &section[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
498/// Format a rusage::Duration showing avgtime in ms.
499pub fn dur_avg(measure: Duration) -> String {
500    White.bold().paint(format!("{:.3} ms/i", measure.as_secs_f64() * 1e3)).to_string()
501}
502
503/// Format a rusage::Duration showing avgtime in ms, with percentage to a global
504/// one.
505pub 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}