Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 141 additions & 5 deletions src/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use std::io::{self, BufRead, Seek, Write};
use std::path::Path;

use crossbeam_channel as channel;
use fst_reader::{FstFilter, FstSignalValue};
use fst_reader::{FstFilter, FstSignalHandle, FstSignalValue};

use crate::{next_vcd_change, NameOptions, NameTree, SignalMap, WaveHierarchy, WaveReader};

Expand Down Expand Up @@ -221,6 +221,40 @@ fn flush_batch(tx: &channel::Sender<TimeBatch>, time: u64, changes: &mut Vec<Sig
let _ = tx.send(TimeBatch { time, changes: full });
}

/// Build per-reader local handle include sets from a filtered merged signal map.
/// Returns `None` when every source handle is still present so the readers can
/// keep their unfiltered fast path.
fn reader_include_sets(
signal_map: &SignalMap,
offsets: &[usize],
source_signal_count: usize,
) -> Option<Vec<HashSet<usize>>> {
if signal_map.len() == source_signal_count {
return None;
}

let mut include_sets: Vec<HashSet<usize>> =
(0..offsets.len()).map(|_| HashSet::new()).collect();

for &handle in signal_map.keys() {
let reader_idx = match offsets.binary_search(&handle) {
Ok(idx) => idx,
Err(0) => continue,
Err(idx) => idx - 1,
};
include_sets[reader_idx].insert(handle - offsets[reader_idx]);
}

Some(include_sets)
}

fn reader_signal_count(reader: &WaveReader) -> usize {
match reader {
WaveReader::Fst(reader) => reader.get_header().max_handle as usize,
WaveReader::Vcd(vcd_data) => vcd_data.id_to_idx.len(),
}
}

/// Read signals from an FST reader and send same-time batches to a channel.
fn read_and_send_signals<R: BufRead + Seek>(
mut fst_reader: fst_reader::FstReader<R>,
Expand Down Expand Up @@ -423,16 +457,20 @@ fn compare_signal_channels<W: Write>(
fn send_wave_changes(
reader: WaveReader,
handle_offset: usize,
include: Option<HashSet<usize>>,
start: u64,
end: Option<u64>,
tx: channel::Sender<TimeBatch>,
) -> io::Result<()> {
match reader {
WaveReader::Fst(fst_reader) => {
let include = include.map(|handles| {
handles.into_iter().map(FstSignalHandle::from_index).collect()
});
let filter = FstFilter {
start,
end,
include: None,
include,
};
read_and_send_signals(*fst_reader, filter, handle_offset, tx)?;
}
Expand All @@ -448,6 +486,9 @@ fn send_wave_changes(
break;
}
}
if include.as_ref().is_some_and(|handles| !handles.contains(&handle)) {
continue;
}
if !batch.is_empty() && (time != batch_time || batch.len() >= BATCH_SIZE) {
flush_batch(&tx, batch_time, &mut batch);
}
Expand All @@ -472,14 +513,21 @@ fn send_wave_changes(
fn send_merged_wave_changes(
readers: Vec<WaveReader>,
offsets: &[usize],
include_sets: Option<Vec<HashSet<usize>>>,
start: u64,
end: Option<u64>,
tx: channel::Sender<TimeBatch>,
) -> io::Result<()> {
debug_assert_eq!(readers.len(), offsets.len());
if let Some(sets) = &include_sets {
debug_assert_eq!(readers.len(), sets.len());
}

if readers.len() == 1 {
return send_wave_changes(
readers.into_iter().next().unwrap(),
offsets[0],
include_sets.and_then(|sets| sets.into_iter().next()),
start,
end,
tx,
Expand All @@ -490,10 +538,12 @@ fn send_merged_wave_changes(
let mut inner_rxs = Vec::with_capacity(readers.len());
let mut threads = Vec::new();

let mut include_sets = include_sets.map(Vec::into_iter);
for (reader, &offset) in readers.into_iter().zip(offsets.iter()) {
let include = include_sets.as_mut().and_then(|sets| sets.next());
let (inner_tx, inner_rx) = channel::bounded(CHANNEL_BOUND);
threads.push(std::thread::spawn(move || {
send_wave_changes(reader, offset, start, end, inner_tx)
send_wave_changes(reader, offset, include, start, end, inner_tx)
}));
inner_rxs.push(inner_rx);
}
Expand Down Expand Up @@ -644,12 +694,16 @@ pub fn diff_wave_sets<W: Write>(
hier2,
offsets2,
} = sets;
let source_signal_count1 = readers1.iter().map(reader_signal_count).sum();
let source_signal_count2 = readers2.iter().map(reader_signal_count).sum();
let include_sets1 = reader_include_sets(&hier1.signal_map, &offsets1, source_signal_count1);
let include_sets2 = reader_include_sets(&hier2.signal_map, &offsets2, source_signal_count2);

let thread1 = std::thread::spawn(move || {
send_merged_wave_changes(readers1, &offsets1, start, end, tx1)
send_merged_wave_changes(readers1, &offsets1, include_sets1, start, end, tx1)
});
let thread2 = std::thread::spawn(move || {
send_merged_wave_changes(readers2, &offsets2, start, end, tx2)
send_merged_wave_changes(readers2, &offsets2, include_sets2, start, end, tx2)
});

let result = compare_signal_channels(
Expand Down Expand Up @@ -695,3 +749,85 @@ pub fn open_and_read_wave_sets(
offsets2,
})
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{SignalInfo, VarEntry, VarMeta, IMPLICIT_DIRECTION};

fn signal_map(handles: &[usize]) -> SignalMap {
handles
.iter()
.copied()
.map(|handle| {
(
handle,
SignalInfo {
vars: vec![VarEntry {
name: 0,
meta: VarMeta {
var_type: "wire",
size: 1,
direction: IMPLICIT_DIRECTION,
},
attrs: Vec::new(),
}],
},
)
})
.collect()
}

#[test]
fn reader_include_sets_split_merged_handles_by_reader_offset() {
let map = signal_map(&[0, 1, 4, 6, 9]);

let include_sets = reader_include_sets(&map, &[0, 4, 9], 12).unwrap();

assert_eq!(include_sets.len(), 3);
assert_eq!(include_sets[0], HashSet::from([0, 1]));
assert_eq!(include_sets[1], HashSet::from([0, 2]));
assert_eq!(include_sets[2], HashSet::from([0]));
}

#[test]
fn reader_include_sets_preserve_empty_filtered_readers() {
let map = signal_map(&[5]);

let include_sets = reader_include_sets(&map, &[0, 5], 10).unwrap();

assert_eq!(include_sets.len(), 2);
assert!(include_sets[0].is_empty());
assert_eq!(include_sets[1], HashSet::from([0]));
}

#[test]
fn reader_include_sets_skip_unfiltered_sources() {
let map = signal_map(&[0, 1, 2, 3, 4]);

assert!(reader_include_sets(&map, &[0, 3], 5).is_none());
}

#[test]
fn native_reader_counts_match_unfiltered_hierarchies() {
for path in ["tests/data/counter.fst", "tests/data/counter.vcd"] {
let (reader, hierarchy) =
crate::open_wave_file(Path::new(path), &NameOptions::default()).unwrap();
let source_signal_count = reader_signal_count(&reader);

assert_eq!(source_signal_count, hierarchy.signal_map.len(), "{path}");
assert!(reader_include_sets(&hierarchy.signal_map, &[0], source_signal_count).is_none());
}

let paths = [
Path::new("tests/data/set_clk.vcd"),
Path::new("tests/data/set_counter.vcd"),
];
let (readers, hierarchy, offsets) =
crate::open_wave_files(&paths, &NameOptions::default(), None).unwrap();
let source_signal_count = readers.iter().map(reader_signal_count).sum();

assert_eq!(source_signal_count, hierarchy.signal_map.len());
assert!(reader_include_sets(&hierarchy.signal_map, &offsets, source_signal_count).is_none());
}
}
33 changes: 33 additions & 0 deletions tests/set_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,39 @@ fn test_cli_wavediff_sets_only_value_diff() {
assert!(stdout.contains("t.the_sub.cyc_plus_one"));
}

#[test]
fn test_cli_wavediff_set_filter_excludes_value_diff() {
// The value difference lives in set_counter_modified.vcd. Filtering to the
// disjoint clk reader should avoid streaming/reporting that difference.
let output = run_wavediff_cli(&[
"--filter", "*.clk",
"--set1", "tests/data/set_clk.vcd",
"--set1", "tests/data/set_counter_modified.vcd",
"--set2", "tests/data/counter.vcd",
]);
assert!(
output.status.success(),
"Expected filtered set diff to exit 0, stderr: {} stdout: {}",
String::from_utf8_lossy(&output.stderr),
String::from_utf8_lossy(&output.stdout)
);
}

#[test]
fn test_cli_wavediff_set_filter_includes_value_diff() {
// Filtering to a signal in the second set1 reader exercises non-zero
// handle offsets while preserving the expected value diff.
let output = run_wavediff_cli(&[
"--filter", "*.cyc_plus_one",
"--set1", "tests/data/set_clk.vcd",
"--set1", "tests/data/set_counter_modified.vcd",
"--set2", "tests/data/counter.vcd",
]);
assert_eq!(output.status.code(), Some(1), "Expected exit 1");
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("t.the_sub.cyc_plus_one"));
}

#[test]
fn test_cli_wavediff_set1_only_no_positional_fails() {
// Only --set1 without positional args should fail
Expand Down
Loading