firebase_rs_sdk/performance/
api.rs1use 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}