firebase_rs_sdk/performance/
api.rs1use 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 pub fn app(&self) -> &FirebaseApp {
53 &self.inner.app
54 }
55
56 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 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 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 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
136pub 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}