mcap/modules/Default_Allocator/os_posix.jai
Musa Mahmood 22cad98500 [0.1.15] Startup: a catalog cache, the catalog on the background threads, rpmalloc, and [startup] timings
The catalog is kept in .catalog-cache in the library, each entry with the size and modified time of
the file it was read from; a start takes the entries whose file is unchanged from it and reads only
the rest. It is saved once the catalog is complete and has changed (at most every 30 s) and at
shutdown. After a reboot the catalog was complete 145 ms after launch, against 3.5 s reading every
file.

The catalog loads on the background Thread_Group, every worker of it, instead of the four preview
workers. rpmalloc (modules/Default_Allocator, 2.0.1-jai.16) takes the place of Jai's single locked
heap: entries from the cache 380 -> 50 ms, and reading the files now scales with threads.
thread_idle is called where workers idle. check_targets.jai checks other OSes with Jai's own
allocator, as rpmalloc runs at compile time with the target's OS layer.

-cold drops the library from the system's file cache first; -no-catalog-cache and -catalog-threads N
are for measuring. [startup] log lines time the listing, the first frame, the cache, the first
preview and the complete catalog.
2026-10-13 07:26:42 +01:00

409 lines
18 KiB
Plaintext

// OS entry points for macOS and Linux (2.0.1: rpmalloc.c:878-1161, the POSIX branches), plus
// the POSIX part of rpmalloc_initialize (2.0.1: rpmalloc.c:2776-2782).
//
// Everything goes through libc on every CPU. The 1.4.4 Jai port's raw syscalls caused review
// findings H4, H7 and M1; none of that code exists here.
//
// #jai deliberate differences from upstream:
// - A failed map returns null without asserting (finding N1).
// - macOS decommits with MADV_FREE_REUSABLE, retried on EAGAIN, and commits with
// MADV_FREE_REUSE. Upstream prefers MADV_DONTNEED, which exists on macOS but does not reduce
// the process footprint there, so decommit is a no-op on macOS (upstream bug U21).
// - Every entry point works before initialize(), like os_windows.jai.
// Fills os_map_granularity and os_page_size. Idempotent.
// 2.0.1: rpmalloc.c:2776-2782 (the POSIX part of rpmalloc_initialize)
os_initialize :: () {
if os_page_size return;
os_map_granularity = cast(u64) sysconf_p(_SC_PAGESIZE);
os_page_size = os_map_granularity;
}
os_set_page_name :: (address: *void, size: u64) { // 2.0.1: rpmalloc.c:885
#if OS == .LINUX {
name := ifx os_huge_pages then global_config.huge_page_name else global_config.page_name;
if address == MAP_FAILED_P || !name return;
// prctl wants a C string; names are short, copy into a terminated buffer.
buffer: [80] u8;
n := min(name.count, buffer.count - 1);
memcpy(buffer.data, name.data, n);
buffer[n] = 0;
// If the kernel lacks CONFIG_ANON_VMA_NAME, or the name is invalid, this does nothing.
prctl_p(PR_SET_VMA, PR_SET_VMA_ANON_NAME, cast(u64) address, size, cast(u64) buffer.data);
}
}
// #jai fix E9: a mapping that is committed as well. On POSIX a fresh anonymous mapping is usable as it
// is, so this only counts the commit (no madvise); see os_windows.jai for why huge blocks use it.
os_mmap_committed :: (size: u64, alignment: u64, offset: *u64, mapped_size: *u64) -> *void {
ptr := os_mmap(size, alignment, offset, mapped_size);
#if ENABLE_DECOMMIT if ptr statistics_os_commit(size);
return ptr;
}
os_mmap :: (size: u64, alignment: u64, offset: *u64, mapped_size: *u64) -> *void { // 2.0.1: rpmalloc.c:900
os_initialize();
#if TEST_HOOKS if test_take_fault(*test_fail_maps) return os_map_failed(size, alignment, offset, mapped_size);
map_size := size + alignment; // UPSTREAM-BUG U1: wraps for sizes near U64_MAX.
flags: s32 = MAP_PRIVATE_P | MAP_ANONYMOUS_P | MAP_UNINITIALIZED_P;
#if OS == .MACOS {
fd := VM_MAKE_TAG_240;
if os_huge_pages fd |= VM_FLAGS_SUPERPAGE_SIZE_2MB;
ptr := mmap_p(null, map_size, PROT_READ_WRITE_P, flags, fd, 0);
if (ptr == MAP_FAILED_P || !ptr) && os_huge_pages {
// Superpage allocations can fail through physical memory fragmentation; fall back
// to regular pages.
ptr = mmap_p(null, map_size, PROT_READ_WRITE_P, flags, VM_MAKE_TAG_240, 0);
}
} else {
huge_flags: s32 = 0;
if os_huge_pages {
huge_flags = MAP_HUGETLB_P;
// Explicitly request the detected huge page size; the system default huge page size
// can be a different (unusable) size like 1 GiB.
if os_huge_page_shift huge_flags |= cast(s32)(os_huge_page_shift << MAP_HUGE_SHIFT_P);
}
ptr := mmap_p(null, map_size, PROT_READ_WRITE_P, huge_flags | flags, -1, 0);
if (ptr == MAP_FAILED_P || !ptr) && os_huge_pages {
// Huge page allocations can fail: fall back to normal pages and advise transparent
// huge pages (best effort; for example EINVAL when THP is disabled system-wide).
ptr = mmap_p(null, map_size, PROT_READ_WRITE_P, flags, -1, 0);
if ptr && ptr != MAP_FAILED_P madvise_p(ptr, map_size, MADV_HUGEPAGE_P);
} else if os_thp && ptr && ptr != MAP_FAILED_P {
// Transparent huge page mode: ask for huge pages without a preallocated pool.
madvise_p(ptr, map_size, MADV_HUGEPAGE_P);
}
os_set_page_name(ptr, map_size);
}
if ptr == MAP_FAILED_P ptr = null;
if !ptr return os_map_failed(size, alignment, offset, mapped_size);
if alignment {
padding := cast(u64) ptr & (alignment - 1);
if padding padding = alignment - padding;
rpmalloc_assert(padding <= alignment, "Internal failure in padding");
rpmalloc_assert(!(padding % 8), "Internal failure in padding");
// Unmap the alignment padding head and tail, so no unusable address space (or huge page
// reservation) is kept. Both the mmap result and the alignment are multiples of the
// mapping page size, so head and tail stay page aligned.
if padding munmap_p(ptr, padding);
ptr = pointer_offset(ptr, padding);
if alignment - padding munmap_p(pointer_offset(ptr, size), alignment - padding);
offset.* = 0;
map_size = size;
}
mapped_size.* = map_size;
statistics_os_map(map_size); // 2.0.1: rpmalloc.c:1023-1033
return ptr;
}
// The failure tail of os_mmap (2.0.1: rpmalloc.c:990-998).
// #jai returns null without asserting when there is no map_fail_callback (finding N1).
os_map_failed :: (size: u64, alignment: u64, offset: *u64, mapped_size: *u64) -> *void {
iface := global_memory_interface;
if iface && iface.map_fail_callback && iface.map_fail_callback(size + alignment)
return os_mmap(size, alignment, offset, mapped_size);
return null;
}
os_commit_on_demand :: inline () -> bool { // 2.0.1: rpmalloc.c:1040
#if ENABLE_DECOMMIT return !global_config.disable_decommit;
else return false;
}
os_mcommit :: (address: *void, size: u64) -> s32 { // 2.0.1: rpmalloc.c:1049
#if ENABLE_DECOMMIT {
#if TEST_HOOKS if test_take_fault(*test_fail_commits) return 1;
// POSIX commits on first touch, so upstream does nothing here (its mprotect is commented
// out). #jai on macOS, tell the kernel the pages given back with MADV_FREE_REUSABLE are
// in use again, so the footprint accounting stays right (the pairing Apple documents).
#if OS == .MACOS madvise_p(address, size, MADV_FREE_REUSE_P);
statistics_os_commit(size); // 2.0.1: rpmalloc.c:1067-1078
}
return 0;
}
os_mdecommit :: (address: *void, size: u64) -> s32 { // 2.0.1: rpmalloc.c:1086
#if ENABLE_DECOMMIT {
// Unlike os_mcommit, honor the live flag (the Linux fallback below can set it mid-run).
if global_config.disable_decommit return 1;
#if OS == .MACOS {
// #jai MADV_FREE_REUSABLE, not MADV_DONTNEED (upstream bug U21).
ret := madvise_p(address, size, MADV_FREE_REUSABLE_P);
while ret == -1 && errno_p().* == EAGAIN_P {
errno_p().* = 0;
ret = madvise_p(address, size, MADV_FREE_REUSABLE_P);
}
failed := ret == -1;
} else {
failed := madvise_p(address, size, MADV_DONTNEED_P) != 0;
if failed && os_huge_pages {
// MADV_DONTNEED on huge page mappings needs Linux 5.18 or later: keep pages
// committed and stop further decommit attempts.
global_config.disable_decommit = true;
return 1;
}
}
if failed {
rpmalloc_assert(false, "Failed to decommit virtual memory block");
return 1;
}
statistics_os_decommit(size); // 2.0.1: rpmalloc.c:1129-1133
}
return 0;
}
os_munmap :: (address: *void, offset: u64, mapped_size: u64) { // 2.0.1: rpmalloc.c:1142
address = pointer_offset(address, -cast(s64) offset);
#if ENABLE_UNMAP {
if munmap_p(address, mapped_size)
rpmalloc_assert(false, "Failed to unmap virtual memory block");
statistics_os_unmap(mapped_size); // 2.0.1: rpmalloc.c:1154-1158
}
}
os_thread_yield :: () { // 2.0.1: rpmalloc.c:396 (sched_yield)
sched_yield_p();
}
// Coarse monotonic time for the huge block cache.
os_monotonic_time_ms :: () -> u64 { // 2.0.1: rpmalloc.c:1586
ts: timespec_p;
clock_gettime_p(CLOCK_MONOTONIC_FOR_CACHE, *ts);
return cast(u64) ts.tv_sec * 1000 + cast(u64) ts.tv_nsec / 1_000_000;
}
//
// Thread-local heap storage, thread identity and the thread exit hook.
//
// #jai upstream keeps the thread heap in a C11 _Thread_local variable and uses a pthread key only
// for the thread exit destructor. This Jai version has no #thread_local, and keeping the heap in
// the Context would let copied contexts share a heap between threads (review H3), so one pthread
// key does both: it stores the heap pointer and runs the destructor at thread exit. The destructor
// matters more here than upstream: Jai's Thread module never sends THREAD_STOP on POSIX (finding
// N4), so it is what returns a finished Jai thread's heap.
os_tls_create :: (destructor: (value: *void) #c_call) -> bool {
return pthread_key_create_p(*tls_key, destructor) == 0; // 2.0.1: rpmalloc.c:2954
}
os_tls_destroy :: () {
pthread_key_delete_p(tls_key); // 2.0.1: rpmalloc.c:3102
tls_key = 0;
}
// #jai fix E10: the calling thread's stack, [low, high); 0, 0 if it cannot be found out (the Context
// cache of the thread heap is then never used).
os_thread_stack_range :: () -> low: u64, high: u64 {
self := pthread_self_p();
#if OS == .MACOS {
high := cast(u64) pthread_get_stackaddr_np_p(self); // The top: the stack grows down.
size := pthread_get_stacksize_np_p(self);
if !high || size > high return 0, 0;
return high - size, high;
} else {
attr: [8] u64; // pthread_attr_t: 56 bytes on x86-64, 64 on ARM64 glibc.
if pthread_getattr_np_p(self, attr.data) != 0 return 0, 0;
address: *void;
size: u64;
ok := pthread_attr_getstack_p(attr.data, *address, *size) == 0;
pthread_attr_destroy_p(attr.data);
if !ok return 0, 0;
return cast(u64) address, cast(u64) address + size;
}
}
os_tls_get :: inline () -> *void {
return pthread_getspecific_p(tls_key);
}
os_tls_set :: (value: *void) {
pthread_setspecific_p(tls_key, value); // 2.0.1: rpmalloc.c:805
}
// #jai upstream reads the thread pointer (__builtin_thread_pointer or fs/gs/tpidr registers,
// rpmalloc.c:763-790). pthread_self is equally unique among live threads and never 0.
// See os_windows.jai (#jai fix U22). POSIX exit() does not run other threads' key destructors, and
// the exiting thread's own run only from pthread_exit, so there is nothing to skip here.
os_process_exiting :: inline () -> bool { return false; }
os_thread_id :: inline () -> u64 {
return cast(u64) pthread_self_p();
}
// Huge page detection (2.0.1: rpmalloc.c:2831-2892, the Linux and Apple branches).
os_detect_huge_pages :: () {
#if OS == .LINUX {
huge_page_size: u64 = 0;
meminfo: [4096] u8;
n := os_read_system_file("/proc/meminfo", meminfo.data, meminfo.count);
if n {
text := string.{n, meminfo.data};
at := find_substring(text, "Hugepagesize:");
if at >= 0 huge_page_size = parse_decimal(advance_string(text, at + 13)) * 1024;
}
huge_page_size = usable_huge_page_size(huge_page_size);
if huge_page_size {
// Hugepagesize only reports the configured size, not whether any huge pages are
// available. Probe by mapping one huge page with the flags real mappings use.
huge_shift: u64 = 0;
while (cast(u64) 1 << huge_shift) < huge_page_size huge_shift += 1;
probe_flags := MAP_PRIVATE_P | MAP_ANONYMOUS_P | MAP_HUGETLB_P | cast(s32)(huge_shift << MAP_HUGE_SHIFT_P);
probe := mmap_p(null, huge_page_size, PROT_READ_WRITE_P, probe_flags, -1, 0);
if probe != MAP_FAILED_P && probe {
munmap_p(probe, huge_page_size);
os_huge_pages = true;
os_huge_page_shift = huge_shift;
os_page_size = huge_page_size;
os_map_granularity = huge_page_size;
}
}
} else #if OS == .MACOS && CPU == .X64 {
// Superpages are only supported on x86-64; Apple silicon has no huge page support.
os_huge_pages = true;
os_page_size = 2 * 1024 * 1024;
os_map_granularity = os_page_size;
}
}
// Transparent huge pages (2.0.1: rpmalloc.c:2921-2936, Linux only).
os_configure_thp :: () {
#if OS == .LINUX {
if global_config.enable_thp && !os_huge_pages && !global_config.disable_thp {
// With THP disabled system-wide ("never") madvise(MADV_HUGEPAGE) does nothing, so
// report THP as disabled rather than claim it is in use.
state: [64] u8;
n := os_read_system_file("/sys/kernel/mm/transparent_hugepage/enabled", state.data, state.count);
thp_never := n && find_substring(string.{n, state.data}, "[never]") >= 0;
if !thp_never os_thp = true;
}
if global_config.disable_thp prctl_p(PR_SET_THP_DISABLE_P, 1, 0, 0, 0);
}
}
#if OS == .LINUX {
// Read a small (pseudo) file such as /proc/meminfo into a caller buffer with raw open/read, so
// nothing allocates during initialisation. Returns the bytes read (the buffer is always zero
// terminated), or 0.
os_read_system_file :: (path: string, buffer: *u8, capacity: s64) -> s64 { // 2.0.1: rpmalloc.c:2684
if capacity < 2 return 0;
fd := open_p(path.data, O_RDONLY_P | O_CLOEXEC_P); // path is a zero-terminated literal
if fd < 0 return 0;
total: s64 = 0;
while total + 1 < capacity {
got := read_p(fd, buffer + total, cast(u64)((capacity - 1) - total));
if got <= 0 break;
total += got;
}
close_p(fd);
buffer[total] = 0;
return total;
}
find_substring :: (text: string, pattern: string) -> s64 {
if pattern.count > text.count return -1;
for i: 0..text.count - pattern.count {
match := true;
for j: 0..pattern.count-1 if text[i + j] != pattern[j] { match = false; break; }
if match return i;
}
return -1;
}
advance_string :: (text: string, n: s64) -> string {
if n >= text.count return "";
return string.{text.count - n, text.data + n};
}
// strtol(..., 10) for a non-negative number: skips leading blanks, stops at the first non-digit.
parse_decimal :: (text: string) -> u64 {
i := 0;
while i < text.count && (text[i] == #char " " || text[i] == #char "\t") i += 1;
value: u64 = 0;
while i < text.count && text[i] >= #char "0" && text[i] <= #char "9" {
value = value * 10 + (text[i] - #char "0");
i += 1;
}
return value;
}
}
#scope_file
libc_rpm :: #library,system "libc";
mmap_p :: (address: *void, length: u64, prot: s32, flags: s32, fd: s32, offset: s64) -> *void #foreign libc_rpm "mmap";
munmap_p :: (address: *void, length: u64) -> s32 #foreign libc_rpm "munmap";
madvise_p :: (address: *void, length: u64, advice: s32) -> s32 #foreign libc_rpm "madvise";
sysconf_p :: (name: s32) -> s64 #foreign libc_rpm "sysconf";
sched_yield_p :: () -> s32 #foreign libc_rpm "sched_yield";
timespec_p :: struct {
tv_sec: s64;
tv_nsec: s64;
}
clock_gettime_p :: (clock_id: s32, ts: *timespec_p) -> s32 #foreign libc_rpm "clock_gettime";
// 6 is CLOCK_MONOTONIC_COARSE on Linux (read through the vDSO, as upstream prefers) and
// CLOCK_MONOTONIC on macOS, which has no coarse clock.
CLOCK_MONOTONIC_FOR_CACHE :: 6;
MAP_FAILED_P :: cast(*void) -1;
PROT_READ_WRITE_P :: 0x1 | 0x2;
MAP_PRIVATE_P :: 0x0002;
#if OS == .MACOS {
errno_p :: () -> *s32 #foreign libc_rpm "__error";
MAP_ANONYMOUS_P :: 0x1000;
MAP_UNINITIALIZED_P :: 0;
_SC_PAGESIZE :: 29;
EAGAIN_P :: 35;
MADV_FREE_REUSABLE_P :: 7;
MADV_FREE_REUSE_P :: 8;
VM_MAKE_TAG_240 :: cast(s32)(240 << 24); // VM_MAKE_TAG(240U)
VM_FLAGS_SUPERPAGE_SIZE_2MB :: 2 << 16; // VM_FLAGS_SUPERPAGE_SIZE_2MB (x86-64 macOS)
} else {
prctl_p :: (option: s32, arg2: u64, arg3: u64, arg4: u64, arg5: u64) -> s32 #foreign libc_rpm "prctl";
MAP_ANONYMOUS_P :: 0x20;
MAP_UNINITIALIZED_P :: 0x400_0000; // Only honoured on no-MMU kernels.
MAP_HUGETLB_P :: 0x4_0000;
MAP_HUGE_SHIFT_P :: 26;
MADV_DONTNEED_P :: 4;
MADV_HUGEPAGE_P :: 14;
_SC_PAGESIZE :: 30;
PR_SET_VMA :: 0x5356_4d41;
PR_SET_VMA_ANON_NAME :: 0;
}
// Thread-local storage and huge page detection.
libc_tls :: #library,system "libc";
#if OS == .MACOS pthread_key_t_p :: u64; // unsigned long
else pthread_key_t_p :: u32; // unsigned int
tls_key: pthread_key_t_p;
pthread_key_create_p :: (key: *pthread_key_t_p, destructor: (value: *void) #c_call) -> s32 #foreign libc_tls "pthread_key_create";
pthread_key_delete_p :: (key: pthread_key_t_p) -> s32 #foreign libc_tls "pthread_key_delete";
pthread_getspecific_p :: (key: pthread_key_t_p) -> *void #foreign libc_tls "pthread_getspecific";
pthread_setspecific_p :: (key: pthread_key_t_p, value: *void) -> s32 #foreign libc_tls "pthread_setspecific";
pthread_self_p :: () -> *void #foreign libc_tls "pthread_self";
#if OS == .MACOS {
pthread_get_stackaddr_np_p :: (thread: *void) -> *void #foreign libc_tls "pthread_get_stackaddr_np";
pthread_get_stacksize_np_p :: (thread: *void) -> u64 #foreign libc_tls "pthread_get_stacksize_np";
} else {
pthread_getattr_np_p :: (thread: *void, attr: *u64) -> s32 #foreign libc_tls "pthread_getattr_np";
pthread_attr_getstack_p :: (attr: *u64, address: **void, size: *u64) -> s32 #foreign libc_tls "pthread_attr_getstack";
pthread_attr_destroy_p :: (attr: *u64) -> s32 #foreign libc_tls "pthread_attr_destroy";
}
#if OS == .LINUX {
open_p :: (path: *u8, flags: s32) -> s32 #foreign libc_tls "open";
read_p :: (fd: s32, buffer: *u8, count: u64) -> s64 #foreign libc_tls "read";
close_p :: (fd: s32) -> s32 #foreign libc_tls "close";
O_RDONLY_P :: 0;
O_CLOEXEC_P :: 0x8_0000;
PR_SET_THP_DISABLE_P :: 41;
}