cpumap 0.2.1

GUI/TUI to view and edit CPU affinities of processes and threads on Linux
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::fmt;
use hwloc2;
use crate::util;
use sysinfo;

/// Uniquely identifies a topology object.
#[derive(Clone)]
pub struct TopologyObjectID {
    /// Type of the object (cache, package, PU, etc.)
    pub type_id: hwloc2::ObjectType,
    /// hwloc logical index of the object among objects of same type.
    index:   u32
}
impl TopologyObjectID {
    /// Construct ID for given obejct.
    pub fn from_obj(obj: &hwloc2::TopologyObject) -> TopologyObjectID {
        TopologyObjectID{ type_id: obj.object_type(), index: obj.logical_index() }
    }
    /// Construct ID from cached topology object info.
    pub fn from_cached(obj: &TopologyObjectInfo) -> TopologyObjectID {
        TopologyObjectID{ type_id: obj.object_type.clone(), index: obj.logical_index }
    }
    /// True if this ID is the ID of the root object of the hierarchy.
    pub fn is_root(&self) -> bool {
        self.type_id == hwloc2::ObjectType::Machine
    }
}

/// Traits needed for hash table support.
impl PartialEq for TopologyObjectID {
    fn eq(&self, other: &Self) -> bool {
        return self.type_id == other.type_id && self.index == other.index;
    }
}
impl Eq for TopologyObjectID{}
impl Hash for TopologyObjectID {
    fn hash<H: Hasher>(&self, state: &mut H) {
        (self.type_id.clone() as u32).hash(state);
        self.index.hash(state);
    }
}

/// Specifies style to draw an object label, if a non-default style should be used.
#[derive(Debug)]
pub enum TopologyObjectLabelStyle {
    /// Label for a low-level cache that should be smaller to save space.
    SmallCache
}
/// Specifies how to draw a label for a topology object.
#[derive(Debug)]
pub struct TopologyObjectLabel {
    /// Sub-labels to be drawn as lines when we need to save horizontal space.
    pub lines:          Vec<String>,
    /// Any non-default style to use.
    pub style_override: Option<TopologyObjectLabelStyle>
}

/// Precomputed info about a topology object, to avoid repeated recursive computation.
#[derive(Debug)]
pub struct TopologyObjectInfo {
    /// Type of the object (cache, package, PU, etc.)
    pub object_type:    hwloc2::ObjectType,
    /// OS index, used e.g. for PUs to max exactly to OS PU indices.
    pub os_index:       u32,
    /// Depth from root.
    pub depth:          u32,
    /// Arity (how many children the object has).
    pub arity:          u32,
    /// Logical index - unique among objects of the same type.
    pub logical_index:  u32,
    /// If this is a cache, size of the cache in bytes.
    pub cache_size:     Option<u64>,
    /// If this object contains PUs, set of all PUs contained.
    pub cpuset:         Option<hwloc2::CpuSet>,
    /// Whether to draw this object in a horizontal layout (children side-by-side horizontally).
    pub use_horizontal: bool,
    /// Whether this object has at least one sibling in the same parent.
    pub has_siblings:   bool,
    /// Whether to draw this object with a frame (to easier differentiate it from other objects).
    pub use_frame:      bool,
    /// Number of nest levels within this object where all children have a frame.
    pub frame_level:    u16,
    /// Whether this object recursively contains only a single core (may have multiple PUs).
    pub is_single_core: bool,
    /// Name of the CPU this object is in, if any.
    pub cpu_name:       Option<String>,
    /// Specifies how to draw the label for this object.
    pub label:          Option<TopologyObjectLabel>,
    /// Indices of this objects' children, used for children iterators.
    child_indices:      Vec<usize>
}
impl TopologyObjectInfo {
    /// Get ID of this topology object.
    pub fn id(&self) -> TopologyObjectID {
        TopologyObjectID { type_id: self.object_type.clone(), index: self.logical_index }
    }

    /// Get the number of children of this topology object.
    pub fn child_count(&self) -> usize {
        self.child_indices.len()
    }

    /// True if this is a PU object.
    pub fn is_pu(&self) -> bool {
        self.object_type == hwloc2::ObjectType::PU
    }

    /// True if this is a package object.
    pub fn is_package(&self) -> bool {
        self.object_type == hwloc2::ObjectType::Package
    }
}

/// Iterator over TopologyObjectInfo items representing children of a single topology object.
#[derive(Clone)]
pub struct TopologyObjectChildren<'a> {
    /// Reference to `TopologyCache.storage`.
    storage:       &'a Vec<TopologyObjectInfo>,
    /// Reference to `TopologyObjectInfo.child_indices` of the parent.
    child_indices: &'a Vec<usize>,
    /// Index (into `child_indices`) of the next child to iterate.
    next_index:    usize,
    /// Index (into `child_indices`) of the first child in the iterator (needed for subslicing).
    start:         usize,
    /// Index (into `child_indices`) of the last child in the iterator (needed for subslicing).
    end:           usize
}

/// Shortened Debug implementation, for less spammy output.
impl<'a> fmt::Debug for TopologyObjectChildren<'a> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "storage.len(): {},", self.storage.len())?;
        write!(f, "child_idices: {:?},", self.child_indices)?;
        write!(f, "next_index: {:?}, start: {:?}, end: {:?}\n",
               self.next_index, self.start, self.end)
    }
}
impl<'a> Iterator for TopologyObjectChildren<'a> {
    type Item = &'a TopologyObjectInfo;
    fn next(&mut self) -> Option<&'a TopologyObjectInfo> {
        let result = self.peek();
        if result.is_some() {
            self.next_index += 1;
        }
        result
    }
}
impl<'a> ExactSizeIterator for TopologyObjectChildren<'a> {
    fn len(&self) -> usize {
        assert!(self.next_index <= self.end, "next_index overflow");
        assert!(self.end <= self.child_indices.len(), "end overflow");
        self.end - self.next_index
    }
}
impl<'a> TopologyObjectChildren<'a> {
    /// Get the first item without advancing the iterator. None if the iterator is empty.
    pub fn peek(&self) -> Option<&'a TopologyObjectInfo> {
        assert!(self.next_index <= self.end, "invalid next item index in TopologyObjectChildren");
        self.child_indices.get(self.next_index).and_then(|idx| {
            if self.next_index < self.end {
                Some(&self.storage[*idx])
            } else {
                None
            }
        })
    }

    /// Get an iterator of a 'subslice' of this iterator.
    ///
    /// start: First index (inclusive) of the subiterator relative to current iterator.
    /// end:   Last index (exclusive) of the subiterator relative to current iterator.
    pub fn subiter(&self, start: usize, end: usize) -> TopologyObjectChildren<'a> {
        assert!(start <= end, "start and end of subiter in wrong order");
        let new_start = usize::min(self.next_index + start, self.child_indices.len());
        let new_end   = usize::min(self.next_index + end,   self.child_indices.len());
        TopologyObjectChildren {
            storage:       self.storage,
            child_indices: self.child_indices,
            next_index:    new_start,
            start:         new_start,
            end:           new_end
        }
    }

    /// Get item at specified index in the iterator (basically implementing D's RandomAcccessRange).
    pub fn get(&self, index: usize) -> &'a TopologyObjectInfo {
        let idx = self.child_indices.get(self.next_index + index)
                      .expect(format!("index out of range: {} (context: {:?})", index, self).as_str());
        &self.storage[*idx]
    }

    /// Call `func` for each 'group' of children of same type (e.g. caches containing same subcaches/cores)
    ///
    /// The second parameter to the lambda is `true` only for the last group.
    pub fn by_group<F>(self, mut func: F ) where F: FnMut(TopologyObjectChildren<'a>, bool) {
        let mut group_start = 0;
        for (i, child) in self.clone().enumerate() {
            if TopologyCache::objects_differ(self.get(group_start), child) {
                let group = self.subiter(group_start, i);
                group_start = i;
                func(group, false);
            }
        }
        let group = self.subiter(group_start, self.len());
        func(group, true);
    }
}

/// Stores precomputed data about all topology objects, with direct (table) as well as recursive access.
pub struct TopologyCache {
    /// Maps topology object ID to object indices in `storage`.
    cache:   HashMap<TopologyObjectID, usize>,
    /// Stores actual topology object ID.
    storage: Vec<TopologyObjectInfo>,
    /// CPU sets of each logical core.
    ///
    /// This has one set per logical core, and each set only contains a single logical core/PU.
    cpusets_pu:   Vec<hwloc2::CpuSet>,
    /// CPU sets of each physical core.
    ///
    /// Each set contains all logical cores of corresponding physical core.
    cpusets_core: Vec<hwloc2::CpuSet>,
    /// CPU sets of each L3 cache.
    ///
    /// Each set contains all logical cores of cores under the L3 caches.
    cpusets_l3:   Vec<hwloc2::CpuSet>,
}

impl TopologyCache {
    /// Construct a TopologyCache for all topology objects within given `root`.
    pub fn new(root: &hwloc2::TopologyObject, cpus: &[sysinfo::Cpu]) -> Option<TopologyCache> {
        let mut cache   = HashMap::new();
        let mut storage = Vec::new();

        /// Recursively process children of `object` and extract/cache topology object info.
        fn recursive_process_children(
            object:  &hwloc2::TopologyObject,
            cpus:    &[sysinfo::Cpu],
            cache:   &mut HashMap<TopologyObjectID, usize>,
            storage: &mut Vec<TopologyObjectInfo>)
        {
            let mut child_indices = Vec::new();
            for child in object.children() {
                recursive_process_children( child, cpus, cache, storage );
                // The child was just pushed at the end of the 'recursive_process_children()' call
                child_indices.push(storage.len() - 1);
            }

            let key = TopologyObjectID{ type_id: object.object_type(), index: object.logical_index() };
            let cpu_name = if key.type_id == hwloc2::ObjectType::Package {
                Some(TopologyCache::cpu_name(cpus, object))
            } else {
                None
            };
            let value = TopologyObjectInfo {
                object_type:    key.type_id.clone(),
                os_index:       object.os_index(),
                depth:          object.depth(),
                arity:          object.arity(),
                logical_index:  object.logical_index(),
                cache_size:     object.cache_attributes().and_then(|attr| Some(attr.size)),
                cpuset:         object.cpuset(),
                use_horizontal: TopologyCache::topology_object_use_horizontal(object),
                has_siblings:   TopologyCache::has_siblings(object),
                use_frame:      TopologyCache::topology_object_use_frame(object),
                frame_level:    TopologyCache::topology_object_frame_level(object),
                is_single_core: TopologyCache::is_single_core(object),
                cpu_name:       cpu_name.clone(),
                label:          TopologyCache::topology_object_label(object, cpu_name),
                child_indices
            };
            let index: usize = storage.len();
            cache.insert( key, index );
            // Pushing to storage here at the end means root is at the end of storage.
            storage.push( value );
        }

        recursive_process_children(root, cpus, &mut cache, &mut storage);

        /// Initialize `cpusets` to contain all CPU sets of topology objects of type `obj_type` in the hierarchy under `obj`.
        fn init_cpusets_by_type(obj: &hwloc2::TopologyObject, obj_type: hwloc2::ObjectType, cpusets: &mut Vec<hwloc2::CpuSet>) {
            if obj.object_type() == obj_type {
                cpusets.push(obj.cpuset().expect("We should be able to get the list of processing units of a core/logical core/L3"));
            } else {
                for child in obj.children() {
                    init_cpusets_by_type(child, obj_type.clone(), cpusets);
                }
            }
        }
        let mut cpusets_pu   = Vec::new();
        let mut cpusets_core = Vec::new();
        let mut cpusets_l3   = Vec::new();
        init_cpusets_by_type(root, hwloc2::ObjectType::PU,      &mut cpusets_pu);
        init_cpusets_by_type(root, hwloc2::ObjectType::Core,    &mut cpusets_core);
        init_cpusets_by_type(root, hwloc2::ObjectType::L3Cache, &mut cpusets_l3);

        Some(TopologyCache{
            cache,
            storage,
            cpusets_pu,
            cpusets_core,
            cpusets_l3
        })
    }

    /// Get info about the root object of the topology. Should recursively own all cores.
    pub fn root(&self) -> &TopologyObjectInfo {
        self.storage.last().expect("There should be at least one topology object - the root")
    }

    /// Get CPU sets of each logical core.
    pub fn cpusets_pu(&self) -> &[hwloc2::CpuSet] {
        self.cpusets_pu.as_slice()
    }

    /// Get CPU sets of each physical core.
    pub fn cpusets_core(&self) -> &[hwloc2::CpuSet] {
        self.cpusets_core.as_slice()
    }

    /// Get CPU sets of each L3 cache.
    pub fn cpusets_l3(&self) -> &[hwloc2::CpuSet] {
        self.cpusets_l3.as_slice()
    }

    /// Get info for topology object with given ID.
    pub fn get_by_id(&self, id: TopologyObjectID) -> &TopologyObjectInfo {
        &self.storage[*self.cache.get(&id).expect("ID of a nonexistent topology object")]
    }

    /// Get info for the next topology object (if any) with same object type as `id`.
    pub fn next_by_type(&self, id: TopologyObjectID) -> Option<TopologyObjectID> {
        let new_id = TopologyObjectID{ type_id: id.type_id, index: id.index + 1};
        self.cache.get(&new_id).and_then(|_item| Some(new_id))
    }

    /// Get info for the previous topology object (if any) with same object type as `id`.
    pub fn previous_by_type(&self, id: TopologyObjectID) -> Option<TopologyObjectID> {
        if id.index == 0 {
            return None
        }
        let new_id = TopologyObjectID{ type_id: id.type_id, index: id.index - 1};
        self.cache.get(&new_id).and_then(|_item| Some(new_id))
    }

    /// Get info for given topology object.
    pub fn get_by_obj(&self, obj: &hwloc2::TopologyObject) -> &TopologyObjectInfo {
        self.get_by_id(TopologyObjectID::from_obj(obj))
    }

    /// Get an iterator of all children of topology object corresponding to `info`.
    pub fn get_children<'a>(&'a self, info: &'a TopologyObjectInfo) -> TopologyObjectChildren<'a> {
        TopologyObjectChildren{
            storage: &self.storage,
            child_indices: &info.child_indices,
            next_index: 0,
            start: 0, 
            end: info.child_indices.len()
        }
    }

    /// Get parent, if any, topology object corresponding to `info`.
    pub fn get_parent(&self, info: &TopologyObjectInfo) -> Option<&TopologyObjectInfo> {
        let id = TopologyObjectID::from_cached(info);
        let own_index = *self.cache.get(&id).expect("topology object not in cache");
        // Inefficient, but not called from any performance-critical code.
        self.storage.iter().find(|&obj| obj.child_indices.iter().any(|&c| c == own_index))
    }

    /// Determine if `info` is a child or a child of a recursive child of `possible_parent`.
    pub fn is_recursive_child_of(&self, info: &TopologyObjectInfo, possible_parent: &TopologyObjectID) -> bool {
        let id = TopologyObjectID::from_cached(info);
        if id == *possible_parent {
            true
        } else if let Some(parent) = self.get_parent(self.get_by_id(id)) {
            self.is_recursive_child_of(parent, possible_parent)
        } else {
            false
        }
    }

    /// Return info about the sibling object displayed above `info` in a topology view.
    ///
    /// Since topologi view depends on `by_group()` and `use_horizontal`, topology cache can 
    /// compute their relative layout, if not precise positions.
    ///
    /// May return `info`, if there is nothing above.
    pub fn sibling_up(&self, info: &TopologyObjectID) -> TopologyObjectID {
        self.sibling_relative(info, (0, -1))
    }

    /// Return info about the sibling object displayed below `info` in a topology view.
    ///
    /// Since topologi view depends on `by_group()` and `use_horizontal`, topology cache can 
    /// compute their relative layout, if not precise positions.
    ///
    /// May return `info`, if there is nothing below.
    pub fn sibling_down(&self, info: &TopologyObjectID) -> TopologyObjectID {
        self.sibling_relative(info, (0, 1))
    }

    /// Get sibling at specified relative position to `info` in a topology view.
    ///
    /// * `delta` is the relative position of the sibling, represented in number of objects X/Y
    ///   on a grid of siblings on a topology view.
    ///
    /// Will only get siblings in the same group (see `by_group()`). If `delta` would go out of
    /// bounds, returns the furthest sibling in that direction.
    fn sibling_relative(&self, info: &TopologyObjectID, delta: (isize, isize)) -> TopologyObjectID {
        let parent = if let Some(parent) = self.get_parent(self.get_by_id(info.clone())) {
            parent
        } else {
            return info.clone();
        };

        let mut sibling_id:TopologyObjectID = info.clone();
        self.get_children(parent).by_group(|children, _is_last| {
            let group_sibling_count = children.clone().count();
            let (rows, cols) = util::squarish_grid_dimensions(group_sibling_count, parent.use_horizontal);
            let index = children.clone().position(|c: &TopologyObjectInfo| TopologyObjectID::from_cached(c) == *info);
            if let Some(index_in_group) = index {
                let (x, y) = (index_in_group % cols, index_in_group / cols);
                let (delta_x, delta_y) = delta;

                let result_x = (cols-1).min(x.saturating_add_signed(delta_x));
                let result_y = (rows-1).min(y.saturating_add_signed(delta_y));
                let sibling_index_in_group = result_y * cols + result_x;
                let sibling = children.clone().skip(sibling_index_in_group).next().unwrap();
                sibling_id = TopologyObjectID::from_cached(sibling);
            } else {
                return;
            }
        });

        sibling_id    
    }

    /// True if `a` and `b` are different 'kinds' of objects for purpose of grouping (e.g. big vs LITTLE cores).
    pub fn objects_differ(a: &TopologyObjectInfo, b: &TopologyObjectInfo) -> bool {
        if a.object_type.clone() != b.object_type.clone() {
            return true
        } 
        use hwloc2::ObjectType;
        match(a.object_type.clone(), b.object_type.clone()) {
            (ObjectType::L1Cache | ObjectType::L2Cache | ObjectType::L3Cache | ObjectType::L4Cache | ObjectType::L5Cache, 
             ObjectType::L1Cache | ObjectType::L2Cache | ObjectType::L3Cache | ObjectType::L4Cache | ObjectType::L5Cache) => {
                if a.cache_size.unwrap() != b.cache_size.unwrap()  {
                    return true
                }
             },
            (ObjectType::Package, ObjectType::Package) => {
                if a.cpu_name != b.cpu_name {
                    return true
                }
            },
            _ => {}
        }
            
        a.arity != b.arity || a.depth != b.depth
    }

    /// Determine whether `obj` should be rendered in a horizontal manner, taking more X than Y space.
    /// 
    /// May be done in some cases by a horizontal layout, in others by a grid where the 'longer'
    /// side is horizontal, etc.
    /// 
    /// If false, using vertical space is prioritized.
    fn topology_object_use_horizontal(obj: &hwloc2::TopologyObject) -> bool {
        if obj.object_type() == hwloc2::ObjectType::PU {
            // PUs themselves are 'vertical', and cores above them 'horizontal' as a result - i.e. 
            // PUs are shown side by side horizontally.
            false
        } else if obj.object_type() == hwloc2::ObjectType::Package {
            // Packages (CPUs) are vertical, i.e. any core groups with shared last-level-caches in
            // those packages are stacked vertically. This doesn't matter for single-core-group
            // CPUs, but uses space optimally for e.g. Zen1-2 CPUs with multiple core groups.
            false
        } else {
            let children = obj.children();
            if children.len() == 0 {
                // Leaf object that is not a PU - for completeness, we don't currently draw such objects.
                true
            } else if children.len() == 1 {
                // Caches on top of a single core and similar cases where an object is 'layered' on
                // top of a child are stacked vertically.
                false
            } else {
                let child_orientation = Self::topology_object_use_horizontal(children[0]);
                let all_children_same_orientation =
                    children[1..].iter().any(|&child|{Self::topology_object_use_horizontal(child) != child_orientation});
                if !all_children_same_orientation {
                    // Inconsistent child directions, use horizontal as default
                    true
                } else {
                    // Default case for multiple children - opposite orientation vs the child nodes.
                    !child_orientation
                }
            }
        }
    }

    /// Determine if we should draw a frame around a topology object.
    /// 
    /// Usually frames are used to differentiate an object from its siblings on the same level.
    fn topology_object_use_frame(obj: &hwloc2::TopologyObject) -> bool {
        match obj.object_type() {
            hwloc2::ObjectType::Core =>    false,
            hwloc2::ObjectType::PU =>      false,
            hwloc2::ObjectType::Package => true,
            _ =>                           TopologyCache::has_siblings(obj)
        }
    }

    /// Number of nest levels within this object (including itself) where all children have `use_frame` as true.
    ///
    /// 0 if `use_frame` is false.
    fn topology_object_frame_level(obj: &hwloc2::TopologyObject) -> u16 {
        if Self::topology_object_use_frame(obj) {
            if obj.children().iter().all(|c| Self::topology_object_use_frame(c)) {
                obj.children().iter().map(|c| Self::topology_object_frame_level(c)).max().unwrap_or(0) + 1
            } else {
                1
            }
        } else {
            0
        }
    }

    /// Determine if `obj` has any siblings.
    pub fn has_siblings(obj: &hwloc2::TopologyObject) -> bool {
        if let Some(parent) = obj.parent() {
            parent.children().len() > 1
        } else {
            false
        }
    }

    /// Determine if the hierarchy under `obj` consists of a single core and related objects (e.g. cache)
    /// 
    /// Such 'single cores' are then drawn in a specific way, from the topmost object that is still 
    /// related to (contains in children recursively) the one core.
    pub fn is_single_core(obj: &hwloc2::TopologyObject) -> bool {
        if obj.object_type() == hwloc2::ObjectType::Core {
            return true;
        }
        let children = obj.children();
        // Either a node owning multiple cores, or a leaf node that is not a core.
        if children.len() != 1 {
            return false;
        }
        return Self::is_single_core(children[0]);
    }

    /// Get a human-readable name of given physical CPU (hwloc Package).
    pub fn cpu_name(cpus: &[sysinfo::Cpu], obj: &hwloc2::TopologyObject) -> String {
        assert!(obj.object_type() == hwloc2::ObjectType::Package, "`cpu_name` should only be called for 'Package' objects");
        TopologyCache::cpu_name_recursive(cpus, obj).unwrap_or(String::from("CPU"))
    }

    /// Implementation of `cpu_name`, recursively searches for the first `PU` to get corresponding name.
    fn cpu_name_recursive(cpus: &[sysinfo::Cpu], obj: &hwloc2::TopologyObject) -> Option<String> {
        if obj.object_type() == hwloc2::ObjectType::PU {
            // Use `sysinfo` to get name corresponding to the PU's OS index.
            let index = obj.os_index() as usize; 
            if index >= cpus.len() {
                // Default name if hwloc2 os_index somehow does not match sysinfo cpus.
                return Some(String::from("CPU"));
            }
            return Some(util::cpu_brand_transform(cpus[index].brand()));
        } else {
            // Get to the first child that is a PU, and so we can get its name.
            for child in obj.children() {
                let name = TopologyCache::cpu_name_recursive(cpus, child);
                if name.is_some() {
                    return name;
                }
            }
        }
        // Reached a leaf node that is not a PU.
        return None;
    }

    /// Prepare text/style to draw for a given object's label.
    fn topology_object_label(obj: &hwloc2::TopologyObject, cpu_name: Option<String>) -> Option<TopologyObjectLabel> {
        use hwloc2::ObjectType;

        match obj.object_type() {
            ObjectType::L1Cache | ObjectType::L2Cache | ObjectType::L3Cache 
                                | ObjectType::L4Cache | ObjectType::L5Cache => {
                let size = obj.cache_attributes().and_then(|attr| Some(attr.size)).unwrap();
                let name = match obj.object_type() {
                    ObjectType::L1Cache => "L1d",
                    ObjectType::L2Cache => "L2",
                    ObjectType::L3Cache => "L3",
                    ObjectType::L4Cache => "L4",
                    ObjectType::L5Cache => "L5",
                    _ => {unreachable!("guaranteed by above match");}
                };
                // L1/L2 caches use a smaller font as they tend to be per-core.
                let style_override = if [ObjectType::L1Cache, ObjectType::L2Cache].contains( &obj.object_type()) {
                    Some(TopologyObjectLabelStyle::SmallCache)
                    // ui.style_mut().override_text_style = Some(egui::TextStyle::Name("SmallCache".into()));
                } else {
                    None
                };
                let lines = vec![
                    String::from(name), 
                    util::format_cache_size(size, 2)
                ];
                Some(TopologyObjectLabel{ lines, style_override })
            },
            ObjectType::Package => {
                // ui.label(obj.cpu_name.as_ref().unwrap());
                Some(TopologyObjectLabel{
                    lines: vec![cpu_name.as_ref().unwrap().clone()],
                    style_override: None
                })
            },
            ObjectType::Machine | ObjectType::Core => {
                // Don't render these to reduce clutter.
                None
            },
            _ => {
                Some(TopologyObjectLabel{
                    lines: vec![format!("{:?}", obj.object_type())],
                    style_override: None
                })
            }
        }
    }
}