1use crate::stats;
2use anyhow::Result;
3use prometheus_client::collector::Collector;
4use prometheus_client::encoding::DescriptorEncoder;
5use prometheus_client::encoding::EncodeMetric;
6use prometheus_client::metrics::gauge::ConstGauge;
7use prometheus_client::metrics::info::Info;
8use serde::Serialize;
9use std::sync::atomic::{AtomicU8, Ordering};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
12#[serde(rename_all = "snake_case")]
13pub enum AllocatorKind {
14 Undeclared,
15 Glibc,
16 Jemalloc,
17 Mimalloc,
18}
19
20#[derive(Clone, Debug, Serialize)]
21pub struct AllocatorSnapshot {
22 pub kind: AllocatorKind,
23 pub comparable: Option<AllocatorComparisonStats>,
24 pub specific: Option<AllocatorSpecificDetails>,
25}
26
27#[derive(Debug, Clone)]
28pub struct PrometheusCollector {}
29
30impl PrometheusCollector {
31 pub fn register(registry: &mut prometheus_client::registry::Registry) {
32 registry.register_collector(Box::new(Self {}))
33 }
34}
35
36impl Collector for PrometheusCollector {
37 fn encode(&self, mut encoder: DescriptorEncoder) -> Result<(), std::fmt::Error> {
38 let snapshot = snapshot();
39 let info_metric = Info::new(vec![("allocator", snapshot.kind.as_str())]);
40 let info_encoder = encoder.encode_descriptor(
41 "allocator_info",
42 "allocator identity for this process",
43 None,
44 info_metric.metric_type(),
45 )?;
46 info_metric.encode(info_encoder)?;
47 let configured_metric = ConstGauge::new(u64::from(snapshot.kind != AllocatorKind::Undeclared));
48 let configured_encoder = encoder.encode_descriptor(
49 "allocator_configured",
50 "whether allocator kind was explicitly declared for this process",
51 None,
52 configured_metric.metric_type(),
53 )?;
54 configured_metric.encode(configured_encoder)?;
55
56 let mut encode = |value: Option<u64>, name: &'static str, help: &str| {
57 let Some(value) = prometheus_gauge_value(value) else {
58 return Ok(());
59 };
60 let metric = ConstGauge::new(value);
61 let metric_encoder = encoder.encode_descriptor(name, help, None, metric.metric_type())?;
62 metric.encode(metric_encoder)?;
63 Ok(())
64 };
65
66 let Some(comparable) = snapshot.comparable.as_ref() else {
67 return Ok(());
68 };
69
70 encode(
71 comparable.allocated_bytes,
72 "allocator_allocated_bytes",
73 "bytes allocated according to the current allocator",
74 )?;
75 encode(
76 comparable.active_bytes,
77 "allocator_active_bytes",
78 "bytes currently active according to the current allocator",
79 )?;
80 encode(
81 comparable.resident_bytes,
82 "allocator_resident_bytes",
83 "resident bytes attributed to the current allocator",
84 )?;
85 encode(
86 comparable.mapped_bytes,
87 "allocator_mapped_bytes",
88 "bytes mapped or reserved by the current allocator",
89 )?;
90 encode(
91 comparable.retained_bytes,
92 "allocator_retained_bytes",
93 "bytes retained but not currently active according to the current allocator",
94 )?;
95 encode(
96 comparable.metadata_bytes,
97 "allocator_metadata_bytes",
98 "bytes used for allocator metadata",
99 )?;
100 encode(
101 comparable.committed_bytes,
102 "allocator_committed_bytes",
103 "bytes committed by the current allocator",
104 )?;
105 encode(
106 comparable.allocator_structures,
107 "allocator_structures",
108 "allocator structures such as heaps or arenas",
109 )?;
110 Ok(())
111 }
112}
113
114fn prometheus_gauge_value(value: Option<u64>) -> Option<u64> {
115 value.filter(|value| *value != u64::MAX)
116}
117
118#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
119pub struct AllocatorComparisonStats {
120 pub allocated_bytes: Option<u64>,
121 pub active_bytes: Option<u64>,
122 pub resident_bytes: Option<u64>,
123 pub mapped_bytes: Option<u64>,
124 pub retained_bytes: Option<u64>,
125 pub metadata_bytes: Option<u64>,
126 pub committed_bytes: Option<u64>,
127 pub allocator_structures: Option<u64>,
128}
129
130#[derive(Clone, Debug, Serialize)]
131#[serde(tag = "kind", rename_all = "snake_case")]
132pub enum AllocatorSpecificDetails {
133 Glibc(GlibcStats),
134 #[cfg(feature = "allocator-jemalloc")]
135 Jemalloc(JemallocStats),
136 #[cfg(feature = "allocator-mimalloc")]
137 Mimalloc(MimallocStats),
138}
139
140#[derive(Clone, Debug, Serialize)]
141pub struct GlibcStats {
142 pub system_max: u64,
143 pub system_current: u64,
144 pub free_bytes: u64,
145 pub mmap_current: u64,
146 pub in_use_bytes: u64,
147 pub heaps: u64,
148}
149
150impl From<&stats::malloc::MallocInfo> for GlibcStats {
151 fn from(info: &stats::malloc::MallocInfo) -> Self {
152 Self {
153 system_max: info.system_max(),
154 system_current: info.system_current(),
155 free_bytes: info.free_bytes(),
156 mmap_current: info.mmap_bytes(),
157 in_use_bytes: info.in_use_bytes(),
158 heaps: info.heaps(),
159 }
160 }
161}
162
163impl From<&GlibcStats> for AllocatorComparisonStats {
164 fn from(stats: &GlibcStats) -> Self {
165 Self {
166 allocated_bytes: Some(stats.in_use_bytes),
167 active_bytes: None,
168 resident_bytes: None,
169 mapped_bytes: Some(stats.system_current.saturating_add(stats.mmap_current)),
170 retained_bytes: Some(stats.free_bytes),
171 metadata_bytes: None,
172 committed_bytes: None,
173 allocator_structures: Some(stats.heaps),
174 }
175 }
176}
177
178#[cfg(feature = "allocator-jemalloc")]
179#[derive(Clone, Debug, Serialize)]
180pub struct JemallocStats {
181 pub allocated: u64,
182 pub active: u64,
183 pub metadata: u64,
184 pub resident: u64,
185 pub mapped: u64,
186 pub retained: u64,
187 pub background_thread: bool,
188}
189
190#[cfg(feature = "allocator-jemalloc")]
191impl From<&JemallocStats> for AllocatorComparisonStats {
192 fn from(stats: &JemallocStats) -> Self {
193 Self {
194 allocated_bytes: Some(stats.allocated),
195 active_bytes: Some(stats.active),
196 resident_bytes: Some(stats.resident),
197 mapped_bytes: Some(stats.mapped),
198 retained_bytes: Some(stats.retained),
199 metadata_bytes: Some(stats.metadata),
200 committed_bytes: None,
201 allocator_structures: None,
202 }
203 }
204}
205
206#[cfg(feature = "allocator-mimalloc")]
207#[derive(Clone, Debug, Serialize)]
208pub struct MimallocStats {
209 pub version: u32,
210 pub reserved_current: u64,
211 pub reserved_peak: u64,
212 pub committed_current: u64,
213 pub committed_peak: u64,
214 pub reset_current: u64,
215 pub purged_current: u64,
216 pub process_rss_current: u64,
217 pub process_rss_peak: u64,
218 pub process_commit_current: u64,
219 pub process_commit_peak: u64,
220 pub page_faults: u64,
221 pub arenas: u64,
222}
223
224#[cfg(feature = "allocator-mimalloc")]
225impl From<&MimallocStats> for AllocatorComparisonStats {
226 fn from(stats: &MimallocStats) -> Self {
227 Self {
228 allocated_bytes: None,
229 active_bytes: None,
230 resident_bytes: Some(stats.process_rss_current),
231 mapped_bytes: Some(stats.reserved_current),
232 retained_bytes: None,
233 metadata_bytes: None,
234 committed_bytes: Some(stats.committed_current),
235 allocator_structures: Some(stats.arenas),
236 }
237 }
238}
239
240#[cfg(feature = "allocator-mimalloc")]
241#[repr(C)]
242struct MiStatCount {
243 total: i64,
244 peak: i64,
245 current: i64,
246}
247
248#[cfg(feature = "allocator-mimalloc")]
249#[repr(C)]
250struct MiStatCounter {
251 total: i64,
252}
253
254#[cfg(feature = "allocator-mimalloc")]
255#[repr(C)]
256struct MiStats {
257 version: i32,
258 pages: MiStatCount,
259 reserved: MiStatCount,
260 committed: MiStatCount,
261 reset: MiStatCount,
262 purged: MiStatCount,
263 page_committed: MiStatCount,
264 pages_abandoned: MiStatCount,
265 threads: MiStatCount,
266 malloc_normal: MiStatCount,
267 malloc_huge: MiStatCount,
268 malloc_requested: MiStatCount,
269 mmap_calls: MiStatCounter,
270 commit_calls: MiStatCounter,
271 reset_calls: MiStatCounter,
272 purge_calls: MiStatCounter,
273 arena_count: MiStatCounter,
274 malloc_normal_count: MiStatCounter,
275 malloc_huge_count: MiStatCounter,
276 malloc_guarded_count: MiStatCounter,
277 arena_rollback_count: MiStatCounter,
278 arena_purges: MiStatCounter,
279 pages_extended: MiStatCounter,
280 pages_retire: MiStatCounter,
281 page_searches: MiStatCounter,
282 segments: MiStatCount,
283 segments_abandoned: MiStatCount,
284 segments_cache: MiStatCount,
285 segments_reserved: MiStatCount,
286 pages_reclaim_on_alloc: MiStatCounter,
287 pages_reclaim_on_free: MiStatCounter,
288 pages_reabandon_full: MiStatCounter,
289 pages_unabandon_busy_wait: MiStatCounter,
290 stat_reserved: [MiStatCount; 4],
291 stat_counter_reserved: [MiStatCounter; 4],
292 malloc_bins: [MiStatCount; 74],
293 page_bins: [MiStatCount; 74],
294}
295
296static CONFIGURED_ALLOCATOR: AtomicU8 = AtomicU8::new(AllocatorKind::Undeclared.as_u8());
297
298impl AllocatorKind {
299 const fn as_u8(self) -> u8 {
300 match self {
301 Self::Undeclared => 0,
302 Self::Glibc => 1,
303 Self::Jemalloc => 2,
304 Self::Mimalloc => 3,
305 }
306 }
307
308 const fn from_u8(value: u8) -> Self {
309 match value {
310 1 => Self::Glibc,
311 2 => Self::Jemalloc,
312 3 => Self::Mimalloc,
313 _ => Self::Undeclared,
314 }
315 }
316
317 pub const fn as_str(self) -> &'static str {
318 match self {
319 Self::Undeclared => "undeclared",
320 Self::Glibc => "glibc",
321 Self::Jemalloc => "jemalloc",
322 Self::Mimalloc => "mimalloc",
323 }
324 }
325}
326
327pub fn configure(kind: AllocatorKind) {
328 CONFIGURED_ALLOCATOR.store(kind.as_u8(), Ordering::Release);
329}
330
331pub fn configured() -> AllocatorKind {
332 AllocatorKind::from_u8(CONFIGURED_ALLOCATOR.load(Ordering::Acquire))
333}
334
335pub fn snapshot() -> AllocatorSnapshot {
336 snapshot_for(configured())
337}
338
339pub fn snapshot_for(kind: AllocatorKind) -> AllocatorSnapshot {
340 match kind {
341 AllocatorKind::Undeclared => backend_snapshot(
342 kind,
343 Err(anyhow::anyhow!(
344 "allocator kind is undeclared; add declare_allocator_kind!(...) next to #[global_allocator]"
345 )),
346 ),
347 AllocatorKind::Glibc => {
348 let result = glibc_snapshot().map(|stats| {
349 (
350 AllocatorComparisonStats::from(&stats),
351 AllocatorSpecificDetails::Glibc(stats),
352 )
353 });
354 backend_snapshot(kind, result)
355 },
356 AllocatorKind::Jemalloc => backend_snapshot(kind, jemalloc_snapshot_pair()),
357 AllocatorKind::Mimalloc => backend_snapshot(kind, mimalloc_snapshot_pair()),
358 }
359}
360
361fn backend_snapshot(
362 kind: AllocatorKind,
363 result: Result<(AllocatorComparisonStats, AllocatorSpecificDetails)>,
364) -> AllocatorSnapshot {
365 match result {
366 Ok((comparable, specific)) => AllocatorSnapshot {
367 kind,
368 comparable: Some(comparable),
369 specific: Some(specific),
370 },
371 Err(_) => AllocatorSnapshot {
372 kind,
373 comparable: None,
374 specific: None,
375 },
376 }
377}
378
379fn glibc_snapshot() -> Result<GlibcStats> {
380 stats::malloc::info()
381 .map(|info| GlibcStats::from(&info))
382 .map_err(anyhow::Error::from)
383}
384
385#[cfg(feature = "allocator-jemalloc")]
386fn jemalloc_snapshot_pair() -> Result<(AllocatorComparisonStats, AllocatorSpecificDetails)> {
387 let stats = jemalloc_snapshot()?;
388 Ok((
389 AllocatorComparisonStats::from(&stats),
390 AllocatorSpecificDetails::Jemalloc(stats),
391 ))
392}
393
394#[cfg(not(feature = "allocator-jemalloc"))]
395fn jemalloc_snapshot_pair() -> Result<(AllocatorComparisonStats, AllocatorSpecificDetails)> {
396 anyhow::bail!("jemalloc support is not compiled in; enable the `allocator-jemalloc` feature")
397}
398
399#[cfg(feature = "allocator-jemalloc")]
400fn jemalloc_snapshot() -> Result<JemallocStats> {
401 use tikv_jemalloc_ctl::{background_thread, epoch, stats};
402
403 epoch::advance().map_err(anyhow::Error::from)?;
404
405 Ok(JemallocStats {
406 allocated: stats::allocated::read().map_err(anyhow::Error::from)? as u64,
407 active: stats::active::read().map_err(anyhow::Error::from)? as u64,
408 metadata: stats::metadata::read().map_err(anyhow::Error::from)? as u64,
409 resident: stats::resident::read().map_err(anyhow::Error::from)? as u64,
410 mapped: stats::mapped::read().map_err(anyhow::Error::from)? as u64,
411 retained: stats::retained::read().map_err(anyhow::Error::from)? as u64,
412 background_thread: background_thread::read().unwrap_or(false),
413 })
414}
415
416#[cfg(feature = "allocator-mimalloc")]
417fn mimalloc_snapshot_pair() -> Result<(AllocatorComparisonStats, AllocatorSpecificDetails)> {
418 let stats = mimalloc_snapshot()?;
419 Ok((
420 AllocatorComparisonStats::from(&stats),
421 AllocatorSpecificDetails::Mimalloc(stats),
422 ))
423}
424
425#[cfg(not(feature = "allocator-mimalloc"))]
426fn mimalloc_snapshot_pair() -> Result<(AllocatorComparisonStats, AllocatorSpecificDetails)> {
427 anyhow::bail!("mimalloc support is not compiled in; enable the `allocator-mimalloc` feature")
428}
429
430#[cfg(feature = "allocator-mimalloc")]
431fn mimalloc_snapshot() -> Result<MimallocStats> {
432 use libmimalloc_sys::mi_process_info;
433
434 unsafe extern "C" {
435 fn mi_stats_get(stats_size: usize, stats: *mut MiStats);
438 }
439
440 let mut stats = MiStats {
441 version: 0,
442 pages: zero_count(),
443 reserved: zero_count(),
444 committed: zero_count(),
445 reset: zero_count(),
446 purged: zero_count(),
447 page_committed: zero_count(),
448 pages_abandoned: zero_count(),
449 threads: zero_count(),
450 malloc_normal: zero_count(),
451 malloc_huge: zero_count(),
452 malloc_requested: zero_count(),
453 mmap_calls: zero_counter(),
454 commit_calls: zero_counter(),
455 reset_calls: zero_counter(),
456 purge_calls: zero_counter(),
457 arena_count: zero_counter(),
458 malloc_normal_count: zero_counter(),
459 malloc_huge_count: zero_counter(),
460 malloc_guarded_count: zero_counter(),
461 arena_rollback_count: zero_counter(),
462 arena_purges: zero_counter(),
463 pages_extended: zero_counter(),
464 pages_retire: zero_counter(),
465 page_searches: zero_counter(),
466 segments: zero_count(),
467 segments_abandoned: zero_count(),
468 segments_cache: zero_count(),
469 segments_reserved: zero_count(),
470 pages_reclaim_on_alloc: zero_counter(),
471 pages_reclaim_on_free: zero_counter(),
472 pages_reabandon_full: zero_counter(),
473 pages_unabandon_busy_wait: zero_counter(),
474 stat_reserved: [zero_count(), zero_count(), zero_count(), zero_count()],
475 stat_counter_reserved: [
476 zero_counter(),
477 zero_counter(),
478 zero_counter(),
479 zero_counter(),
480 ],
481 malloc_bins: std::array::from_fn(|_| zero_count()),
482 page_bins: std::array::from_fn(|_| zero_count()),
483 };
484
485 let mut elapsed_msecs = 0usize;
486 let mut user_msecs = 0usize;
487 let mut system_msecs = 0usize;
488 let mut current_rss = 0usize;
489 let mut peak_rss = 0usize;
490 let mut current_commit = 0usize;
491 let mut peak_commit = 0usize;
492 let mut page_faults = 0usize;
493
494 unsafe {
495 mi_stats_get(std::mem::size_of::<MiStats>(), &mut stats);
496 mi_process_info(
497 &mut elapsed_msecs,
498 &mut user_msecs,
499 &mut system_msecs,
500 &mut current_rss,
501 &mut peak_rss,
502 &mut current_commit,
503 &mut peak_commit,
504 &mut page_faults,
505 );
506 }
507
508 let _ = (elapsed_msecs, user_msecs, system_msecs);
509
510 if stats.version == 0 {
511 anyhow::bail!("mimalloc statistics are unavailable");
512 }
513
514 Ok(MimallocStats {
515 version: stats.version as u32,
516 reserved_current: stats.reserved.current.max(0) as u64,
517 reserved_peak: stats.reserved.peak.max(0) as u64,
518 committed_current: stats.committed.current.max(0) as u64,
519 committed_peak: stats.committed.peak.max(0) as u64,
520 reset_current: stats.reset.current.max(0) as u64,
521 purged_current: stats.purged.current.max(0) as u64,
522 process_rss_current: sanitize_mimalloc_process_value(current_rss),
523 process_rss_peak: sanitize_mimalloc_process_value(peak_rss),
524 process_commit_current: sanitize_mimalloc_process_value(current_commit),
525 process_commit_peak: sanitize_mimalloc_process_value(peak_commit),
526 page_faults: page_faults as u64,
527 arenas: stats.arena_count.total.max(0) as u64,
528 })
529}
530
531#[cfg(feature = "allocator-mimalloc")]
532fn sanitize_mimalloc_process_value(value: usize) -> u64 {
533 if value == usize::MAX { 0 } else { value as u64 }
534}
535
536#[cfg(feature = "allocator-mimalloc")]
537const fn zero_count() -> MiStatCount {
538 MiStatCount {
539 total: 0,
540 peak: 0,
541 current: 0,
542 }
543}
544
545#[cfg(feature = "allocator-mimalloc")]
546const fn zero_counter() -> MiStatCounter {
547 MiStatCounter { total: 0 }
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553 use parking_lot::Mutex;
554 use prometheus_client::encoding::text::encode;
555 use prometheus_client::registry::Registry;
556
557 static TEST_GUARD: Mutex<()> = Mutex::new(());
558
559 #[test]
560 fn snapshot_reports_undeclared_allocator_without_stats() {
561 let _guard = TEST_GUARD.lock();
562 configure(AllocatorKind::Undeclared);
563
564 let snapshot = snapshot();
565 assert_eq!(snapshot.kind, AllocatorKind::Undeclared);
566 assert!(snapshot.comparable.is_none());
567 assert!(snapshot.specific.is_none());
568 }
569
570 #[test]
571 fn prometheus_collector_emits_allocator_info_metric() {
572 let _guard = TEST_GUARD.lock();
573 configure(AllocatorKind::Glibc);
574
575 let mut registry = Registry::default();
576 PrometheusCollector::register(&mut registry);
577
578 let mut output = String::new();
579 encode(&mut output, ®istry).expect("allocator metrics should encode");
580
581 assert!(output.contains("# TYPE allocator_info info"));
582 assert!(output.contains("allocator_info_info{allocator=\"glibc\"} 1"));
583 assert!(output.contains("allocator_configured 1"));
584 }
585
586 #[test]
587 fn prometheus_collector_emits_undeclared_allocator_signal() {
588 let _guard = TEST_GUARD.lock();
589 configure(AllocatorKind::Undeclared);
590
591 let mut registry = Registry::default();
592 PrometheusCollector::register(&mut registry);
593
594 let mut output = String::new();
595 encode(&mut output, ®istry).expect("allocator metrics should encode");
596
597 assert!(output.contains("allocator_info_info{allocator=\"undeclared\"} 1"));
598 assert!(output.contains("allocator_configured 0"));
599 }
600
601 #[test]
602 fn glibc_comparison_stats_use_in_use_and_free_bytes() {
603 let comparable = AllocatorComparisonStats::from(&GlibcStats {
604 system_max: 8192,
605 system_current: 4096,
606 free_bytes: 1024,
607 mmap_current: 512,
608 in_use_bytes: 3584,
609 heaps: 3,
610 });
611
612 assert_eq!(comparable.allocated_bytes, Some(3584));
613 assert_eq!(comparable.mapped_bytes, Some(4608));
614 assert_eq!(comparable.retained_bytes, Some(1024));
615 assert_eq!(comparable.allocator_structures, Some(3));
616 }
617
618 #[test]
619 fn prometheus_gauge_value_rejects_u64_max() {
620 assert_eq!(prometheus_gauge_value(Some(u64::MAX)), None);
621 assert_eq!(
622 prometheus_gauge_value(Some(u64::MAX - 1)),
623 Some(u64::MAX - 1)
624 );
625 assert_eq!(prometheus_gauge_value(None), None);
626 }
627
628 #[cfg(feature = "allocator-mimalloc")]
629 #[test]
630 fn mimalloc_comparison_stats_keep_process_level_fields() {
631 let comparable = AllocatorComparisonStats::from(&MimallocStats {
632 version: 1,
633 reserved_current: 8192,
634 reserved_peak: 12288,
635 committed_current: 4096,
636 committed_peak: 6144,
637 reset_current: 0,
638 purged_current: 0,
639 process_rss_current: 3072,
640 process_rss_peak: 6144,
641 process_commit_current: 4096,
642 process_commit_peak: 8192,
643 page_faults: 0,
644 arenas: 2,
645 });
646
647 assert_eq!(comparable.allocated_bytes, None);
648 assert_eq!(comparable.resident_bytes, Some(3072));
649 assert_eq!(comparable.mapped_bytes, Some(8192));
650 assert_eq!(comparable.committed_bytes, Some(4096));
651 assert_eq!(comparable.allocator_structures, Some(2));
652 }
653
654 #[cfg(feature = "allocator-mimalloc")]
655 #[test]
656 fn mimalloc_process_values_do_not_preserve_wrapped_max() {
657 assert_eq!(sanitize_mimalloc_process_value(usize::MAX), 0);
658 assert_eq!(sanitize_mimalloc_process_value(4096), 4096);
659 }
660
661 #[cfg(feature = "allocator-mimalloc")]
662 #[test]
663 fn mimalloc_comparison_stats_use_sanitized_process_rss() {
664 let comparable = AllocatorComparisonStats::from(&MimallocStats {
665 version: 1,
666 reserved_current: 8192,
667 reserved_peak: 12288,
668 committed_current: 4096,
669 committed_peak: 6144,
670 reset_current: 0,
671 purged_current: 0,
672 process_rss_current: 0,
673 process_rss_peak: 6144,
674 process_commit_current: 4096,
675 process_commit_peak: 8192,
676 page_faults: 0,
677 arenas: 2,
678 });
679
680 assert_eq!(comparable.resident_bytes, Some(0));
681 assert_eq!(comparable.allocated_bytes, None);
682 assert_eq!(comparable.committed_bytes, Some(4096));
683 }
684}