1#[cfg(feature = "metrics")]
2use {
3 log::{Level, log_enabled, warn},
4 solana_metrics::{datapoint_debug, datapoint_info},
5 std::mem::{self, MaybeUninit},
6};
7use {
8 std::{ffi::CString, fmt, os::raw::c_uint, ptr},
9 thiserror::Error,
10};
11
12#[derive(Debug, Error)]
13pub enum Error {
14 #[error("jemalloc mallctl {name} failed: {error}")]
15 Mallctl {
16 name: String,
17 error: jemalloc_ctl::Error,
18 },
19
20 #[error("jemalloc mallctl {name} failed with errno code {code}")]
21 MallctlCommand { name: String, code: i32 },
22
23 #[error("jemalloc decay duration {millis} ms does not fit ssize_t")]
24 DecayMillisOutOfRange { millis: u64 },
25
26 #[error("jemalloc returned invalid decay duration {millis} ms")]
27 InvalidDecayMillis { millis: isize },
28
29 #[error("jemalloc page byte count overflows usize: {pages} * {page_size}")]
30 PageByteCountOverflow { pages: usize, page_size: usize },
31}
32
33type JemallocBool = u8;
35const JEMALLOC_FALSE: JemallocBool = 0;
36#[cfg(feature = "metrics")]
39const MALLCTL_ENOENT: i32 = 2;
40
41#[derive(Debug, Clone, Copy, Default)]
42pub struct Jemalloc;
43
44impl Jemalloc {
45 pub(crate) fn create_arena() -> Result<Arena, Error> {
46 unsafe { read_raw::<c_uint>("arenas.create") }.map(|id| Arena {
48 id: ArenaId::from_raw(id),
49 })
50 }
51
52 pub fn current_thread_arena() -> Result<ArenaId, Error> {
53 unsafe { read_raw::<c_uint>("thread.arena") }.map(ArenaId::from_raw)
55 }
56
57 pub fn disable_current_thread_tcache() -> Result<(), Error> {
58 unsafe { write_raw::<JemallocBool>("thread.tcache.enabled", JEMALLOC_FALSE) }
60 }
61
62 fn bind_current_thread_permanently(arena: ArenaId) -> Result<ArenaId, Error> {
63 unsafe { update_raw::<c_uint>("thread.arena", arena.as_raw()) }.map(ArenaId::from_raw)
65 }
66
67 pub fn advance_epoch() -> Result<(), Error> {
68 jemalloc_ctl::epoch::advance()
69 .map(|_| ())
70 .map_err(|error| Error::Mallctl {
71 name: "epoch".to_string(),
72 error,
73 })
74 }
75
76 fn page_size() -> Result<usize, Error> {
77 unsafe { read_raw::<usize>("arenas.page") }
79 }
80}
81
82#[repr(transparent)]
83#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
84pub struct ArenaId(c_uint);
85
86impl ArenaId {
87 pub const fn from_raw(raw: c_uint) -> Self {
88 Self(raw)
89 }
90
91 pub const fn as_raw(self) -> c_uint {
92 self.0
93 }
94}
95
96impl fmt::Display for ArenaId {
97 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
98 self.0.fmt(formatter)
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub struct Arena {
104 id: ArenaId,
105}
106
107#[derive(Debug, Clone, Copy)]
108enum ArenaCommand {
109 Purge,
110}
111
112impl ArenaCommand {
113 const fn name(self) -> &'static str {
114 match self {
115 Self::Purge => "purge",
116 }
117 }
118}
119
120impl Arena {
121 pub const fn id(self) -> ArenaId {
122 self.id
123 }
124
125 pub fn bind_current_thread_permanently(self) -> Result<ArenaId, Error> {
126 Jemalloc::bind_current_thread_permanently(self.id)
127 }
128
129 pub(crate) fn set_dirty_decay(self, decay: Decay) -> Result<(), Error> {
130 unsafe { write_raw::<isize>(&self.mallctl_name("dirty_decay_ms"), decay.to_mallctl()?) }
132 }
133
134 pub(crate) fn set_muzzy_decay(self, decay: Decay) -> Result<(), Error> {
135 unsafe { write_raw::<isize>(&self.mallctl_name("muzzy_decay_ms"), decay.to_mallctl()?) }
137 }
138
139 pub(crate) fn set_oversize_threshold(self, bytes: usize) -> Result<(), Error> {
140 unsafe { write_raw::<usize>(&self.mallctl_name("oversize_threshold"), bytes) }
142 }
143
144 pub(crate) fn set_retain_grow_limit(self, bytes: usize) -> Result<(), Error> {
145 unsafe { write_raw::<usize>(&self.mallctl_name("retain_grow_limit"), bytes) }
147 }
148
149 pub fn purge(self) -> Result<(), Error> {
150 self.run_command(ArenaCommand::Purge)
151 }
152
153 pub fn stats(self) -> Result<ArenaStats, Error> {
154 arena_stats(self.id)
155 }
156
157 #[cfg(feature = "metrics")]
158 fn extent_stats(self) -> Result<Vec<ArenaExtentStats>, Error> {
159 arena_extent_stats(self.id)
160 }
161
162 #[cfg(feature = "metrics")]
163 pub fn report_stats(self, metric_name: &'static str, stats: &ArenaStats) {
164 datapoint_info!(
165 metric_name,
166 "kind" => "arena",
167 "arena_id" => self.id.to_string(),
168 ("mapped_bytes", stats.mapped as i64, i64),
169 ("retained_bytes", stats.retained as i64, i64),
170 ("active_bytes", stats.active as i64, i64),
171 ("dirty_bytes", stats.dirty as i64, i64),
172 ("muzzy_bytes", stats.muzzy as i64, i64),
173 ("active_pages", stats.active_pages as i64, i64),
174 ("dirty_pages", stats.dirty_pages as i64, i64),
175 ("muzzy_pages", stats.muzzy_pages as i64, i64),
176 ("dirty_decay_ms", decay_as_millis(stats.dirty_decay), i64),
177 ("muzzy_decay_ms", decay_as_millis(stats.muzzy_decay), i64),
178 ("dirty_purge_sweeps", stats.dirty_purges.sweeps as i64, i64),
179 (
180 "dirty_purge_madvise",
181 stats.dirty_purges.madvise as i64,
182 i64
183 ),
184 (
185 "dirty_purged_pages",
186 stats.dirty_purges.purged_pages as i64,
187 i64
188 ),
189 ("muzzy_purge_sweeps", stats.muzzy_purges.sweeps as i64, i64),
190 (
191 "muzzy_purge_madvise",
192 stats.muzzy_purges.madvise as i64,
193 i64
194 ),
195 (
196 "muzzy_purged_pages",
197 stats.muzzy_purges.purged_pages as i64,
198 i64
199 ),
200 );
201
202 if !log_enabled!(Level::Debug) {
203 return;
204 }
205
206 let extent_stats = match self.extent_stats() {
207 Ok(stats) => stats,
208 Err(error) => {
209 warn!(
210 "failed to read jemalloc arena extent stats; arena_id: {}; error: {error}",
211 self.id
212 );
213 return;
214 }
215 };
216
217 for stats in extent_stats
218 .into_iter()
219 .filter(|stats| stats.total_bytes() > 0)
220 {
221 datapoint_debug!(
222 metric_name,
223 "kind" => "extent",
224 "arena_id" => self.id.to_string(),
225 "extent_index" => stats.extent_index.to_string(),
226 ("ndirty", stats.ndirty as i64, i64),
227 ("nmuzzy", stats.nmuzzy as i64, i64),
228 ("nretained", stats.nretained as i64, i64),
229 ("dirty_bytes", stats.dirty_bytes as i64, i64),
230 ("muzzy_bytes", stats.muzzy_bytes as i64, i64),
231 ("retained_bytes", stats.retained_bytes as i64, i64),
232 ("total_extents", stats.total_extents() as i64, i64),
233 ("total_bytes", stats.total_bytes() as i64, i64),
234 );
235 }
236 }
237
238 fn mallctl_name(self, name: &str) -> String {
239 format!("arena.{}.{}", self.id.as_raw(), name)
240 }
241
242 fn run_command(self, command: ArenaCommand) -> Result<(), Error> {
243 let name = self.mallctl_name(command.name());
244 let mallctl_name = mallctl_name(&name);
245 let code = unsafe {
248 jemalloc_sys::mallctl(
249 mallctl_name.as_ptr(),
250 ptr::null_mut(),
251 ptr::null_mut(),
252 ptr::null_mut(),
253 0,
254 )
255 };
256
257 if code == 0 {
258 Ok(())
259 } else {
260 Err(Error::MallctlCommand { name, code })
261 }
262 }
263}
264
265#[cfg(feature = "metrics")]
266fn decay_as_millis(decay: Decay) -> i64 {
267 match decay {
268 Decay::Never => -1,
269 Decay::Immediate => 0,
270 Decay::Millis(millis) => i64::try_from(millis).unwrap_or(i64::MAX),
271 }
272}
273
274#[derive(Debug, Clone, Copy, PartialEq, Eq)]
275pub enum Decay {
276 Never,
277 Immediate,
278 Millis(u64),
279}
280
281impl Decay {
282 fn to_mallctl(self) -> Result<isize, Error> {
283 match self {
284 Self::Never => Ok(-1),
285 Self::Immediate => Ok(0),
286 Self::Millis(millis) => {
287 isize::try_from(millis).map_err(|_| Error::DecayMillisOutOfRange { millis })
288 }
289 }
290 }
291
292 fn from_mallctl(millis: isize) -> Result<Self, Error> {
293 match millis {
294 -1 => Ok(Self::Never),
295 0 => Ok(Self::Immediate),
296 millis if millis > 0 => u64::try_from(millis)
297 .map(Self::Millis)
298 .map_err(|_| Error::InvalidDecayMillis { millis }),
299 millis => Err(Error::InvalidDecayMillis { millis }),
300 }
301 }
302}
303
304impl fmt::Display for Decay {
305 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
306 match self {
307 Self::Never => formatter.write_str("never"),
308 Self::Immediate => formatter.write_str("immediate"),
309 Self::Millis(millis) => write!(formatter, "{millis} ms"),
310 }
311 }
312}
313
314#[derive(Debug, Clone, Copy, PartialEq, Eq)]
315pub struct ArenaStats {
316 pub mapped: usize,
317 pub retained: usize,
318 pub active: usize,
319 pub dirty: usize,
320 pub muzzy: usize,
321 pub active_pages: usize,
322 pub dirty_pages: usize,
323 pub muzzy_pages: usize,
324 pub dirty_decay: Decay,
325 pub muzzy_decay: Decay,
326 pub dirty_purges: PurgeStats,
327 pub muzzy_purges: PurgeStats,
328}
329
330#[cfg(feature = "metrics")]
331#[derive(Debug, Clone, Copy, PartialEq, Eq)]
332struct ArenaExtentStats {
333 extent_index: usize,
334 ndirty: usize,
335 nmuzzy: usize,
336 nretained: usize,
337 dirty_bytes: usize,
338 muzzy_bytes: usize,
339 retained_bytes: usize,
340}
341
342#[cfg(feature = "metrics")]
343impl ArenaExtentStats {
344 pub const fn total_extents(self) -> usize {
345 self.ndirty
346 .saturating_add(self.nmuzzy)
347 .saturating_add(self.nretained)
348 }
349
350 pub const fn total_bytes(self) -> usize {
351 self.dirty_bytes
352 .saturating_add(self.muzzy_bytes)
353 .saturating_add(self.retained_bytes)
354 }
355}
356
357#[derive(Debug, Clone, Copy, PartialEq, Eq)]
358pub struct PurgeStats {
359 pub sweeps: u64,
360 pub madvise: u64,
361 pub purged_pages: u64,
362}
363
364fn arena_stats(arena_id: ArenaId) -> Result<ArenaStats, Error> {
365 let page_size = Jemalloc::page_size()?;
366 unsafe {
369 let active_pages = read_arena_stat::<usize>(arena_id, "pactive")?;
370 let dirty_pages = read_arena_stat::<usize>(arena_id, "pdirty")?;
371 let muzzy_pages = read_arena_stat::<usize>(arena_id, "pmuzzy")?;
372
373 Ok(ArenaStats {
374 mapped: read_arena_stat::<usize>(arena_id, "mapped")?,
375 retained: read_arena_stat::<usize>(arena_id, "retained")?,
376 active: pages_to_bytes(active_pages, page_size)?,
377 dirty: pages_to_bytes(dirty_pages, page_size)?,
378 muzzy: pages_to_bytes(muzzy_pages, page_size)?,
379 active_pages,
380 dirty_pages,
381 muzzy_pages,
382 dirty_decay: Decay::from_mallctl(read_raw::<isize>(&format!(
383 "arena.{}.dirty_decay_ms",
384 arena_id.as_raw()
385 ))?)?,
386 muzzy_decay: Decay::from_mallctl(read_raw::<isize>(&format!(
387 "arena.{}.muzzy_decay_ms",
388 arena_id.as_raw()
389 ))?)?,
390 dirty_purges: PurgeStats {
391 sweeps: read_arena_stat::<u64>(arena_id, "dirty_npurge")?,
392 madvise: read_arena_stat::<u64>(arena_id, "dirty_nmadvise")?,
393 purged_pages: read_arena_stat::<u64>(arena_id, "dirty_purged")?,
394 },
395 muzzy_purges: PurgeStats {
396 sweeps: read_arena_stat::<u64>(arena_id, "muzzy_npurge")?,
397 madvise: read_arena_stat::<u64>(arena_id, "muzzy_nmadvise")?,
398 purged_pages: read_arena_stat::<u64>(arena_id, "muzzy_purged")?,
399 },
400 })
401 }
402}
403
404#[cfg(feature = "metrics")]
405fn arena_extent_stats(arena_id: ArenaId) -> Result<Vec<ArenaExtentStats>, Error> {
406 let mut extent_stats = Vec::new();
407
408 for extent_index in 0.. {
409 let ndirty_name = format!("extents.{extent_index}.ndirty");
410 let Some(ndirty) = unsafe { read_arena_stat_optional::<usize>(arena_id, &ndirty_name) }?
412 else {
413 break;
414 };
415
416 unsafe {
418 extent_stats.push(ArenaExtentStats {
419 extent_index,
420 ndirty,
421 nmuzzy: read_arena_stat::<usize>(
422 arena_id,
423 &format!("extents.{extent_index}.nmuzzy"),
424 )?,
425 nretained: read_arena_stat::<usize>(
426 arena_id,
427 &format!("extents.{extent_index}.nretained"),
428 )?,
429 dirty_bytes: read_arena_stat::<usize>(
430 arena_id,
431 &format!("extents.{extent_index}.dirty_bytes"),
432 )?,
433 muzzy_bytes: read_arena_stat::<usize>(
434 arena_id,
435 &format!("extents.{extent_index}.muzzy_bytes"),
436 )?,
437 retained_bytes: read_arena_stat::<usize>(
438 arena_id,
439 &format!("extents.{extent_index}.retained_bytes"),
440 )?,
441 });
442 }
443 }
444
445 Ok(extent_stats)
446}
447
448fn pages_to_bytes(pages: usize, page_size: usize) -> Result<usize, Error> {
449 pages
450 .checked_mul(page_size)
451 .ok_or(Error::PageByteCountOverflow { pages, page_size })
452}
453
454unsafe fn read_arena_stat<T: Copy>(arena_id: ArenaId, stat_name: &str) -> Result<T, Error> {
459 unsafe { read_raw::<T>(&format!("stats.arenas.{}.{stat_name}", arena_id.as_raw())) }
461}
462
463#[cfg(feature = "metrics")]
464unsafe fn read_arena_stat_optional<T: Copy>(
469 arena_id: ArenaId,
470 stat_name: &str,
471) -> Result<Option<T>, Error> {
472 unsafe { read_raw_optional::<T>(&format!("stats.arenas.{}.{stat_name}", arena_id.as_raw())) }
474}
475
476unsafe fn read_raw<T: Copy>(name: &str) -> Result<T, Error> {
481 let mallctl_name = mallctl_name(name);
482
483 unsafe { jemalloc_ctl::raw::read::<T>(mallctl_name.as_bytes_with_nul()) }.map_err(|error| {
485 Error::Mallctl {
486 name: name.to_string(),
487 error,
488 }
489 })
490}
491
492#[cfg(feature = "metrics")]
493unsafe fn read_raw_optional<T: Copy>(name: &str) -> Result<Option<T>, Error> {
498 let mallctl_name = mallctl_name(name);
499 let mut value = MaybeUninit::<T>::uninit();
500 let mut len = mem::size_of::<T>();
501
502 let ret = unsafe {
505 jemalloc_sys::mallctl(
506 mallctl_name.as_ptr(),
507 value.as_mut_ptr().cast(),
508 &mut len,
509 ptr::null_mut(),
510 0,
511 )
512 };
513
514 match ret {
515 0 => {
516 assert_eq!(len, mem::size_of::<T>());
517 Ok(Some(unsafe { value.assume_init() }))
519 }
520 MALLCTL_ENOENT => Ok(None),
521 code => Err(Error::MallctlCommand {
522 name: name.to_string(),
523 code,
524 }),
525 }
526}
527
528unsafe fn write_raw<T: Copy>(name: &str, value: T) -> Result<(), Error> {
533 let mallctl_name = mallctl_name(name);
534
535 unsafe { jemalloc_ctl::raw::write::<T>(mallctl_name.as_bytes_with_nul(), value) }.map_err(
537 |error| Error::Mallctl {
538 name: name.to_string(),
539 error,
540 },
541 )
542}
543
544unsafe fn update_raw<T: Copy>(name: &str, value: T) -> Result<T, Error> {
550 let mallctl_name = mallctl_name(name);
551
552 unsafe { jemalloc_ctl::raw::update::<T>(mallctl_name.as_bytes_with_nul(), value) }.map_err(
554 |error| Error::Mallctl {
555 name: name.to_string(),
556 error,
557 },
558 )
559}
560
561fn mallctl_name(name: &str) -> CString {
562 CString::new(name).expect("mallctl name must not contain nul bytes")
563}
564
565#[cfg(test)]
566#[global_allocator]
567static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc;
568
569#[cfg(test)]
570mod tests {
571 use crate::jemalloc::ArenaId;
572
573 #[test]
574 fn preserves_raw_arena_id() {
575 let arena_id = ArenaId::from_raw(42);
576
577 assert_eq!(arena_id.as_raw(), 42);
578 assert_eq!(arena_id.to_string(), "42");
579 }
580}