Skip to main content

reifydb_profiler/
scope.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use 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}