firebase_rs_sdk/performance/
api.rs

1use std::collections::HashMap;
2use std::sync::{Arc, LazyLock, Mutex};
3use std::time::{Duration, Instant};
4
5use crate::app;
6use crate::app::FirebaseApp;
7use crate::component::types::{
8    ComponentError, DynService, InstanceFactoryOptions, InstantiationMode,
9};
10use crate::component::{Component, ComponentType};
11use crate::performance::constants::PERFORMANCE_COMPONENT_NAME;
12use crate::performance::error::{internal_error, invalid_argument, PerformanceResult};
13
14#[derive(Clone, Debug)]
15pub struct Performance {
16    inner: Arc<PerformanceInner>,
17}
18
19#[derive(Debug)]
20struct PerformanceInner {
21    app: FirebaseApp,
22    traces: Mutex<HashMap<String, PerformanceTrace>>,
23}
24
25#[derive(Clone, Debug, PartialEq)]
26pub struct PerformanceTrace {
27    pub name: String,
28    pub duration: Duration,
29    pub metrics: HashMap<String, i64>,
30}
31
32#[derive(Clone, Debug)]
33pub struct TraceHandle {
34    performance: Performance,
35    name: String,
36    start: Instant,
37    metrics: HashMap<String, i64>,
38}
39
40impl Performance {
41    fn new(app: FirebaseApp) -> Self {
42        Self {
43            inner: Arc::new(PerformanceInner {
44                app,
45                traces: Mutex::new(HashMap::new()),
46            }),
47        }
48    }
49
50    pub fn app(&self) -> &FirebaseApp {
51        &self.inner.app
52    }
53
54    pub fn new_trace(&self, name: &str) -> PerformanceResult<TraceHandle> {
55        if name.trim().is_empty() {
56            return Err(invalid_argument("Trace name must not be empty"));
57        }
58        Ok(TraceHandle {
59            performance: self.clone(),
60            name: name.to_string(),
61            start: Instant::now(),
62            metrics: HashMap::new(),
63        })
64    }
65
66    pub fn recorded_trace(&self, name: &str) -> Option<PerformanceTrace> {
67        self.inner.traces.lock().unwrap().get(name).cloned()
68    }
69}
70
71impl TraceHandle {
72    pub fn put_metric(&mut self, name: &str, value: i64) -> PerformanceResult<()> {
73        if name.trim().is_empty() {
74            return Err(invalid_argument("Metric name must not be empty"));
75        }
76        self.metrics.insert(name.to_string(), value);
77        Ok(())
78    }
79
80    pub fn stop(self) -> PerformanceResult<PerformanceTrace> {
81        let duration = self.start.elapsed();
82        let trace = PerformanceTrace {
83            name: self.name.clone(),
84            duration,
85            metrics: self.metrics.clone(),
86        };
87        self.performance
88            .inner
89            .traces
90            .lock()
91            .unwrap()
92            .insert(self.name.clone(), trace.clone());
93        Ok(trace)
94    }
95}
96
97static PERFORMANCE_COMPONENT: LazyLock<()> = LazyLock::new(|| {
98    let component = Component::new(
99        PERFORMANCE_COMPONENT_NAME,
100        Arc::new(performance_factory),
101        ComponentType::Public,
102    )
103    .with_instantiation_mode(InstantiationMode::Lazy);
104    let _ = app::registry::register_component(component);
105});
106
107fn performance_factory(
108    container: &crate::component::ComponentContainer,
109    _options: InstanceFactoryOptions,
110) -> Result<DynService, ComponentError> {
111    let app = container.root_service::<FirebaseApp>().ok_or_else(|| {
112        ComponentError::InitializationFailed {
113            name: PERFORMANCE_COMPONENT_NAME.to_string(),
114            reason: "Firebase app not attached to component container".to_string(),
115        }
116    })?;
117
118    let performance = Performance::new((*app).clone());
119    Ok(Arc::new(performance) as DynService)
120}
121
122fn ensure_registered() {
123    LazyLock::force(&PERFORMANCE_COMPONENT);
124}
125
126pub fn register_performance_component() {
127    ensure_registered();
128}
129
130pub fn get_performance(app: Option<FirebaseApp>) -> PerformanceResult<Arc<Performance>> {
131    ensure_registered();
132    let app = match app {
133        Some(app) => app,
134        None => crate::app::api::get_app(None).map_err(|err| internal_error(err.to_string()))?,
135    };
136
137    let provider = app::registry::get_provider(&app, PERFORMANCE_COMPONENT_NAME);
138    if let Some(perf) = provider.get_immediate::<Performance>() {
139        return Ok(perf);
140    }
141
142    match provider.initialize::<Performance>(serde_json::Value::Null, None) {
143        Ok(perf) => Ok(perf),
144        Err(crate::component::types::ComponentError::InstanceUnavailable { .. }) => provider
145            .get_immediate::<Performance>()
146            .ok_or_else(|| internal_error("Performance component not available")),
147        Err(err) => Err(internal_error(err.to_string())),
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use crate::app::api::initialize_app;
155    use crate::app::{FirebaseAppSettings, FirebaseOptions};
156
157    fn unique_settings() -> FirebaseAppSettings {
158        use std::sync::atomic::{AtomicUsize, Ordering};
159        static COUNTER: AtomicUsize = AtomicUsize::new(0);
160        FirebaseAppSettings {
161            name: Some(format!(
162                "performance-{}",
163                COUNTER.fetch_add(1, Ordering::SeqCst)
164            )),
165            ..Default::default()
166        }
167    }
168
169    #[test]
170    fn trace_records_duration_and_metrics() {
171        let options = FirebaseOptions {
172            project_id: Some("project".into()),
173            ..Default::default()
174        };
175        let app = initialize_app(options, Some(unique_settings())).unwrap();
176        let performance = get_performance(Some(app)).unwrap();
177        let mut trace = performance.new_trace("load").unwrap();
178        trace.put_metric("items", 3).unwrap();
179        std::thread::sleep(Duration::from_millis(10));
180        let result = trace.stop().unwrap();
181        assert_eq!(result.metrics.get("items"), Some(&3));
182        assert!(result.duration >= Duration::from_millis(10));
183
184        let stored = performance.recorded_trace("load").unwrap();
185        assert_eq!(stored.metrics.get("items"), Some(&3));
186    }
187}