Skip to main content

reifydb_profiler/
category.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use reifydb_core::profiler::ProfilerCategoryId;
5use serde::{Deserialize, Serialize};
6use tracing::{Level, level_filters::LevelFilter};
7
8#[repr(u8)]
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub enum ProfilerCategory {
11	Query = 0,
12	Txn = 1,
13	Storage = 2,
14	Plan = 3,
15	Cdc = 4,
16	Flow = 5,
17	Subscription = 6,
18	Server = 7,
19	Wire = 8,
20	Auth = 9,
21	Catalog = 10,
22	Engine = 11,
23	Mutate = 12,
24	Transport = 13,
25	Task = 14,
26	Policy = 15,
27	Ffi = 16,
28	Cache = 17,
29	Shape = 18,
30	Api = 19,
31	Actor = 20,
32}
33
34pub const CATEGORY_COUNT: usize = 21;
35
36pub const ALL_CATEGORIES: [ProfilerCategory; CATEGORY_COUNT] = [
37	ProfilerCategory::Query,
38	ProfilerCategory::Txn,
39	ProfilerCategory::Storage,
40	ProfilerCategory::Plan,
41	ProfilerCategory::Cdc,
42	ProfilerCategory::Flow,
43	ProfilerCategory::Subscription,
44	ProfilerCategory::Server,
45	ProfilerCategory::Wire,
46	ProfilerCategory::Auth,
47	ProfilerCategory::Catalog,
48	ProfilerCategory::Engine,
49	ProfilerCategory::Mutate,
50	ProfilerCategory::Transport,
51	ProfilerCategory::Task,
52	ProfilerCategory::Policy,
53	ProfilerCategory::Ffi,
54	ProfilerCategory::Cache,
55	ProfilerCategory::Shape,
56	ProfilerCategory::Api,
57	ProfilerCategory::Actor,
58];
59
60impl ProfilerCategory {
61	pub const fn as_id(self) -> ProfilerCategoryId {
62		ProfilerCategoryId(self as u8)
63	}
64
65	pub const fn from_id(id: ProfilerCategoryId) -> Option<Self> {
66		match id.0 {
67			0 => Some(ProfilerCategory::Query),
68			1 => Some(ProfilerCategory::Txn),
69			2 => Some(ProfilerCategory::Storage),
70			3 => Some(ProfilerCategory::Plan),
71			4 => Some(ProfilerCategory::Cdc),
72			5 => Some(ProfilerCategory::Flow),
73			6 => Some(ProfilerCategory::Subscription),
74			7 => Some(ProfilerCategory::Server),
75			8 => Some(ProfilerCategory::Wire),
76			9 => Some(ProfilerCategory::Auth),
77			10 => Some(ProfilerCategory::Catalog),
78			11 => Some(ProfilerCategory::Engine),
79			12 => Some(ProfilerCategory::Mutate),
80			13 => Some(ProfilerCategory::Transport),
81			14 => Some(ProfilerCategory::Task),
82			15 => Some(ProfilerCategory::Policy),
83			16 => Some(ProfilerCategory::Ffi),
84			17 => Some(ProfilerCategory::Cache),
85			18 => Some(ProfilerCategory::Shape),
86			19 => Some(ProfilerCategory::Api),
87			20 => Some(ProfilerCategory::Actor),
88			_ => None,
89		}
90	}
91
92	pub fn from_span_name(name: &str) -> Option<Self> {
93		if name.starts_with("flow::ffi::") {
94			Some(ProfilerCategory::Ffi)
95		} else if name.starts_with("flow::") {
96			Some(ProfilerCategory::Flow)
97		} else if name.starts_with("transaction::") {
98			Some(ProfilerCategory::Txn)
99		} else if name.starts_with("store::single::")
100			|| name.starts_with("store::multi::")
101			|| name.starts_with("drop::")
102		{
103			Some(ProfilerCategory::Storage)
104		} else if name.starts_with("volcano::") || name.starts_with("vm::") {
105			Some(ProfilerCategory::Query)
106		} else if name.starts_with("rql::") {
107			Some(ProfilerCategory::Plan)
108		} else if name.starts_with("catalog::") {
109			Some(ProfilerCategory::Catalog)
110		} else if name.starts_with("cache::") {
111			Some(ProfilerCategory::Cache)
112		} else if name.starts_with("shape_store::") || name.starts_with("row_shape_registry::") {
113			Some(ProfilerCategory::Shape)
114		} else if name.starts_with("api::") {
115			Some(ProfilerCategory::Api)
116		} else if name.starts_with("actor::") {
117			Some(ProfilerCategory::Actor)
118		} else if name.starts_with("cdc::") {
119			Some(ProfilerCategory::Cdc)
120		} else if name.starts_with("subscription::") {
121			Some(ProfilerCategory::Subscription)
122		} else if name.starts_with("server::") {
123			Some(ProfilerCategory::Server)
124		} else if name.starts_with("wire::") {
125			Some(ProfilerCategory::Wire)
126		} else if name.starts_with("auth::") {
127			Some(ProfilerCategory::Auth)
128		} else if name.starts_with("engine::")
129			|| name.starts_with("executor::")
130			|| name.starts_with("session::")
131		{
132			Some(ProfilerCategory::Engine)
133		} else if name.starts_with("mutate::") {
134			Some(ProfilerCategory::Mutate)
135		} else if name.starts_with("http::") || name.starts_with("dispatch::") {
136			Some(ProfilerCategory::Transport)
137		} else if name.starts_with("task::") {
138			Some(ProfilerCategory::Task)
139		} else if name.starts_with("policy::") {
140			Some(ProfilerCategory::Policy)
141		} else if name.starts_with("ffi::")
142			|| name.starts_with("procedure::")
143			|| name.starts_with("transform::")
144		{
145			Some(ProfilerCategory::Ffi)
146		} else {
147			None
148		}
149	}
150
151	pub const fn name(self) -> &'static str {
152		match self {
153			ProfilerCategory::Query => "query",
154			ProfilerCategory::Txn => "txn",
155			ProfilerCategory::Storage => "storage",
156			ProfilerCategory::Plan => "plan",
157			ProfilerCategory::Cdc => "cdc",
158			ProfilerCategory::Flow => "flow",
159			ProfilerCategory::Subscription => "subscription",
160			ProfilerCategory::Server => "server",
161			ProfilerCategory::Wire => "wire",
162			ProfilerCategory::Auth => "auth",
163			ProfilerCategory::Catalog => "catalog",
164			ProfilerCategory::Engine => "engine",
165			ProfilerCategory::Mutate => "mutate",
166			ProfilerCategory::Transport => "transport",
167			ProfilerCategory::Task => "task",
168			ProfilerCategory::Policy => "policy",
169			ProfilerCategory::Ffi => "ffi",
170			ProfilerCategory::Cache => "cache",
171			ProfilerCategory::Shape => "shape",
172			ProfilerCategory::Api => "api",
173			ProfilerCategory::Actor => "actor",
174		}
175	}
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
179pub enum ProfilerLevel {
180	Trace,
181	Debug,
182	Info,
183	Warn,
184	Error,
185}
186
187impl ProfilerLevel {
188	pub fn as_level_filter(self) -> LevelFilter {
189		match self {
190			Self::Trace => LevelFilter::TRACE,
191			Self::Debug => LevelFilter::DEBUG,
192			Self::Info => LevelFilter::INFO,
193			Self::Warn => LevelFilter::WARN,
194			Self::Error => LevelFilter::ERROR,
195		}
196	}
197
198	pub fn admits(self, level: &Level) -> bool {
199		*level <= self.as_level_filter()
200	}
201}
202
203#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
204pub struct CategorySet {
205	levels: [Option<ProfilerLevel>; CATEGORY_COUNT],
206}
207
208impl CategorySet {
209	pub const fn empty() -> Self {
210		Self {
211			levels: [None; CATEGORY_COUNT],
212		}
213	}
214
215	pub const fn all() -> Self {
216		Self::all_at(ProfilerLevel::Trace)
217	}
218
219	pub const fn all_at(level: ProfilerLevel) -> Self {
220		Self {
221			levels: [Some(level); CATEGORY_COUNT],
222		}
223	}
224
225	pub fn contains(&self, c: ProfilerCategory) -> bool {
226		self.levels[c as usize].is_some()
227	}
228
229	pub fn level_for(&self, c: ProfilerCategory) -> Option<ProfilerLevel> {
230		self.levels[c as usize]
231	}
232
233	pub fn insert(&mut self, c: ProfilerCategory) {
234		self.insert_at(c, ProfilerLevel::Trace);
235	}
236
237	pub fn insert_at(&mut self, c: ProfilerCategory, level: ProfilerLevel) {
238		self.levels[c as usize] = Some(level);
239	}
240
241	pub fn remove(&mut self, c: ProfilerCategory) {
242		self.levels[c as usize] = None;
243	}
244
245	pub fn with(mut self, c: ProfilerCategory) -> Self {
246		self.insert(c);
247		self
248	}
249
250	pub fn with_level(mut self, c: ProfilerCategory, level: ProfilerLevel) -> Self {
251		self.insert_at(c, level);
252		self
253	}
254
255	pub fn without(mut self, c: ProfilerCategory) -> Self {
256		self.remove(c);
257		self
258	}
259
260	pub fn is_empty(&self) -> bool {
261		self.levels.iter().all(|l| l.is_none())
262	}
263}
264
265#[cfg(test)]
266mod tests {
267	use super::*;
268
269	#[test]
270	fn from_span_name_known_prefixes() {
271		assert_eq!(ProfilerCategory::from_span_name("flow::engine::apply"), Some(ProfilerCategory::Flow));
272		assert_eq!(
273			ProfilerCategory::from_span_name("flow::engine::process_batch"),
274			Some(ProfilerCategory::Flow)
275		);
276		assert_eq!(ProfilerCategory::from_span_name("transaction::commit"), Some(ProfilerCategory::Txn));
277		assert_eq!(ProfilerCategory::from_span_name("store::multi::write"), Some(ProfilerCategory::Storage));
278		assert_eq!(ProfilerCategory::from_span_name("store::single::scan"), Some(ProfilerCategory::Storage));
279		assert_eq!(ProfilerCategory::from_span_name("drop::range"), Some(ProfilerCategory::Storage));
280		assert_eq!(ProfilerCategory::from_span_name("volcano::project"), Some(ProfilerCategory::Query));
281		assert_eq!(ProfilerCategory::from_span_name("vm::executor"), Some(ProfilerCategory::Query));
282		assert_eq!(ProfilerCategory::from_span_name("rql::parse"), Some(ProfilerCategory::Plan));
283		assert_eq!(ProfilerCategory::from_span_name("cdc::write"), Some(ProfilerCategory::Cdc));
284		assert_eq!(
285			ProfilerCategory::from_span_name("subscription::consume"),
286			Some(ProfilerCategory::Subscription)
287		);
288		assert_eq!(ProfilerCategory::from_span_name("server::deliver"), Some(ProfilerCategory::Server));
289		assert_eq!(ProfilerCategory::from_span_name("wire::encode_frames"), Some(ProfilerCategory::Wire));
290		assert_eq!(ProfilerCategory::from_span_name("auth::authenticate"), Some(ProfilerCategory::Auth));
291		assert_eq!(ProfilerCategory::from_span_name("engine::query_as"), Some(ProfilerCategory::Engine));
292		assert_eq!(ProfilerCategory::from_span_name("executor::compile"), Some(ProfilerCategory::Engine));
293		assert_eq!(ProfilerCategory::from_span_name("session::query"), Some(ProfilerCategory::Engine));
294		assert_eq!(ProfilerCategory::from_span_name("mutate::table::insert"), Some(ProfilerCategory::Mutate));
295		assert_eq!(ProfilerCategory::from_span_name("http::query"), Some(ProfilerCategory::Transport));
296		assert_eq!(
297			ProfilerCategory::from_span_name("dispatch::send_server_message"),
298			Some(ProfilerCategory::Transport)
299		);
300		assert_eq!(ProfilerCategory::from_span_name("task::spawn"), Some(ProfilerCategory::Task));
301		assert_eq!(ProfilerCategory::from_span_name("policy::enforce"), Some(ProfilerCategory::Policy));
302		assert_eq!(ProfilerCategory::from_span_name("ffi::callback"), Some(ProfilerCategory::Ffi));
303		assert_eq!(ProfilerCategory::from_span_name("procedure::ffi::execute"), Some(ProfilerCategory::Ffi));
304		assert_eq!(ProfilerCategory::from_span_name("transform::ffi::apply"), Some(ProfilerCategory::Ffi));
305		// flow::ffi:: must beat the broader flow:: -> Flow rule so FFI boundary cost is attributed to Ffi.
306		assert_eq!(ProfilerCategory::from_span_name("flow::ffi::vtable_call"), Some(ProfilerCategory::Ffi));
307		assert_eq!(ProfilerCategory::from_span_name("flow::engine::apply"), Some(ProfilerCategory::Flow));
308	}
309
310	#[test]
311	fn catalog_split_out_of_plan() {
312		// catalog:: was moved out of Plan into its own Catalog category so Plan reflects
313		// real query planning (rql::) rather than being dominated by metadata lookups.
314		assert_eq!(
315			ProfilerCategory::from_span_name("catalog::column::find_by_name"),
316			Some(ProfilerCategory::Catalog)
317		);
318		assert_eq!(ProfilerCategory::from_span_name("rql::plan"), Some(ProfilerCategory::Plan));
319	}
320
321	#[test]
322	fn from_span_name_new_subsystem_prefixes() {
323		// cache::, shape_store::/row_shape_registry::, api::, actor:: were previously unmapped and
324		// their spans silently dropped; each now buckets into its own selectable category.
325		assert_eq!(ProfilerCategory::from_span_name("cache::row_shape::load"), Some(ProfilerCategory::Cache));
326		assert_eq!(ProfilerCategory::from_span_name("shape_store::create"), Some(ProfilerCategory::Shape));
327		// row_shape_registry:: is the same row-shape subsystem as shape_store::, so it shares Shape.
328		assert_eq!(ProfilerCategory::from_span_name("row_shape_registry::load"), Some(ProfilerCategory::Shape));
329		assert_eq!(ProfilerCategory::from_span_name("api::stop_fast"), Some(ProfilerCategory::Api));
330		assert_eq!(ProfilerCategory::from_span_name("actor::query_pool"), Some(ProfilerCategory::Actor));
331	}
332
333	#[test]
334	fn from_span_name_flow_covers_non_engine_prefixes() {
335		// Flow was widened from `flow::engine::` to `flow::` so the already-instrumented
336		// coordinator/pool/worker spans are captured, not just the engine internals.
337		assert_eq!(
338			ProfilerCategory::from_span_name("flow::coordinator::consume"),
339			Some(ProfilerCategory::Flow)
340		);
341		assert_eq!(ProfilerCategory::from_span_name("flow::pool::submit"), Some(ProfilerCategory::Flow));
342		assert_eq!(ProfilerCategory::from_span_name("flow::actor::tick"), Some(ProfilerCategory::Flow));
343		assert_eq!(ProfilerCategory::from_span_name("flow::engine::apply"), Some(ProfilerCategory::Flow));
344	}
345
346	#[test]
347	fn from_span_name_unknown_returns_none() {
348		assert_eq!(ProfilerCategory::from_span_name("random::other::name"), None);
349		assert_eq!(ProfilerCategory::from_span_name(""), None);
350		assert_eq!(ProfilerCategory::from_span_name("tracing::subsystem::start"), None);
351	}
352
353	#[test]
354	fn id_roundtrip() {
355		for c in ALL_CATEGORIES {
356			assert_eq!(ProfilerCategory::from_id(c.as_id()), Some(c));
357		}
358		assert_eq!(ProfilerCategory::from_id(ProfilerCategoryId(CATEGORY_COUNT as u8)), None);
359		assert_eq!(ProfilerCategory::from_id(ProfilerCategoryId(255)), None);
360	}
361
362	#[test]
363	fn category_set_membership() {
364		let mut s = CategorySet::empty();
365		assert!(s.is_empty());
366		s.insert(ProfilerCategory::Flow);
367		assert!(s.contains(ProfilerCategory::Flow));
368		assert!(!s.contains(ProfilerCategory::Query));
369		s.insert(ProfilerCategory::Query);
370		assert!(s.contains(ProfilerCategory::Query));
371		s.remove(ProfilerCategory::Flow);
372		assert!(!s.contains(ProfilerCategory::Flow));
373
374		let all = CategorySet::all();
375		for c in ALL_CATEGORIES {
376			assert!(all.contains(c));
377		}
378	}
379
380	#[test]
381	fn category_set_default_insert_is_trace() {
382		let mut s = CategorySet::empty();
383		s.insert(ProfilerCategory::Flow);
384		assert_eq!(s.level_for(ProfilerCategory::Flow), Some(ProfilerLevel::Trace));
385	}
386
387	#[test]
388	fn category_set_per_category_level() {
389		let mut s = CategorySet::empty();
390		s.insert_at(ProfilerCategory::Flow, ProfilerLevel::Trace);
391		s.insert_at(ProfilerCategory::Query, ProfilerLevel::Debug);
392		s.insert_at(ProfilerCategory::Storage, ProfilerLevel::Info);
393
394		assert_eq!(s.level_for(ProfilerCategory::Flow), Some(ProfilerLevel::Trace));
395		assert_eq!(s.level_for(ProfilerCategory::Query), Some(ProfilerLevel::Debug));
396		assert_eq!(s.level_for(ProfilerCategory::Storage), Some(ProfilerLevel::Info));
397		assert_eq!(s.level_for(ProfilerCategory::Plan), None);
398
399		assert!(s.contains(ProfilerCategory::Flow));
400		assert!(s.contains(ProfilerCategory::Query));
401		assert!(s.contains(ProfilerCategory::Storage));
402		assert!(!s.contains(ProfilerCategory::Plan));
403	}
404
405	#[test]
406	fn category_set_with_level_round_trips() {
407		let s = CategorySet::empty()
408			.with_level(ProfilerCategory::Flow, ProfilerLevel::Trace)
409			.with_level(ProfilerCategory::Plan, ProfilerLevel::Debug);
410		assert_eq!(s.level_for(ProfilerCategory::Flow), Some(ProfilerLevel::Trace));
411		assert_eq!(s.level_for(ProfilerCategory::Plan), Some(ProfilerLevel::Debug));
412		assert_eq!(s.level_for(ProfilerCategory::Cdc), None);
413	}
414
415	#[test]
416	fn category_set_all_at_sets_uniform_level() {
417		let s = CategorySet::all_at(ProfilerLevel::Debug);
418		for c in ALL_CATEGORIES {
419			assert_eq!(s.level_for(c), Some(ProfilerLevel::Debug));
420		}
421	}
422
423	#[test]
424	fn profile_level_admits_at_or_less_verbose() {
425		assert!(ProfilerLevel::Debug.admits(&Level::DEBUG));
426		assert!(ProfilerLevel::Debug.admits(&Level::INFO));
427		assert!(ProfilerLevel::Debug.admits(&Level::WARN));
428		assert!(ProfilerLevel::Debug.admits(&Level::ERROR));
429		assert!(!ProfilerLevel::Debug.admits(&Level::TRACE));
430
431		assert!(ProfilerLevel::Trace.admits(&Level::TRACE));
432		assert!(!ProfilerLevel::Error.admits(&Level::WARN));
433	}
434}