1use std::{
5 future::Future,
6 mem,
7 sync::{
8 Arc, OnceLock,
9 atomic::{AtomicBool, AtomicU64, Ordering},
10 },
11};
12
13use dashmap::DashMap;
14use once_cell::sync::Lazy;
15use reifydb_runtime::{
16 context::clock::{Clock, Instant},
17 sync::mutex::Mutex,
18};
19use reifydb_value::{reifydb_assertions, value::duration::Duration};
20use serde::{Deserialize, Serialize};
21use tokio::task_local;
22
23use crate::{
24 intern::DimInterner,
25 record::MinimalSpanRecord,
26 sink::{NoopSink, ProfilerSink},
27 summary::ProfilerSummary,
28};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
31pub struct ScopeId(pub u64);
32
33static NEXT_SCOPE_ID: AtomicU64 = AtomicU64::new(1);
34
35fn next_scope_id() -> ScopeId {
36 ScopeId(NEXT_SCOPE_ID.fetch_add(1, Ordering::Relaxed))
37}
38
39pub struct ScopeState {
40 pub id: ScopeId,
41 pub name: &'static str,
42 pub started_at: Instant,
43 pub started_at_nanos: u128,
44 pub records: Mutex<Vec<MinimalSpanRecord>>,
45 pub batch_threshold: usize,
46 pub closed: AtomicBool,
47 pub sink: Arc<dyn ProfilerSink>,
48 pub interner: OnceLock<Arc<DimInterner>>,
49}
50
51impl ScopeState {
52 pub fn push(&self, rec: MinimalSpanRecord) {
53 if let Some(drained) = self.push_locked(rec) {
54 self.flush_batch(drained);
55 }
56 }
57
58 #[inline]
59 fn push_locked(&self, rec: MinimalSpanRecord) -> Option<Vec<MinimalSpanRecord>> {
60 if self.closed.load(Ordering::Acquire) {
61 return None;
62 }
63 let mut guard = self.records.lock();
64 guard.push(rec);
65 if self.batch_threshold > 0 && guard.len() >= self.batch_threshold {
66 Some(mem::take(&mut *guard))
67 } else {
68 None
69 }
70 }
71
72 #[inline]
73 fn flush_batch(&self, drained: Vec<MinimalSpanRecord>) {
74 reifydb_assertions! {
75 let count = drained.len();
76 assert!(
77 count > 0,
78 "flush_batch must never emit an empty profiler batch: a zero-record on_scope_batch \
79 call would report a spurious flush to the sink; reached with batch_threshold {} and {count} records",
80 self.batch_threshold
81 );
82 }
83 let total_duration = Duration::from_std(self.started_at.elapsed());
84 let summary = ProfilerSummary::from_records(
85 self.id,
86 self.name,
87 self.started_at_nanos,
88 total_duration,
89 drained,
90 self.interner.get().cloned(),
91 );
92 self.sink.on_scope_batch(&summary);
93 }
94
95 pub fn attach_interner(&self, interner: Arc<DimInterner>) {
96 let _ = self.interner.set(interner);
97 }
98}
99
100pub(crate) struct ScopeRegistry {
101 scopes: DashMap<ScopeId, Arc<ScopeState>>,
102}
103
104impl ScopeRegistry {
105 fn new() -> Self {
106 Self {
107 scopes: DashMap::new(),
108 }
109 }
110
111 pub(crate) fn insert(&self, state: Arc<ScopeState>) {
112 self.scopes.insert(state.id, state);
113 }
114
115 pub(crate) fn get(&self, id: ScopeId) -> Option<Arc<ScopeState>> {
116 self.scopes.get(&id).map(|r| Arc::clone(r.value()))
117 }
118
119 pub(crate) fn remove(&self, id: ScopeId) -> Option<Arc<ScopeState>> {
120 self.scopes.remove(&id).map(|(_, v)| v)
121 }
122}
123
124impl Default for ScopeRegistry {
125 fn default() -> Self {
126 Self::new()
127 }
128}
129
130pub(crate) static REGISTRY: Lazy<ScopeRegistry> = Lazy::new(ScopeRegistry::default);
131
132task_local! {
133 pub(crate) static ACTIVE_SCOPE: ScopeId;
134}
135
136pub struct ProfilerScope;
137
138pub struct ScopeHandle {
139 state: Arc<ScopeState>,
140}
141
142const DEFAULT_BATCH_THRESHOLD: usize = 256;
143
144impl ProfilerScope {
145 pub fn start(name: &'static str, clock: Clock) -> ScopeHandle {
146 Self::start_with_sink(name, Arc::new(NoopSink), clock)
147 }
148
149 pub fn start_with_sink(name: &'static str, sink: Arc<dyn ProfilerSink>, clock: Clock) -> ScopeHandle {
150 let state = build_scope_state(name, sink, &clock);
151 REGISTRY.insert(Arc::clone(&state));
152 ScopeHandle {
153 state,
154 }
155 }
156
157 pub fn ambient(name: &'static str, sink: Arc<dyn ProfilerSink>, clock: &Clock) -> Arc<ScopeState> {
158 let state = build_scope_state(name, sink, clock);
159 REGISTRY.insert(Arc::clone(&state));
160 state
161 }
162}
163
164fn build_scope_state(name: &'static str, sink: Arc<dyn ProfilerSink>, clock: &Clock) -> Arc<ScopeState> {
165 let id = next_scope_id();
166 Arc::new(ScopeState {
167 id,
168 name,
169 started_at: clock.instant(),
170 started_at_nanos: clock.now().to_nanos() as u128,
171 records: Mutex::new(Vec::with_capacity(DEFAULT_BATCH_THRESHOLD)),
172 batch_threshold: DEFAULT_BATCH_THRESHOLD,
173 closed: AtomicBool::new(false),
174 sink,
175 interner: OnceLock::new(),
176 })
177}
178
179impl ScopeHandle {
180 pub fn id(&self) -> ScopeId {
181 self.state.id
182 }
183
184 pub fn name(&self) -> &'static str {
185 self.state.name
186 }
187
188 pub async fn run<F, R>(&self, fut: F) -> R
189 where
190 F: Future<Output = R>,
191 {
192 ACTIVE_SCOPE.scope(self.state.id, fut).await
193 }
194
195 pub fn run_sync<F, R>(&self, f: F) -> R
196 where
197 F: FnOnce() -> R,
198 {
199 ACTIVE_SCOPE.sync_scope(self.state.id, f)
200 }
201
202 pub fn finish(self) -> ProfilerSummary {
203 self.state.closed.store(true, Ordering::Release);
204 REGISTRY.remove(self.state.id);
205 let records: Vec<MinimalSpanRecord> = mem::take(&mut *self.state.records.lock());
206 let total_duration = Duration::from_std(self.state.started_at.elapsed());
207 let summary = ProfilerSummary::from_records(
208 self.state.id,
209 self.state.name,
210 self.state.started_at_nanos,
211 total_duration,
212 records,
213 self.state.interner.get().cloned(),
214 );
215 self.state.sink.on_scope_closed(&summary);
216 summary
217 }
218}
219
220pub fn active_scope() -> Option<ScopeId> {
221 ACTIVE_SCOPE.try_with(|id| *id).ok()
222}
223
224pub fn lookup_scope(id: ScopeId) -> Option<Arc<ScopeState>> {
225 REGISTRY.get(id)
226}
227
228#[cfg(test)]
229mod tests {
230 use std::sync::atomic::{AtomicUsize, Ordering};
231
232 use reifydb_runtime::context::clock::Clock;
233
234 use super::*;
235 use crate::{category::ProfilerCategory, record::MinimalSpanRecord};
236
237 #[test]
238 fn scope_id_monotonic() {
239 let a = next_scope_id();
240 let b = next_scope_id();
241 assert!(b.0 > a.0);
242 }
243
244 #[test]
245 fn finish_drains_records_and_marks_closed() {
246 let handle = ProfilerScope::start("test.scope", Clock::Real);
247 let id = handle.id();
248 let state = lookup_scope(id).expect("scope registered");
249 state.push(MinimalSpanRecord::new(ProfilerCategory::Query, 1, 100));
250 state.push(MinimalSpanRecord::new(ProfilerCategory::Query, 2, 200));
251
252 let summary = handle.finish();
253 assert_eq!(summary.records.len(), 2);
254 assert_eq!(summary.category(ProfilerCategory::Query).calls, 2);
255 assert!(lookup_scope(id).is_none());
256 }
257
258 #[test]
259 fn push_after_finish_is_ignored() {
260 let handle = ProfilerScope::start("test.scope", Clock::Real);
261 let state = lookup_scope(handle.id()).unwrap();
262 let _ = handle.finish();
263 state.push(MinimalSpanRecord::new(ProfilerCategory::Storage, 1, 50));
264 assert!(state.records.lock().is_empty());
265 }
266
267 #[test]
268 fn batch_threshold_drains_via_sink() {
269 #[derive(Default)]
270 struct CountingSink {
271 batches: AtomicUsize,
272 }
273 impl ProfilerSink for CountingSink {
274 fn on_scope_closed(&self, _s: &ProfilerSummary) {}
275 fn on_scope_batch(&self, _s: &ProfilerSummary) {
276 self.batches.fetch_add(1, Ordering::Relaxed);
277 }
278 }
279 let sink: Arc<CountingSink> = Arc::new(CountingSink::default());
280 let handle = ProfilerScope::start_with_sink("test.scope", sink.clone(), Clock::Real);
281 let state = lookup_scope(handle.id()).unwrap();
282 for i in 0..DEFAULT_BATCH_THRESHOLD {
283 state.push(MinimalSpanRecord::new(ProfilerCategory::Flow, i as u64, 10));
284 }
285 assert_eq!(sink.batches.load(Ordering::Relaxed), 1);
286 assert!(state.records.lock().is_empty());
287 let _ = handle.finish();
288 }
289
290 #[tokio::test]
291 async fn run_sets_active_scope() {
292 let handle = ProfilerScope::start("async.scope", Clock::Real);
293 let id = handle.id();
294 let observed: ScopeId = handle.run(async move { active_scope().unwrap() }).await;
295 assert_eq!(observed, id);
296 let _ = handle.finish();
297 }
298
299 #[test]
300 fn run_sync_sets_active_scope() {
301 let handle = ProfilerScope::start("sync.scope", Clock::Real);
302 let id = handle.id();
303 let observed = handle.run_sync(active_scope);
304 assert_eq!(observed, Some(id));
305 let _ = handle.finish();
306 }
307}