1use super::*;
3use serde::Serialize;
4use std::sync::atomic::{AtomicU64, Ordering};
5
6static NEXT_RECORDER: AtomicU64 = AtomicU64::new(1);
7
8type Marker = unsafe extern "C" fn(u64, *mut u32, u32, *mut u32) -> i32;
9type Clock = unsafe extern "C" fn(u32, u32, *mut u64) -> i32;
10
11pub(super) struct MarkerApi {
12 _library: libloading::Library,
13 marker: Marker,
14 frequency: u64,
15 device: [u32; 2],
16}
17impl MarkerApi {
18 pub(super) fn load(device: &Device) -> Result<Self> {
19 let api = &device.0.endpoint.0.instance.api;
20 let mut info = amdf_endpoint_info_t {
21 type_: AMDF_STRUCTURE_TYPE_ENDPOINT_INFO,
22 structure_size: size_of::<amdf_endpoint_info_t>() as u32,
23 ..Default::default()
24 };
25 check("profile endpoint", unsafe {
26 entry!(api.core, endpoint_query_info)(device.0.endpoint.0.raw, &mut info)
27 })?;
28 if info.native_identity.type_ != AMDF_ENDPOINT_NATIVE_IDENTITY_TYPE_LINUX_DEVICE {
29 return Err(Error::Unsupported(
30 "GPU timestamps require a Linux device identity".into(),
31 ));
32 }
33 let (library, marker, clock, native) = unsafe {
36 let library = libloading::Library::new(api.bridge_path())?;
37 let marker = *library
38 .get::<Marker>(b"hrx_fabric_gpu_profile_marker\0")
39 .map_err(|_| missing("GPU profile markers"))?;
40 let clock = *library
41 .get::<Clock>(b"hrx_fabric_gpu_profile_clock\0")
42 .map_err(|_| missing("GPU profile clock"))?;
43 (
44 library,
45 marker,
46 clock,
47 info.native_identity.value.linux_device,
48 )
49 };
50 let mut frequency = 0;
51 let status = unsafe { clock(native.major, native.minor, &mut frequency) };
52 if status != 0 || frequency == 0 {
53 return Err(Error::Unsupported(format!(
54 "GPU profile clock unavailable: errno {status}"
55 )));
56 }
57 Ok(Self {
58 _library: library,
59 marker,
60 frequency,
61 device: [native.major, native.minor],
62 })
63 }
64 pub(super) fn emit(&self, address: u64, words: &mut Vec<u32>) -> Result<()> {
65 let mut packet = [0u32; 32];
66 let mut count = 0;
67 let status = unsafe {
68 (self.marker)(
69 address,
70 packet.as_mut_ptr(),
71 packet.len() as u32,
72 &mut count,
73 )
74 };
75 if status != 0 || count == 0 || count as usize > packet.len() {
76 return Err(Error::Unsupported(format!(
77 "GPU timestamp marker unavailable: errno {status}"
78 )));
79 }
80 words.extend_from_slice(&packet[..count as usize]);
81 Ok(())
82 }
83}
84
85#[derive(Debug, Clone, Serialize)]
87pub struct DeviceInterval {
88 pub label: String,
90 pub start_tick: u64,
92 pub end_tick: u64,
94}
95#[derive(Debug, Clone, Serialize)]
98pub struct DeviceProfile {
99 pub process: u32,
101 pub device: [u32; 2],
103 pub queue: usize,
105 pub recorder: u64,
107 pub execution: u64,
109 pub frequency_hz: u64,
111 pub intervals: Vec<DeviceInterval>,
113 pub interval_union_ms: f64,
115 pub span_ms: f64,
117 pub gaps_ms: f64,
119}
120
121pub(crate) struct ProfileCapture {
122 pub(super) buffer: Buffer,
123 pub(super) api: MarkerApi,
124 labels: Vec<String>,
125 queue: usize,
126 recorder: u64,
127 execution: u64,
128 previous_end: u64,
129}
130impl ProfileCapture {
131 pub(super) fn new(queue: &Queue, labels: &[String]) -> Result<Self> {
132 if labels.is_empty() {
133 return Err(Error::Message("empty profile".into()));
134 }
135 let bytes = labels
136 .len()
137 .checked_mul(16)
138 .ok_or_else(|| Error::Message("profile size overflow".into()))?;
139 let api = MarkerApi::load(queue.device())?;
140 let buffer = queue
141 .device()
142 .fabric()
143 .allocate_shared(bytes, std::slice::from_ref(queue.device()))?;
144 #[allow(deprecated, reason = "fetch_update supports the Rust 1.91 MSRV")]
145 let recorder = NEXT_RECORDER
146 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| v.checked_add(1))
147 .map_err(|_| Error::Message("profile recorder identity exhausted".into()))?;
148 Ok(Self {
149 buffer,
150 api,
151 labels: labels.to_vec(),
152 queue: queue.identity(),
153 recorder,
154 execution: 0,
155 previous_end: 0,
156 })
157 }
158 pub(crate) fn read(&mut self) -> Result<DeviceProfile> {
159 let mut bytes = vec![0; self.labels.len() * 16];
160 self.buffer.read(0, &mut bytes)?;
162 let intervals = bytes
163 .as_chunks::<16>()
164 .0
165 .iter()
166 .zip(&self.labels)
167 .map(|(bytes, label)| DeviceInterval {
168 label: label.clone(),
169 start_tick: u64::from_le_bytes(bytes[..8].try_into().unwrap()),
170 end_tick: u64::from_le_bytes(bytes[8..].try_into().unwrap()),
171 })
172 .collect::<Vec<_>>();
173 let (union, span) = interval_totals(&intervals)?;
174 if intervals.iter().any(|i| i.start_tick <= self.previous_end) {
175 return Err(Error::Message("stale or reset GPU profile counter".into()));
176 }
177 self.previous_end = intervals.iter().map(|i| i.end_tick).max().unwrap_or(0);
178 self.execution = self
179 .execution
180 .checked_add(1)
181 .ok_or_else(|| Error::Message("profile sample identity exhausted".into()))?;
182 let ms = 1000. / self.api.frequency as f64;
183 Ok(DeviceProfile {
184 process: std::process::id(),
185 device: self.api.device,
186 queue: self.queue,
187 recorder: self.recorder,
188 execution: self.execution,
189 frequency_hz: self.api.frequency,
190 intervals,
191 interval_union_ms: union as f64 * ms,
192 span_ms: span as f64 * ms,
193 gaps_ms: (span - union) as f64 * ms,
194 })
195 }
196}
197fn interval_totals(intervals: &[DeviceInterval]) -> Result<(u64, u64)> {
198 if intervals.is_empty() {
199 return Ok((0, 0));
200 }
201 let mut ordered = intervals.iter().collect::<Vec<_>>();
202 ordered.sort_by_key(|i| i.start_tick);
203 if ordered
204 .iter()
205 .any(|i| i.start_tick == 0 || i.end_tick < i.start_tick)
206 {
207 return Err(Error::Message(
208 "unwritten, reversed, or wrapped GPU timestamps".into(),
209 ));
210 }
211 let first = ordered[0].start_tick;
212 let mut end = first;
213 let mut union = 0;
214 for i in ordered {
215 let start = end.max(i.start_tick);
216 if i.end_tick > start {
217 union += i.end_tick - start;
218 }
219 end = end.max(i.end_tick);
220 }
221 Ok((union, end - first))
222}
223
224pub struct ProfiledGpu {
227 pub(crate) commands: PreparedGpu,
228 pub(crate) capture: ProfileCapture,
229}
230impl ProfiledGpu {
231 pub unsafe fn run(&mut self) -> Result<DeviceProfile> {
236 unsafe { self.commands.dispatch()? }.wait()?;
237 self.capture.read()
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244 #[test]
245 fn union_does_not_double_count_overlap_and_detects_invalid_ticks() {
246 let interval = |start_tick, end_tick| DeviceInterval {
247 label: String::new(),
248 start_tick,
249 end_tick,
250 };
251 assert_eq!(
252 interval_totals(&[interval(20, 30), interval(10, 25), interval(40, 45)]).unwrap(),
253 (25, 35)
254 );
255 assert!(interval_totals(&[interval(0, 1)]).is_err());
256 assert!(interval_totals(&[interval(2, 1)]).is_err());
257 }
258}