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