1use libc::{c_int, c_uchar};
16use std::{
17 cell::{Cell, RefCell},
18 marker::PhantomData,
19};
20
21use crate::cache::Cache;
22use crate::ffi_util::from_cstr_and_free;
23use crate::{DB, DBCommon, ThreadMode, TransactionDB};
24use crate::{Error, db::DBInner, ffi};
25
26#[derive(Debug, Copy, Clone, PartialEq, Eq)]
37#[repr(i32)]
38pub enum PerfStatsLevel {
39 Uninitialized = 0,
41 Disable = 1,
43 EnableCount = 2,
45 EnableWait = 3,
47 EnableTimeExceptForMutex = 4,
49 EnableTimeAndCPUTimeExceptForMutex = 5,
52 EnableTime = 6,
54 OutOfBound = 7,
56}
57
58include!("perf_enum.rs");
60
61pub fn set_perf_stats(lvl: PerfStatsLevel) {
63 unsafe {
64 ffi::rocksdb_set_perf_level(lvl as c_int);
65 }
66}
67
68pub struct PerfContext {
71 pub(crate) inner: *mut ffi::rocksdb_perfcontext_t,
72 reusable: bool,
73}
74
75thread_local! {
76 static ACTIVE_MANUAL_PERF_CONTEXTS: Cell<usize> = const { Cell::new(0) };
77 static REUSABLE_PERF_CONTEXT: RefCell<PerfContext> = RefCell::new({
78 let inner = unsafe { ffi::rocksdb_perfcontext_create() };
79 assert!(!inner.is_null(), "Could not create Perf Context");
80 PerfContext {
81 inner,
82 reusable: true,
83 }
84 });
85}
86
87impl Default for PerfContext {
88 fn default() -> Self {
89 let ctx = unsafe { ffi::rocksdb_perfcontext_create() };
90 assert!(!ctx.is_null(), "Could not create Perf Context");
91 ACTIVE_MANUAL_PERF_CONTEXTS.with(|count| count.set(count.get() + 1));
92
93 Self {
94 inner: ctx,
95 reusable: false,
96 }
97 }
98}
99
100impl Drop for PerfContext {
101 fn drop(&mut self) {
102 if !self.reusable {
103 ACTIVE_MANUAL_PERF_CONTEXTS.with(|count| count.set(count.get() - 1));
104 }
105 unsafe {
106 ffi::rocksdb_perfcontext_destroy(self.inner);
107 }
108 }
109}
110
111impl PerfContext {
112 #[inline]
114 pub fn reset(&mut self) {
115 unsafe {
116 ffi::rocksdb_perfcontext_reset(self.inner);
117 }
118 }
119
120 pub fn report(&self, exclude_zero_counters: bool) -> String {
122 unsafe {
123 let ptr =
124 ffi::rocksdb_perfcontext_report(self.inner, c_uchar::from(exclude_zero_counters));
125 from_cstr_and_free(ptr)
126 }
127 }
128
129 #[inline]
131 pub fn metric(&self, id: PerfMetric) -> u64 {
132 unsafe { ffi::rocksdb_perfcontext_metric(self.inner, id as c_int) }
133 }
134}
135
136pub fn with_thread_local<F, R>(f: F) -> R
147where
148 F: FnOnce(&mut PerfContext) -> R,
149{
150 ACTIVE_MANUAL_PERF_CONTEXTS.with(|count| {
151 assert_eq!(
152 count.get(),
153 0,
154 "with_thread_local cannot run while a manual PerfContext is alive on the same thread"
155 );
156 });
157 REUSABLE_PERF_CONTEXT.with(|ctx| {
158 let mut ctx = ctx.try_borrow_mut().unwrap_or_else(|_| {
159 panic!("with_thread_local cannot be called reentrantly on the same thread")
160 });
161 ctx.reset();
162 f(&mut ctx)
163 })
164}
165
166pub struct MemoryUsageStats {
168 pub mem_table_total: u64,
170 pub mem_table_unflushed: u64,
172 pub mem_table_readers_total: u64,
174 pub cache_total: u64,
176}
177
178pub struct MemoryUsage {
180 inner: *mut ffi::rocksdb_memory_usage_t,
181}
182
183impl Drop for MemoryUsage {
184 fn drop(&mut self) {
185 unsafe {
186 ffi::rocksdb_approximate_memory_usage_destroy(self.inner);
187 }
188 }
189}
190
191impl MemoryUsage {
192 pub fn approximate_mem_table_total(&self) -> u64 {
194 unsafe { ffi::rocksdb_approximate_memory_usage_get_mem_table_total(self.inner) }
195 }
196
197 pub fn approximate_mem_table_unflushed(&self) -> u64 {
199 unsafe { ffi::rocksdb_approximate_memory_usage_get_mem_table_unflushed(self.inner) }
200 }
201
202 pub fn approximate_mem_table_readers_total(&self) -> u64 {
204 unsafe { ffi::rocksdb_approximate_memory_usage_get_mem_table_readers_total(self.inner) }
205 }
206
207 pub fn approximate_cache_total(&self) -> u64 {
209 unsafe { ffi::rocksdb_approximate_memory_usage_get_cache_total(self.inner) }
210 }
211}
212
213pub struct MemoryUsageBuilder<'a> {
230 inner: *mut ffi::rocksdb_memory_consumers_t,
231 base_dbs: Vec<*mut ffi::rocksdb_t>,
232 _marker: PhantomData<&'a ()>,
234}
235
236impl Drop for MemoryUsageBuilder<'_> {
237 fn drop(&mut self) {
238 unsafe {
239 ffi::rocksdb_memory_consumers_destroy(self.inner);
240 }
241 for base_db in &self.base_dbs {
242 unsafe {
243 ffi::rocksdb_transactiondb_close_base_db(*base_db);
244 }
245 }
246 }
247}
248
249impl<'a> MemoryUsageBuilder<'a> {
250 pub fn new() -> Result<Self, Error> {
252 let mc = unsafe { ffi::rocksdb_memory_consumers_create() };
253 if mc.is_null() {
254 Err(Error::new(
255 "Could not create MemoryUsage builder".to_owned(),
256 ))
257 } else {
258 Ok(Self {
259 inner: mc,
260 base_dbs: Vec::new(),
261 _marker: PhantomData,
262 })
263 }
264 }
265
266 pub fn add_tx_db<T: ThreadMode>(&mut self, db: &'a TransactionDB<T>) {
268 unsafe {
269 let base_db = ffi::rocksdb_transactiondb_get_base_db(db.inner);
270 ffi::rocksdb_memory_consumers_add_db(self.inner, base_db);
271 self.base_dbs.push(base_db);
273 }
274 }
275
276 pub fn add_db<T: ThreadMode, D: DBInner>(&mut self, db: &'a DBCommon<T, D>) {
278 unsafe {
279 ffi::rocksdb_memory_consumers_add_db(self.inner, db.inner.inner());
280 }
281 }
282
283 pub fn add_cache(&mut self, cache: &'a Cache) {
285 unsafe {
286 ffi::rocksdb_memory_consumers_add_cache(self.inner, cache.0.inner.as_ptr());
287 }
288 }
289
290 pub fn build(&self) -> Result<MemoryUsage, Error> {
292 unsafe {
293 let mu = ffi_try!(ffi::rocksdb_approximate_memory_usage_create(self.inner));
294 Ok(MemoryUsage { inner: mu })
295 }
296 }
297}
298
299pub fn get_memory_usage_stats(
301 dbs: Option<&[&DB]>,
302 caches: Option<&[&Cache]>,
303) -> Result<MemoryUsageStats, Error> {
304 let mut builder = MemoryUsageBuilder::new()?;
305 if let Some(dbs_) = dbs {
306 for db in dbs_ {
307 builder.add_db(db);
308 }
309 }
310 if let Some(caches_) = caches {
311 for cache in caches_ {
312 builder.add_cache(cache);
313 }
314 }
315
316 let mu = builder.build()?;
317 Ok(MemoryUsageStats {
318 mem_table_total: mu.approximate_mem_table_total(),
319 mem_table_unflushed: mu.approximate_mem_table_unflushed(),
320 mem_table_readers_total: mu.approximate_mem_table_readers_total(),
321 cache_total: mu.approximate_cache_total(),
322 })
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328 use crate::{DB, Options};
329 use std::panic;
330 use tempfile::TempDir;
331
332 #[test]
333 fn perf_stats_level_matches_rocksdb_11_1_2() {
334 assert_eq!(PerfStatsLevel::Uninitialized as i32, 0);
335 assert_eq!(PerfStatsLevel::Disable as i32, 1);
336 assert_eq!(PerfStatsLevel::EnableCount as i32, 2);
337 assert_eq!(PerfStatsLevel::EnableWait as i32, 3);
338 assert_eq!(PerfStatsLevel::EnableTimeExceptForMutex as i32, 4);
339 assert_eq!(PerfStatsLevel::EnableTimeAndCPUTimeExceptForMutex as i32, 5);
340 assert_eq!(PerfStatsLevel::EnableTime as i32, 6);
341 assert_eq!(PerfStatsLevel::OutOfBound as i32, 7);
342 }
343
344 #[test]
345 fn with_thread_local_reuses_context_after_unwind() {
346 let first = with_thread_local(|ctx| ctx.inner);
347 let panic_result = panic::catch_unwind(|| {
348 with_thread_local(|_| panic!("test panic"));
349 });
350 assert!(panic_result.is_err());
351 let second = with_thread_local(|ctx| ctx.inner);
352
353 assert_eq!(first, second);
354 }
355
356 #[test]
357 fn with_thread_local_resets_context_before_use() {
358 let temp_dir = TempDir::new().unwrap();
359 let mut opts = Options::default();
360 opts.create_if_missing(true);
361 let db = DB::open(&opts, temp_dir.path()).unwrap();
362 db.put(b"key", b"value").unwrap();
363
364 set_perf_stats(PerfStatsLevel::EnableCount);
365 let comparison_count = with_thread_local(|ctx| {
366 db.get(b"key").unwrap();
367 ctx.metric(PerfMetric::UserKeyComparisonCount)
368 });
369 assert!(comparison_count > 0);
370 assert_eq!(
371 with_thread_local(|ctx| ctx.metric(PerfMetric::UserKeyComparisonCount)),
372 0
373 );
374 set_perf_stats(PerfStatsLevel::Disable);
375 }
376
377 #[test]
378 #[should_panic(expected = "with_thread_local cannot be called reentrantly on the same thread")]
379 fn with_thread_local_rejects_reentrant_use() {
380 with_thread_local(|_| with_thread_local(|_| ()));
381 }
382
383 #[test]
384 #[should_panic(
385 expected = "with_thread_local cannot run while a manual PerfContext is alive on the same thread"
386 )]
387 fn with_thread_local_rejects_active_manual_context() {
388 let _manual = PerfContext::default();
389 with_thread_local(|_| ());
390 }
391
392 #[test]
393 fn test_perf_context_with_db_operations() {
394 let temp_dir = TempDir::new().unwrap();
395 let mut opts = Options::default();
396 opts.create_if_missing(true);
397 let db = DB::open(&opts, temp_dir.path()).unwrap();
398
399 let n = 10;
401 for i in 0..n {
402 let k = vec![i as u8];
403 db.put(&k, &k).unwrap();
404 if i % 2 == 0 {
405 db.delete(&k).unwrap();
406 }
407 }
408
409 set_perf_stats(PerfStatsLevel::EnableCount);
410 let mut ctx = PerfContext::default();
411
412 let mut iter = db.raw_iterator();
414 iter.seek_to_first();
415 let mut valid_count = 0;
416 while iter.valid() {
417 valid_count += 1;
418 iter.next();
419 }
420
421 assert_eq!(
423 valid_count, 5,
424 "Iterator should find 5 valid entries (odd numbers)"
425 );
426
427 let internal_key_skipped = ctx.metric(PerfMetric::InternalKeySkippedCount);
429 let internal_delete_skipped = ctx.metric(PerfMetric::InternalDeleteSkippedCount);
430
431 assert!(
435 internal_key_skipped >= (n / 2) as u64,
436 "internal_key_skipped ({}) should be >= {} (deletions)",
437 internal_key_skipped,
438 n / 2
439 );
440 assert_eq!(
441 internal_delete_skipped,
442 (n / 2) as u64,
443 "internal_delete_skipped ({internal_delete_skipped}) should equal {} (deleted entries)",
444 n / 2
445 );
446 assert_eq!(
447 ctx.metric(PerfMetric::SeekInternalSeekTime),
448 0,
449 "Time metrics should be 0 with EnableCount"
450 );
451
452 ctx.reset();
454 assert_eq!(ctx.metric(PerfMetric::InternalKeySkippedCount), 0);
455 assert_eq!(ctx.metric(PerfMetric::InternalDeleteSkippedCount), 0);
456
457 set_perf_stats(PerfStatsLevel::EnableTime);
459
460 let mut iter = db.raw_iterator();
462 iter.seek_to_last();
463 let mut backward_count = 0;
464 while iter.valid() {
465 backward_count += 1;
466 iter.prev();
467 }
468 assert_eq!(
469 backward_count, 5,
470 "Backward iteration should also find 5 valid entries"
471 );
472
473 let key_skipped_after = ctx.metric(PerfMetric::InternalKeySkippedCount);
475 let delete_skipped_after = ctx.metric(PerfMetric::InternalDeleteSkippedCount);
476
477 assert!(
479 key_skipped_after >= internal_key_skipped,
480 "After second iteration, internal_key_skipped ({key_skipped_after}) should be >= first iteration ({internal_key_skipped})",
481 );
482 assert_eq!(
483 delete_skipped_after,
484 (n / 2) as u64,
485 "internal_delete_skipped should still be {} after second iteration",
486 n / 2
487 );
488
489 set_perf_stats(PerfStatsLevel::Disable);
491 }
492}