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 不再动态调整阈值。
29        const MMAP_THRESHOLD: libc::c_int = 256 * 1024;
30        const ARENA_MAX: libc::c_int = 4;
31        // SAFETY: mallopt 只修改分配器参数,任何时候调用都安全;返回 0 表示参数不被接受,忽略即可。
32        #[allow(unsafe_code)]
33        unsafe {
34            libc::mallopt(libc::M_MMAP_THRESHOLD, MMAP_THRESHOLD);
35            libc::mallopt(libc::M_ARENA_MAX, ARENA_MAX);
36        }
37    }
38}
39
40/// 显式通知 glibc 分配器向操作系统释放无用堆页面。
41pub fn trim_memory() {
42    #[cfg(all(target_os = "linux", target_env = "gnu"))]
43    {
44        #[allow(unsafe_code)]
45        unsafe {
46            libc::malloc_trim(0);
47        }
48    }
49}
50
51/// 从 sysfs 读分配缓存要的拓扑。单节点机器只读一个文件。
52#[cfg(all(target_os = "linux", target_env = "gnu"))]
53mod topology {
54    use std::fs;
55
56    const SYS: &str = "/sys/devices/system";
57
58    pub(crate) fn detect() -> (Vec<(usize, usize)>, Option<usize>) {
59        let mut cpu_node = Vec::new();
60        let nodes = read(&format!("{SYS}/node/online")).map_or_else(Vec::new, |s| cpu_list(&s));
61        if nodes.len() > 1 {
62            for node in nodes {
63                if let Some(cpus) = read(&format!("{SYS}/node/node{node}/cpulist")) {
64                    cpu_node.extend(cpu_list(&cpus).into_iter().map(|cpu| (cpu, node)));
65                }
66            }
67        }
68        (cpu_node, l2_share())
69    }
70
71    /// cpu0 的 L2 大小除以共用它的逻辑 CPU 数(SMT 兄弟共用一份 L2)。
72    fn l2_share() -> Option<usize> {
73        for index in 0.. {
74            let dir = format!("{SYS}/cpu/cpu0/cache/index{index}");
75            let level = read(&format!("{dir}/level"))?;
76            if level.trim() != "2" {
77                continue;
78            }
79            let size = size(&read(&format!("{dir}/size"))?)?;
80            let sharing = read(&format!("{dir}/shared_cpu_list"))
81                .map_or(1, |s| cpu_list(&s).len())
82                .max(1);
83            return Some(size / sharing);
84        }
85        None
86    }
87
88    fn read(path: &str) -> Option<String> {
89        fs::read_to_string(path).ok()
90    }
91
92    /// "1024K"、"2M"、"512"。
93    pub(crate) fn size(s: &str) -> Option<usize> {
94        let s = s.trim();
95        let (digits, unit) = match s.as_bytes().last()? {
96            b'K' => (&s[..s.len() - 1], 1 << 10),
97            b'M' => (&s[..s.len() - 1], 1 << 20),
98            b'G' => (&s[..s.len() - 1], 1 << 30),
99            _ => (s, 1),
100        };
101        digits.parse::<usize>().ok().map(|n| n * unit)
102    }
103
104    /// "0-3,8,10-11" → [0, 1, 2, 3, 8, 10, 11]。
105    pub(crate) fn cpu_list(s: &str) -> Vec<usize> {
106        let mut out = Vec::new();
107        for part in s.trim().split(',').filter(|p| !p.is_empty()) {
108            let mut ends = part.splitn(2, '-').map(|n| n.trim().parse::<usize>());
109            match (ends.next(), ends.next()) {
110                (Some(Ok(a)), None) => out.push(a),
111                (Some(Ok(a)), Some(Ok(b))) if a <= b => out.extend(a..=b),
112                _ => {}
113            }
114        }
115        out
116    }
117
118    #[cfg(test)]
119    mod tests {
120        use super::*;
121
122        #[test]
123        fn sysfs_lists_and_sizes_parse() {
124            assert_eq!(cpu_list("0-3,8,10-11\n"), vec![0, 1, 2, 3, 8, 10, 11]);
125            assert_eq!(cpu_list("0"), vec![0]);
126            assert_eq!(cpu_list(""), Vec::<usize>::new());
127            assert_eq!(size("1024K\n"), Some(1 << 20));
128            assert_eq!(size("2M"), Some(2 << 20));
129            assert_eq!(size("512"), Some(512));
130            assert_eq!(size("x"), None);
131        }
132
133        #[test]
134        fn this_machine_reports_an_l2_share() {
135            let (_, l2) = detect();
136            if std::path::Path::new("/sys/devices/system/cpu/cpu0/cache/index2").exists() {
137                assert!(l2.is_some_and(|b| b >= 4096), "{l2:?}");
138            }
139        }
140    }
141}