-
Notifications
You must be signed in to change notification settings - Fork 130
Add /proc/buddyinfo parsing #348
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jwake
wants to merge
2
commits into
eminence:master
Choose a base branch
from
jwake:buddyinfo
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,242 @@ | ||
| use std::str::FromStr; | ||
| use std::{collections::HashMap, fmt::Display, ops::Add}; | ||
|
|
||
| #[cfg(feature = "serde1")] | ||
| use serde::{Deserialize, Serialize}; | ||
|
|
||
| use crate::{Pages, ProcError}; | ||
|
|
||
| #[derive(Debug, Clone)] | ||
| #[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))] | ||
| /// Free memory fragmentation data for a specific NUMA node and memory zone. | ||
| pub struct BuddyInfoEntry { | ||
| /// The NUMA node | ||
| pub node: u8, | ||
|
|
||
| /// The memory zone | ||
| pub zone: MemoryZoneType, | ||
|
|
||
| /// A map of chunk size (in number of pages) to free chunk count | ||
| free_chunks: HashMap<Pages, u64>, | ||
| } | ||
|
|
||
| #[derive(Debug, Clone)] | ||
| #[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))] | ||
| /// Free memory fragmentation data for the entire system. | ||
| /// | ||
| /// Contains one entry per unique (NUMA node, memory zone) on the system. | ||
| pub struct BuddyInfo { | ||
| /// The complete set of entries | ||
| entries: Vec<BuddyInfoEntry>, | ||
| } | ||
|
|
||
| /// Kernel memory zone types. | ||
| #[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))] | ||
| #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] | ||
| pub enum MemoryZoneType { | ||
| DMA = 1, | ||
| DMA32 = 2, | ||
| Normal = 3, | ||
| HighMem = 4, | ||
| Movable = 5, | ||
| Device = 6, | ||
| } | ||
|
|
||
| impl FromStr for MemoryZoneType { | ||
| type Err = ProcError; | ||
|
|
||
| fn from_str(value: &str) -> Result<MemoryZoneType, Self::Err> { | ||
| match value { | ||
| "DMA" => Ok(MemoryZoneType::DMA), | ||
| "DMA32" => Ok(MemoryZoneType::DMA32), | ||
| "Normal" => Ok(MemoryZoneType::Normal), | ||
| "HighMem" => Ok(MemoryZoneType::HighMem), | ||
| "Movable" => Ok(MemoryZoneType::Movable), | ||
| "Device" => Ok(MemoryZoneType::Device), | ||
| _ => Err(ProcError::Other(format!("{} is not a valid zone type", value))), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl Display for MemoryZoneType { | ||
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| match self { | ||
| MemoryZoneType::DMA => write!(f, "DMA"), | ||
| MemoryZoneType::DMA32 => write!(f, "DMA32"), | ||
| MemoryZoneType::Normal => write!(f, "Normal"), | ||
| MemoryZoneType::HighMem => write!(f, "HighMem"), | ||
| MemoryZoneType::Movable => write!(f, "Movable"), | ||
| MemoryZoneType::Device => write!(f, "Device"), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl crate::FromBufRead for BuddyInfo { | ||
| fn from_buf_read<R: std::io::BufRead>(r: R) -> crate::ProcResult<Self> { | ||
| let mut entries = Vec::new(); | ||
|
|
||
| for line in r.lines().flatten() { | ||
| if !line.is_empty() { | ||
| let mut s = line.split_whitespace(); | ||
|
|
||
| // Skip "Node" literal | ||
| s.next(); | ||
|
|
||
| let node_id_str = expect!(s.next()).trim_end_matches(','); | ||
|
|
||
| let node = from_str!(u8, node_id_str); | ||
|
|
||
| // Skip "zone" literal | ||
| s.next(); | ||
|
|
||
| let zone = MemoryZoneType::from_str(expect!(s.next()))?; | ||
|
|
||
| let page_sizes = (0u64..).map(|x| 1 << x); | ||
|
|
||
| let mut free_chunks = HashMap::new(); | ||
| for (size, count) in page_sizes.zip(s) { | ||
| let count = from_str!(u64, count); | ||
| free_chunks.insert(Pages(size), count); | ||
| } | ||
|
|
||
| entries.push(BuddyInfoEntry { | ||
| node, | ||
| zone, | ||
| free_chunks, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| Ok(BuddyInfo { entries }) | ||
| } | ||
| } | ||
|
|
||
| impl BuddyInfo { | ||
| /// Get the entry for a specific NUMA node and memory zone | ||
| pub fn get(&self, numa_node: u8, zone: MemoryZoneType) -> Option<&BuddyInfoEntry> { | ||
| self.entries.iter().find(|x| x.node == numa_node && x.zone == zone) | ||
| } | ||
|
|
||
| /// Get all entries on the given NUMA node | ||
| pub fn on_node(&self, numa_node: u8) -> impl Iterator<Item = &BuddyInfoEntry> + use<'_> { | ||
| self.entries.iter().filter(move |x| x.node == numa_node) | ||
| } | ||
|
|
||
| /// Get all entries in the given memory zone | ||
| pub fn in_zone(&self, zone: MemoryZoneType) -> impl Iterator<Item = &BuddyInfoEntry> + use<'_> { | ||
| self.entries.iter().filter(move |x| x.zone == zone) | ||
| } | ||
|
|
||
| /// Get an iterator over the entries in this BuddyInfo | ||
| pub fn iter(&self) -> impl Iterator<Item = &BuddyInfoEntry> + use<'_> { | ||
| self.entries.iter() | ||
| } | ||
| } | ||
|
|
||
| /// Implement into_iter() for the underlying Vec of entries | ||
| impl IntoIterator for BuddyInfo { | ||
| type Item = <Vec<BuddyInfoEntry> as IntoIterator>::Item; | ||
| type IntoIter = <Vec<BuddyInfoEntry> as IntoIterator>::IntoIter; | ||
| fn into_iter(self) -> Self::IntoIter { | ||
| self.entries.into_iter() | ||
| } | ||
| } | ||
|
|
||
| #[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))] | ||
| #[derive(Clone, Copy, Debug)] | ||
| pub struct BuddyInfoItem { | ||
| pub npages: Pages, | ||
| pub nchunks: u64, | ||
| } | ||
|
|
||
| impl BuddyInfoEntry { | ||
| /// Get the total number of free pages across all nodes and zones | ||
| pub fn total_free(&self) -> Pages { | ||
| self.filtered(|_| true) | ||
| } | ||
|
|
||
| /// Get the number of free pages available in chunks of exactly `npages`` pages | ||
| pub fn free_in_chunks_of(&self, npages: u64) -> Pages { | ||
| self.filtered(|c| c == npages.into()) | ||
| } | ||
|
|
||
| /// Get the total number of free pages available in chunks of at least `npages`` pages | ||
| pub fn free_in_chunks_gteq(&self, npages: u64) -> Pages { | ||
| self.filtered(|c| c >= npages.into()) | ||
| } | ||
|
|
||
| /// Get the total number of free pages available in chunks of less than `npages`` pages | ||
| pub fn free_in_chunks_lt(&self, npages: u64) -> Pages { | ||
| self.filtered(|c| c < npages.into()) | ||
| } | ||
|
|
||
| /// Iterate over available (number of pages in chunk, number of chunks) items | ||
| pub fn iter(&self) -> impl Iterator<Item = BuddyInfoItem> + use<'_> { | ||
| self.free_chunks.iter().map(|x| BuddyInfoItem { | ||
| npages: *x.0, | ||
| nchunks: *x.1, | ||
| }) | ||
| } | ||
|
|
||
| fn filtered<F>(&self, op: F) -> Pages | ||
| where | ||
| F: Fn(Pages) -> bool, | ||
| { | ||
| self.free_chunks | ||
| .iter() | ||
| .filter(|x| op(*x.0)) | ||
| .map(|x| *x.0 * *x.1) | ||
| .reduce(Pages::add) | ||
| .unwrap_or(0.into()) | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn test_buddyinfo_parsing() { | ||
| let data = r#"Node 0, zone DMA 0 0 0 0 0 0 0 0 1 1 2 | ||
| Node 0, zone DMA32 3 2 1 1 1 2 2 2 3 3 480 | ||
| Node 0, zone Normal 2778 421 1223 21849 8276 3067 458 91 43 38 25537 | ||
| Node 1, zone Normal 18848 6919 20881 10325 5615 2880 936 393 102 20 27681"#; | ||
|
|
||
| let r = std::io::Cursor::new(data.as_bytes()); | ||
|
|
||
| use crate::FromRead; | ||
|
|
||
| let info = BuddyInfo::from_read(r).unwrap(); | ||
|
|
||
| assert_eq!(info.entries.len(), 4); | ||
| let entry = info.get(0, MemoryZoneType::Normal); | ||
| assert!(entry.is_some()); | ||
| let entry = entry.unwrap(); | ||
|
|
||
| assert_eq!(entry.free_in_chunks_of(32), (3067 * 1 << 5).into()); | ||
|
|
||
| let pages_greater_than_2mb = entry.free_in_chunks_gteq(1 << 9); | ||
| assert_eq!(pages_greater_than_2mb, ((38 * 1 << 9) + (25537 * 1 << 10)).into()); | ||
|
|
||
| let pages_smaller_than_2mb = entry.free_in_chunks_lt(1 << 9); | ||
| assert_eq!( | ||
| pages_smaller_than_2mb, | ||
| (2778 | ||
| + (421 * 2) | ||
| + (1223 * 1 << 2) | ||
| + (21849 * 1 << 3) | ||
| + (8276 * 1 << 4) | ||
| + (3067 * 1 << 5) | ||
| + (458 * 1 << 6) | ||
| + (91 * 1 << 7) | ||
| + (43 * 1 << 8)) | ||
| .into() | ||
| ); | ||
|
|
||
| // Test some helpers | ||
| assert_eq!(info.on_node(1).count(), 1); | ||
| assert_eq!(info.on_node(32).count(), 0); | ||
| assert_eq!(info.in_zone(MemoryZoneType::DMA).count(), 1); | ||
| assert_eq!(info.in_zone(MemoryZoneType::Movable).count(), 0); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| macro_rules! gen_ops { | ||
| (@gen_op $type:ident, $underlying:ident, $opTrait:ident, $opFn: ident, $op:tt) => | ||
| { | ||
| impl<T: Into<$underlying>> std::ops::$opTrait<T> for $type { | ||
| type Output = $type; | ||
|
|
||
| fn $opFn(self, rhs: T) -> Self::Output { | ||
| $type(self.0 $op rhs.into()) | ||
| } | ||
| } | ||
| }; | ||
| (@gen_op_assign $type:ident, $underlying:ident, $opTrait:ident, $opFn: ident, $op: tt) => | ||
| { | ||
| impl<T: Into<$underlying>> std::ops::$opTrait<T> for $type { | ||
| fn $opFn(&mut self, rhs: T) { | ||
| self.0 $op rhs.into() | ||
| } | ||
| } | ||
| }; | ||
| ($type:ident, $underlying:ident) => { | ||
| gen_ops!(@gen_op $type, $underlying, Add, add, +); | ||
| gen_ops!(@gen_op_assign $type, $underlying, AddAssign, add_assign, +=); | ||
| gen_ops!(@gen_op $type, $underlying, Sub, sub, -); | ||
| gen_ops!(@gen_op_assign $type, $underlying, SubAssign, sub_assign, -=); | ||
| gen_ops!(@gen_op $type, $underlying, Mul, mul, *); | ||
| gen_ops!(@gen_op_assign $type, $underlying, MulAssign, mul_assign, *=); | ||
| gen_ops!(@gen_op $type, $underlying, Div, div, /); | ||
| gen_ops!(@gen_op_assign $type, $underlying, DivAssign, div_assign, /=); | ||
|
|
||
| }; | ||
| } | ||
|
|
||
| macro_rules! wrap_numeric { | ||
| ($newtype: ident, $underlying: ident, $($c:tt)+) => { | ||
| #[doc = stringify!($($c)+)] | ||
| #[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))] | ||
| #[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] | ||
| pub struct $newtype(pub $underlying); | ||
|
|
||
| impl From<$newtype> for $underlying { | ||
| fn from(value: $newtype) -> Self { | ||
| value.0 | ||
| } | ||
| } | ||
|
|
||
| impl From<$underlying> for $newtype { | ||
| fn from(value: $underlying) -> Self { | ||
| $newtype(value) | ||
| } | ||
| } | ||
|
|
||
| gen_ops!($newtype, $underlying); | ||
|
|
||
| impl std::fmt::Display for $newtype { | ||
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| std::fmt::Display::fmt(&self.0, f) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| wrap_numeric!(Pages, u64, "A quantity of pages of memory"); | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| #[test] | ||
| fn test_pages() { | ||
| let p = Pages(64); | ||
| let mut p2 = (p * 2u32) + 4u32; | ||
| assert!(p2 == 132u64.into()); | ||
|
|
||
| let formatted = format!("{}", p2); | ||
| assert_eq!(formatted, "132"); | ||
| p2 /= 2u32; | ||
| let formatted = format!("{}", p2); | ||
| assert_eq!(formatted, "66"); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| use procfs_core::{BuddyInfo, ProcResult}; | ||
|
|
||
| use crate::Current; | ||
|
|
||
| impl Current for BuddyInfo { | ||
| const PATH: &'static str = "/proc/buddyinfo"; | ||
| } | ||
|
|
||
| pub fn buddyinfo() -> ProcResult<BuddyInfo> { | ||
| BuddyInfo::current() | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use procfs_core::MemoryZoneType; | ||
|
|
||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn test_buddyinfo() { | ||
| let info = buddyinfo().unwrap(); | ||
| assert!(info.iter().count() > 0); | ||
| assert!(info.iter().any(|x| x.zone == MemoryZoneType::Normal)); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is missing some bits for handling the serde stuff: