1use std::fmt;
49use std::sync::atomic::{AtomicU64, Ordering};
50use std::sync::Mutex;
51use std::time::{SystemTime, UNIX_EPOCH};
52
53use super::resource::{
54 Access, AllocTag, BlockId, DeviceBlock, DeviceMemoryResource, Generation,
55 ResourceBudgetSnapshot, ResourceError, ResourceResult, StreamId,
56};
57
58#[derive(Clone, Copy, Debug, Eq, PartialEq)]
62pub enum LogAction {
63 Allocate,
64 Deallocate,
65 ReapPending,
66}
67
68impl fmt::Display for LogAction {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 match self {
71 LogAction::Allocate => f.write_str("allocate"),
72 LogAction::Deallocate => f.write_str("deallocate"),
73 LogAction::ReapPending => f.write_str("reap_pending"),
74 }
75 }
76}
77
78#[derive(Clone, Debug, Eq, PartialEq)]
83pub enum LogResult {
84 Ok,
85 Err { kind: &'static str, message: String },
86}
87
88impl LogResult {
89 pub fn from_result<T>(r: &ResourceResult<T>) -> Self {
92 match r {
93 Ok(_) => LogResult::Ok,
94 Err(e) => LogResult::Err {
95 kind: classify_error(e),
96 message: truncate(format!("{}", e), 256),
97 },
98 }
99 }
100}
101
102fn classify_error(e: &ResourceError) -> &'static str {
103 match e {
104 ResourceError::OutOfBudget { .. } => "OutOfBudget",
105 ResourceError::Driver(_) => "Driver",
106 ResourceError::StreamMisuse(_) => "StreamMisuse",
107 ResourceError::UseAfterFree { .. } => "UseAfterFree",
108 ResourceError::OutOfBounds { .. } => "OutOfBounds",
109 }
110}
111
112fn truncate(mut s: String, cap: usize) -> String {
113 if s.len() > cap {
114 let mut end = cap;
121 while end > 0 && !s.is_char_boundary(end) {
122 end -= 1;
123 }
124 s.truncate(end);
125 s.push('…');
126 }
127 s
128}
129
130#[derive(Clone, Debug)]
133pub struct LogRecord {
134 pub action: LogAction,
135 pub device_ordinal: u32,
136 pub stream_id: Option<StreamId>,
137 pub ptr: Option<u64>,
138 pub bytes: Option<usize>,
139 pub tag: Option<AllocTag>,
140 pub generation: Option<Generation>,
141 pub thread_id: u64,
142 pub order_counter: u64,
143 pub timestamp_nanos: u128,
144 pub result: LogResult,
145}
146
147static ORDER_COUNTER: AtomicU64 = AtomicU64::new(1);
152
153fn next_order_counter() -> u64 {
154 ORDER_COUNTER.fetch_add(1, Ordering::Relaxed)
155}
156
157fn now_nanos() -> u128 {
158 SystemTime::now()
159 .duration_since(UNIX_EPOCH)
160 .map(|d| d.as_nanos())
161 .unwrap_or(0)
162}
163
164fn current_thread_id_u64() -> u64 {
165 let s = format!("{:?}", std::thread::current().id());
169 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
170 for b in s.as_bytes() {
171 h ^= *b as u64;
172 h = h.wrapping_mul(0x100_0000_01b3);
173 }
174 h
175}
176
177#[derive(Debug)]
183pub enum SinkError {
184 Refused(String),
186 Io(String),
188}
189
190impl fmt::Display for SinkError {
191 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192 match self {
193 SinkError::Refused(m) => write!(f, "log sink refused record: {}", m),
194 SinkError::Io(m) => write!(f, "log sink io error: {}", m),
195 }
196 }
197}
198
199impl std::error::Error for SinkError {}
200
201pub trait LoggingSink: Send + Sync {
205 fn emit(&self, record: LogRecord) -> Result<(), SinkError>;
208}
209
210pub struct InMemorySink {
214 records: Mutex<Vec<LogRecord>>,
215}
216
217impl InMemorySink {
218 pub fn new() -> Self {
219 Self {
220 records: Mutex::new(Vec::new()),
221 }
222 }
223
224 pub fn snapshot(&self) -> Vec<LogRecord> {
226 self.records.lock().expect("InMemorySink poisoned").clone()
227 }
228
229 pub fn clear(&self) {
232 self.records.lock().expect("InMemorySink poisoned").clear();
233 }
234
235 pub fn len(&self) -> usize {
237 self.records.lock().expect("InMemorySink poisoned").len()
238 }
239
240 pub fn is_empty(&self) -> bool {
241 self.len() == 0
242 }
243}
244
245impl Default for InMemorySink {
246 fn default() -> Self {
247 Self::new()
248 }
249}
250
251impl LoggingSink for InMemorySink {
252 fn emit(&self, record: LogRecord) -> Result<(), SinkError> {
253 self.records
254 .lock()
255 .expect("InMemorySink poisoned")
256 .push(record);
257 Ok(())
258 }
259}
260
261pub struct NullSink;
277
278impl NullSink {
279 pub fn new() -> Self {
280 Self
281 }
282}
283
284impl Default for NullSink {
285 fn default() -> Self {
286 Self
287 }
288}
289
290impl LoggingSink for NullSink {
291 fn emit(&self, _record: LogRecord) -> Result<(), SinkError> {
292 Ok(())
293 }
294}
295
296pub struct LoggingResource {
298 inner: Box<dyn DeviceMemoryResource + Send + Sync>,
299 sink: std::sync::Arc<dyn LoggingSink>,
300 dropped_records: AtomicU64,
304}
305
306impl LoggingResource {
307 pub fn new(
310 inner: Box<dyn DeviceMemoryResource + Send + Sync>,
311 sink: std::sync::Arc<dyn LoggingSink>,
312 ) -> Self {
313 Self {
314 inner,
315 sink,
316 dropped_records: AtomicU64::new(0),
317 }
318 }
319
320 pub fn dropped_records(&self) -> u64 {
323 self.dropped_records.load(Ordering::Relaxed)
324 }
325
326 fn emit(&self, record: LogRecord) {
327 if self.sink.emit(record).is_err() {
328 self.dropped_records.fetch_add(1, Ordering::Relaxed);
329 }
330 }
331}
332
333impl DeviceMemoryResource for LoggingResource {
334 fn allocate(
335 &self,
336 bytes: usize,
337 stream: StreamId,
338 tag: AllocTag,
339 ) -> ResourceResult<DeviceBlock> {
340 let result = self.inner.allocate(bytes, stream, tag);
341 let (ptr, gen, recorded_bytes) = match &result {
342 Ok(b) => (Some(b.ptr), Some(b.generation), Some(b.bytes)),
343 Err(_) => (None, None, Some(bytes)),
344 };
345 self.emit(LogRecord {
346 action: LogAction::Allocate,
347 device_ordinal: self.inner.device_ordinal(),
348 stream_id: Some(stream),
349 ptr,
350 bytes: recorded_bytes,
351 tag: Some(tag),
352 generation: gen,
353 thread_id: current_thread_id_u64(),
354 order_counter: next_order_counter(),
355 timestamp_nanos: now_nanos(),
356 result: LogResult::from_result(&result),
357 });
358 result
359 }
360
361 fn deallocate(&self, block: DeviceBlock) -> ResourceResult<()> {
362 let ptr = block.ptr;
366 let bytes = block.bytes;
367 let tag = block.tag;
368 let gen = block.generation;
369 let stream = block.alloc_stream;
370 let dev = block.device_ordinal;
371
372 let result = self.inner.deallocate(block);
373 self.emit(LogRecord {
374 action: LogAction::Deallocate,
375 device_ordinal: dev,
376 stream_id: Some(stream),
377 ptr: Some(ptr),
378 bytes: Some(bytes),
379 tag: Some(tag),
380 generation: Some(gen),
381 thread_id: current_thread_id_u64(),
382 order_counter: next_order_counter(),
383 timestamp_nanos: now_nanos(),
384 result: LogResult::from_result(&result),
385 });
386 result
387 }
388
389 fn device_ordinal(&self) -> u32 {
390 self.inner.device_ordinal()
391 }
392
393 fn bytes_outstanding(&self) -> usize {
394 self.inner.bytes_outstanding()
395 }
396
397 fn budget_snapshot(&self) -> Option<ResourceBudgetSnapshot> {
398 self.inner.budget_snapshot()
399 }
400
401 fn reap_pending(&self) -> ResourceResult<()> {
402 let result = self.inner.reap_pending();
403 self.emit(LogRecord {
404 action: LogAction::ReapPending,
405 device_ordinal: self.inner.device_ordinal(),
406 stream_id: None,
407 ptr: None,
408 bytes: None,
409 tag: None,
410 generation: None,
411 thread_id: current_thread_id_u64(),
412 order_counter: next_order_counter(),
413 timestamp_nanos: now_nanos(),
414 result: LogResult::from_result(&result),
415 });
416 result
417 }
418
419 fn record_block_use(&self, block: &DeviceBlock, use_stream: StreamId) -> ResourceResult<()> {
420 self.inner.record_block_use(block, use_stream)
429 }
430
431 fn supports_block_use_tracking(&self) -> bool {
432 self.inner.supports_block_use_tracking()
433 }
434
435 fn prepare_block_use(
436 &self,
437 block: BlockId,
438 use_stream: StreamId,
439 access: Access,
440 ) -> ResourceResult<()> {
441 self.inner.prepare_block_use(block, use_stream, access)
444 }
445
446 fn finish_block_use(
447 &self,
448 block: BlockId,
449 use_stream: StreamId,
450 access: Access,
451 ) -> ResourceResult<()> {
452 self.inner.finish_block_use(block, use_stream, access)
454 }
455}
456
457#[cfg(test)]
458mod tests {
459 use super::super::direct::DirectCudaResource;
460 use super::super::resource::BlockState;
461 use super::*;
462 use std::sync::Arc;
463
464 use crate::CudaDevice;
465
466 fn try_device() -> Option<Arc<CudaDevice>> {
467 CudaDevice::new(0).ok().map(Arc::new)
468 }
469
470 #[test]
471 fn pass_through_alloc_dealloc_emits_two_ok_records() {
472 let Some(device) = try_device() else {
473 return;
474 };
475 let inner = Box::new(DirectCudaResource::new(Arc::clone(&device), 0));
476 let sink = Arc::new(InMemorySink::new());
477 let r = LoggingResource::new(inner, sink.clone());
478
479 let block = r
480 .allocate(1024, StreamId::DEFAULT, AllocTag("logging-test"))
481 .expect("alloc");
482 assert_eq!(block.bytes, 1024);
483 assert_eq!(block.state, BlockState::Live);
484 r.deallocate(block).expect("dealloc");
485
486 let recs = sink.snapshot();
487 assert_eq!(recs.len(), 2, "expected 2 records, got {:?}", recs);
488 assert_eq!(recs[0].action, LogAction::Allocate);
489 assert_eq!(recs[0].result, LogResult::Ok);
490 assert_eq!(recs[0].bytes, Some(1024));
491 assert_eq!(recs[0].stream_id, Some(StreamId::DEFAULT));
492 assert!(recs[0].ptr.is_some());
493
494 assert_eq!(recs[1].action, LogAction::Deallocate);
495 assert_eq!(recs[1].result, LogResult::Ok);
496 assert_eq!(recs[1].ptr, recs[0].ptr);
497 assert_eq!(recs[1].generation, recs[0].generation);
498 }
499
500 #[test]
501 fn order_counter_strictly_increases_across_records() {
502 let Some(device) = try_device() else {
503 return;
504 };
505 let inner = Box::new(DirectCudaResource::new(Arc::clone(&device), 0));
506 let sink = Arc::new(InMemorySink::new());
507 let r = LoggingResource::new(inner, sink.clone());
508
509 for _ in 0..4 {
510 let b = r
511 .allocate(64, StreamId::DEFAULT, AllocTag::UNTAGGED)
512 .expect("alloc");
513 r.deallocate(b).expect("dealloc");
514 }
515 r.reap_pending().expect("reap");
516
517 let recs = sink.snapshot();
518 assert_eq!(recs.len(), 9); let mut last = 0u64;
520 for rec in &recs {
521 assert!(
522 rec.order_counter > last,
523 "order_counter must strictly increase: prev={}, now={}",
524 last,
525 rec.order_counter
526 );
527 last = rec.order_counter;
528 }
529 }
530
531 #[test]
532 fn failed_alloc_records_error_result() {
533 let Some(device) = try_device() else {
534 return;
535 };
536 let inner = Box::new(DirectCudaResource::new(Arc::clone(&device), 0));
537 let sink = Arc::new(InMemorySink::new());
538 let r = LoggingResource::new(inner, sink.clone());
539
540 let _ = r.allocate(0, StreamId::DEFAULT, AllocTag::UNTAGGED);
542 let recs = sink.snapshot();
543 assert_eq!(recs.len(), 1);
544 assert_eq!(recs[0].action, LogAction::Allocate);
545 assert!(matches!(recs[0].result, LogResult::Err { kind, .. } if kind == "Driver"));
546 assert_eq!(recs[0].bytes, Some(0));
548 assert!(recs[0].ptr.is_none());
549 assert!(recs[0].generation.is_none());
550 }
551
552 #[test]
553 fn failed_dealloc_records_error_result() {
554 let Some(device) = try_device() else {
555 return;
556 };
557 let inner = Box::new(DirectCudaResource::new(Arc::clone(&device), 0));
558 let sink = Arc::new(InMemorySink::new());
559 let r = LoggingResource::new(inner, sink.clone());
560
561 let bogus = DeviceBlock {
562 ptr: 0xdead_beef,
563 device_ordinal: 0,
564 alloc_stream: StreamId::DEFAULT,
565 bytes: 16,
566 align: 1,
567 tag: AllocTag::UNTAGGED,
568 generation: Generation::next(),
569 state: BlockState::Live,
570 };
571 let res = r.deallocate(bogus);
572 assert!(res.is_err());
573 let recs = sink.snapshot();
574 assert_eq!(recs.len(), 1);
575 assert_eq!(recs[0].action, LogAction::Deallocate);
576 assert!(matches!(recs[0].result, LogResult::Err { kind, .. } if kind == "UseAfterFree"));
577 assert_eq!(recs[0].ptr, Some(0xdead_beef));
578 }
579
580 #[test]
581 fn reap_pending_emits_record() {
582 let Some(device) = try_device() else {
583 return;
584 };
585 let inner = Box::new(DirectCudaResource::new(Arc::clone(&device), 0));
586 let sink = Arc::new(InMemorySink::new());
587 let r = LoggingResource::new(inner, sink.clone());
588
589 r.reap_pending().expect("reap");
590 let recs = sink.snapshot();
591 assert_eq!(recs.len(), 1);
592 assert_eq!(recs[0].action, LogAction::ReapPending);
593 assert_eq!(recs[0].result, LogResult::Ok);
594 assert!(recs[0].stream_id.is_none());
595 assert!(recs[0].ptr.is_none());
596 }
597
598 #[test]
599 fn sink_failure_increments_dropped_records_but_does_not_break_alloc() {
600 struct RefuseAllSink;
603 impl LoggingSink for RefuseAllSink {
604 fn emit(&self, _r: LogRecord) -> Result<(), SinkError> {
605 Err(SinkError::Refused("test sink refuses all".into()))
606 }
607 }
608
609 let Some(device) = try_device() else {
610 return;
611 };
612 let inner = Box::new(DirectCudaResource::new(Arc::clone(&device), 0));
613 let sink = Arc::new(RefuseAllSink);
614 let r = LoggingResource::new(inner, sink);
615
616 let block = r
618 .allocate(128, StreamId::DEFAULT, AllocTag("refuse-test"))
619 .expect("alloc must succeed even when sink refuses");
620 r.deallocate(block).expect("dealloc must succeed too");
621 assert_eq!(r.dropped_records(), 2);
623 }
624
625 #[test]
626 fn forwards_bytes_outstanding_and_device_ordinal() {
627 let Some(device) = try_device() else {
628 return;
629 };
630 let inner = Box::new(DirectCudaResource::new(Arc::clone(&device), 3));
631 let sink = Arc::new(InMemorySink::new());
632 let r = LoggingResource::new(inner, sink);
633
634 assert_eq!(r.device_ordinal(), 3);
635 assert_eq!(r.bytes_outstanding(), 0);
636 }
637
638 #[test]
639 fn truncate_handles_non_ascii_at_cap_without_panicking() {
640 let s = String::from("héllo");
649 let out = truncate(s, 2);
650 assert!(out.starts_with('h'));
651 assert!(out.ends_with('…'));
652
653 let out = truncate(String::from("ok"), 100);
655 assert_eq!(out, "ok");
656
657 let out = truncate(String::from("héllo"), 0);
659 assert_eq!(out, "…");
660
661 let out = truncate(String::from("abcdefgh"), 3);
664 assert_eq!(out, "abc…");
665 }
666
667 #[test]
668 fn null_sink_accepts_records_without_retention() {
669 let sink = NullSink::new();
670 let rec = LogRecord {
671 action: LogAction::Allocate,
672 device_ordinal: 0,
673 stream_id: Some(StreamId::DEFAULT),
674 ptr: Some(0xdead_beef),
675 bytes: Some(64),
676 tag: Some(AllocTag::UNTAGGED),
677 generation: Some(Generation::next()),
678 thread_id: 0,
679 order_counter: 1,
680 timestamp_nanos: 0,
681 result: LogResult::Ok,
682 };
683 sink.emit(rec).expect("NullSink never errors");
684 }
687}