Skip to main content

trace2power/
lib.rs

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