Skip to main content

agave_jemalloc/
jemalloc.rs

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
33// Jemalloc uses a one-byte C99 `_Bool` on supported targets.
34type JemallocBool = u8;
35const JEMALLOC_FALSE: JemallocBool = 0;
36// jemalloc's mallctl APIs return POSIX errno values. ENOENT means the probed
37// indexed node is outside the mallctl namespace.
38#[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        // SAFETY: `arenas.create` returns an unsigned arena index.
47        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        // SAFETY: `thread.arena` reads an unsigned arena index.
54        unsafe { read_raw::<c_uint>("thread.arena") }.map(ArenaId::from_raw)
55    }
56
57    pub fn disable_current_thread_tcache() -> Result<(), Error> {
58        // SAFETY: `JemallocBool` matches jemalloc's C `bool` representation.
59        unsafe { write_raw::<JemallocBool>("thread.tcache.enabled", JEMALLOC_FALSE) }
60    }
61
62    fn bind_current_thread_permanently(arena: ArenaId) -> Result<ArenaId, Error> {
63        // SAFETY: `thread.arena` reads and writes an unsigned arena index.
64        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        // SAFETY: `arenas.page` reads a `size_t`.
78        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        // SAFETY: `arena.<i>.dirty_decay_ms` writes an `ssize_t`.
131        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        // SAFETY: `arena.<i>.muzzy_decay_ms` writes an `ssize_t`.
136        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        // SAFETY: `arena.<i>.oversize_threshold` writes a `size_t`.
141        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        // SAFETY: `arena.<i>.retain_grow_limit` writes a `size_t`.
146        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        // SAFETY: every command in the closed set takes no old or new value and cannot invalidate
246        // live allocations or thread state.
247        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    // SAFETY: the page counts, mapped bytes, and retained bytes are `size_t`; decay values are
367    // `ssize_t`; and purge counters are `uint64_t` in jemalloc's mallctl API.
368    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        // SAFETY: all `stats.arenas.<i>.extents.<j>` values read here are `size_t`.
411        let Some(ndirty) = unsafe { read_arena_stat_optional::<usize>(arena_id, &ndirty_name) }?
412        else {
413            break;
414        };
415
416        // SAFETY: all `stats.arenas.<i>.extents.<j>` values read here are `size_t`.
417        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
454/// # Safety
455///
456/// `stat_name` must contain no nul bytes, and `T` must be the exact type of the named jemalloc
457/// arena statistic.
458unsafe fn read_arena_stat<T: Copy>(arena_id: ArenaId, stat_name: &str) -> Result<T, Error> {
459    // SAFETY: the caller guarantees that `T` matches the named statistic.
460    unsafe { read_raw::<T>(&format!("stats.arenas.{}.{stat_name}", arena_id.as_raw())) }
461}
462
463#[cfg(feature = "metrics")]
464/// # Safety
465///
466/// `stat_name` must contain no nul bytes, and `T` must be the exact type of the named jemalloc
467/// arena statistic.
468unsafe fn read_arena_stat_optional<T: Copy>(
469    arena_id: ArenaId,
470    stat_name: &str,
471) -> Result<Option<T>, Error> {
472    // SAFETY: the caller guarantees that `T` matches the named statistic.
473    unsafe { read_raw_optional::<T>(&format!("stats.arenas.{}.{stat_name}", arena_id.as_raw())) }
474}
475
476/// # Safety
477///
478/// `name` must contain no nul bytes. `T` must have the size and alignment of the named readable
479/// jemalloc control, and every value the control can write must be valid for `T`.
480unsafe fn read_raw<T: Copy>(name: &str) -> Result<T, Error> {
481    let mallctl_name = mallctl_name(name);
482
483    // SAFETY: the caller guarantees that `T` satisfies the control's representation requirements.
484    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")]
493/// # Safety
494///
495/// `name` must contain no nul bytes. `T` must have the size and alignment of the named readable
496/// jemalloc control, and every value the control can write must be valid for `T`.
497unsafe 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    // SAFETY: the caller guarantees that `T` satisfies the control's representation requirements.
503    // `mallctl_name` is nul-terminated, and the old value buffer is valid for `len` bytes.
504    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            // SAFETY: jemalloc returned success and initialized exactly `size_of::<T>()` bytes.
518            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
528/// # Safety
529///
530/// `name` must contain no nul bytes. `T` must have the size and alignment of the named writable
531/// jemalloc control, and `value` must have a representation accepted by that control.
532unsafe fn write_raw<T: Copy>(name: &str, value: T) -> Result<(), Error> {
533    let mallctl_name = mallctl_name(name);
534
535    // SAFETY: the caller guarantees that `T` satisfies the control's representation requirements.
536    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
544/// # Safety
545///
546/// `name` must contain no nul bytes. `T` must have the size and alignment of the named control,
547/// every value the control can write must be valid for `T`, and `value` must have a representation
548/// accepted by that control.
549unsafe fn update_raw<T: Copy>(name: &str, value: T) -> Result<T, Error> {
550    let mallctl_name = mallctl_name(name);
551
552    // SAFETY: the caller guarantees that `T` satisfies the control's representation requirements.
553    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}