use crate::trace::canonicalize::trace_event_key;
use crate::trace::event::{TraceData, TraceEvent, TraceEventKind};
use crate::trace::independence::{Resource, accesses_conflict, independent, resource_footprint};
use crate::types::TaskId;
use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Race {
pub earlier: usize,
pub later: usize,
}
#[derive(Debug, Clone)]
pub struct BacktrackPoint {
pub race: Race,
pub divergence_index: usize,
}
#[derive(Debug)]
pub struct RaceAnalysis {
pub races: Vec<Race>,
pub backtrack_points: Vec<BacktrackPoint>,
}
impl RaceAnalysis {
#[must_use]
pub fn race_count(&self) -> usize {
self.races.len()
}
#[must_use]
pub fn is_race_free(&self) -> bool {
self.races.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RaceKind {
Resource(Resource),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DetectedRace {
pub race: Race,
pub kind: RaceKind,
pub earlier_task: Option<TaskId>,
pub later_task: Option<TaskId>,
pub earlier_kind: TraceEventKind,
pub later_kind: TraceEventKind,
}
#[derive(Debug, Clone)]
pub struct RaceReport {
pub races: Vec<DetectedRace>,
}
impl RaceReport {
#[must_use]
pub fn race_count(&self) -> usize {
self.races.len()
}
#[must_use]
pub fn is_race_free(&self) -> bool {
self.races.is_empty()
}
}
#[derive(Debug, Clone, Default)]
struct TaskVectorClock {
entries: BTreeMap<TaskId, u64>,
}
impl TaskVectorClock {
fn get(&self, task: TaskId) -> u64 {
self.entries.get(&task).copied().unwrap_or(0)
}
fn increment(&mut self, task: TaskId) {
let entry = self.entries.entry(task).or_insert(0);
*entry += 1;
}
fn happens_before(&self, other: &Self) -> bool {
let mut strictly = false;
for task in self.entries.keys().chain(other.entries.keys()) {
let a = self.get(*task);
let b = other.get(*task);
if a > b {
return false;
}
if a < b {
strictly = true;
}
}
strictly
}
}
#[derive(Debug, Clone)]
pub struct HappensBeforeGraph {
_events: Vec<TraceEvent>,
_edges: Vec<Vec<usize>>,
clocks: Vec<Option<TaskVectorClock>>,
}
impl HappensBeforeGraph {
#[must_use]
pub fn from_trace(events: &[TraceEvent]) -> Self {
let mut edges = vec![Vec::new(); events.len()];
let mut clocks = Vec::with_capacity(events.len());
let mut last_by_task: BTreeMap<TaskId, usize> = BTreeMap::new();
let mut task_clocks: BTreeMap<TaskId, TaskVectorClock> = BTreeMap::new();
for (idx, event) in events.iter().enumerate() {
if let Some(task) = event_task_id(event) {
if let Some(prev) = last_by_task.insert(task, idx) {
edges[prev].push(idx);
}
let mut clock = task_clocks.get(&task).cloned().unwrap_or_default();
clock.increment(task);
task_clocks.insert(task, clock.clone());
clocks.push(Some(clock));
} else {
clocks.push(None);
}
}
Self {
_events: events.to_vec(),
_edges: edges,
clocks,
}
}
#[must_use]
pub fn happens_before(&self, a: usize, b: usize) -> bool {
match (self.clocks.get(a), self.clocks.get(b)) {
(Some(Some(ca)), Some(Some(cb))) => ca.happens_before(cb),
_ => false,
}
}
}
#[derive(Debug)]
pub struct RaceDetector {
hb: HappensBeforeGraph,
races: Vec<DetectedRace>,
}
impl RaceDetector {
#[must_use]
pub fn from_trace(events: &[TraceEvent]) -> Self {
let hb = HappensBeforeGraph::from_trace(events);
let footprints: Vec<_> = events.iter().map(resource_footprint).collect();
let tasks: Vec<_> = events.iter().map(event_task_id).collect();
let mut races = Vec::new();
for i in 0..events.len() {
for j in (i + 1)..events.len() {
let Some(task_i) = tasks[i] else { continue };
let Some(task_j) = tasks[j] else { continue };
if task_i == task_j {
continue;
}
let Some(resource) = conflicting_resource(&footprints[i], &footprints[j]) else {
continue;
};
if hb.happens_before(i, j) {
continue;
}
races.push(DetectedRace {
race: Race {
earlier: i,
later: j,
},
kind: RaceKind::Resource(resource),
earlier_task: Some(task_i),
later_task: Some(task_j),
earlier_kind: events[i].kind,
later_kind: events[j].kind,
});
}
}
Self { hb, races }
}
#[must_use]
pub fn races(&self) -> &[DetectedRace] {
&self.races
}
#[must_use]
pub fn is_race_free(&self) -> bool {
self.races.is_empty()
}
#[must_use]
pub fn hb_graph(&self) -> &HappensBeforeGraph {
&self.hb
}
#[must_use]
pub fn into_report(self) -> RaceReport {
RaceReport { races: self.races }
}
}
#[must_use]
pub fn detect_hb_races(events: &[TraceEvent]) -> RaceReport {
RaceDetector::from_trace(events).into_report()
}
fn event_task_id(event: &TraceEvent) -> Option<TaskId> {
match &event.data {
TraceData::Task { task, .. }
| TraceData::Cancel { task, .. }
| TraceData::Obligation { task, .. }
| TraceData::Futurelock { task, .. }
| TraceData::Worker { task, .. }
| TraceData::Chaos {
task: Some(task), ..
} => Some(*task),
_ => None,
}
}
fn conflicting_resource(
left: &[crate::trace::independence::ResourceAccess],
right: &[crate::trace::independence::ResourceAccess],
) -> Option<Resource> {
for a in left {
for b in right {
if accesses_conflict(a, b) {
return Some(a.resource.clone());
}
}
}
None
}
#[must_use]
pub fn detect_races(events: &[TraceEvent]) -> RaceAnalysis {
let n = events.len();
let mut races = Vec::new();
for i in 0..n {
for j in (i + 1)..n {
if independent(&events[i], &events[j]) {
continue;
}
let has_intervening = (i + 1..j).any(|k| {
!independent(&events[i], &events[k]) && !independent(&events[k], &events[j])
});
if !has_intervening {
races.push(Race {
earlier: i,
later: j,
});
}
}
}
let backtrack_points = races
.iter()
.map(|race| BacktrackPoint {
race: race.clone(),
divergence_index: race.earlier,
})
.collect();
RaceAnalysis {
races,
backtrack_points,
}
}
#[must_use]
pub fn racing_events(events: &[TraceEvent]) -> Vec<usize> {
let analysis = detect_races(events);
let mut indices: Vec<usize> = analysis
.races
.iter()
.flat_map(|r| [r.earlier, r.later])
.collect();
indices.sort_unstable();
indices.dedup();
indices
}
#[must_use]
pub fn estimated_classes(events: &[TraceEvent]) -> usize {
let analysis = detect_races(events);
let hb = HappensBeforeGraph::from_trace(events);
let mut sorted: Vec<&Race> = analysis
.races
.iter()
.filter(|race| !hb.happens_before(race.earlier, race.later))
.collect();
if sorted.is_empty() {
return 1;
}
sorted.sort_by_key(|r| (r.later, r.earlier));
let mut non_overlapping = 1usize;
let mut last_later = 0;
for race in &sorted {
if race.earlier >= last_later {
non_overlapping += 1;
last_later = race.later;
}
}
non_overlapping
}
#[derive(Debug, Clone, Default)]
pub struct ResourceRaceDistribution {
pub counts: BTreeMap<String, usize>,
}
impl ResourceRaceDistribution {
fn from_report(report: &RaceReport) -> Self {
let mut counts = BTreeMap::new();
for race in &report.races {
let key = match &race.kind {
RaceKind::Resource(r) => format!("{r:?}"),
};
*counts.entry(key).or_insert(0) += 1;
}
Self { counts }
}
#[must_use]
pub fn total(&self) -> usize {
self.counts.values().sum()
}
#[must_use]
pub fn resource_count(&self) -> usize {
self.counts.len()
}
}
#[derive(Debug, Clone)]
pub struct TraceCoverageAnalysis {
pub event_count: usize,
pub immediate_race_count: usize,
pub hb_race_count: usize,
pub estimated_classes: usize,
pub backtrack_point_count: usize,
pub racing_event_count: usize,
pub race_density: f64,
pub resource_distribution: ResourceRaceDistribution,
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn trace_coverage_analysis(events: &[TraceEvent]) -> TraceCoverageAnalysis {
let immediate = detect_races(events);
let hb_report = detect_hb_races(events);
let est = estimated_classes(events);
let racing = racing_events(events);
let resource_distribution = ResourceRaceDistribution::from_report(&hb_report);
let race_density = if events.is_empty() {
0.0
} else {
racing.len() as f64 / events.len() as f64
};
TraceCoverageAnalysis {
event_count: events.len(),
immediate_race_count: immediate.race_count(),
hb_race_count: hb_report.race_count(),
estimated_classes: est,
backtrack_point_count: immediate.backtrack_points.len(),
racing_event_count: racing.len(),
race_density,
resource_distribution,
}
}
#[derive(Debug, Clone, Default)]
pub struct SleepSet {
explored: BTreeSet<u64>,
}
impl SleepSet {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn contains(&self, bp: &BacktrackPoint, events: &[TraceEvent]) -> bool {
self.explored.contains(&Self::bp_key(bp, events))
}
pub fn insert(&mut self, bp: &BacktrackPoint, events: &[TraceEvent]) {
self.explored.insert(Self::bp_key(bp, events));
}
#[must_use]
pub fn len(&self) -> usize {
self.explored.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.explored.is_empty()
}
fn bp_key(bp: &BacktrackPoint, events: &[TraceEvent]) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = crate::util::DetHasher::default();
bp.divergence_index.hash(&mut hasher);
bp.race.earlier.hash(&mut hasher);
bp.race.later.hash(&mut hasher);
let earlier_key = events.get(bp.race.earlier).map(trace_event_key);
let later_key = events.get(bp.race.later).map(trace_event_key);
match (earlier_key, later_key) {
(Some(a), Some(b)) => {
let a_ord = (a.kind, a.primary, a.secondary, a.tertiary);
let b_ord = (b.kind, b.primary, b.secondary, b.tertiary);
let (first, second) = if a_ord <= b_ord { (a, b) } else { (b, a) };
first.hash(&mut hasher);
second.hash(&mut hasher);
}
(Some(a), None) => a.hash(&mut hasher),
(None, Some(b)) => b.hash(&mut hasher),
(None, None) => {}
}
hasher.finish()
}
}
use std::collections::BTreeSet;
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{CancelReason, RegionId, TaskId, Time};
fn tid(n: u32) -> TaskId {
TaskId::new_for_test(n, 0)
}
fn rid(n: u32) -> RegionId {
RegionId::new_for_test(n, 0)
}
#[test]
fn no_races_in_independent_trace() {
let events = [
TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1)),
TraceEvent::spawn(2, Time::ZERO, tid(2), rid(2)),
];
let analysis = detect_races(&events);
assert!(analysis.is_race_free());
assert_eq!(estimated_classes(&events), 1);
}
#[test]
fn race_between_dependent_events() {
let events = [
TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1)),
TraceEvent::complete(2, Time::ZERO, tid(1), rid(1)),
];
let analysis = detect_races(&events);
assert_eq!(analysis.race_count(), 1);
assert_eq!(analysis.races[0].earlier, 0);
assert_eq!(analysis.races[0].later, 1);
}
#[test]
fn no_race_with_transitive_dependency() {
let events = [
TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1)),
TraceEvent::poll(2, Time::ZERO, tid(1), rid(1)),
TraceEvent::complete(3, Time::ZERO, tid(1), rid(1)),
];
let analysis = detect_races(&events);
assert_eq!(analysis.race_count(), 2);
assert!(
!analysis
.races
.iter()
.any(|r| r.earlier == 0 && r.later == 2)
);
}
#[test]
fn races_in_concurrent_task_region_interaction() {
let events = [
TraceEvent::region_created(1, Time::ZERO, rid(1), None),
TraceEvent::spawn(2, Time::ZERO, tid(1), rid(1)),
TraceEvent::spawn(3, Time::ZERO, tid(2), rid(1)),
];
let analysis = detect_races(&events);
let race_pairs: Vec<(usize, usize)> = analysis
.races
.iter()
.map(|r| (r.earlier, r.later))
.collect();
assert!(race_pairs.contains(&(0, 1)));
assert!(race_pairs.contains(&(0, 2)));
assert!(!race_pairs.contains(&(1, 2)));
}
#[test]
fn racing_events_deduplicates() {
let events = [
TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1)),
TraceEvent::complete(2, Time::ZERO, tid(1), rid(1)),
];
let indices = racing_events(&events);
assert_eq!(indices, vec![0, 1]);
}
#[test]
fn empty_trace_no_races() {
let analysis = detect_races(&[]);
assert!(analysis.is_race_free());
}
#[test]
fn estimated_classes_grows_with_races() {
let events = [
TraceEvent::region_created(1, Time::ZERO, rid(1), None),
TraceEvent::spawn(2, Time::ZERO, tid(1), rid(1)),
TraceEvent::spawn(3, Time::ZERO, tid(2), rid(1)),
];
let est = estimated_classes(&events);
assert!(est >= 2);
}
#[test]
fn estimated_classes_fail_closed_for_same_task_sequence() {
let events = [
TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1)),
TraceEvent::complete(2, Time::ZERO, tid(1), rid(1)),
];
assert_eq!(detect_races(&events).race_count(), 1);
assert_eq!(estimated_classes(&events), 1);
}
#[test]
fn backtrack_points_correspond_to_races() {
let events = [
TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1)),
TraceEvent::complete(2, Time::ZERO, tid(1), rid(1)),
];
let analysis = detect_races(&events);
assert_eq!(analysis.backtrack_points.len(), analysis.race_count());
assert_eq!(analysis.backtrack_points[0].divergence_index, 0);
}
#[test]
fn hb_race_detector_ignores_same_task() {
let events = [
TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1)),
TraceEvent::complete(2, Time::ZERO, tid(1), rid(1)),
];
let report = detect_hb_races(&events);
assert!(report.is_race_free());
}
#[test]
fn hb_race_detector_detects_region_conflict() {
let reason = CancelReason::user("test");
let events = [
TraceEvent::cancel_request(1, Time::ZERO, tid(1), rid(1), reason.clone()),
TraceEvent::cancel_request(2, Time::ZERO, tid(2), rid(1), reason),
];
let report = detect_hb_races(&events);
assert_eq!(report.race_count(), 1);
assert_eq!(
report.races[0].kind,
RaceKind::Resource(Resource::Region(rid(1)))
);
}
#[test]
fn hb_race_detector_skips_non_task_events() {
let events = [
TraceEvent::timer_scheduled(1, Time::ZERO, 7, Time::from_nanos(10)),
TraceEvent::timer_fired(2, Time::from_nanos(10), 7),
];
let report = detect_hb_races(&events);
assert!(report.is_race_free());
}
#[test]
fn trace_coverage_analysis_empty() {
let analysis = trace_coverage_analysis(&[]);
assert_eq!(analysis.event_count, 0);
assert_eq!(analysis.immediate_race_count, 0);
assert_eq!(analysis.hb_race_count, 0);
assert_eq!(analysis.estimated_classes, 1);
assert_eq!(analysis.racing_event_count, 0);
assert!((analysis.race_density - 0.0).abs() < f64::EPSILON);
}
#[test]
fn trace_coverage_analysis_concurrent_tasks() {
let events = [
TraceEvent::region_created(1, Time::ZERO, rid(1), None),
TraceEvent::spawn(2, Time::ZERO, tid(1), rid(1)),
TraceEvent::spawn(3, Time::ZERO, tid(2), rid(1)),
];
let analysis = trace_coverage_analysis(&events);
assert_eq!(analysis.event_count, 3);
assert!(analysis.immediate_race_count >= 1);
assert!(analysis.estimated_classes >= 2);
assert!(analysis.racing_event_count >= 1);
assert!(analysis.race_density > 0.0);
}
#[test]
fn resource_race_distribution_from_hb() {
let reason = CancelReason::user("test");
let events = [
TraceEvent::cancel_request(1, Time::ZERO, tid(1), rid(1), reason.clone()),
TraceEvent::cancel_request(2, Time::ZERO, tid(2), rid(1), reason),
];
let report = detect_hb_races(&events);
let dist = ResourceRaceDistribution::from_report(&report);
assert_eq!(dist.total(), 1);
assert_eq!(dist.resource_count(), 1);
}
#[test]
fn sleep_set_deduplication() {
let events = [
TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1)),
TraceEvent::complete(2, Time::ZERO, tid(1), rid(1)),
];
let bp = BacktrackPoint {
race: Race {
earlier: 0,
later: 1,
},
divergence_index: 0,
};
let mut sleep = SleepSet::new();
assert!(!sleep.contains(&bp, &events));
sleep.insert(&bp, &events);
assert!(sleep.contains(&bp, &events));
assert_eq!(sleep.len(), 1);
}
#[test]
fn sleep_set_distinguishes_same_shape_races_on_different_tasks() {
let left_events = [
TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1)),
TraceEvent::complete(2, Time::ZERO, tid(1), rid(1)),
];
let right_events = [
TraceEvent::spawn(1, Time::ZERO, tid(2), rid(1)),
TraceEvent::complete(2, Time::ZERO, tid(2), rid(1)),
];
let bp = BacktrackPoint {
race: Race {
earlier: 0,
later: 1,
},
divergence_index: 0,
};
let mut sleep = SleepSet::new();
sleep.insert(&bp, &left_events);
assert!(
!sleep.contains(&bp, &right_events),
"sleep-set key must include semantic endpoint identity, not just kind/index shape"
);
}
#[test]
fn sleep_set_deduplicates_same_race_when_trace_order_is_reversed() {
let reason = CancelReason::user("test");
let left_events = [
TraceEvent::cancel_request(1, Time::ZERO, tid(1), rid(1), reason.clone()),
TraceEvent::cancel_request(2, Time::ZERO, tid(2), rid(1), reason.clone()),
];
let right_events = [
TraceEvent::cancel_request(1, Time::ZERO, tid(2), rid(1), reason.clone()),
TraceEvent::cancel_request(2, Time::ZERO, tid(1), rid(1), reason),
];
let bp = BacktrackPoint {
race: Race {
earlier: 0,
later: 1,
},
divergence_index: 0,
};
let mut sleep = SleepSet::new();
sleep.insert(&bp, &left_events);
assert!(
sleep.contains(&bp, &right_events),
"sleep-set key should treat the same race as explored even when the endpoints swap order"
);
}
#[test]
fn detected_race_debug_clone_eq() {
let race = DetectedRace {
race: Race {
earlier: 0,
later: 1,
},
kind: RaceKind::Resource(Resource::GlobalClock),
earlier_task: None,
later_task: None,
earlier_kind: TraceEventKind::Spawn,
later_kind: TraceEventKind::Spawn,
};
let cloned = race.clone();
assert_eq!(race, cloned);
let dbg = format!("{race:?}");
assert!(dbg.contains("DetectedRace"));
}
}