Skip to main content

pagesift_sys/
lib.rs

1//! 进程级调优。整个 pagesift 只有这里有 unsafe:调用 glibc 的 `mallopt`,以及小块分配缓存
2//! ([`SmallCache`],全局分配器)。
3//!
4//! 只应该由 pagesift 自己的可执行程序调用;Python、Node 绑定跑在别人的进程里,
5//! 不能替宿主改分配器参数。
6
7#![deny(unsafe_code)]
8
9mod alloc;
10pub use alloc::SmallCache;
11#[doc(hidden)]
12pub use alloc::small_cache_simulate_numa;
13
14/// 固定 glibc 的 mmap 阈值。
15///
16/// glibc 默认会在释放大块内存后把 mmap 阈值调高(最高 32 MB),之后页面大小的缓冲区
17/// (几百 KB 到几 MB 的 HTML、DOM、Markdown)都改从堆里分配,用完不还给系统。
18/// 并发爬取时 RSS 因此是实际使用量的两倍多。固定在 256 KB 后这些块用完立即归还:
19/// 本地 20 站点爬取的峰值 RSS 从 188 MB 降到约 75 MB,CPU 多约 8%。
20/// 非 glibc 平台什么也不做。
21///
22/// 同时把 CPU 拓扑(NUMA 节点、每个逻辑 CPU 分到的 L2)告诉 [`SmallCache`]。
23pub fn tune_allocator() {
24    #[cfg(all(target_os = "linux", target_env = "gnu"))]
25    {
26        let (cpu_node, l2_share) = topology::detect();
27        alloc::configure(&cpu_node, l2_share);
28        /// [策略] 显式设置后 glibc 不再动态调整阈值。256 KB 是本机比较过的几档里最好的(见上)。
29        const MMAP_THRESHOLD: libc::c_int = 256 * 1024;
30        // SAFETY: mallopt 只修改分配器参数,任何时候调用都安全;返回 0 表示参数不被接受,忽略即可。
31        #[allow(unsafe_code)]
32        unsafe {
33            libc::mallopt(libc::M_MMAP_THRESHOLD, MMAP_THRESHOLD);
34            libc::mallopt(libc::M_ARENA_MAX, arena_max(available_cpus()));
35        }
36    }
37}
38
39/// glibc 的 arena 数。4 个 arena 只在 16 个逻辑 CPU 上验证过(每 4 个 CPU 一个);192 vCPU 上上百个线程
40/// 共用 4 个 arena 没测过。所以保持验证过的比例:每 4 个可用
41/// CPU 一个,不少于 4 个——16 个 CPU 及以下和原来完全一样,更大的机器 arena 跟着线程数增长,
42/// 每个 arena 上争锁的线程数不变。小块分配本来就不走 glibc([`SmallCache`]),这里管的是中等大小的块。
43#[cfg(all(target_os = "linux", target_env = "gnu"))]
44fn arena_max(cpus: usize) -> libc::c_int {
45    // [策略] 先验包 v0:0.81 在 16 个逻辑 CPU 上验证过的 4 个 arena,即每 4 个 CPU 一个
46    const VALIDATED_ARENAS: usize = 4;
47    const VALIDATED_CPUS: usize = 16; // [策略] 同上
48    let arenas = cpus
49        .div_ceil(VALIDATED_CPUS / VALIDATED_ARENAS)
50        .max(VALIDATED_ARENAS);
51    libc::c_int::try_from(arenas).unwrap_or(libc::c_int::MAX)
52}
53
54/// 进程能用的逻辑 CPU 数(亲和性和 cgroup 配额之内)。
55#[cfg(all(target_os = "linux", target_env = "gnu"))]
56fn available_cpus() -> usize {
57    std::thread::available_parallelism().map_or(1, |n| n.get())
58}
59
60/// 显式通知 glibc 分配器向操作系统释放无用堆页面。
61pub fn trim_memory() {
62    #[cfg(all(target_os = "linux", target_env = "gnu"))]
63    {
64        #[allow(unsafe_code)]
65        unsafe {
66            libc::malloc_trim(0);
67        }
68    }
69}
70
71/// 从 sysfs 读分配缓存要的拓扑。单节点机器只读一个文件。
72#[cfg(all(target_os = "linux", target_env = "gnu"))]
73mod topology {
74    use std::fs;
75
76    const SYS: &str = "/sys/devices/system";
77
78    pub(crate) fn detect() -> (Vec<(usize, usize)>, Option<usize>) {
79        let mut cpu_node = Vec::new();
80        let nodes = read(&format!("{SYS}/node/online")).map_or_else(Vec::new, |s| cpu_list(&s));
81        if nodes.len() > 1 {
82            for node in nodes {
83                if let Some(cpus) = read(&format!("{SYS}/node/node{node}/cpulist")) {
84                    cpu_node.extend(cpu_list(&cpus).into_iter().map(|cpu| (cpu, node)));
85                }
86            }
87        }
88        (cpu_node, l2_share())
89    }
90
91    /// cpu0 的 L2 大小除以共用它的逻辑 CPU 数(SMT 兄弟共用一份 L2)。
92    fn l2_share() -> Option<usize> {
93        for index in 0.. {
94            let dir = format!("{SYS}/cpu/cpu0/cache/index{index}");
95            let level = read(&format!("{dir}/level"))?;
96            if level.trim() != "2" {
97                continue;
98            }
99            let size = size(&read(&format!("{dir}/size"))?)?;
100            let sharing = read(&format!("{dir}/shared_cpu_list"))
101                .map_or(1, |s| cpu_list(&s).len())
102                .max(1);
103            return Some(size / sharing);
104        }
105        None
106    }
107
108    fn read(path: &str) -> Option<String> {
109        fs::read_to_string(path).ok()
110    }
111
112    /// "1024K"、"2M"、"512"。
113    pub(crate) fn size(s: &str) -> Option<usize> {
114        let s = s.trim();
115        let (digits, unit) = match s.as_bytes().last()? {
116            b'K' => (&s[..s.len() - 1], 1 << 10),
117            b'M' => (&s[..s.len() - 1], 1 << 20),
118            b'G' => (&s[..s.len() - 1], 1 << 30),
119            _ => (s, 1),
120        };
121        digits.parse::<usize>().ok().map(|n| n * unit)
122    }
123
124    /// "0-3,8,10-11" → [0, 1, 2, 3, 8, 10, 11]。
125    pub(crate) fn cpu_list(s: &str) -> Vec<usize> {
126        let mut out = Vec::new();
127        for part in s.trim().split(',').filter(|p| !p.is_empty()) {
128            let mut ends = part.splitn(2, '-').map(|n| n.trim().parse::<usize>());
129            match (ends.next(), ends.next()) {
130                (Some(Ok(a)), None) => out.push(a),
131                (Some(Ok(a)), Some(Ok(b))) if a <= b => out.extend(a..=b),
132                _ => {}
133            }
134        }
135        out
136    }
137
138    #[cfg(test)]
139    mod tests {
140        use super::*;
141
142        #[test]
143        fn sysfs_lists_and_sizes_parse() {
144            assert_eq!(cpu_list("0-3,8,10-11\n"), vec![0, 1, 2, 3, 8, 10, 11]);
145            assert_eq!(cpu_list("0"), vec![0]);
146            assert_eq!(cpu_list(""), Vec::<usize>::new());
147            assert_eq!(size("1024K\n"), Some(1 << 20));
148            assert_eq!(size("2M"), Some(2 << 20));
149            assert_eq!(size("512"), Some(512));
150            assert_eq!(size("x"), None);
151        }
152
153        #[test]
154        fn this_machine_reports_an_l2_share() {
155            let (_, l2) = detect();
156            if std::path::Path::new("/sys/devices/system/cpu/cpu0/cache/index2").exists() {
157                assert!(l2.is_some_and(|b| b >= 4096), "{l2:?}");
158            }
159        }
160    }
161}
162
163#[cfg(test)]
164#[cfg(all(target_os = "linux", target_env = "gnu"))]
165mod tests {
166    #[test]
167    fn arenas_keep_the_validated_ratio_and_floor() {
168        // 16 个 CPU 及以下和 0.81 一样是 4 个
169        for cpus in [1, 2, 4, 8, 15, 16] {
170            assert_eq!(super::arena_max(cpus), 4, "{cpus}");
171        }
172        assert_eq!(super::arena_max(17), 5);
173        assert_eq!(super::arena_max(192), 48);
174        assert_eq!(super::arena_max(384), 96);
175    }
176}