Skip to main content

softgpu_core/
trace.rs

1//! Structured, bounded runtime traces.
2
3use crate::fidelity::FidelityLevel;
4use serde::Serialize;
5use std::sync::Mutex;
6use std::time::{SystemTime, UNIX_EPOCH};
7
8/// Stable event categories for SoftGPU runtime observation.
9#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
10#[serde(tag = "type", rename_all = "snake_case")]
11pub enum TraceEvent {
12    RuntimeInit {
13        seq: u64,
14        refcount: u32,
15        profile_id: String,
16        profile_revision: String,
17        fidelity: String,
18    },
19    RuntimeShutdown {
20        seq: u64,
21        refcount: u32,
22    },
23    AgentIterateBegin {
24        seq: u64,
25        agent_count: usize,
26    },
27    AgentIterateVisit {
28        seq: u64,
29        agent_handle: u64,
30        kind: String,
31    },
32    AgentGetInfo {
33        seq: u64,
34        agent_handle: u64,
35        attribute: String,
36        outcome: String,
37    },
38    MemoryAllocate {
39        seq: u64,
40        space_handle: u64,
41        size: usize,
42        ptr: u64,
43        outcome: String,
44    },
45    MemoryFree {
46        seq: u64,
47        ptr: u64,
48        outcome: String,
49    },
50    SignalCreate {
51        seq: u64,
52        signal_handle: u64,
53        initial: i64,
54    },
55    SignalDestroy {
56        seq: u64,
57        signal_handle: u64,
58    },
59    QueueCreate {
60        seq: u64,
61        queue_id: u64,
62        size: u32,
63        agent_handle: u64,
64    },
65    QueueDestroy {
66        seq: u64,
67        queue_id: u64,
68    },
69    QueueDoorbell {
70        seq: u64,
71        queue_id: u64,
72        value: i64,
73    },
74    QueueIndexStore {
75        seq: u64,
76        queue_id: u64,
77        which: String,
78        value: u64,
79    },
80    PacketObserved {
81        seq: u64,
82        queue_id: u64,
83        packet_index: u64,
84        packet_type: u16,
85    },
86    PacketValidateFailed {
87        seq: u64,
88        queue_id: u64,
89        detail: String,
90    },
91    /// Validated AQL fields; does **not** imply kernel execution.
92    DispatchValidated {
93        seq: u64,
94        queue_id: u64,
95        packet_index: u64,
96        packet_type: u16,
97        dimensions: u16,
98        workgroup_size: [u16; 3],
99        grid_size: [u32; 3],
100        private_segment_size: u32,
101        group_segment_size: u32,
102        kernel_object: u64,
103        kernarg_class: String,
104        completion_signal: u64,
105    },
106    DispatchRejected {
107        seq: u64,
108        queue_id: u64,
109        packet_index: u64,
110        packet_type: u16,
111        detail: String,
112        contract: String,
113    },
114    /// Experimental no-execution completion (see `docs/aql-diagnostic-contract.md`).
115    DiagnosticComplete {
116        seq: u64,
117        queue_id: u64,
118        packet_index: u64,
119        completion_signal: u64,
120        contract: String,
121        note: String,
122    },
123    Unsupported {
124        seq: u64,
125        api: String,
126        detail: String,
127    },
128}
129
130/// Sink that records events without panicking the runtime.
131pub trait TraceSink: Send {
132    fn record(&mut self, event: TraceEvent);
133}
134
135/// In-memory ring with a fixed capacity.
136#[derive(Debug, Default)]
137pub struct TraceLog {
138    seq: u64,
139    events: Vec<TraceEvent>,
140    capacity: usize,
141}
142
143impl TraceLog {
144    pub fn new(capacity: usize) -> Self {
145        Self {
146            seq: 0,
147            events: Vec::new(),
148            capacity: capacity.max(1),
149        }
150    }
151
152    pub fn next_seq(&mut self) -> u64 {
153        self.seq = self.seq.saturating_add(1);
154        self.seq
155    }
156
157    pub fn events(&self) -> &[TraceEvent] {
158        &self.events
159    }
160
161    pub fn fidelity_label(level: FidelityLevel) -> String {
162        level.as_str().to_string()
163    }
164}
165
166impl TraceSink for TraceLog {
167    fn record(&mut self, event: TraceEvent) {
168        if self.events.len() >= self.capacity {
169            self.events.remove(0);
170        }
171        self.events.push(event);
172    }
173}
174
175/// Shared optional trace holder used by the runtime.
176#[derive(Debug)]
177pub struct SharedTrace {
178    inner: Mutex<TraceLog>,
179}
180
181impl SharedTrace {
182    pub fn new(capacity: usize) -> Self {
183        Self {
184            inner: Mutex::new(TraceLog::new(capacity)),
185        }
186    }
187
188    pub fn with_log<R>(&self, f: impl FnOnce(&mut TraceLog) -> R) -> R {
189        let mut guard = self
190            .inner
191            .lock()
192            .unwrap_or_else(|poisoned| poisoned.into_inner());
193        f(&mut guard)
194    }
195
196    pub fn snapshot(&self) -> Vec<TraceEvent> {
197        self.with_log(|log| log.events().to_vec())
198    }
199}
200
201/// Wall-clock helper for optional JSON Lines tooling (not used as GPU time).
202pub fn unix_millis_now() -> u128 {
203    SystemTime::now()
204        .duration_since(UNIX_EPOCH)
205        .map(|d| d.as_millis())
206        .unwrap_or(0)
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn ring_drops_oldest() {
215        let mut log = TraceLog::new(2);
216        log.record(TraceEvent::Unsupported {
217            seq: 1,
218            api: "a".into(),
219            detail: "1".into(),
220        });
221        log.record(TraceEvent::Unsupported {
222            seq: 2,
223            api: "b".into(),
224            detail: "2".into(),
225        });
226        log.record(TraceEvent::Unsupported {
227            seq: 3,
228            api: "c".into(),
229            detail: "3".into(),
230        });
231        assert_eq!(log.events().len(), 2);
232        match &log.events()[0] {
233            TraceEvent::Unsupported { api, .. } => assert_eq!(api, "b"),
234            _ => panic!("unexpected"),
235        }
236    }
237}