blob: b244d61555de5d07bdd4a0075f00a71597af101f [file]
// Copyright 2022 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use std::cmp::Ordering;
use std::cmp::Reverse;
use std::ops::Sub;
use std::time::Duration;
use anyhow::anyhow;
use anyhow::bail;
use anyhow::Result;
use base::warn;
fn abs_diff<T: Sub<Output = T> + Ord>(x: T, y: T) -> T {
if x < y {
y - x
} else {
x - y
}
}
#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)]
pub struct CoreOffset {
pub core: usize,
pub offset: i128,
}
impl Ord for CoreOffset {
// CoreOffsets are ordered by offset ascending, then by their core number ascending
fn cmp(&self, other: &Self) -> Ordering {
(self.offset, self.core).cmp(&(other.offset, other.core))
}
}
impl PartialOrd for CoreOffset {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Default, Debug, Clone, Eq, PartialEq)]
pub struct CoreGroup {
pub cores: Vec<CoreOffset>,
}
impl CoreGroup {
fn size(&self) -> usize {
self.cores.len()
}
fn add(&self, core: CoreOffset, limit: u128) -> Result<Self> {
let diff_from_min = abs_diff(self.cores.iter().min().unwrap().offset, core.offset) as u128;
let diff_from_max = abs_diff(self.cores.iter().max().unwrap().offset, core.offset) as u128;
let can_add = diff_from_min < limit && diff_from_max < limit;
if can_add {
let mut new = self.clone();
new.cores.push(core);
Ok(new)
} else {
Err(anyhow!(
"offset {} not within {} of all members of core group",
core.offset,
limit
))
}
}
}
#[derive(Default, Debug, Clone, Eq, PartialEq)]
pub struct CoreGrouping {
groups: Vec<CoreGroup>,
}
impl Ord for CoreGrouping {
fn cmp(&self, other: &Self) -> Ordering {
// Ordered by largest group size descending, then by number of groups ascending
(Reverse(self.largest_group().size()), self.groups.len())
.cmp(&(Reverse(other.largest_group().size()), other.groups.len()))
}
}
impl PartialOrd for CoreGrouping {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl CoreGrouping {
pub(super) fn new(groups: Vec<CoreGroup>) -> Result<Self> {
// Other functions in this type rely on the fact that there is at least one CoreGroup.
if groups.is_empty() {
return Err(anyhow!("Cannot create an empty CoreGrouping."));
}
Ok(CoreGrouping { groups })
}
pub fn size(&self) -> usize {
self.groups.len()
}
pub fn largest_group_index(&self) -> usize {
// Sort by Reverse(size) then group index, and take the min.
// We could sort by size then Reverse(index), but then getting the index out is hard.
self.groups
.iter()
.enumerate()
.map(|(i, g)| (Reverse(g.size()), i))
.min()
.unwrap_or((Reverse(0), 0))
.1
}
pub fn largest_group(&self) -> &CoreGroup {
&self.groups[self.largest_group_index()]
}
pub fn core_grouping_bitmask(&self) -> u64 {
// If there's only one group, then all cores are in sync
if self.size() == 0 {
return 0;
}
let mut bitmask = 0u64;
let largest_group_index = self.largest_group_index();
for (i, group) in self.groups.iter().enumerate() {
// The largest group is considered the in-sync group
if i == largest_group_index {
continue;
}
// Set the bitmask to 1 for all cores in all other groups
for core in &group.cores {
if core.core > 63 {
warn!("Core grouping bitmask cannot contain core {}", core.core);
continue;
}
bitmask |= 1 << core.core;
}
}
bitmask
}
fn add_group(&mut self, group: CoreGroup) {
self.groups.push(group)
}
fn add_core_to_last_group(&self, core: CoreOffset, in_sync_threshold: u128) -> Option<Self> {
let last_group = self.groups.len() - 1;
self.groups[last_group]
.add(core, in_sync_threshold)
.map(|new_group| {
let mut new_grouping = self.clone();
new_grouping.groups[last_group] = new_group;
new_grouping
})
.ok()
}
}
/// Group cores by their offsets that are within `in_sync_threshold` of each other.
///
/// This uses a generic integer grouping algorithm. Because we're grouping within a threshold,
/// there are potentially multiple ways we can group the integers, and we want to find the "best"
/// grouping. Our definition of best grouping is:
/// 1. The grouping who's largest group is the largest.
/// 2. If there are multiple groupings with the same size largest group, take the one with the
/// fewest groups.
///
/// We could naively generate all possible groupings by iterating through the sorted integers and,
/// for each grouping, either adding that integer as it's own group or adding it to the last group
/// of that grouping. This could generate 2^N groupings, where N is the number of integers. Instead,
/// we still iterate through the sorted integers and, for each grouping, we add it to the last
/// group of that grouping. But we only add the integer as it's own group to the current best
/// grouping. This optimization avoids creating groupings that we know will not be the "best"
/// grouping, because adding the integer as it's own group means that all subsequent integers
/// cannot be grouped with the existing groups, and thus we only care about the optimal existing
/// groups.
pub(super) fn group_core_offsets(
offsets: &[(usize, i128)],
in_sync_threshold: Duration,
tsc_frequency: u64,
) -> Result<CoreGrouping> {
if offsets.is_empty() {
bail!("Per-core offsets cannot be empty");
}
// Convert threshold to TSC ticks
let in_sync_threshold_ticks =
in_sync_threshold.as_nanos() * tsc_frequency as u128 / 1_000_000_000u128;
let mut cores: Vec<CoreOffset> = offsets
.iter()
.map(|(i, offset)| CoreOffset {
core: *i,
offset: *offset,
})
.collect();
// Cores are sorted by their ascending by their offset then ascending by their core #. See the
// Ord implementation for CoreOffset.
cores.sort();
let mut grouping_options: Vec<CoreGrouping> = vec![CoreGrouping::new(vec![CoreGroup {
cores: vec![cores[0]],
}])?];
for core in &cores[1..] {
let mut best = grouping_options[0].clone();
best.add_group(CoreGroup { cores: vec![*core] });
let mut next_grouping_options = vec![best];
for grouping_option in &grouping_options {
if let Some(new_grouping) =
grouping_option.add_core_to_last_group(*core, in_sync_threshold_ticks)
{
next_grouping_options.push(new_grouping);
}
}
next_grouping_options.sort();
grouping_options = next_grouping_options;
}
Ok(grouping_options[0].clone())
}
#[cfg(test)]
mod tests {
use super::super::TscState;
use super::*;
#[test]
fn test_simple_offset_grouping() {
let offsets = vec![(0, 10), (1, 10), (2, 10), (3, 1000)];
let state = TscState::new(1_000_000_000, offsets, Duration::from_nanos(1))
.expect("TscState::new should not fail for this test");
let group0 = CoreGroup {
cores: vec![
CoreOffset {
core: 0,
offset: 10,
},
CoreOffset {
core: 1,
offset: 10,
},
CoreOffset {
core: 2,
offset: 10,
},
],
};
let group1 = CoreGroup {
cores: vec![CoreOffset {
core: 3,
offset: 1000,
}],
};
assert_eq!(
state.core_grouping,
CoreGrouping::new(vec![group0.clone(), group1])
.expect("CoreGrouping::new should not fail here")
);
assert_eq!(state.core_grouping.largest_group().clone(), group0);
assert_eq!(state.core_grouping.core_grouping_bitmask(), 0b1000u64);
}
#[test]
fn test_ambiguous_offset_grouping() {
// Could be grouped in several ways:
// - [10, 20] [30, 40] [50] <--- we like to have core0 be in a larger group
// - [10] [20, 30] [40, 50]
// - [10, 20] [30] [40, 50]
let offsets = vec![(0, 10), (1, 20), (2, 30), (3, 40), (4, 50)];
let state = TscState::new(1_000_000_000, offsets, Duration::from_nanos(20))
.expect("TscState::new should not fail for this test");
let group0 = CoreGroup {
cores: vec![
CoreOffset {
core: 0,
offset: 10,
},
CoreOffset {
core: 1,
offset: 20,
},
],
};
let group1 = CoreGroup {
cores: vec![
CoreOffset {
core: 2,
offset: 30,
},
CoreOffset {
core: 3,
offset: 40,
},
],
};
let group2 = CoreGroup {
cores: vec![CoreOffset {
core: 4,
offset: 50,
}],
};
assert_eq!(
state.core_grouping,
CoreGrouping::new(vec![group0.clone(), group1, group2])
.expect("CoreGrouping::new should not fail here")
);
// largest_group should return the first group over other equally large groups
assert_eq!(state.core_grouping.largest_group().clone(), group0);
assert_eq!(state.core_grouping.core_grouping_bitmask(), 0b11100u64);
}
#[test]
fn test_worst_case_grouping() {
// Worst case for the grouping algorithm, where if your algorithm isn't smart you can
// generate 2^129 groupings, which would be too large for a Vec to hold. This test just
// verifies that we don't crash or run out of memory.
let offsets = (0..129).map(|i| (i, 0)).collect();
TscState::new(1_000_000_000, offsets, Duration::from_nanos(1))
.expect("TscState::new should not fail for this test");
}
}