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