Coverage Report

Created: 2026-07-13 08:11

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/num_cpus-1.17.0/src/linux.rs
Line
Count
Source
1
use std::collections::HashMap;
2
use std::fs::File;
3
use std::io::{BufRead, BufReader, Read};
4
use std::mem;
5
use std::path::{Path, PathBuf};
6
use std::sync::atomic::{AtomicUsize, Ordering};
7
use std::sync::Once;
8
9
use libc;
10
11
macro_rules! debug {
12
    ($($args:expr),*) => ({
13
        if false {
14
        //if true {
15
            println!($($args),*);
16
        }
17
    });
18
}
19
20
macro_rules! some {
21
    ($e:expr) => {{
22
        match $e {
23
            Some(v) => v,
24
            None => {
25
                debug!("NONE: {:?}", stringify!($e));
26
                return None;
27
            }
28
        }
29
    }};
30
}
31
32
52.5k
pub fn get_num_cpus() -> usize {
33
52.5k
    match cgroups_num_cpus() {
34
0
        Some(n) => n,
35
52.5k
        None => logical_cpus(),
36
    }
37
52.5k
}
38
39
52.5k
fn logical_cpus() -> usize {
40
52.5k
    let mut set: libc::cpu_set_t = unsafe { mem::zeroed() };
41
52.5k
    if unsafe { libc::sched_getaffinity(0, mem::size_of::<libc::cpu_set_t>(), &mut set) } == 0 {
42
52.5k
        let mut count: u32 = 0;
43
53.8M
        for i in 0..libc::CPU_SETSIZE as usize {
44
53.8M
            if unsafe { libc::CPU_ISSET(i, &set) } {
45
1.68M
                count += 1
46
52.1M
            }
47
        }
48
52.5k
        count as usize
49
    } else {
50
0
        let cpus = unsafe { libc::sysconf(libc::_SC_NPROCESSORS_ONLN) };
51
0
        if cpus < 1 {
52
0
            1
53
        } else {
54
0
            cpus as usize
55
        }
56
    }
57
52.5k
}
58
59
0
pub fn get_num_physical_cpus() -> usize {
60
0
    let file = match File::open("/proc/cpuinfo") {
61
0
        Ok(val) => val,
62
0
        Err(_) => return get_num_cpus(),
63
    };
64
0
    let reader = BufReader::new(file);
65
0
    let mut map = HashMap::new();
66
0
    let mut physid: u32 = 0;
67
0
    let mut cores: usize = 0;
68
0
    let mut chgcount = 0;
69
0
    for line in reader.lines().filter_map(|result| result.ok()) {
70
0
        let mut it = line.split(':');
71
0
        let (key, value) = match (it.next(), it.next()) {
72
0
            (Some(key), Some(value)) => (key.trim(), value.trim()),
73
0
            _ => continue,
74
        };
75
0
        if key == "physical id" {
76
0
            match value.parse() {
77
0
                Ok(val) => physid = val,
78
0
                Err(_) => break,
79
            };
80
0
            chgcount += 1;
81
0
        }
82
0
        if key == "cpu cores" {
83
0
            match value.parse() {
84
0
                Ok(val) => cores = val,
85
0
                Err(_) => break,
86
            };
87
0
            chgcount += 1;
88
0
        }
89
0
        if chgcount == 2 {
90
0
            map.insert(physid, cores);
91
0
            chgcount = 0;
92
0
        }
93
    }
94
0
    let count = map.into_iter().fold(0, |acc, (_, cores)| acc + cores);
95
96
0
    if count == 0 {
97
0
        get_num_cpus()
98
    } else {
99
0
        count
100
    }
101
0
}
102
103
/// Cached CPUs calculated from cgroups.
104
///
105
/// If 0, check logical cpus.
106
// Allow deprecation warnings, we want to work on older rustc
107
#[allow(warnings)]
108
static CGROUPS_CPUS: AtomicUsize = ::std::sync::atomic::ATOMIC_USIZE_INIT;
109
110
52.5k
fn cgroups_num_cpus() -> Option<usize> {
111
    #[allow(warnings)]
112
    static ONCE: Once = ::std::sync::ONCE_INIT;
113
114
52.5k
    ONCE.call_once(init_cgroups);
115
116
52.5k
    let cpus = CGROUPS_CPUS.load(Ordering::Acquire);
117
118
52.5k
    if cpus > 0 {
119
0
        Some(cpus)
120
    } else {
121
52.5k
        None
122
    }
123
52.5k
}
124
125
2
fn init_cgroups() {
126
    // Should only be called once
127
2
    debug_assert!(CGROUPS_CPUS.load(Ordering::SeqCst) == 0);
128
129
    // Fails in Miri by default (cannot open files), and Miri does not have parallelism anyway.
130
2
    if cfg!(miri) {
131
0
        return;
132
2
    }
133
134
2
    if let Some(quota) = load_cgroups("/proc/self/cgroup", "/proc/self/mountinfo") {
135
0
        if quota == 0 {
136
0
            return;
137
0
        }
138
139
0
        let logical = logical_cpus();
140
0
        let count = ::std::cmp::min(quota, logical);
141
142
0
        CGROUPS_CPUS.store(count, Ordering::SeqCst);
143
2
    }
144
2
}
145
146
2
fn load_cgroups<P1, P2>(cgroup_proc: P1, mountinfo_proc: P2) -> Option<usize>
147
2
where
148
2
    P1: AsRef<Path>,
149
2
    P2: AsRef<Path>,
150
{
151
2
    let subsys = some!(Subsys::load_cpu(cgroup_proc));
152
2
    let mntinfo = some!(MountInfo::load_cpu(mountinfo_proc, subsys.version));
153
2
    let cgroup = some!(Cgroup::translate(mntinfo, subsys));
154
2
    cgroup.cpu_quota()
155
2
}
156
157
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158
enum CgroupVersion {
159
    V1,
160
    V2,
161
}
162
163
struct Cgroup {
164
    version: CgroupVersion,
165
    base: PathBuf,
166
}
167
168
struct MountInfo {
169
    version: CgroupVersion,
170
    root: String,
171
    mount_point: String,
172
}
173
174
struct Subsys {
175
    version: CgroupVersion,
176
    base: String,
177
}
178
179
impl Cgroup {
180
2
    fn new(version: CgroupVersion, dir: PathBuf) -> Cgroup {
181
2
        Cgroup { version: version, base: dir }
182
2
    }
183
184
2
    fn translate(mntinfo: MountInfo, subsys: Subsys) -> Option<Cgroup> {
185
        // Translate the subsystem directory via the host paths.
186
2
        debug!(
187
            "subsys = {:?}; root = {:?}; mount_point = {:?}",
188
            subsys.base, mntinfo.root, mntinfo.mount_point
189
        );
190
191
2
        let rel_from_root = some!(Path::new(&subsys.base).strip_prefix(&mntinfo.root).ok());
192
193
2
        debug!("rel_from_root: {:?}", rel_from_root);
194
195
        // join(mp.MountPoint, relPath)
196
2
        let mut path = PathBuf::from(mntinfo.mount_point);
197
2
        path.push(rel_from_root);
198
2
        Some(Cgroup::new(mntinfo.version, path))
199
2
    }
200
201
2
    fn cpu_quota(&self) -> Option<usize> {
202
2
        let (quota_us, period_us) = match self.version {
203
2
            CgroupVersion::V1 => (some!(self.quota_us()), some!(self.period_us())),
204
0
            CgroupVersion::V2 => some!(self.max()),
205
        };
206
207
        // protect against dividing by zero
208
0
        if period_us == 0 {
209
0
            return None;
210
0
        }
211
212
        // Ceil the division, since we want to be able to saturate
213
        // the available CPUs, and flooring would leave a CPU un-utilized.
214
215
0
        Some((quota_us as f64 / period_us as f64).ceil() as usize)
216
2
    }
217
218
2
    fn quota_us(&self) -> Option<usize> {
219
2
        self.param("cpu.cfs_quota_us")
220
2
    }
221
222
0
    fn period_us(&self) -> Option<usize> {
223
0
        self.param("cpu.cfs_period_us")
224
0
    }
225
226
0
    fn max(&self) -> Option<(usize, usize)> {
227
0
        let max = some!(self.raw_param("cpu.max"));
228
0
        let mut max = some!(max.lines().next()).split(' ');
229
230
0
        let quota = some!(max.next().and_then(|quota| quota.parse().ok()));
231
0
        let period = some!(max.next().and_then(|period| period.parse().ok()));
232
233
0
        Some((quota, period))
234
0
    }
235
236
2
    fn param(&self, param: &str) -> Option<usize> {
237
2
        let buf = some!(self.raw_param(param));
238
239
2
        buf.trim().parse().ok()
240
2
    }
241
242
2
    fn raw_param(&self, param: &str) -> Option<String> {
243
2
        let mut file = some!(File::open(self.base.join(param)).ok());
244
245
2
        let mut buf = String::new();
246
2
        some!(file.read_to_string(&mut buf).ok());
247
248
2
        Some(buf)
249
2
    }
250
}
251
252
impl MountInfo {
253
2
    fn load_cpu<P: AsRef<Path>>(proc_path: P, version: CgroupVersion) -> Option<MountInfo> {
254
2
        let file = some!(File::open(proc_path).ok());
255
2
        let file = BufReader::new(file);
256
257
2
        file.lines()
258
16
            .filter_map(|result| result.ok())
259
2
            .filter_map(MountInfo::parse_line)
260
2
            .find(|mount_info| mount_info.version == version)
261
2
    }
262
263
16
    fn parse_line(line: String) -> Option<MountInfo> {
264
16
        let mut fields = line.split(' ');
265
266
        // 7 5 0:6 </> /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:7 - cgroup cgroup rw,cpu,cpuacct
267
16
        let mnt_root = some!(fields.nth(3));
268
        // 7 5 0:6 / </sys/fs/cgroup/cpu,cpuacct> rw,nosuid,nodev,noexec,relatime shared:7 - cgroup cgroup rw,cpu,cpuacct
269
16
        let mnt_point = some!(fields.next());
270
271
        // Ignore all fields until the separator(-).
272
        // Note: there could be zero or more optional fields before hyphen.
273
        // See: https://man7.org/linux/man-pages/man5/proc.5.html
274
        // 7 5 0:6 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:7 <-> cgroup cgroup rw,cpu,cpuacct
275
        // Note: we cannot use `?` here because we need to support Rust 1.13.
276
36
        match fields.find(|&s| s == "-") {
277
16
            Some(_) => {}
278
0
            None => return None,
279
        };
280
281
        // 7 5 0:6 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:7 - <cgroup> cgroup rw,cpu,cpuacct
282
16
        let version = match fields.next() {
283
16
            Some("cgroup") => CgroupVersion::V1,
284
12
            Some("cgroup2") => CgroupVersion::V2,
285
12
            _ => return None,
286
        };
287
288
        // cgroups2 only has a single mount point
289
4
        if version == CgroupVersion::V1 {
290
            // 7 5 0:6 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:7 - cgroup cgroup <rw,cpu,cpuacct>
291
4
            let super_opts = some!(fields.nth(1));
292
293
            // We only care about the 'cpu' option
294
10
            if !super_opts.split(',').any(|opt| opt == "cpu") {
295
2
                return None;
296
2
            }
297
0
        }
298
299
2
        Some(MountInfo {
300
2
            version: version,
301
2
            root: mnt_root.to_owned(),
302
2
            mount_point: mnt_point.to_owned(),
303
2
        })
304
16
    }
305
}
306
307
impl Subsys {
308
2
    fn load_cpu<P: AsRef<Path>>(proc_path: P) -> Option<Subsys> {
309
2
        let file = some!(File::open(proc_path).ok());
310
2
        let file = BufReader::new(file);
311
312
2
        file.lines()
313
26
            .filter_map(|result| result.ok())
314
2
            .filter_map(Subsys::parse_line)
315
4
            .fold(None, |previous, line| {
316
                // already-found v1 trumps v2 since it explicitly specifies its controllers
317
4
                if previous.is_some() && line.version == CgroupVersion::V2 {
318
2
                    return previous;
319
2
                }
320
321
2
                Some(line)
322
4
            })
323
2
    }
324
325
26
    fn parse_line(line: String) -> Option<Subsys> {
326
        // Example format:
327
        // 11:cpu,cpuacct:/
328
26
        let mut fields = line.split(':');
329
330
26
        let sub_systems = some!(fields.nth(1));
331
332
26
        let version = if sub_systems.is_empty() {
333
2
            CgroupVersion::V2
334
        } else {
335
24
            CgroupVersion::V1
336
        };
337
338
26
        if version == CgroupVersion::V1 && !sub_systems.split(',').any(|sub| sub == "cpu") {
339
22
            return None;
340
4
        }
341
342
4
        fields.next().map(|path| Subsys {
343
4
            version: version,
344
4
            base: path.to_owned(),
345
4
        })
346
26
    }
347
}
348
349
#[cfg(test)]
350
mod tests {
351
    mod v1 {
352
        use super::super::{Cgroup, CgroupVersion, MountInfo, Subsys};
353
        use std::path::{Path, PathBuf};
354
355
        // `static_in_const` feature is not stable in Rust 1.13.
356
        static FIXTURES_PROC: &'static str = "fixtures/cgroups/proc/cgroups";
357
358
        static FIXTURES_CGROUPS: &'static str = "fixtures/cgroups/cgroups";
359
360
        macro_rules! join {
361
            ($base:expr, $($path:expr),+) => ({
362
                Path::new($base)
363
                    $(.join($path))+
364
            })
365
        }
366
367
        #[test]
368
        fn test_load_mountinfo() {
369
            // test only one optional fields
370
            let path = join!(FIXTURES_PROC, "mountinfo");
371
372
            let mnt_info = MountInfo::load_cpu(path, CgroupVersion::V1).unwrap();
373
374
            assert_eq!(mnt_info.root, "/");
375
            assert_eq!(mnt_info.mount_point, "/sys/fs/cgroup/cpu,cpuacct");
376
377
            // test zero optional field
378
            let path = join!(FIXTURES_PROC, "mountinfo_zero_opt");
379
380
            let mnt_info = MountInfo::load_cpu(path, CgroupVersion::V1).unwrap();
381
382
            assert_eq!(mnt_info.root, "/");
383
            assert_eq!(mnt_info.mount_point, "/sys/fs/cgroup/cpu,cpuacct");
384
385
            // test multi optional fields
386
            let path = join!(FIXTURES_PROC, "mountinfo_multi_opt");
387
388
            let mnt_info = MountInfo::load_cpu(path, CgroupVersion::V1).unwrap();
389
390
            assert_eq!(mnt_info.root, "/");
391
            assert_eq!(mnt_info.mount_point, "/sys/fs/cgroup/cpu,cpuacct");
392
        }
393
394
        #[test]
395
        fn test_load_subsys() {
396
            let path = join!(FIXTURES_PROC, "cgroup");
397
398
            let subsys = Subsys::load_cpu(path).unwrap();
399
400
            assert_eq!(subsys.base, "/");
401
            assert_eq!(subsys.version, CgroupVersion::V1);
402
        }
403
404
        #[test]
405
        fn test_cgroup_mount() {
406
            let cases = &[
407
                ("/", "/sys/fs/cgroup/cpu", "/", Some("/sys/fs/cgroup/cpu")),
408
                (
409
                    "/docker/01abcd",
410
                    "/sys/fs/cgroup/cpu",
411
                    "/docker/01abcd",
412
                    Some("/sys/fs/cgroup/cpu"),
413
                ),
414
                (
415
                    "/docker/01abcd",
416
                    "/sys/fs/cgroup/cpu",
417
                    "/docker/01abcd/",
418
                    Some("/sys/fs/cgroup/cpu"),
419
                ),
420
                (
421
                    "/docker/01abcd",
422
                    "/sys/fs/cgroup/cpu",
423
                    "/docker/01abcd/large",
424
                    Some("/sys/fs/cgroup/cpu/large"),
425
                ),
426
                // fails
427
                ("/docker/01abcd", "/sys/fs/cgroup/cpu", "/", None),
428
                ("/docker/01abcd", "/sys/fs/cgroup/cpu", "/docker", None),
429
                ("/docker/01abcd", "/sys/fs/cgroup/cpu", "/elsewhere", None),
430
                (
431
                    "/docker/01abcd",
432
                    "/sys/fs/cgroup/cpu",
433
                    "/docker/01abcd-other-dir",
434
                    None,
435
                ),
436
            ];
437
438
            for &(root, mount_point, subsys, expected) in cases.iter() {
439
                let mnt_info = MountInfo {
440
                    version: CgroupVersion::V1,
441
                    root: root.into(),
442
                    mount_point: mount_point.into(),
443
                };
444
                let subsys = Subsys {
445
                    version: CgroupVersion::V1,
446
                    base: subsys.into(),
447
                };
448
449
                let actual = Cgroup::translate(mnt_info, subsys).map(|c| c.base);
450
                let expected = expected.map(PathBuf::from);
451
                assert_eq!(actual, expected);
452
            }
453
        }
454
455
        #[test]
456
        fn test_cgroup_cpu_quota() {
457
            let cgroup = Cgroup::new(CgroupVersion::V1, join!(FIXTURES_CGROUPS, "good"));
458
            assert_eq!(cgroup.cpu_quota(), Some(6));
459
        }
460
461
        #[test]
462
        fn test_cgroup_cpu_quota_divide_by_zero() {
463
            let cgroup = Cgroup::new(CgroupVersion::V1, join!(FIXTURES_CGROUPS, "zero-period"));
464
            assert!(cgroup.quota_us().is_some());
465
            assert_eq!(cgroup.period_us(), Some(0));
466
            assert_eq!(cgroup.cpu_quota(), None);
467
        }
468
469
        #[test]
470
        fn test_cgroup_cpu_quota_ceil() {
471
            let cgroup = Cgroup::new(CgroupVersion::V1, join!(FIXTURES_CGROUPS, "ceil"));
472
            assert_eq!(cgroup.cpu_quota(), Some(2));
473
        }
474
    }
475
476
    mod v2 {
477
        use super::super::{Cgroup, CgroupVersion, MountInfo, Subsys};
478
        use std::path::{Path, PathBuf};
479
480
        // `static_in_const` feature is not stable in Rust 1.13.
481
        static FIXTURES_PROC: &'static str = "fixtures/cgroups2/proc/cgroups";
482
483
        static FIXTURES_CGROUPS: &'static str = "fixtures/cgroups2/cgroups";
484
485
        macro_rules! join {
486
            ($base:expr, $($path:expr),+) => ({
487
                Path::new($base)
488
                    $(.join($path))+
489
            })
490
        }
491
492
        #[test]
493
        fn test_load_mountinfo() {
494
            // test only one optional fields
495
            let path = join!(FIXTURES_PROC, "mountinfo");
496
497
            let mnt_info = MountInfo::load_cpu(path, CgroupVersion::V2).unwrap();
498
499
            assert_eq!(mnt_info.root, "/");
500
            assert_eq!(mnt_info.mount_point, "/sys/fs/cgroup");
501
        }
502
503
        #[test]
504
        fn test_load_subsys() {
505
            let path = join!(FIXTURES_PROC, "cgroup");
506
507
            let subsys = Subsys::load_cpu(path).unwrap();
508
509
            assert_eq!(subsys.base, "/");
510
            assert_eq!(subsys.version, CgroupVersion::V2);
511
        }
512
513
        #[test]
514
        fn test_load_subsys_multi() {
515
            let path = join!(FIXTURES_PROC, "cgroup_multi");
516
517
            let subsys = Subsys::load_cpu(path).unwrap();
518
519
            assert_eq!(subsys.base, "/");
520
            assert_eq!(subsys.version, CgroupVersion::V1);
521
        }
522
523
        #[test]
524
        fn test_cgroup_mount() {
525
            let cases = &[
526
                ("/", "/sys/fs/cgroup/cpu", "/", Some("/sys/fs/cgroup/cpu")),
527
                (
528
                    "/docker/01abcd",
529
                    "/sys/fs/cgroup/cpu",
530
                    "/docker/01abcd",
531
                    Some("/sys/fs/cgroup/cpu"),
532
                ),
533
                (
534
                    "/docker/01abcd",
535
                    "/sys/fs/cgroup/cpu",
536
                    "/docker/01abcd/",
537
                    Some("/sys/fs/cgroup/cpu"),
538
                ),
539
                (
540
                    "/docker/01abcd",
541
                    "/sys/fs/cgroup/cpu",
542
                    "/docker/01abcd/large",
543
                    Some("/sys/fs/cgroup/cpu/large"),
544
                ),
545
                // fails
546
                ("/docker/01abcd", "/sys/fs/cgroup/cpu", "/", None),
547
                ("/docker/01abcd", "/sys/fs/cgroup/cpu", "/docker", None),
548
                ("/docker/01abcd", "/sys/fs/cgroup/cpu", "/elsewhere", None),
549
                (
550
                    "/docker/01abcd",
551
                    "/sys/fs/cgroup/cpu",
552
                    "/docker/01abcd-other-dir",
553
                    None,
554
                ),
555
            ];
556
557
            for &(root, mount_point, subsys, expected) in cases.iter() {
558
                let mnt_info = MountInfo {
559
                    version: CgroupVersion::V1,
560
                    root: root.into(),
561
                    mount_point: mount_point.into(),
562
                };
563
                let subsys = Subsys {
564
                    version: CgroupVersion::V1,
565
                    base: subsys.into(),
566
                };
567
568
                let actual = Cgroup::translate(mnt_info, subsys).map(|c| c.base);
569
                let expected = expected.map(PathBuf::from);
570
                assert_eq!(actual, expected);
571
            }
572
        }
573
574
        #[test]
575
        fn test_cgroup_cpu_quota() {
576
            let cgroup = Cgroup::new(CgroupVersion::V2, join!(FIXTURES_CGROUPS, "good"));
577
            assert_eq!(cgroup.cpu_quota(), Some(6));
578
        }
579
580
        #[test]
581
        fn test_cgroup_cpu_quota_divide_by_zero() {
582
            let cgroup = Cgroup::new(CgroupVersion::V2, join!(FIXTURES_CGROUPS, "zero-period"));
583
            let period = cgroup.max().map(|max| max.1);
584
585
            assert_eq!(period, Some(0));
586
            assert_eq!(cgroup.cpu_quota(), None);
587
        }
588
589
        #[test]
590
        fn test_cgroup_cpu_quota_ceil() {
591
            let cgroup = Cgroup::new(CgroupVersion::V2, join!(FIXTURES_CGROUPS, "ceil"));
592
            assert_eq!(cgroup.cpu_quota(), Some(2));
593
        }
594
    }
595
}