Skip to main content

sklears_simd/
performance_hooks.rs

1//! Performance monitoring hooks for SIMD operations
2//!
3//! This module provides a hook system that allows users to register callbacks
4//! that get executed during SIMD operations for performance monitoring and debugging.
5
6#[cfg(not(feature = "no-std"))]
7use std::collections::HashMap;
8#[cfg(not(feature = "no-std"))]
9use std::fmt;
10#[cfg(not(feature = "no-std"))]
11use std::string::ToString;
12#[cfg(not(feature = "no-std"))]
13use std::sync::{Arc, Mutex, RwLock};
14#[cfg(not(feature = "no-std"))]
15use std::thread::ThreadId;
16#[cfg(not(feature = "no-std"))]
17use std::time::{Duration, Instant};
18
19#[cfg(feature = "no-std")]
20use alloc::{
21    collections::BTreeMap as HashMap,
22    string::{String, ToString},
23    sync::Arc,
24    vec,
25    vec::Vec,
26};
27#[cfg(feature = "no-std")]
28use core::fmt;
29#[cfg(feature = "no-std")]
30use spin::{Mutex, RwLock};
31
32// Type aliases for conditional compilation
33
34#[cfg(feature = "no-std")]
35pub type ThreadId = u64; // Mock thread ID for no-std
36#[cfg(feature = "no-std")]
37#[derive(Debug, Clone, Copy, Default)]
38pub struct Duration(u64); // Mock duration in microseconds
39#[cfg(feature = "no-std")]
40#[derive(Debug, Clone, Copy)]
41pub struct Instant; // Mock instant stub for no-std
42
43#[cfg(feature = "no-std")]
44impl Instant {
45    pub fn now() -> Self {
46        Instant // Mock implementation
47    }
48}
49
50#[cfg(feature = "no-std")]
51impl Duration {
52    pub fn from_millis(_millis: u64) -> Self {
53        Duration(0) // Mock implementation
54    }
55
56    pub fn from_nanos(_nanos: u64) -> Self {
57        Duration(0) // Mock implementation
58    }
59
60    pub fn as_nanos(&self) -> u128 {
61        self.0 as u128 * 1000 // Mock implementation
62    }
63}
64
65#[cfg(feature = "no-std")]
66impl core::ops::Div<u32> for Duration {
67    type Output = Duration;
68
69    fn div(self, rhs: u32) -> Self::Output {
70        Duration(if rhs == 0 {
71            self.0
72        } else {
73            self.0 / rhs as u64
74        })
75    }
76}
77
78/// Hook execution phase
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
80pub enum HookPhase {
81    BeforeOperation,
82    AfterOperation,
83    OnError,
84    OnOptimization,
85}
86
87/// Performance event information
88#[derive(Debug, Clone)]
89pub struct PerformanceEvent {
90    pub operation_name: String,
91    pub phase: HookPhase,
92    pub timestamp: Instant,
93    pub thread_id: ThreadId,
94    pub input_size: usize,
95    pub output_size: usize,
96    pub execution_time: Option<Duration>,
97    pub error_message: Option<String>,
98    pub metadata: HashMap<String, String>,
99}
100
101impl PerformanceEvent {
102    pub fn new(
103        operation_name: String,
104        phase: HookPhase,
105        input_size: usize,
106        output_size: usize,
107    ) -> Self {
108        Self {
109            operation_name,
110            phase,
111            timestamp: Instant::now(),
112            #[cfg(not(feature = "no-std"))]
113            thread_id: std::thread::current().id(),
114            #[cfg(feature = "no-std")]
115            thread_id: 0, // Mock thread ID
116            input_size,
117            output_size,
118            execution_time: None,
119            error_message: None,
120            metadata: HashMap::new(),
121        }
122    }
123
124    pub fn with_execution_time(mut self, duration: Duration) -> Self {
125        self.execution_time = Some(duration);
126        self
127    }
128
129    pub fn with_error(mut self, error: String) -> Self {
130        self.error_message = Some(error);
131        self
132    }
133
134    pub fn with_metadata(mut self, key: String, value: String) -> Self {
135        self.metadata.insert(key, value);
136        self
137    }
138}
139
140/// Performance hook trait
141pub trait PerformanceHook: Send + Sync {
142    /// Called when a performance event occurs
143    fn on_event(&self, event: &PerformanceEvent);
144
145    /// Get the name of the hook
146    fn name(&self) -> &str;
147
148    /// Get the phases this hook is interested in
149    fn interested_phases(&self) -> Vec<HookPhase> {
150        vec![HookPhase::BeforeOperation, HookPhase::AfterOperation]
151    }
152
153    /// Check if this hook should be called for a specific operation
154    fn should_handle(&self, operation_name: &str) -> bool {
155        let _ = operation_name; // Default: handle all operations
156        true
157    }
158}
159
160/// Hook manager for registering and executing performance hooks
161pub struct HookManager {
162    hooks: RwLock<HashMap<String, Arc<dyn PerformanceHook>>>,
163    enabled: RwLock<bool>,
164    event_buffer: Mutex<Vec<PerformanceEvent>>,
165    max_buffer_size: usize,
166}
167
168impl Default for HookManager {
169    fn default() -> Self {
170        Self::new()
171    }
172}
173
174impl HookManager {
175    /// Create a new hook manager
176    pub fn new() -> Self {
177        Self {
178            hooks: RwLock::new(HashMap::new()),
179            enabled: RwLock::new(true),
180            event_buffer: Mutex::new(Vec::new()),
181            max_buffer_size: 10000,
182        }
183    }
184
185    /// Helper function to handle RwLock read locking in both std and no-std environments
186    #[cfg(not(feature = "no-std"))]
187    fn read_hooks(
188        &self,
189    ) -> std::sync::RwLockReadGuard<'_, HashMap<String, Arc<dyn PerformanceHook>>> {
190        self.hooks.read().expect("operation should succeed")
191    }
192
193    #[cfg(feature = "no-std")]
194    fn read_hooks(&self) -> spin::RwLockReadGuard<'_, HashMap<String, Arc<dyn PerformanceHook>>> {
195        self.hooks.read()
196    }
197
198    /// Helper function to handle RwLock write locking in both std and no-std environments
199    #[cfg(not(feature = "no-std"))]
200    fn write_hooks(
201        &self,
202    ) -> std::sync::RwLockWriteGuard<'_, HashMap<String, Arc<dyn PerformanceHook>>> {
203        self.hooks.write().expect("operation should succeed")
204    }
205
206    #[cfg(feature = "no-std")]
207    fn write_hooks(&self) -> spin::RwLockWriteGuard<'_, HashMap<String, Arc<dyn PerformanceHook>>> {
208        self.hooks.write()
209    }
210
211    /// Helper function to handle enabled RwLock read locking in both std and no-std environments
212    #[cfg(not(feature = "no-std"))]
213    fn read_enabled(&self) -> std::sync::RwLockReadGuard<'_, bool> {
214        self.enabled.read().expect("operation should succeed")
215    }
216
217    #[cfg(feature = "no-std")]
218    fn read_enabled(&self) -> spin::RwLockReadGuard<'_, bool> {
219        self.enabled.read()
220    }
221
222    /// Helper function to handle enabled RwLock write locking in both std and no-std environments
223    #[cfg(not(feature = "no-std"))]
224    fn write_enabled(&self) -> std::sync::RwLockWriteGuard<'_, bool> {
225        self.enabled.write().expect("operation should succeed")
226    }
227
228    #[cfg(feature = "no-std")]
229    fn write_enabled(&self) -> spin::RwLockWriteGuard<'_, bool> {
230        self.enabled.write()
231    }
232
233    /// Helper function to handle event buffer Mutex locking in both std and no-std environments
234    #[cfg(not(feature = "no-std"))]
235    fn lock_event_buffer(&self) -> std::sync::MutexGuard<'_, Vec<PerformanceEvent>> {
236        self.event_buffer
237            .lock()
238            .expect("lock should not be poisoned")
239    }
240
241    #[cfg(feature = "no-std")]
242    fn lock_event_buffer(&self) -> spin::MutexGuard<'_, Vec<PerformanceEvent>, spin::Spin> {
243        self.event_buffer.lock()
244    }
245
246    /// Register a performance hook
247    pub fn register_hook(&self, hook: Arc<dyn PerformanceHook>) -> Result<(), HookError> {
248        let name = hook.name().to_string();
249        let mut hooks = self.write_hooks();
250
251        if hooks.contains_key(&name) {
252            return Err(HookError::AlreadyRegistered(name));
253        }
254
255        hooks.insert(name, hook);
256        Ok(())
257    }
258
259    /// Unregister a performance hook
260    pub fn unregister_hook(&self, name: &str) -> Result<(), HookError> {
261        let mut hooks = self.write_hooks();
262        if hooks.remove(name).is_none() {
263            return Err(HookError::NotFound(name.to_string()));
264        }
265        Ok(())
266    }
267
268    /// Fire a performance event
269    pub fn fire_event(&self, event: PerformanceEvent) {
270        // Check if monitoring is enabled
271        if !*self.read_enabled() {
272            return;
273        }
274
275        // Store event in buffer
276        {
277            let mut buffer = self.lock_event_buffer();
278            buffer.push(event.clone());
279
280            // Limit buffer size
281            if buffer.len() > self.max_buffer_size {
282                buffer.remove(0);
283            }
284        }
285
286        // Execute hooks
287        let hooks = self.read_hooks();
288        for hook in hooks.values() {
289            if hook.should_handle(&event.operation_name)
290                && hook.interested_phases().contains(&event.phase)
291            {
292                hook.on_event(&event);
293            }
294        }
295    }
296
297    /// Enable or disable performance monitoring
298    pub fn set_enabled(&self, enabled: bool) {
299        *self.write_enabled() = enabled;
300    }
301
302    /// Check if performance monitoring is enabled
303    pub fn is_enabled(&self) -> bool {
304        *self.read_enabled()
305    }
306
307    /// Get the event buffer
308    pub fn get_events(&self) -> Vec<PerformanceEvent> {
309        self.lock_event_buffer().clone()
310    }
311
312    /// Clear the event buffer
313    pub fn clear_events(&self) {
314        self.lock_event_buffer().clear();
315    }
316
317    /// Get statistics about registered hooks
318    pub fn get_hook_stats(&self) -> HookStats {
319        let hooks = self.read_hooks();
320        let buffer = self.lock_event_buffer();
321
322        HookStats {
323            total_hooks: hooks.len(),
324            hook_names: hooks.keys().cloned().collect(),
325            total_events: buffer.len(),
326            is_enabled: self.is_enabled(),
327        }
328    }
329
330    /// Set maximum buffer size
331    pub fn set_max_buffer_size(&mut self, size: usize) {
332        self.max_buffer_size = size;
333    }
334}
335
336/// Hook manager statistics
337#[derive(Debug, Clone)]
338pub struct HookStats {
339    pub total_hooks: usize,
340    pub hook_names: Vec<String>,
341    pub total_events: usize,
342    pub is_enabled: bool,
343}
344
345/// Hook error types
346#[derive(Debug, Clone)]
347pub enum HookError {
348    AlreadyRegistered(String),
349    NotFound(String),
350    ExecutionFailed(String),
351}
352
353impl fmt::Display for HookError {
354    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
355        match self {
356            HookError::AlreadyRegistered(name) => {
357                write!(f, "Hook '{}' is already registered", name)
358            }
359            HookError::NotFound(name) => write!(f, "Hook '{}' not found", name),
360            HookError::ExecutionFailed(msg) => write!(f, "Hook execution failed: {}", msg),
361        }
362    }
363}
364
365#[cfg(not(feature = "no-std"))]
366impl std::error::Error for HookError {}
367
368#[cfg(feature = "no-std")]
369impl core::error::Error for HookError {}
370
371/// Global hook manager instance
372pub static GLOBAL_HOOK_MANAGER: once_cell::sync::Lazy<HookManager> =
373    once_cell::sync::Lazy::new(HookManager::new);
374
375/// Convenience functions for global hook manager
376pub mod global {
377    use super::*;
378
379    /// Register a hook globally
380    pub fn register_hook(hook: Arc<dyn PerformanceHook>) -> Result<(), HookError> {
381        GLOBAL_HOOK_MANAGER.register_hook(hook)
382    }
383
384    /// Fire an event globally
385    pub fn fire_event(event: PerformanceEvent) {
386        GLOBAL_HOOK_MANAGER.fire_event(event);
387    }
388
389    /// Enable/disable global monitoring
390    pub fn set_enabled(enabled: bool) {
391        GLOBAL_HOOK_MANAGER.set_enabled(enabled);
392    }
393
394    /// Get global hook statistics
395    pub fn get_stats() -> HookStats {
396        GLOBAL_HOOK_MANAGER.get_hook_stats()
397    }
398
399    /// Clear global event buffer
400    pub fn clear_events() {
401        GLOBAL_HOOK_MANAGER.clear_events();
402    }
403}
404
405/// Macro for creating performance monitoring scopes
406#[macro_export]
407macro_rules! perf_scope {
408    ($operation_name:expr, $input_size:expr, $output_size:expr, $body:expr) => {{
409        use $crate::performance_hooks::{global, HookPhase, PerformanceEvent};
410
411        // Fire before event
412        let before_event = PerformanceEvent::new(
413            $operation_name.to_string(),
414            HookPhase::BeforeOperation,
415            $input_size,
416            $output_size,
417        );
418        global::fire_event(before_event);
419
420        // Execute operation
421        let start_time = Instant::now();
422        let result = $body;
423        let execution_time = start_time.elapsed();
424
425        // Fire after event
426        let after_event = PerformanceEvent::new(
427            $operation_name.to_string(),
428            HookPhase::AfterOperation,
429            $input_size,
430            $output_size,
431        )
432        .with_execution_time(execution_time);
433
434        global::fire_event(after_event);
435
436        result
437    }};
438}
439
440/// Built-in performance hooks
441pub mod builtin_hooks {
442    use super::*;
443    #[cfg(feature = "no-std")]
444    use core::sync::atomic::{AtomicU64, Ordering};
445    #[cfg(not(feature = "no-std"))]
446    use std::sync::atomic::{AtomicU64, Ordering};
447
448    /// Simple logging hook that prints events to console
449    pub struct LoggingHook {
450        name: String,
451    }
452
453    impl LoggingHook {
454        pub fn new(name: String) -> Self {
455            Self { name }
456        }
457    }
458
459    impl PerformanceHook for LoggingHook {
460        fn on_event(&self, event: &PerformanceEvent) {
461            match event.phase {
462                HookPhase::BeforeOperation => {
463                    #[cfg(not(feature = "no-std"))]
464                    println!(
465                        "[{}] Starting {} (input: {}, output: {})",
466                        self.name, event.operation_name, event.input_size, event.output_size
467                    );
468                }
469                HookPhase::AfterOperation =>
470                {
471                    #[cfg(not(feature = "no-std"))]
472                    if let Some(time) = event.execution_time {
473                        println!(
474                            "[{}] Finished {} in {:?}",
475                            self.name, event.operation_name, time
476                        );
477                    }
478                }
479                HookPhase::OnError =>
480                {
481                    #[cfg(not(feature = "no-std"))]
482                    if let Some(error) = &event.error_message {
483                        println!(
484                            "[{}] Error in {}: {}",
485                            self.name, event.operation_name, error
486                        );
487                    }
488                }
489                HookPhase::OnOptimization => {
490                    #[cfg(not(feature = "no-std"))]
491                    println!(
492                        "[{}] Optimization applied to {}",
493                        self.name, event.operation_name
494                    );
495                }
496            }
497        }
498
499        fn name(&self) -> &str {
500            &self.name
501        }
502    }
503
504    /// Statistics collection hook
505    pub struct StatsHook {
506        name: String,
507        operation_counts: RwLock<HashMap<String, AtomicU64>>,
508        total_execution_time: RwLock<HashMap<String, AtomicU64>>, // nanoseconds
509        total_elements_processed: RwLock<HashMap<String, AtomicU64>>,
510    }
511
512    impl StatsHook {
513        pub fn new(name: String) -> Self {
514            Self {
515                name,
516                operation_counts: RwLock::new(HashMap::new()),
517                total_execution_time: RwLock::new(HashMap::new()),
518                total_elements_processed: RwLock::new(HashMap::new()),
519            }
520        }
521
522        /// Helper function to handle operation counts RwLock read locking
523        #[cfg(not(feature = "no-std"))]
524        fn read_counts(&self) -> std::sync::RwLockReadGuard<'_, HashMap<String, AtomicU64>> {
525            self.operation_counts
526                .read()
527                .expect("operation should succeed")
528        }
529
530        #[cfg(feature = "no-std")]
531        fn read_counts(&self) -> spin::RwLockReadGuard<'_, HashMap<String, AtomicU64>> {
532            self.operation_counts.read()
533        }
534
535        /// Helper function to handle operation counts RwLock write locking
536        #[cfg(not(feature = "no-std"))]
537        fn write_counts(&self) -> std::sync::RwLockWriteGuard<'_, HashMap<String, AtomicU64>> {
538            self.operation_counts
539                .write()
540                .expect("operation should succeed")
541        }
542
543        #[cfg(feature = "no-std")]
544        fn write_counts(&self) -> spin::RwLockWriteGuard<'_, HashMap<String, AtomicU64>> {
545            self.operation_counts.write()
546        }
547
548        /// Helper function to handle execution time RwLock read locking
549        #[cfg(not(feature = "no-std"))]
550        fn read_times(&self) -> std::sync::RwLockReadGuard<'_, HashMap<String, AtomicU64>> {
551            self.total_execution_time
552                .read()
553                .expect("operation should succeed")
554        }
555
556        #[cfg(feature = "no-std")]
557        fn read_times(&self) -> spin::RwLockReadGuard<'_, HashMap<String, AtomicU64>> {
558            self.total_execution_time.read()
559        }
560
561        /// Helper function to handle execution time RwLock write locking
562        #[cfg(not(feature = "no-std"))]
563        fn write_times(&self) -> std::sync::RwLockWriteGuard<'_, HashMap<String, AtomicU64>> {
564            self.total_execution_time
565                .write()
566                .expect("operation should succeed")
567        }
568
569        #[cfg(feature = "no-std")]
570        fn write_times(&self) -> spin::RwLockWriteGuard<'_, HashMap<String, AtomicU64>> {
571            self.total_execution_time.write()
572        }
573
574        /// Helper function to handle elements processed RwLock read locking
575        #[cfg(not(feature = "no-std"))]
576        fn read_elements(&self) -> std::sync::RwLockReadGuard<'_, HashMap<String, AtomicU64>> {
577            self.total_elements_processed
578                .read()
579                .expect("operation should succeed")
580        }
581
582        #[cfg(feature = "no-std")]
583        fn read_elements(&self) -> spin::RwLockReadGuard<'_, HashMap<String, AtomicU64>> {
584            self.total_elements_processed.read()
585        }
586
587        /// Helper function to handle elements processed RwLock write locking
588        #[cfg(not(feature = "no-std"))]
589        fn write_elements(&self) -> std::sync::RwLockWriteGuard<'_, HashMap<String, AtomicU64>> {
590            self.total_elements_processed
591                .write()
592                .expect("operation should succeed")
593        }
594
595        #[cfg(feature = "no-std")]
596        fn write_elements(&self) -> spin::RwLockWriteGuard<'_, HashMap<String, AtomicU64>> {
597            self.total_elements_processed.write()
598        }
599
600        pub fn get_stats(&self) -> HashMap<String, OperationStats> {
601            let counts = self.read_counts();
602            let times = self.read_times();
603            let elements = self.read_elements();
604
605            let mut stats = HashMap::new();
606            for (op_name, count) in counts.iter() {
607                let count_val = count.load(Ordering::Relaxed);
608                let time_val = times
609                    .get(op_name)
610                    .map(|t| Duration::from_nanos(t.load(Ordering::Relaxed)))
611                    .unwrap_or_default();
612                let elements_val = elements
613                    .get(op_name)
614                    .map(|e| e.load(Ordering::Relaxed))
615                    .unwrap_or(0);
616
617                stats.insert(
618                    op_name.clone(),
619                    OperationStats {
620                        call_count: count_val,
621                        total_time: time_val,
622                        total_elements: elements_val,
623                        avg_time: if count_val > 0 {
624                            time_val / count_val as u32
625                        } else {
626                            Duration::default()
627                        },
628                    },
629                );
630            }
631            stats
632        }
633
634        pub fn reset_stats(&self) {
635            self.write_counts().clear();
636            self.write_times().clear();
637            self.write_elements().clear();
638        }
639    }
640
641    impl PerformanceHook for StatsHook {
642        fn on_event(&self, event: &PerformanceEvent) {
643            if event.phase == HookPhase::AfterOperation {
644                let op_name = &event.operation_name;
645
646                // Update call count
647                {
648                    let mut counts = self.write_counts();
649                    counts
650                        .entry(op_name.clone())
651                        .or_insert_with(|| AtomicU64::new(0))
652                        .fetch_add(1, Ordering::Relaxed);
653                }
654
655                // Update execution time
656                if let Some(time) = event.execution_time {
657                    let mut times = self.write_times();
658                    times
659                        .entry(op_name.clone())
660                        .or_insert_with(|| AtomicU64::new(0))
661                        .fetch_add(time.as_nanos() as u64, Ordering::Relaxed);
662                }
663
664                // Update elements processed
665                {
666                    let mut elements = self.write_elements();
667                    elements
668                        .entry(op_name.clone())
669                        .or_insert_with(|| AtomicU64::new(0))
670                        .fetch_add(event.input_size as u64, Ordering::Relaxed);
671                }
672            }
673        }
674
675        fn name(&self) -> &str {
676            &self.name
677        }
678
679        fn interested_phases(&self) -> Vec<HookPhase> {
680            vec![HookPhase::AfterOperation]
681        }
682    }
683
684    #[derive(Debug, Clone)]
685    pub struct OperationStats {
686        pub call_count: u64,
687        pub total_time: Duration,
688        pub total_elements: u64,
689        pub avg_time: Duration,
690    }
691}
692
693#[allow(non_snake_case)]
694#[cfg(all(test, not(feature = "no-std")))]
695mod tests {
696    use super::builtin_hooks::*;
697    use super::*;
698
699    #[test]
700    fn test_hook_registration() {
701        let manager = HookManager::new();
702        let hook = Arc::new(LoggingHook::new("test_hook".to_string()));
703
704        assert!(manager.register_hook(hook).is_ok());
705
706        let stats = manager.get_hook_stats();
707        assert_eq!(stats.total_hooks, 1);
708        assert!(stats.hook_names.contains(&"test_hook".to_string()));
709    }
710
711    #[test]
712    fn test_event_firing() {
713        let manager = HookManager::new();
714        let hook = Arc::new(LoggingHook::new("test_hook".to_string()));
715        manager
716            .register_hook(hook)
717            .expect("operation should succeed");
718
719        let event = PerformanceEvent::new(
720            "test_operation".to_string(),
721            HookPhase::BeforeOperation,
722            100,
723            100,
724        );
725
726        manager.fire_event(event);
727
728        let events = manager.get_events();
729        assert_eq!(events.len(), 1);
730        assert_eq!(events[0].operation_name, "test_operation");
731    }
732
733    #[test]
734    fn test_stats_hook() {
735        let manager = HookManager::new();
736        let stats_hook = Arc::new(StatsHook::new("stats".to_string()));
737        let stats_hook_clone = stats_hook.clone();
738
739        manager
740            .register_hook(stats_hook)
741            .expect("operation should succeed");
742
743        // Fire some events
744        for i in 0..5 {
745            let event =
746                PerformanceEvent::new("test_op".to_string(), HookPhase::AfterOperation, 100, 100)
747                    .with_execution_time(Duration::from_millis(i));
748
749            manager.fire_event(event);
750        }
751
752        let stats = stats_hook_clone.get_stats();
753        assert!(stats.contains_key("test_op"));
754
755        let op_stats = &stats["test_op"];
756        assert_eq!(op_stats.call_count, 5);
757        assert_eq!(op_stats.total_elements, 500);
758    }
759
760    #[test]
761    fn test_global_hooks() {
762        let hook = Arc::new(LoggingHook::new("global_test".to_string()));
763        global::register_hook(hook).expect("operation should succeed");
764
765        let event = PerformanceEvent::new(
766            "global_test_op".to_string(),
767            HookPhase::BeforeOperation,
768            50,
769            50,
770        );
771
772        global::fire_event(event);
773
774        let stats = global::get_stats();
775        assert!(stats.hook_names.contains(&"global_test".to_string()));
776    }
777
778    #[test]
779    fn test_enable_disable() {
780        let manager = HookManager::new();
781        assert!(manager.is_enabled());
782
783        manager.set_enabled(false);
784        assert!(!manager.is_enabled());
785
786        manager.set_enabled(true);
787        assert!(manager.is_enabled());
788    }
789
790    #[test]
791    fn test_error_handling() {
792        let manager = HookManager::new();
793        let hook1 = Arc::new(LoggingHook::new("duplicate".to_string()));
794        let hook2 = Arc::new(LoggingHook::new("duplicate".to_string()));
795
796        assert!(manager.register_hook(hook1).is_ok());
797        assert!(matches!(
798            manager.register_hook(hook2),
799            Err(HookError::AlreadyRegistered(_))
800        ));
801
802        assert!(matches!(
803            manager.unregister_hook("nonexistent"),
804            Err(HookError::NotFound(_))
805        ));
806    }
807}