makepad/libs/relax/base/src/boot_stats.rs

159 lines
6 KiB
Rust

//! Counters the native boot window reads once per frame (the "Ignition Field"). The loader's
//! threads only ever do a relaxed atomic add or store here: no lock, no allocation (so nothing
//! of the compiler zone is kept in a static), and the window reads them at its own pace.
//!
//! An arm of the field is a crate: the first `ARMS - 1` crates that get a function installed each
//! open an arm of their own (in the order the program first needs them), the rest share the last
//! one.
use std::sync::atomic::{AtomicU32, AtomicU64, AtomicU8, Ordering::Relaxed};
pub const ARMS: usize = 16;
/// The arm every crate past the first `ARMS - 1` shares.
pub const REST: usize = ARMS - 1;
const CRATES: usize = 1024;
const NAME_WORDS: usize = 3;
pub const NAME_BYTES: usize = NAME_WORDS * 8;
const UNSET: u8 = 0xff;
/// O3 installs the window can still see when it falls behind (older ones are skipped).
pub const O3_RING: usize = 1024;
#[allow(clippy::declare_interior_mutable_const)]
const ZERO32: AtomicU32 = AtomicU32::new(0);
#[allow(clippy::declare_interior_mutable_const)]
const ZERO64: AtomicU64 = AtomicU64::new(0);
#[allow(clippy::declare_interior_mutable_const)]
const UNSET8: AtomicU8 = AtomicU8::new(UNSET);
#[allow(clippy::declare_interior_mutable_const)]
const NAME: [AtomicU64; NAME_WORDS] = [ZERO64; NAME_WORDS];
/// Functions installed per arm (compiled this run, or linked from the cache).
static FNS: [AtomicU32; ARMS] = [ZERO32; ARMS];
static ARM_OF_CRATE: [AtomicU8; CRATES] = [UNSET8; CRATES];
static ARMS_OPEN: AtomicU32 = AtomicU32::new(0);
static ARM_NAME: [[AtomicU64; NAME_WORDS]; ARMS] = [NAME; ARMS];
pub static FILES_PARSED: AtomicU32 = AtomicU32::new(0);
pub static LINES_PARSED: AtomicU64 = AtomicU64::new(0);
/// ms since process start (clock::ms_since_process_start) of main()'s first call; 0 before it
pub static MAIN_AT_MS: AtomicU64 = AtomicU64::new(0);
pub static O3_INSTALLED: AtomicU32 = AtomicU32::new(0);
pub static O3_SUPERSEDED: AtomicU32 = AtomicU32::new(0);
static O3_ARMS: [AtomicU8; O3_RING] = [UNSET8; O3_RING];
/// The function of each ring entry's install, to pick the star it ignites (hash of its slot).
static O3_KEYS: [AtomicU32; O3_RING] = [ZERO32; O3_RING];
/// The first settle (the compiles' state dropped, the heap trimmed): its end in ms since process
/// start + 1 (0: not yet), and the resident set (MB) around it.
pub static SETTLE_END_MS: AtomicU64 = AtomicU64::new(0);
pub static SETTLE_RSS_BEFORE_MB: AtomicU32 = AtomicU32::new(0);
pub static SETTLE_RSS_AFTER_MB: AtomicU32 = AtomicU32::new(0);
/// The arm of crate `krate`; `name` is asked only when the crate opens an arm.
pub fn arm_of(krate: u32, name: impl FnOnce() -> [u8; NAME_BYTES]) -> usize {
let Some(slot) = ARM_OF_CRATE.get(krate as usize) else {
return REST;
};
let arm = slot.load(Relaxed);
if arm != UNSET {
return arm as usize;
}
let next = ARMS_OPEN.fetch_add(1, Relaxed) as usize;
let arm = next.min(REST);
if arm < REST {
let bytes = name();
for (word, chunk) in ARM_NAME[arm].iter().zip(bytes.chunks(8)) {
word.store(u64::from_le_bytes(chunk.try_into().unwrap()), Relaxed);
}
}
// two threads can race for a crate's first function: the later store wins, the other
// thread's arm number stays unused
slot.store(arm as u8, Relaxed);
arm
}
/// A crate name cut to the name field (zero padded).
pub fn name_bytes(name: &str) -> [u8; NAME_BYTES] {
let mut out = [0; NAME_BYTES];
let n = name.len().min(NAME_BYTES);
out[..n].copy_from_slice(&name.as_bytes()[..n]);
out
}
pub fn fn_installed(arm: usize) {
FNS[arm.min(REST)].fetch_add(1, Relaxed);
}
pub fn fns(arm: usize) -> u32 {
FNS[arm.min(REST)].load(Relaxed)
}
/// The arms opened so far (REST included once it is in use).
pub fn arms_open() -> usize {
(ARMS_OPEN.load(Relaxed) as usize).min(ARMS)
}
pub fn arm_name(arm: usize) -> [u8; NAME_BYTES] {
let mut out = [0; NAME_BYTES];
if arm < REST {
for (chunk, word) in out.chunks_mut(8).zip(&ARM_NAME[arm]) {
chunk.copy_from_slice(&word.load(Relaxed).to_le_bytes());
}
}
out
}
pub fn o3_installed(arm: usize, key: u32) {
let n = O3_INSTALLED.fetch_add(1, Relaxed) as usize % O3_RING;
O3_KEYS[n].store(key, Relaxed);
O3_ARMS[n].store(arm.min(REST) as u8, Relaxed);
}
/// The `index`-th O3 install overall (arm, key), while it is within the last `O3_RING`.
pub fn o3_install(index: u32) -> (usize, u32) {
let n = index as usize % O3_RING;
(O3_ARMS[n].load(Relaxed) as usize % ARMS, O3_KEYS[n].load(Relaxed))
}
pub fn settle_done(rss_before: u64, rss_after: u64) {
if SETTLE_END_MS.load(Relaxed) != 0 {
return;
}
SETTLE_RSS_BEFORE_MB.store((rss_before >> 20) as u32, Relaxed);
SETTLE_RSS_AFTER_MB.store((rss_after >> 20) as u32, Relaxed);
SETTLE_END_MS.store(crate::clock::ms_since_process_start() as u64 + 1, Relaxed);
}
pub fn main_called() {
MAIN_AT_MS.store(crate::clock::ms_since_process_start() as u64 + 1, Relaxed);
}
#[cfg(test)]
mod tests {
use super::*;
// one test: the counters are process-wide statics
#[test]
fn arms_open_in_order_and_the_rest_share_one() {
for krate in 0..40u32 {
let arm = arm_of(900 + krate, || name_bytes(&format!("crate_{krate}")));
assert_eq!(arm, (krate as usize).min(REST));
// the same crate keeps its arm
assert_eq!(arm_of(900 + krate, || unreachable!()), arm);
}
let name = arm_name(3);
assert!(name.starts_with(b"crate_3\0"));
fn_installed(3);
fn_installed(3);
fn_installed(REST + 5);
assert_eq!(fns(3), 2);
assert_eq!(fns(REST), 1);
// a crate past the table is the rest arm and does not open one
assert_eq!(arm_of(100_000, || unreachable!()), REST);
let long = name_bytes("a_crate_name_longer_than_the_field");
assert_eq!(&long[..], b"a_crate_name_longer_than");
o3_installed(2, 77);
let n = O3_INSTALLED.load(Relaxed) - 1;
assert_eq!(o3_install(n), (2, 77));
}
}