Skip to main content

reifydb_profiler/
scope.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
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 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}