Skip to main content

libtrace2power/
lib.rs

1// Copyright (c) 2024-2026 Antmicro <www.antmicro.com>
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::BTreeSet;
5use std::str::FromStr;
6use std::{collections::HashMap, io};
7use std::{fs, hash, path};
8
9use clap::Parser;
10use rayon::prelude::*;
11use stats::PackedStats;
12use wellen::{self, Hierarchy, ScopeRef, SignalRef, Var, VarRef, simple::Waveform};
13
14mod exporters;
15pub mod netlist;
16pub mod stats;
17pub mod util;
18
19use netlist::Netlist;
20use util::VarRefsIter;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23struct HashVarRef(VarRef);
24
25impl hash::Hash for HashVarRef {
26    fn hash<H: hash::Hasher>(&self, state: &mut H) {
27        self.0.index().hash(state);
28    }
29}
30
31/// trace2power - Extract acccumulated power activity data from VCD/FST
32#[derive(Parser)]
33pub struct Args {
34    /// Trace file
35    pub input_file: path::PathBuf,
36    /// Clock frequency (in Hz)
37    #[arg(short, long, value_parser = clap::value_parser!(f64))]
38    pub clk_freq: f64,
39    /// Clock signal name
40    #[arg(long)]
41    pub clock_name: Option<String>,
42    /// Format to extract data into
43    #[arg(short = 'f', long, default_value = "tcl")]
44    pub output_format: OutputFormat,
45    /// Scope in which signals should be looked for. By default it's the global hierarchy scope.
46    #[arg(long, short)]
47    pub limit_scope: Option<String>,
48    /// Scope in which power will be calculated. By default it's equal to `limit_scope`.
49    /// Must be a subset of `limit_scope`.
50    #[arg(long)]
51    pub limit_scope_power: Option<String>,
52    /// Yosys JSON netlist of DUT. Can be used to identify ports of primitives when exporting data.
53    /// Allows skipping unnecessary or unwanted signals
54    #[arg(short, long)]
55    pub netlist: Option<path::PathBuf>,
56    /// Name of the top module (DUT)
57    #[arg(short, long)]
58    pub top: Option<String>,
59    /// Scope at which the DUT is located. The loaded netlist will be rooted at this point.
60    #[arg(short = 'T', long)]
61    pub top_scope: Option<String>,
62    /// Export only nets from blackboxes (undefined modules) in provided netlist. Those are assumed
63    /// to be post-synthesis primitives
64    #[arg(short, long)]
65    pub blackboxes_only: bool,
66    /// Remove nets that are in blackboxes and have suspicious names: "VGND", "VNB", "VPB", "VPWR".
67    #[arg(long)]
68    pub remove_virtual_pins: bool,
69    /// Write the output to a specified file instead of stdout.
70    /// In case of per clock cycle output, it must be a directory.
71    #[arg(short, long)]
72    pub output: Option<path::PathBuf>,
73    /// Ignore exporting current date.
74    #[arg(long)]
75    pub ignore_date: bool,
76    /// Ignore exporting current version.
77    #[arg(long)]
78    pub ignore_version: bool,
79    /// Accumulate stats for each clock cycle separately. Output path is required to be a directory.
80    #[arg(long)]
81    pub per_clock_cycle: bool,
82    /// Write stats only for glitches
83    #[arg(long)]
84    pub only_glitches: bool,
85    /// Export without accumulation
86    #[arg(long)]
87    pub export_empty: bool,
88    /// Set activity for input ports in TCL mode
89    #[arg(long)]
90    pub input_ports_activity: bool,
91}
92
93impl Args {
94    pub fn from_cli() -> Self {
95        Args::parse()
96    }
97}
98
99fn indexed_name(mut name: String, variable: &Var) -> String {
100    if let Some(idx) = variable.index() {
101        name += format!("[{}]", idx.lsb()).as_str();
102    }
103    name
104}
105
106fn get_scope_by_full_name(hier: &Hierarchy, scope_str: &str) -> Option<ScopeRef> {
107    hier.lookup_scope(scope_str.split('.').collect::<Vec<_>>().as_slice())
108}
109
110/// Represents a pointin hierarchy - either a scope or top-level hierarchy as they are distinct for
111/// some reason
112#[derive(Copy, Clone)]
113enum LookupPoint {
114    Top,
115    Scope(ScopeRef),
116}
117
118#[derive(Copy, Clone)]
119pub enum OutputFormat {
120    Tcl,
121    Saif,
122}
123
124impl clap::ValueEnum for OutputFormat {
125    fn value_variants<'a>() -> &'a [Self] {
126        &[Self::Tcl, Self::Saif]
127    }
128    fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
129        use clap::builder::PossibleValue;
130        match self {
131            Self::Tcl => Some(PossibleValue::new("tcl")),
132            Self::Saif => Some(PossibleValue::new("saif")),
133        }
134    }
135}
136
137impl FromStr for OutputFormat {
138    type Err = io::Error;
139    fn from_str(s: &str) -> Result<Self, Self::Err> {
140        match s.to_lowercase().as_str() {
141            "tcl" => Ok(Self::Tcl),
142            "saif" => Ok(Self::Saif),
143            other @ _ => Err(io::Error::new(
144                io::ErrorKind::InvalidInput,
145                format!(
146                    "Format {} is not a valid output format forthis program",
147                    other
148                ),
149            )),
150        }
151    }
152}
153
154struct Context {
155    wave: Waveform,
156    clk_period: f64,
157    stats: HashMap<HashVarRef, Vec<PackedStats>>,
158    pub num_of_iterations: u64,
159    lookup_point: LookupPoint,
160    output_fmt: OutputFormat,
161    scope_prefix_length: usize,
162    netlist: Option<Netlist>,
163    top: String,
164    top_scope: Option<ScopeRef>,
165    blackboxes_only: bool,
166    remove_virtual_pins: bool,
167    ignore_date: bool,
168    ignore_version: bool,
169    export_empty: bool,
170    power_scope_prefix: String,
171    input_ports_activity: bool,
172}
173
174impl Context {
175    pub fn build_from_args(args: &Args) -> Self {
176        const LOAD_OPTS: wellen::LoadOptions = wellen::LoadOptions {
177            multi_thread: true,
178            remove_scopes_with_empty_name: false,
179        };
180
181        let mut wave = wellen::simple::read_with_options(
182            args.input_file
183                .to_str()
184                .expect("Arguments should contain a path to input trace file"),
185            &LOAD_OPTS,
186        )
187        .expect("Waveform parsing should end successfully");
188
189        let wave_hierarchy = wave.hierarchy();
190
191        let clk_period = 1.0_f64 / args.clk_freq;
192        let timescale = wave_hierarchy
193            .timescale()
194            .expect("Trace file should contain a timescale");
195        let timescale_norm = (timescale.factor as f64)
196            * (10.0_f64).powf(
197                timescale
198                    .unit
199                    .to_exponent()
200                    .expect("Waveform should contain time unit") as f64,
201            );
202
203        let lookup_point = match &args.limit_scope {
204            None => LookupPoint::Top,
205            Some(scope_str) => LookupPoint::Scope(
206                get_scope_by_full_name(wave_hierarchy, scope_str)
207                    .expect("Requested scope not found"),
208            ),
209        };
210
211        let lookup_scope_name_prefix = match lookup_point {
212            LookupPoint::Top => "".to_string(),
213            LookupPoint::Scope(scope_ref) => {
214                let scope = &wave_hierarchy[scope_ref];
215                scope.full_name(wave_hierarchy).to_string() + "."
216            }
217        };
218
219        let (all_vars, all_signals): (Vec<_>, Vec<_>) = match lookup_point {
220            LookupPoint::Top => wave_hierarchy
221                .var_refs_iter()
222                .map(|var_ref| (var_ref, wave_hierarchy[var_ref].signal_ref()))
223                .unzip(),
224            LookupPoint::Scope(_) => wave_hierarchy
225                .var_refs_iter()
226                .map(|var_ref| (var_ref, &wave_hierarchy[var_ref]))
227                .filter(|(_, var)| {
228                    let fname = indexed_name(var.full_name(wave_hierarchy.into()), var);
229                    fname.starts_with(&lookup_scope_name_prefix)
230                })
231                .map(|(var_ref, var)| (var_ref, var.signal_ref()))
232                .unzip(),
233        };
234
235        let all_signals_power: BTreeSet<_> = match &args.limit_scope_power {
236            None => wave_hierarchy
237                .var_refs_iter()
238                .map(|var_ref| &wave_hierarchy[var_ref])
239                .map(|var| indexed_name(var.full_name(wave_hierarchy.into()), var))
240                .collect::<BTreeSet<_>>(),
241            Some(scope_str) => wave_hierarchy
242                .var_refs_iter()
243                .map(|var_ref| &wave_hierarchy[var_ref])
244                .filter(|var| {
245                    let fname = indexed_name(var.full_name(wave_hierarchy.into()), var);
246                    fname.starts_with(scope_str)
247                })
248                .map(|var| indexed_name(var.full_name(wave_hierarchy.into()), &var))
249                .collect::<BTreeSet<_>>(),
250        };
251
252        let clk_signal: Option<SignalRef> = match &args.clock_name {
253            None => None,
254            Some(clock_name) => {
255                let mut found: Option<SignalRef> = None;
256
257                for var_ref in wave_hierarchy.var_refs_iter() {
258                    let net = &wave_hierarchy[var_ref];
259                    let sig_ref = net.signal_ref();
260                    if net.name(wave_hierarchy) == clock_name {
261                        found = Some(sig_ref)
262                    }
263                }
264
265                found
266            }
267        };
268
269        // TODO load signals that are under a power scope
270        wave.load_signals_multi_threaded(&all_signals);
271
272        let last_time_stamp = *wave
273            .time_table()
274            .last()
275            .expect("Given waveform shouldn't be empty");
276        let num_of_iterations = if args.per_clock_cycle {
277            (last_time_stamp as f64 * timescale_norm / clk_period) as u64
278        } else {
279            1
280        };
281
282        // TODO: A massive optimization that can be done here is to calculate stats only
283        // for exported signals instead of all nets
284        // It's easy to do with the current implementation of DFS (see src/exporter/mod.rs).
285        // However it's single-threaded and parallelizing it efficiently is non-trivial.
286        let stats: HashMap<HashVarRef, Vec<stats::PackedStats>> = all_vars
287            .par_iter()
288            .zip(all_signals)
289            .map(|(var_ref, sig_ref)| {
290                let fname = indexed_name(
291                    wave.hierarchy()[*var_ref].full_name(wave.hierarchy().into()),
292                    &wave.hierarchy()[*var_ref],
293                );
294                if args.limit_scope_power.is_none() || all_signals_power.contains(&fname) {
295                    return (
296                        HashVarRef(*var_ref),
297                        stats::calc_stats_for_each_time_span(
298                            &wave,
299                            args.only_glitches,
300                            clk_signal,
301                            sig_ref,
302                            num_of_iterations,
303                        ),
304                    );
305                }
306                (
307                    HashVarRef(*var_ref),
308                    vec![stats::empty_stats(&wave, sig_ref); num_of_iterations as usize],
309                )
310            })
311            .collect();
312
313        let top_scope = args.top_scope.as_ref().map(|s| {
314            get_scope_by_full_name(wave.hierarchy(), s)
315                .unwrap_or_else(|| panic!("Couldn't find top scope `{}`", s))
316        });
317
318        Self {
319            wave,
320            clk_period,
321            stats,
322            num_of_iterations,
323            lookup_point,
324            output_fmt: args.output_format,
325            scope_prefix_length: lookup_scope_name_prefix.len(),
326            netlist: args.netlist.as_ref().map(|path| {
327                let f = fs::File::open(path).expect("Couldn't open the netlist file");
328                let reader = io::BufReader::new(f);
329                serde_json::from_reader::<_, Netlist>(reader)
330                    .expect("Couldn't parse the netlist file")
331            }),
332            top: args.top.clone().unwrap_or_else(String::new),
333            top_scope,
334            blackboxes_only: args.blackboxes_only,
335            remove_virtual_pins: args.remove_virtual_pins,
336            ignore_date: args.ignore_date,
337            ignore_version: args.ignore_version,
338            export_empty: args.export_empty,
339            power_scope_prefix: args
340                .limit_scope_power
341                .clone()
342                .unwrap_or_else(|| lookup_scope_name_prefix),
343            input_ports_activity: args.input_ports_activity,
344        }
345    }
346}
347
348pub fn process(args: Args) {
349    let ctx = Context::build_from_args(&args);
350    if ctx.num_of_iterations > 1 {
351        process_trace_iterations(&ctx, args.output);
352    } else {
353        process_single_iteration_trace(&ctx, args.output);
354    }
355}
356
357fn process_trace(ctx: &Context, out: impl io::Write, iteration: usize) {
358    match &ctx.output_fmt {
359        OutputFormat::Tcl => exporters::tcl::export(&ctx, out, iteration),
360        OutputFormat::Saif => exporters::saif::export(&ctx, out, iteration),
361    }
362    .expect("Output format should be either 'tcl' or 'saif'")
363}
364
365fn process_trace_iterations(ctx: &Context, output_path: Option<path::PathBuf>) {
366    if let Some(mut path) = output_path {
367        // TODO: multithreading can also be introduced here to process each iteration in parallel
368        for iteration in 0..ctx.num_of_iterations as usize {
369            path.push(format!("{:05}", iteration));
370            let f = fs::File::create(&path).expect("Created file should be valid");
371            let writer = io::BufWriter::new(f);
372            process_trace(&ctx, writer, iteration);
373            path.pop();
374        }
375    } else {
376        for iteration in 0..ctx.num_of_iterations as usize {
377            println!("{1} Iteration {:05} {1}", iteration, str::repeat("-", 10));
378            process_trace(&ctx, io::stdout(), iteration);
379        }
380    }
381}
382
383fn process_single_iteration_trace(ctx: &Context, output_path: Option<path::PathBuf>) {
384    match output_path {
385        None => process_trace(&ctx, io::stdout(), 0),
386        Some(ref path) => {
387            let f = fs::File::create(path).expect("Created file should be valid");
388            let writer = io::BufWriter::new(f);
389            process_trace(&ctx, writer, 0);
390        }
391    }
392}