firebase_rs_sdk/performance/
api.rs

1use std::collections::HashMap;
2use std::sync::{Arc, LazyLock};
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};
13use async_lock::Mutex;
14
15#[derive(Clone, Debug)]
16pub struct Performance {
17    inner: Arc<PerformanceInner>,
18}
19
20#[derive(Debug)]
21struct PerformanceInner {
22    app: FirebaseApp,
23    traces: Mutex<HashMap<String, PerformanceTrace>>,
24}
25
26#[derive(Clone, Debug, PartialEq)]
27pub struct PerformanceTrace {
28    pub name: String,
29    pub duration: Duration,
30    pub metrics: HashMap<String, i64>,
31}
32
33#[derive(Clone, Debug)]
34pub struct TraceHandle {
35    performance: Performance,
36    name: String,
37    start: Instant,
38    metrics: HashMap<String, i64>,
39}
40
41impl Performance {
42    fn new(app: FirebaseApp) -> Self {
43        Self {
44            inner: Arc::new(PerformanceInner {
45                app,
46                traces: Mutex::new(HashMap::new()),
47            }),
48        }
49    }
50
51    /// Returns the [`FirebaseApp`] that owns this Performance monitor.
52    pub fn app(&self) -> &FirebaseApp {
53        &self.inner.app
54    }
55
56    /// Creates a new manual trace, mirroring the JS SDK's `trace()` helper.
57    pub fn new_trace(&self, name: &str) -> PerformanceResult<TraceHandle> {
58        if name.trim().is_empty() {
59            return Err(invalid_argument("Trace name must not be empty"));
60        }
61        Ok(TraceHandle {
62            performance: self.clone(),
63            name: name.to_string(),
64            start: Instant::now(),
65            metrics: HashMap::new(),
66        })
67    }
68
69    /// Returns the most recently recorded trace with `name`, if any.
70    pub async fn recorded_trace(&self, name: &str) -> Option<PerformanceTrace> {
71        self.inner.traces.lock().await.get(name).cloned()
72    }
73}
74
75impl TraceHandle {
76    /// Adds (or replaces) a numeric metric for the trace.
77    pub fn put_metric(&mut self, name: &str, value: i64) -> PerformanceResult<()> {
78        if name.trim().is_empty() {
79            return Err(invalid_argument("Metric name must not be empty"));
80        }
81        self.metrics.insert(name.to_string(), value);
82        Ok(())
83    }
84
85    /// Stops the trace and stores the timing/metrics in the parent [`Performance`] instance.
86    pub async fn stop(self) -> PerformanceResult<PerformanceTrace> {
87        let duration = self.start.elapsed();
88        let trace = PerformanceTrace {
89            name: self.name.clone(),
90            duration,
91            metrics: self.metrics.clone(),
92        };
93        self.performance
94            .inner
95            .traces
96            .lock()
97            .await
98            .insert(self.name.clone(), trace.clone());
99        Ok(trace)
100    }
101}
102
103static PERFORMANCE_COMPONENT: LazyLock<()> = LazyLock::new(|| {
104    let component = Component::new(
105        PERFORMANCE_COMPONENT_NAME,
106        Arc::new(performance_factory),
107        ComponentType::Public,
108    )
109    .with_instantiation_mode(InstantiationMode::Lazy);
110    let _ = app::register_component(component);
111});
112
113fn performance_factory(
114    container: &crate::component::ComponentContainer,
115    _options: InstanceFactoryOptions,
116) -> Result<DynService, ComponentError> {
117    let app = container.root_service::<FirebaseApp>().ok_or_else(|| {
118        ComponentError::InitializationFailed {
119            name: PERFORMANCE_COMPONENT_NAME.to_string(),
120            reason: "Firebase app not attached to component container".to_string(),
121        }
122    })?;
123
124    let performance = Performance::new((*app).clone());
125    Ok(Arc::new(performance) as DynService)
126}
127
128fn ensure_registered() {
129    LazyLock::force(&PERFORMANCE_COMPONENT);
130}
131
132pub fn register_performance_component() {
133    ensure_registered();
134}
135
136/// Resolves (or lazily creates) the [`Performance`] instance associated with the provided app.
137///
138/// This mirrors the behaviour of the JavaScript SDK's `getPerformance` helper. When `app` is
139/// `None`, the default app is resolved asynchronously via [`get_app`](crate::app::get_app).
140pub async fn get_performance(app: Option<FirebaseApp>) -> PerformanceResult<Arc<Performance>> {
141    ensure_registered();
142    let app = match app {
143        Some(app) => app,
144        None => crate::app::get_app(None)
145            .await
146            .map_err(|err| internal_error(err.to_string()))?,
147    };
148
149    let provider = app::get_provider(&app, PERFORMANCE_COMPONENT_NAME);
150    if let Some(perf) = provider.get_immediate::<Performance>() {
151        return Ok(perf);
152    }
153
154    match provider.initialize::<Performance>(serde_json::Value::Null, None) {
155        Ok(perf) => Ok(perf),
156        Err(crate::component::types::ComponentError::InstanceUnavailable { .. }) => provider
157            .get_immediate::<Performance>()
158            .ok_or_else(|| internal_error("Performance component not available")),
159        Err(err) => Err(internal_error(err.to_string())),
160    }
161}
162
163#[cfg(all(test, not(target_arch = "wasm32")))]
164mod tests {
165    use super::*;
166    use crate::app::initialize_app;
167    use crate::app::{FirebaseAppSettings, FirebaseOptions};
168    use tokio::time::sleep;
169
170    fn unique_settings() -> FirebaseAppSettings {
171        use std::sync::atomic::{AtomicUsize, Ordering};
172        static COUNTER: AtomicUsize = AtomicUsize::new(0);
173        FirebaseAppSettings {
174            name: Some(format!(
175                "performance-{}",
176                COUNTER.fetch_add(1, Ordering::SeqCst)
177            )),
178            ..Default::default()
179        }
180    }
181
182    #[tokio::test(flavor = "current_thread")]
183    async fn trace_records_duration_and_metrics() {
184        let options = FirebaseOptions {
185            project_id: Some("project".into()),
186            ..Default::default()
187        };
188        let app = initialize_app(options, Some(unique_settings()))
189            .await
190            .unwrap();
191        let performance = get_performance(Some(app.clone())).await.unwrap();
192        let mut trace = performance.new_trace("load").unwrap();
193        trace.put_metric("items", 3).unwrap();
194        sleep(Duration::from_millis(10)).await;
195        let result = trace.stop().await.unwrap();
196        assert_eq!(result.metrics.get("items"), Some(&3));
197        assert!(result.duration >= Duration::from_millis(10));
198
199        let stored = performance.recorded_trace("load").await.unwrap();
200        assert_eq!(stored.metrics.get("items"), Some(&3));
201    }
202}