metrics: memd: split test code into separate module
Moving the test code into a separate file helps achieve two goals:
the main program file becomes smaller and more readable, and the
test code is compiled only when running the unit tests.
The separation is not perfect: the main program code still has some
knowledge of testing (for instance when building the Paths object),
but it's better than before.
BUG=chromium:729335
TEST=ran "cargo build" and "cargo test"
Change-Id: I2f3515243a0a7959005677d11c34bffdc0b4d3ce
Reviewed-on: https://chromium-review.googlesource.com/1112669
Commit-Ready: Luigi Semenzato <semenzato@chromium.org>
Tested-by: Luigi Semenzato <semenzato@chromium.org>
Reviewed-by: Luigi Semenzato <semenzato@chromium.org>
diff --git a/metrics/memd/src/main.rs b/metrics/memd/src/main.rs
index 6ce4b5d..52d93a1 100644
--- a/metrics/memd/src/main.rs
+++ b/metrics/memd/src/main.rs
@@ -26,9 +26,12 @@
use chrono::prelude::*;
use {libc::__errno_location, libc::c_void};
+#[cfg(not(test))]
use dbus::{Connection, BusType, WatchEvent};
-use std::{io,mem,str,thread};
+use std::{io,mem,str};
+#[cfg(not(test))]
+use std::thread;
use std::cmp::max;
use std::error::Error;
use std::fmt;
@@ -40,6 +43,9 @@
// Not to be confused with chrono::Duration or the deprecated time::Duration.
use std::time::Duration;
+#[cfg(test)]
+mod test;
+
const PAGE_SIZE: usize = 4096; // old friend
const LOG_DIRECTORY: &str = "/var/log/memd";
@@ -70,13 +76,6 @@
const SAMPLE_QUEUE_LENGTH: usize =
(CLIP_COLLECTION_SPAN_MS / 1000 * SAMPLES_PER_SECOND * 2) as usize;
-// For testing: different levels of available RAM in MB.
-const TESTING_LOW_MEM_LOW_AVAILABLE: usize = 150;
-const TESTING_LOW_MEM_MEDIUM_AVAILABLE: usize = 300;
-const TESTING_LOW_MEM_HIGH_AVAILABLE: usize = 1000;
-const TESTING_LOW_MEM_MARGIN: usize = 200;
-const TESTING_MOCK_DBUS_FIFO_NAME: &str = "mock-dbus-fifo";
-
// The names of fields of interest in /proc/vmstat. They must be listed in
// the order in which they appear in /proc/vmstat. When parsing the file,
// if a mandatory field is missing, the program panics. A missing optional
@@ -108,16 +107,6 @@
("pgmajfault_f", false),
];
-macro_rules! print_to_path {
- ($path:expr, $format:expr $(, $arg:expr)*) => {{
- let r = OpenOptions::new().write(true).create(true).open($path);
- match r {
- Err(e) => Err(e),
- Ok(mut f) => f.write_all(format!($format $(, $arg)*).as_bytes())
- }
- }}
-}
-
type Result<T> = std::result::Result<T, Box<Error>>;
fn errno() -> i32 {
@@ -174,18 +163,6 @@
}
}
-fn mkfifo(path: &PathBuf) -> Result<()> {
- let path_name = path.to_str().unwrap();
- let c_path = std::ffi::CString::new(path_name).unwrap();
- // Safe because c_path points to a valid C string.
- let status = unsafe { libc::mkfifo(c_path.as_ptr(), 0o644) };
- if status < 0 {
- Err(format!("mkfifo: {}: {}", path_name, strerror(errno())).into())
- } else {
- Ok(())
- }
-}
-
// Converts the result of an integer expression |e| to modulo |n|. |e| may be
// negative. This differs from plain "%" in that the result of this function
// is always be between 0 and n-1.
@@ -195,10 +172,6 @@
(if x >= 0 { x } else { x + nn }) as usize
}
-fn duration_to_millis(duration: &Duration) -> i64 {
- duration.as_secs() as i64 * 1000 + duration.subsec_nanos() as i64 / 1_000_000
-}
-
// Reads a string from the file named by |path|, representing a u32, and
// returns the value the strings represents.
fn read_int(path: &Path) -> Result<u32> {
@@ -208,81 +181,6 @@
Ok(content.trim().parse::<u32>()?)
}
-// Internally generated event for testing.
-struct TestEvent {
- time: i64,
- event_type: TestEventType,
-}
-
-// The types of events which are generated internally for testing. They
-// simulate state changes (for instance, change in the memory pressure level),
-// chrome events, and kernel events.
-enum TestEventType {
- EnterHighPressure, // enter low available RAM (below margin) state
- EnterLowPressure, // enter high available RAM state
- EnterMediumPressure, // set enough memory pressure to trigger fast sampling
- OomKillBrowser, // fake browser report of OOM kill
- OomKillKernel, // fake kernel report of OOM kill
- TabDiscard, // fake browser report of tab discard
-}
-
-impl TestEvent {
- fn deliver(&self, paths: &Paths, dbus_fifo: &mut File, low_mem_device: &mut File) {
- match self.event_type {
- TestEventType::EnterLowPressure =>
- self.low_mem_notify(TESTING_LOW_MEM_HIGH_AVAILABLE, &paths, low_mem_device),
- TestEventType::EnterMediumPressure =>
- self.low_mem_notify(TESTING_LOW_MEM_MEDIUM_AVAILABLE, &paths, low_mem_device),
- TestEventType::EnterHighPressure =>
- self.low_mem_notify(TESTING_LOW_MEM_LOW_AVAILABLE, &paths, low_mem_device),
- TestEventType::OomKillBrowser => self.send_signal("oom-kill", dbus_fifo),
- TestEventType::OomKillKernel => self.mock_kill(&paths.trace_pipe),
- TestEventType::TabDiscard => self.send_signal("tab-discard", dbus_fifo),
- }
- }
-
- fn low_mem_notify(&self, amount: usize, paths: &Paths, mut low_mem_device: &mut File) {
- write_string(&amount.to_string(), &paths.available, false).
- expect("available file: write failed");
- if amount == TESTING_LOW_MEM_LOW_AVAILABLE {
- // Make low-mem device ready-to-read.
- write!(low_mem_device, ".").expect("low-mem-device: write failed");
- } else {
- let mut buf = [0; PAGE_SIZE];
- read_nonblocking_pipe(&mut low_mem_device, &mut buf)
- .expect("low-mem-device: clear failed");
- }
- }
-
- fn send_signal(&self, signal: &str, dbus_fifo: &mut File) {
- write!(dbus_fifo, "{}\n", signal).expect("mock dbus: write failed");
- }
-
- fn mock_kill(&self, path: &PathBuf) {
- // example string (8 spaces before the first non-space):
- // chrome-13700 [001] .... 867348.061651: oom_kill_process <-out_of_memory
- let s = format!("chrome-13700 [001] .... {}.{:06}: oom_kill_process <-out_of_memory\n",
- self.time / 1000, (self.time % 1000) * 1000);
- write_string(&s, path, true).expect("mock trace_pipe: write failed");
- }
-}
-
-fn non_blocking_select(high_fd: i32, inout_read_fds: &mut libc::fd_set) -> i32 {
- let mut null_timeout = libc::timeval {
- tv_sec: 0 as libc::time_t,
- tv_usec: 0 as libc::suseconds_t,
- };
- let null = std::ptr::null_mut();
- // Safe because we're passing valid values and addresses.
- unsafe {
- libc::select(high_fd,
- inout_read_fds,
- null,
- null,
- &mut null_timeout as *mut libc::timeval)
- }
-}
-
// The Timer trait allows us to mock time for testing.
trait Timer {
// A wrapper for libc::select() with only ready-to-read fds.
@@ -295,134 +193,10 @@
fn quit_request(&self) -> bool;
}
-// Real time mock, for testing only. It removes time races (for better or
-// worse) and makes it possible to run the test on build machines which may be
-// heavily loaded.
-//
-// Time is mocked by assuming that CPU speed is infinite and time passes only
-// when the program is asleep. Time advances in discrete jumps when we call
-// either sleep() or select() with a timeout.
-
-struct MockTimer {
- current_time: i64, // the current time
- test_events: Vec<TestEvent>, // list events to be delivered
- event_index: usize, // index of next event to be delivered
- paths: Paths, // for event delivery
- dbus_fifo_out: File, // for mock dbus event delivery
- low_mem_device: File, // for delivery of low-mem notifications
- quit_request: bool, // for termination
- _trace_pipe_aux: File, // to avoid EOF on trace pipe, which fires select
-}
-
-impl MockTimer {
- fn new(test_events: Vec<TestEvent>, paths: Paths, dbus_fifo_out: File) -> MockTimer {
- let low_mem_device = OpenOptions::new()
- .custom_flags(libc::O_NONBLOCK)
- .read(true)
- .write(true)
- .open(&paths.low_mem_device).expect("low-mem-device: cannot setup");
- // We need to open the fifo one extra time because otherwise the fifo
- // is still ready-to-read after consuming all data in its buffer, even
- // though the following read will return EOF. The ready-to-read status
- // will cause the select() to fire incorrectly. By having another open
- // write descriptor, there is no EOF, and the select does not fire.
- let _trace_pipe_aux = OpenOptions::new()
- .custom_flags(libc::O_NONBLOCK)
- .read(true)
- .write(true)
- .open(&paths.trace_pipe).expect("trace_pipe: cannot setup");
- MockTimer {
- current_time: 0,
- test_events,
- event_index: 0,
- paths,
- dbus_fifo_out,
- low_mem_device,
- quit_request: false,
- _trace_pipe_aux,
- }
- }
-}
-
-impl Timer for MockTimer {
- fn now(&self) -> i64 {
- self.current_time
- }
-
- fn quit_request(&self) -> bool {
- self.quit_request
- }
-
- // Mock select first checks if any events are pending, then produces events
- // that would happen during its sleeping time, and checks if those events
- // are delivered.
- fn select(&mut self,
- high_fd: i32,
- inout_read_fds: &mut libc::fd_set,
- timeout: &Duration) -> i32 {
- // First check for pending events which may have been delivered during
- // a preceding sleep.
- let n = non_blocking_select(high_fd, inout_read_fds);
- if n != 0 {
- return n;
- }
- // Now time can start passing, as long as there are more events.
- let timeout_ms = duration_to_millis(&timeout);
- let end_time = self.current_time + timeout_ms;
- if self.event_index == self.test_events.len() {
- // No more events to deliver, so no need for further select() calls.
- self.quit_request = true;
- self.current_time = end_time;
- return 0;
- }
- // There are still event to be delivered.
- let first_event_time = self.test_events[self.event_index].time;
- if first_event_time > end_time {
- // No event to deliver before the timeout, thus no events to look for.
- self.current_time = end_time;
- return 0;
- }
- // Deliver all events with the time stamp of the first event. (There
- // is at least one.)
- while {
- self.test_events[self.event_index].deliver(&self.paths,
- &mut self.dbus_fifo_out,
- &mut self.low_mem_device);
- self.event_index += 1;
- self.event_index < self.test_events.len() &&
- self.test_events[self.event_index].time == first_event_time
- } {}
- // One or more events were delivered, so run another select.
- let n = non_blocking_select(high_fd, inout_read_fds);
- if n > 0 {
- self.current_time = first_event_time;
- } else {
- self.current_time = end_time;
- }
- n
- }
-
- // Mock sleep produces all events that would happen during that sleep, then
- // updates the time.
- fn sleep(&mut self, sleep_time: &Duration) {
- let start_time = self.current_time;
- let end_time = start_time + duration_to_millis(&sleep_time);
- while self.event_index < self.test_events.len() &&
- self.test_events[self.event_index].time <= end_time {
- self.test_events[self.event_index].deliver(&self.paths,
- &mut self.dbus_fifo_out,
- &mut self.low_mem_device);
- self.event_index += 1;
- }
- if self.event_index == self.test_events.len() {
- self.quit_request = true;
- }
- self.current_time = end_time;
- }
-}
-
+#[cfg(not(test))]
struct GenuineTimer {}
+#[cfg(not(test))]
impl Timer for GenuineTimer {
// Returns current uptime (active time since boot, in milliseconds)
fn now(&self) -> i64 {
@@ -574,7 +348,6 @@
}
}
-
#[derive(Copy, Clone)]
struct Sysinfo(libc::sysinfo);
@@ -807,17 +580,6 @@
Ok(str::from_utf8(&buffer[..length as usize])?)
}
-fn read_nonblocking_pipe(file: &mut File, mut buf: &mut [u8]) -> Result<usize> {
- let status = file.read(&mut buf);
- let read_bytes = match status {
- Ok(n) => n,
- Err(_) if errno() == libc::EAGAIN => 0,
- Err(_) => return Err("cannot read pipe".into()),
- };
- Ok(read_bytes)
-}
-
-
struct Watermarks { min: u32, low: u32, high: u32 }
struct ZoneinfoFile(File);
@@ -852,11 +614,13 @@
fn process_chrome_events(&mut self, watcher: &mut FileWatcher) -> Result<(i32,i32)>;
}
+#[cfg(not(test))]
struct GenuineDbus {
connection: dbus::Connection,
fds: Vec<RawFd>,
}
+#[cfg(not(test))]
impl Dbus for GenuineDbus {
fn get_fds(&self) -> &Vec<RawFd> {
&self.fds
@@ -918,6 +682,7 @@
}
}
+#[cfg(not(test))]
impl GenuineDbus {
// A GenuineDbus object contains a D-Bus connection used to receive
// information from Chrome about events of interest (e.g. tab discards).
@@ -931,55 +696,8 @@
}
}
-struct MockDbus {
- fds: Vec<RawFd>,
- fifo_in: File,
- fifo_out: Option<File>, // using Option merely to use take()
-}
-
-impl Dbus for MockDbus {
- fn get_fds(&self) -> &Vec<RawFd> {
- &self.fds
- }
-
- // Processes any mock chrome events. Events are strings separated by
- // newlines sent to the event pipe. We could check if the pipe fired in
- // the watcher, but it's less code to just do a non-blocking read.
- fn process_chrome_events(&mut self, _watcher: &mut FileWatcher) -> Result<(i32, i32)> {
- let mut buf = [0u8; 4096];
- let read_bytes = read_nonblocking_pipe(&mut self.fifo_in, &mut buf)?;
- let events = str::from_utf8(&buf[..read_bytes])?.lines();
- let mut tab_discard_count = 0;
- let mut oom_kill_count = 0;
- for event in events {
- match event {
- "tab-discard" => tab_discard_count += 1,
- "oom-kill" => oom_kill_count += 1,
- other => return Err(format!("unexpected mock event {:?}", other).into()),
- };
- }
- Ok((tab_discard_count, oom_kill_count))
- }
-}
-
-impl MockDbus {
- fn new(fifo_path: &Path) -> Result<MockDbus> {
- let fifo_in = OpenOptions::new()
- .custom_flags(libc::O_NONBLOCK)
- .read(true)
- .open(&fifo_path)?;
- let fds = vec![fifo_in.as_raw_fd()];
- let fifo_out = OpenOptions::new()
- .custom_flags(libc::O_NONBLOCK)
- .write(true)
- .open(&fifo_path)?;
- Ok(MockDbus { fds, fifo_in, fifo_out: Some(fifo_out) })
- }
-}
-
// The main object.
struct Sampler<'a> {
- testing: bool, // Set to true when running integration test.
always_poll_fast: bool, // When true, program stays in fast poll mode.
paths: &'a Paths, // Paths of files used by the program.
dbus: Box<Dbus>, // Used to receive Chrome event notifications.
@@ -1000,8 +718,7 @@
impl<'a> Sampler<'a> {
- fn new(testing: bool,
- always_poll_fast: bool,
+ fn new(always_poll_fast: bool,
paths: &'a Paths,
timer: Box<Timer>,
dbus: Box<Dbus>) -> Sampler {
@@ -1037,7 +754,6 @@
let sample_header = build_sample_header();
let mut sampler = Sampler {
- testing,
always_poll_fast,
dbus,
low_mem_margin: read_int(&paths.low_mem_margin).unwrap_or(0),
@@ -1082,14 +798,13 @@
sample.sample_type = sample_type;
sample.available = self.current_available;
sample.runnables = get_runnables(&self.files.runnables_file)?;
- sample.info = if self.testing { Sysinfo::fake_sysinfo()? } else { Sysinfo::sysinfo()? };
+ sample.info = if cfg!(test) { Sysinfo::fake_sysinfo()? } else { Sysinfo::sysinfo()? };
get_vmstats(&self.files.vmstat_file, &mut sample.vmstat_values)?;
}
self.refresh_time();
Ok(())
}
-
// Reads oom-kill events from trace_pipe (passed as |fd|) and adds them to
// the sample queue. Returns true if clip collection must be started.
//
@@ -1486,7 +1201,11 @@
TabDiscard, // Chrome browser letting us know about a tab discard.
Timer, // Event was generated after FAST_POLL_PERIOD_DURATION with no other events.
Uninitialized, // Internal use.
+ #[cfg(not(test))]
Unknown, // Unexpected D-Bus signal.
+ #[cfg(test)]
+ #[allow(dead_code)]
+ Unknown, // Never created when testing.
}
impl Default for SampleType {
@@ -1519,6 +1238,7 @@
}
}
+#[cfg(not(test))]
// Returns the sample type for a Chrome signal (the string payload in the DBus
// message).
fn sample_type_from_signal(signal: &str) -> SampleType {
@@ -1532,7 +1252,7 @@
// Path names of various system files, mostly in /proc, /sys, and /dev. They
// are collected into this struct because they get special values when testing.
#[derive(Clone)]
-struct Paths {
+pub struct Paths {
vmstat: PathBuf,
available: PathBuf,
runnables: PathBuf,
@@ -1547,7 +1267,6 @@
static_parameters: PathBuf,
zoneinfo: PathBuf,
procsysvm: PathBuf,
- mock_dbus_fifo: PathBuf,
testing_root: PathBuf,
}
@@ -1617,12 +1336,7 @@
Some("memd")).expect("cannot initialize syslog");
}
- run_memory_daemon(testing, always_poll_fast);
-}
-
-#[test]
-fn memory_daemon_test() {
- run_memory_daemon(true, false);
+ run_memory_daemon(always_poll_fast);
}
fn testing_root() -> String {
@@ -1630,12 +1344,17 @@
format!("/tmp/memd-testing-root-{}", unsafe { libc::getpid() })
}
-fn run_memory_daemon(testing: bool, always_poll_fast: bool) {
+#[test]
+fn memory_daemon_test() {
+ run_memory_daemon(false);
+}
+
+fn run_memory_daemon(always_poll_fast: bool) {
warn!("memd started");
let testing_root = testing_root();
// make_paths! returns a Paths object initializer with these fields.
let paths = make_paths!(
- testing,
+ cfg!(test),
&testing_root,
vmstat: "/proc/vmstat",
available: LOW_MEM_SYSFS.to_string() + "/available",
@@ -1651,11 +1370,11 @@
static_parameters: LOG_DIRECTORY.to_string() + "/" + STATIC_PARAMETERS_LOG,
zoneinfo: "/proc/zoneinfo",
procsysvm: "/proc/sys/vm",
- mock_dbus_fifo: "/".to_string() + TESTING_MOCK_DBUS_FIFO_NAME,
);
- if testing {
- setup_test_environment(&paths);
+ #[cfg(test)]
+ {
+ test::setup_test_environment(&paths);
let var_log = &paths.log_directory.parent().unwrap();
std::fs::create_dir_all(var_log).expect("cannot create /var/log");
}
@@ -1667,583 +1386,18 @@
.expect(&format!("cannot create log directory {:?}", &paths.log_directory));
}
- if !testing {
+ #[cfg(test)]
+ test::test_loop(always_poll_fast, &paths);
+
+ #[cfg(not(test))]
+ {
let timer = Box::new(GenuineTimer {});
let dbus = Box::new(GenuineDbus::new().expect("cannot connect to dbus"));
- let mut sampler = Sampler::new(testing, always_poll_fast, &paths, timer, dbus);
+ let mut sampler = Sampler::new(always_poll_fast, &paths, timer, dbus);
loop {
// Run forever, alternating between slow and fast poll.
sampler.slow_poll().expect("slow poll error");
sampler.fast_poll().expect("fast poll error");
}
- } else {
- for test_desc in TEST_DESCRIPTORS.iter() {
- // Every test run requires a (mock) restart of the daemon.
- println!("--------------\nrunning test:\n{}", test_desc);
- // Clean up log directory.
- std::fs::remove_dir_all(&paths.log_directory).expect("cannot remove /var/log/memd");
- std::fs::create_dir_all(&paths.log_directory).expect("cannot create /var/log/memd");
-
- let events = events_from_test_descriptor(test_desc);
- let mut dbus = Box::new(MockDbus::new(&paths.mock_dbus_fifo)
- .expect("cannot create mock dbus"));
- let timer = Box::new(MockTimer::new(events, paths.clone(),
- dbus.fifo_out.take().unwrap()));
- let mut sampler = Sampler::new(testing, always_poll_fast, &paths, timer, dbus);
- loop {
- // Alternate between slow and fast poll.
- sampler.slow_poll().expect("slow poll error");
- if sampler.quit_request {
- break;
- }
- sampler.fast_poll().expect("fast poll error");
- if sampler.quit_request {
- break;
- }
- }
- verify_test_results(test_desc, &paths.log_directory)
- .expect(&format!("test:{}failed.", test_desc));
- println!("test succeeded\n--------------");
- }
- teardown_test_environment(&paths);
}
}
-
-// ================
-// Test Descriptors
-// ================
-//
-// Define events and expected result using "ASCII graphics".
-//
-// The top lines of the test descriptor (all lines except the last one) define
-// sequences of events. The last line describes the expected result.
-//
-// Events are single characters:
-//
-// M = start medium pressure (fast poll)
-// H = start high pressure (low-mem notification)
-// L = start low pressure (slow poll)
-// <digit> = tab discard
-// K = kernel OOM kill
-// k = chrome notification of OOM kill
-// ' ', . = nop (just wait 1 second)
-// | = ignored (no delay), cosmetic only
-//
-// - each character indicates a 1-second slot
-// - events (if any) happen at the beginning of their slot
-// - multiple events in the same slot are stacked vertically
-//
-// Example:
-//
-// ..H.1..L
-// 2
-//
-// means:
-// - wait 2 seconds
-// - signal high-memory pressure, wait 1 second
-// - wait 1 second
-// - signal two tab discard events (named 1 and 2), wait 1 second
-// - wait 2 more seconds
-// - return to low-memory pressure
-//
-// The last line describes the expected clip logs. Each log is identified by
-// one digit: 0 for memd.clip000.log, 1 for memd.clip001.log etc. The positions
-// of the digits correspond to the time span covered by each clip. So a clip
-// file whose description is 5 characters long is supposed to contain 5 seconds
-// worth of samples.
-//
-// For readability, the descriptor must start and end with newlines, which are
-// removed. Also, indentation (common all-space prefixes) is removed.
-
-const TEST_DESCRIPTORS: &[&str] = &[
- // Very simple test: go from slow poll to fast poll and back. No clips
- // are collected.
- "
- .M.L.
- .....
- ",
-
- // Simple test: start fast poll, signal low-mem, signal tab discard.
- "
- .M...H..1.....L
- ..00000000001..
- ",
- // Two full disjoint clips. Also tests kernel-reported and chrome-reported OOM
- // kills.
- "
- .M......K.............k.....
- ...0000000000....1111111111.
- ",
-
- // Test that clip collection continues for the time span of interest even if
- // memory pressure returns quickly to a low level. Note that the
- // medium-pressure event (M) is at t = 1s, but the fast poll starts at 2s
- // (multiple of 2s slow-poll period).
-
- "
- .MH1L.....
- ..000000..
- ",
-
- // Several discards, which result in three consecutive clips. Tab discards 1
- // and 2 produce an 8-second clip because the first two seconds of data are
- // missing. Also see the note above regarding fast poll start.
- "
- ...M.H12..|...3...6..|.7.....L
- | 4 |
- | 5 |
- ....000000|0011111111|112222..
- ",
-];
-
-fn trim_descriptor(descriptor: &str) -> Vec<Vec<u8>> {
- // Remove vertical bars. Don't check for consistent use because it's easy
- // enough to notice visually.
- let barless_descriptor: String = descriptor.chars().filter(|c| *c != '|').collect();
- // Split string into lines.
- let all_lines: Vec<String> = barless_descriptor.split('\n').map(String::from).collect();
- // A test descriptor must start and end with empty lines, and have at least
- // one line of events, and exactly one line to describe the clip files.
- assert!(all_lines.len() >= 4, "invalid test descriptor format");
- // Remove first and last line.
- let valid_lines = all_lines[1 .. all_lines.len()-1].to_vec();
- // Find indentation amount. Unwrap() cannot fail because of previous assert.
- let indent = valid_lines.iter().map(|s| s.len() - s.trim_left().len()).min().unwrap();
- // Remove indentation.
- let trimmed_lines: Vec<Vec<u8>> =
- valid_lines.iter().map(|s| s[indent..].to_string().into_bytes()).collect();
- trimmed_lines
-}
-
-fn events_from_test_descriptor(descriptor: &str) -> Vec<TestEvent> {
- let all_descriptors = trim_descriptor(descriptor);
- let event_sequences = &all_descriptors[..all_descriptors.len() - 1];
- let max_length = event_sequences.iter().map(|d| d.len()).max().unwrap();
- let mut events = vec![];
- for i in 0 .. max_length {
- for seq in event_sequences {
- // Each character represents one second. Time unit is milliseconds.
- let mut opt_type = None;
- if i < seq.len() {
- match seq[i] {
- b'0' | b'1' | b'2' | b'3' | b'4' |
- b'5' | b'6' | b'7' | b'8' | b'9'
- => opt_type = Some(TestEventType::TabDiscard),
- b'H' => opt_type = Some(TestEventType::EnterHighPressure),
- b'M' => opt_type = Some(TestEventType::EnterMediumPressure),
- b'L' => opt_type = Some(TestEventType::EnterLowPressure),
- b'k' => opt_type = Some(TestEventType::OomKillBrowser),
- b'K' => opt_type = Some(TestEventType::OomKillKernel),
- b'.' | b' ' | b'|' => {},
- x => panic!("unexpected character {} in descriptor '{}'", &x, descriptor),
- }
- }
- if let Some(t) = opt_type {
- events.push(TestEvent { time: i as i64 * 1000, event_type: t });
- }
- }
- }
- events
-}
-
-// Given a descriptor string for the expected clips, returns a vector of start
-// and end time of each clip.
-fn expected_clips(descriptor: &[u8]) -> Vec<(i64, i64)> {
- let mut time = 0;
- let mut clip_start_time = 0;
- let mut previous_clip = b'0' - 1;
- let mut previous_char = 0u8;
- let mut clips = vec![];
-
- for &c in descriptor {
- if c != previous_char {
- if (previous_char as char).is_digit(10) {
- // End of clip.
- clips.push((clip_start_time, time));
- }
- if (c as char).is_digit(10) {
- // Start of clip.
- clip_start_time = time;
- assert_eq!(c, previous_clip + 1, "malformed clip descriptor");
- previous_clip = c;
- }
- }
- previous_char = c;
- time += 1000;
- }
- clips
-}
-
-// Converts a string starting with a timestamp in seconds (#####.##, with two
-// decimal digits) to a timestamp in milliseconds.
-fn time_from_sample_string(line: &str) -> Result<i64> {
- let mut tokens = line.split(|c: char| !c.is_digit(10));
- let seconds = match tokens.next() {
- Some(digits) => digits.parse::<i64>().unwrap(),
- None => return Err("no digits in string".into())
- };
- let centiseconds = match tokens.next() {
- Some(digits) => if digits.len() == 2 {
- digits.parse::<i64>().unwrap()
- } else {
- return Err("expecting 2 decimals".into());
- },
- None => return Err("expecting at least two groups of digits".into()),
- };
- Ok(seconds * 1000 + centiseconds * 10)
-}
-
-macro_rules! assert_approx_eq {
- ($x:expr, $y: expr, $tolerance: expr, $format:expr $(, $arg:expr)*) => {{
- let x = $x;
- let y = $y;
- let tolerance = $tolerance;
- let y_min = y - tolerance;
- let y_max = y + tolerance;
- assert!(x < y_max && x > y_min, $format $(, $arg)*);
- }}
-}
-
-fn check_clip(clip_times: (i64, i64), clip_path: PathBuf, events: &Vec<TestEvent>) -> Result<()> {
- let clip_name = clip_path.to_string_lossy();
- let mut clip_file = File::open(&clip_path)?;
- let mut file_content = String::new();
- clip_file.read_to_string(&mut file_content)?;
- let lines = file_content.lines().collect::<Vec<&str>>();
- // First line is time stamp. Second line is field names. Check count of
- // field names and field values in the third line (don't bother to check
- // the other lines).
- let name_count = lines[1].split_whitespace().count();
- let value_count = lines[2].split_whitespace().count();
- assert_eq!(name_count, value_count);
-
- // Check first and last time stamps.
- let start_time = time_from_sample_string(&lines[2]).expect("cannot parse first timestamp");
- let end_time = time_from_sample_string(&lines[lines.len() - 1])
- .expect("cannot parse last timestamp");
- let expected_start_time = clip_times.0;
- let expected_end_time = clip_times.1;
- // Milliseconds of slack allowed on start/stop times. We allow one full
- // fast poll period to take care of edge cases. The specs don't need to be
- // tight here because it doesn't matter if we collect one fewer sample (or
- // an extra one) at each end.
- let slack_ms = 101i64;
- assert_approx_eq!(start_time, expected_start_time, slack_ms,
- "unexpected start time for {}", clip_name);
- assert_approx_eq!(end_time, expected_end_time, slack_ms,
- "unexpected end time for {}", clip_name);
-
- // Check sample count.
- let expected_sample_count_from_events: usize = events.iter()
- .map(|e| if e.time <= start_time || e.time > end_time {
- 0
- } else {
- match e.event_type {
- // OomKillKernel generates two samples.
- TestEventType::OomKillKernel => 2,
- // These generate 0 samples.
- TestEventType::EnterLowPressure |
- TestEventType::EnterMediumPressure => 0,
- _ => 1,
- }
- })
- .sum();
- // We include samples both at the beginning and end of the range, so we
- // need to add 1. Note that here we use the actual sample times, not the
- // expected times.
- let expected_sample_count_from_timer = ((end_time - start_time) / 100) as usize + 1;
- let expected_sample_count = expected_sample_count_from_events + expected_sample_count_from_timer;
- let sample_count = lines.len() - 2;
- assert_eq!(sample_count, expected_sample_count, "unexpected sample count for {}", clip_name);
- Ok(())
-}
-
-fn verify_test_results(descriptor: &str, log_directory: &PathBuf) -> Result<()> {
- let all_descriptors = trim_descriptor(descriptor);
- let result_descriptor = &all_descriptors[all_descriptors.len() - 1];
- let clips = expected_clips(result_descriptor);
- let events = events_from_test_descriptor(descriptor);
-
- // Check that there are no more clips than expected.
- let files_count = std::fs::read_dir(log_directory)?.count();
- assert_eq!(clips.len() + 1, files_count, "wrong number of clip files");
-
- let mut clip_number = 0;
- for clip in clips {
- let clip_path = log_directory.join(format!("memd.clip{:03}.log", clip_number));
- check_clip(clip, clip_path, &events)?;
- clip_number += 1;
- }
- Ok(())
-}
-
-fn create_dir_all(path: &Path) -> Result<()> {
- let result = std::fs::create_dir_all(path);
- match result {
- Ok(_) => Ok(()),
- Err(e) => Err(format!("create_dir_all: {}: {:?}", path.to_string_lossy(), e).into())
- }
-}
-
-fn teardown_test_environment (paths: &Paths) {
- if let Err(e) = std::fs::remove_dir_all(&paths.testing_root) {
- info!("teardown: could not remove {}: {:?}", paths.testing_root.to_str().unwrap(), e);
- }
-}
-
-fn setup_test_environment(paths: &Paths) {
- std::fs::create_dir(&paths.testing_root).
- expect(&format!("cannot create {}", paths.testing_root.to_str().unwrap()));
- mkfifo(&paths.mock_dbus_fifo).expect("failed to make mock dbus fifo");
- create_dir_all(paths.vmstat.parent().unwrap()).expect("cannot create /proc");
- create_dir_all(paths.available.parent().unwrap()).expect("cannot create ../chromeos-low-mem");
- create_dir_all(paths.trace_pipe.parent().unwrap()).expect("cannot create ../tracing");
- let sys_vm = paths.testing_root.join("proc/sys/vm");
- create_dir_all(&sys_vm).expect("cannot create /proc/sys/vm");
- create_dir_all(paths.low_mem_device.parent().unwrap()).expect("cannot create /dev");
-
- let vmstat_content = "\
-nr_free_pages 50489
-nr_alloc_batch 3326
-nr_inactive_anon 48591
-nr_active_anon 145673
-nr_inactive_file 49597
-nr_active_file 50199
-nr_unevictable 0
-nr_mlock 0
-nr_anon_pages 180171
-nr_mapped 63194
-nr_file_pages 149074
-nr_dirty 136
-nr_writeback 0
-nr_slab_reclaimable 10484
-nr_slab_unreclaimable 16134
-nr_page_table_pages 9255
-nr_kernel_stack 979
-nr_unstable 0
-nr_bounce 0
-nr_vmscan_write 60544150
-nr_vmscan_immediate_reclaim 29587
-nr_writeback_temp 0
-nr_isolated_anon 0
-nr_isolated_file 0
-nr_shmem 8982
-nr_dirtied 3909911
-nr_written 64426230
-nr_pages_scanned 88
-workingset_refault 16082484
-workingset_activate 1186339
-workingset_nodereclaim 0
-nr_anon_transparent_hugepages 2
-nr_free_cma 0
-nr_dirty_threshold 68740
-nr_dirty_background_threshold 5728
-pgpgin 41070493
-pgpgout 16914684
-pswpin 59158894
-pswpout 60513438
-pgalloc_dma 3247
-pgalloc_dma32 131210265
-pgalloc_normal 0
-pgalloc_movable 0
-pgfree 140534607
-pgactivate 70428610
-pgdeactivate 79401090
-pgfault 110047495
-pgmajfault 52561387
-pgmajfault_s 97569
-pgmajfault_a 52314090
-pgmajfault_f 149728
-pgrefill_dma 4749
-pgrefill_dma32 79973171
-pgrefill_normal 0
-pgrefill_movable 0
-pgsteal_kswapd_dma 112
-pgsteal_kswapd_dma32 62450553
-pgsteal_kswapd_normal 0
-pgsteal_kswapd_movable 0
-pgsteal_direct_dma 0
-pgsteal_direct_dma32 18534641
-pgsteal_direct_normal 0
-pgsteal_direct_movable 0
-pgscan_kswapd_dma 4776
-pgscan_kswapd_dma32 126413953
-pgscan_kswapd_normal 0
-pgscan_kswapd_movable 0
-pgscan_direct_dma 11
-pgscan_direct_dma32 36676518
-pgscan_direct_normal 0
-pgscan_direct_movable 0
-pgscan_direct_throttle 0
-pginodesteal 3302
-slabs_scanned 24535326
-kswapd_inodesteal 237022
-kswapd_low_wmark_hit_quickly 24998
-kswapd_high_wmark_hit_quickly 40300
-pageoutrun 87752
-allocstall 376923
-pgrotated 247987
-drop_pagecache 0
-drop_slab 0
-pgmigrate_success 8814551
-pgmigrate_fail 94348
-compact_migrate_scanned 86192323
-compact_free_scanned 1982032044
-compact_isolated 19084449
-compact_stall 20280
-compact_fail 17323
-compact_success 2957
-unevictable_pgs_culled 0
-unevictable_pgs_scanned 0
-unevictable_pgs_rescued 0
-unevictable_pgs_mlocked 0
-unevictable_pgs_munlocked 0
-unevictable_pgs_cleared 0
-unevictable_pgs_stranded 0
-thp_fault_alloc 15
-thp_fault_fallback 0
-thp_collapse_alloc 7894
-thp_collapse_alloc_failed 4784
-thp_split 13
-thp_zero_page_alloc 0
-thp_zero_page_alloc_failed 0
-";
- let zoneinfo_content = "\
-Node 0, zone DMA
- pages free 1756
- min 179
- low 223
- high 268
- scanned 0
- spanned 4095
- present 3999
- managed 3977
- nr_free_pages 1756
- nr_alloc_batch 45
- nr_inactive_anon 0
- nr_active_anon 0
- nr_inactive_file 238
- nr_active_file 377
- nr_unevictable 0
- nr_mlock 0
- nr_anon_pages 0
- nr_mapped 284
- nr_file_pages 615
- nr_dirty 0
- nr_writeback 0
- nr_slab_reclaimable 23
- nr_slab_unreclaimable 22
- nr_page_table_pages 2
- nr_kernel_stack 0
- nr_unstable 0
- nr_bounce 0
- nr_vmscan_write 112
- nr_vmscan_immediate_reclaim 0
- nr_writeback_temp 0
- nr_isolated_anon 0
- nr_isolated_file 0
- nr_shmem 0
- nr_dirtied 352
- nr_written 464
- nr_pages_scanned 0
- workingset_refault 0
- workingset_activate 0
- workingset_nodereclaim 0
- nr_anon_transparent_hugepages 0
- nr_free_cma 0
- protection: (0, 1928, 1928, 1928)
- pagesets
- cpu: 0
- count: 0
- high: 0
- batch: 1
- vm stats threshold: 4
- cpu: 1
- count: 0
- high: 0
- batch: 1
- vm stats threshold: 4
- all_unreclaimable: 1
- start_pfn: 1
- inactive_ratio: 1
-Node 0, zone DMA32
- pages free 56774
- min 22348
- low 27935
- high 33522
- scanned 0
- spanned 506914
- present 506658
- managed 494688
- nr_free_pages 56774
- nr_alloc_batch 3023
- nr_inactive_anon 48059
- nr_active_anon 138044
- nr_inactive_file 47952
- nr_active_file 49885
- nr_unevictable 0
- nr_mlock 0
- nr_anon_pages 170349
- nr_mapped 62610
- nr_file_pages 147990
- nr_dirty 231
- nr_writeback 0
- nr_slab_reclaimable 10414
- nr_slab_unreclaimable 16112
- nr_page_table_pages 9231
- nr_kernel_stack 981
- nr_unstable 0
- nr_bounce 0
- nr_vmscan_write 60554686
- nr_vmscan_immediate_reclaim 29587
- nr_writeback_temp 0
- nr_isolated_anon 0
- nr_isolated_file 0
- nr_shmem 8982
- nr_dirtied 3918108
- nr_written 64443856
- nr_pages_scanned 0
- workingset_refault 16084171
- workingset_activate 1186426
- workingset_nodereclaim 0
- nr_anon_transparent_hugepages 2
- nr_free_cma 0
- protection: (0, 0, 0, 0)
- pagesets
- cpu: 0
- count: 135
- high: 186
- batch: 31
- vm stats threshold: 20
- cpu: 1
- count: 166
- high: 186
- batch: 31
- vm stats threshold: 20
- all_unreclaimable: 0
- start_pfn: 4096
- inactive_ratio: 3
-";
- print_to_path!(&paths.vmstat, "{}", vmstat_content).expect("cannot initialize vmstat");
- print_to_path!(&paths.zoneinfo, "{}", zoneinfo_content).expect("cannot initialize zoneinfo");
- print_to_path!(&paths.available, "{}\n", TESTING_LOW_MEM_HIGH_AVAILABLE)
- .expect("cannot initialize available");
- print_to_path!(&paths.runnables, "0.16 0.18 0.22 4/981 8504")
- .expect("cannot initialize runnables");
- print_to_path!(&paths.tracing_enabled, "1\n").expect("cannot initialize tracing_enabled");
- print_to_path!(&paths.tracing_on, "1\n").expect("cannot initialize tracing_on");
- print_to_path!(&paths.set_ftrace_filter, "").expect("cannot initialize set_ftrace_filter");
- print_to_path!(&paths.current_tracer, "").expect("cannot initialize current_tracer");
- print_to_path!(&paths.low_mem_margin, "{}", TESTING_LOW_MEM_MARGIN)
- .expect("cannot initialize low_mem_margin");
- mkfifo(&paths.trace_pipe).expect("could not make mock trace_pipe");
-
- print_to_path!(sys_vm.join("min_filelist_kbytes"), "100000\n")
- .expect("cannot initialize min_filelist_kbytes");
- print_to_path!(sys_vm.join("min_free_kbytes"), "80000\n")
- .expect("cannot initialize min_free_kbytes");
- print_to_path!(sys_vm.join("extra_free_kbytes"), "60000\n")
- .expect("cannot initialize extra_free_kbytes");
-
- mkfifo(&paths.low_mem_device).expect("could not make mock low-mem device");
-}
diff --git a/metrics/memd/src/test.rs b/metrics/memd/src/test.rs
new file mode 100644
index 0000000..88aa61b
--- /dev/null
+++ b/metrics/memd/src/test.rs
@@ -0,0 +1,674 @@
+// Copyright 2018 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Test code for memd.
+
+use libc;
+use std;
+use std::fs::{File,OpenOptions};
+use std::io::prelude::*;
+use std::os::unix::fs::OpenOptionsExt;
+use std::os::unix::io::{AsRawFd,RawFd};
+use std::path::{Path, PathBuf};
+use std::str;
+use std::time::Duration;
+
+// Imported from main program
+use Dbus;
+use errno;
+use FileWatcher;
+use PAGE_SIZE;
+use Paths;
+use Result;
+use Sampler;
+use strerror;
+use Timer;
+use write_string;
+
+// Different levels of emulated available RAM in MB.
+const LOW_MEM_LOW_AVAILABLE: usize = 150;
+const LOW_MEM_MEDIUM_AVAILABLE: usize = 300;
+const LOW_MEM_HIGH_AVAILABLE: usize = 1000;
+const LOW_MEM_MARGIN: usize = 200;
+const MOCK_DBUS_FIFO_NAME: &str = "mock-dbus-fifo";
+
+macro_rules! print_to_path {
+ ($path:expr, $format:expr $(, $arg:expr)*) => {{
+ let r = OpenOptions::new().write(true).create(true).open($path);
+ match r {
+ Err(e) => Err(e),
+ Ok(mut f) => f.write_all(format!($format $(, $arg)*).as_bytes())
+ }
+ }}
+}
+
+fn duration_to_millis(duration: &Duration) -> i64 {
+ duration.as_secs() as i64 * 1000 + duration.subsec_nanos() as i64 / 1_000_000
+}
+
+fn read_nonblocking_pipe(file: &mut File, mut buf: &mut [u8]) -> Result<usize> {
+ let status = file.read(&mut buf);
+ let read_bytes = match status {
+ Ok(n) => n,
+ Err(_) if errno() == libc::EAGAIN => 0,
+ Err(_) => return Err("cannot read pipe".into()),
+ };
+ Ok(read_bytes)
+}
+
+fn non_blocking_select(high_fd: i32, inout_read_fds: &mut libc::fd_set) -> i32 {
+ let mut null_timeout = libc::timeval {
+ tv_sec: 0 as libc::time_t,
+ tv_usec: 0 as libc::suseconds_t,
+ };
+ let null = std::ptr::null_mut();
+ // Safe because we're passing valid values and addresses.
+ unsafe {
+ libc::select(high_fd,
+ inout_read_fds,
+ null,
+ null,
+ &mut null_timeout as *mut libc::timeval)
+ }
+}
+
+fn mkfifo(path: &PathBuf) -> Result<()> {
+ let path_name = path.to_str().unwrap();
+ let c_path = std::ffi::CString::new(path_name).unwrap();
+ // Safe because c_path points to a valid C string.
+ let status = unsafe { libc::mkfifo(c_path.as_ptr(), 0o644) };
+ if status < 0 {
+ Err(format!("mkfifo: {}: {}", path_name, strerror(errno())).into())
+ } else {
+ Ok(())
+ }
+}
+
+// The types of events which are generated internally for testing. They
+// simulate state changes (for instance, change in the memory pressure level),
+// chrome events, and kernel events.
+enum TestEventType {
+ EnterHighPressure, // enter low available RAM (below margin) state
+ EnterLowPressure, // enter high available RAM state
+ EnterMediumPressure, // set enough memory pressure to trigger fast sampling
+ OomKillBrowser, // fake browser report of OOM kill
+ OomKillKernel, // fake kernel report of OOM kill
+ TabDiscard, // fake browser report of tab discard
+}
+
+// Internally generated event for testing.
+struct TestEvent {
+ time: i64,
+ event_type: TestEventType,
+}
+
+impl TestEvent {
+ fn deliver(&self, paths: &Paths, dbus_fifo: &mut File, low_mem_device: &mut File) {
+ match self.event_type {
+ TestEventType::EnterLowPressure =>
+ self.low_mem_notify(LOW_MEM_HIGH_AVAILABLE, &paths, low_mem_device),
+ TestEventType::EnterMediumPressure =>
+ self.low_mem_notify(LOW_MEM_MEDIUM_AVAILABLE, &paths, low_mem_device),
+ TestEventType::EnterHighPressure =>
+ self.low_mem_notify(LOW_MEM_LOW_AVAILABLE, &paths, low_mem_device),
+ TestEventType::OomKillBrowser => self.send_signal("oom-kill", dbus_fifo),
+ TestEventType::OomKillKernel => self.mock_kill(&paths.trace_pipe),
+ TestEventType::TabDiscard => self.send_signal("tab-discard", dbus_fifo),
+ }
+ }
+
+ fn low_mem_notify(&self, amount: usize, paths: &Paths, mut low_mem_device: &mut File) {
+ write_string(&amount.to_string(), &paths.available, false).
+ expect("available file: write failed");
+ if amount == LOW_MEM_LOW_AVAILABLE {
+ // Make low-mem device ready-to-read.
+ write!(low_mem_device, ".").expect("low-mem-device: write failed");
+ } else {
+ let mut buf = [0; PAGE_SIZE];
+ read_nonblocking_pipe(&mut low_mem_device, &mut buf)
+ .expect("low-mem-device: clear failed");
+ }
+ }
+
+ fn send_signal(&self, signal: &str, dbus_fifo: &mut File) {
+ write!(dbus_fifo, "{}\n", signal).expect("mock dbus: write failed");
+ }
+
+ fn mock_kill(&self, path: &PathBuf) {
+ // example string (8 spaces before the first non-space):
+ // chrome-13700 [001] .... 867348.061651: oom_kill_process <-out_of_memory
+ let s = format!("chrome-13700 [001] .... {}.{:06}: oom_kill_process <-out_of_memory\n",
+ self.time / 1000, (self.time % 1000) * 1000);
+ write_string(&s, path, true).expect("mock trace_pipe: write failed");
+ }
+}
+
+// Real time mock, for testing only. It removes time races (for better or
+// worse) and makes it possible to run the test on build machines which may be
+// heavily loaded.
+//
+// Time is mocked by assuming that CPU speed is infinite and time passes only
+// when the program is asleep. Time advances in discrete jumps when we call
+// either sleep() or select() with a timeout.
+
+struct MockTimer {
+ current_time: i64, // the current time
+ test_events: Vec<TestEvent>, // list events to be delivered
+ event_index: usize, // index of next event to be delivered
+ paths: Paths, // for event delivery
+ dbus_fifo_out: File, // for mock dbus event delivery
+ low_mem_device: File, // for delivery of low-mem notifications
+ quit_request: bool, // for termination
+ _trace_pipe_aux: File, // to avoid EOF on trace pipe, which fires select
+}
+
+impl MockTimer {
+ fn new(test_events: Vec<TestEvent>, paths: Paths, dbus_fifo_out: File) -> MockTimer {
+ let low_mem_device = OpenOptions::new()
+ .custom_flags(libc::O_NONBLOCK)
+ .read(true)
+ .write(true)
+ .open(&paths.low_mem_device).expect("low-mem-device: cannot setup");
+ // We need to open the fifo one extra time because otherwise the fifo
+ // is still ready-to-read after consuming all data in its buffer, even
+ // though the following read will return EOF. The ready-to-read status
+ // will cause the select() to fire incorrectly. By having another open
+ // write descriptor, there is no EOF, and the select does not fire.
+ let _trace_pipe_aux = OpenOptions::new()
+ .custom_flags(libc::O_NONBLOCK)
+ .read(true)
+ .write(true)
+ .open(&paths.trace_pipe).expect("trace_pipe: cannot setup");
+ MockTimer {
+ current_time: 0,
+ test_events,
+ event_index: 0,
+ paths,
+ dbus_fifo_out,
+ low_mem_device,
+ quit_request: false,
+ _trace_pipe_aux,
+ }
+ }
+}
+
+impl Timer for MockTimer {
+ fn now(&self) -> i64 {
+ self.current_time
+ }
+
+ fn quit_request(&self) -> bool {
+ self.quit_request
+ }
+
+ // Mock select first checks if any events are pending, then produces events
+ // that would happen during its sleeping time, and checks if those events
+ // are delivered.
+ fn select(&mut self,
+ high_fd: i32,
+ inout_read_fds: &mut libc::fd_set,
+ timeout: &Duration) -> i32 {
+ // First check for pending events which may have been delivered during
+ // a preceding sleep.
+ let n = non_blocking_select(high_fd, inout_read_fds);
+ if n != 0 {
+ return n;
+ }
+ // Now time can start passing, as long as there are more events.
+ let timeout_ms = duration_to_millis(&timeout);
+ let end_time = self.current_time + timeout_ms;
+ if self.event_index == self.test_events.len() {
+ // No more events to deliver, so no need for further select() calls.
+ self.quit_request = true;
+ self.current_time = end_time;
+ return 0;
+ }
+ // There are still event to be delivered.
+ let first_event_time = self.test_events[self.event_index].time;
+ if first_event_time > end_time {
+ // No event to deliver before the timeout, thus no events to look for.
+ self.current_time = end_time;
+ return 0;
+ }
+ // Deliver all events with the time stamp of the first event. (There
+ // is at least one.)
+ while {
+ self.test_events[self.event_index].deliver(&self.paths,
+ &mut self.dbus_fifo_out,
+ &mut self.low_mem_device);
+ self.event_index += 1;
+ self.event_index < self.test_events.len() &&
+ self.test_events[self.event_index].time == first_event_time
+ } {}
+ // One or more events were delivered, so run another select.
+ let n = non_blocking_select(high_fd, inout_read_fds);
+ if n > 0 {
+ self.current_time = first_event_time;
+ } else {
+ self.current_time = end_time;
+ }
+ n
+ }
+
+ // Mock sleep produces all events that would happen during that sleep, then
+ // updates the time.
+ fn sleep(&mut self, sleep_time: &Duration) {
+ let start_time = self.current_time;
+ let end_time = start_time + duration_to_millis(&sleep_time);
+ while self.event_index < self.test_events.len() &&
+ self.test_events[self.event_index].time <= end_time {
+ self.test_events[self.event_index].deliver(&self.paths,
+ &mut self.dbus_fifo_out,
+ &mut self.low_mem_device);
+ self.event_index += 1;
+ }
+ if self.event_index == self.test_events.len() {
+ self.quit_request = true;
+ }
+ self.current_time = end_time;
+ }
+}
+
+struct MockDbus {
+ fds: Vec<RawFd>,
+ fifo_in: File,
+ fifo_out: Option<File>, // using Option merely to use take()
+}
+
+impl Dbus for MockDbus {
+ fn get_fds(&self) -> &Vec<RawFd> {
+ &self.fds
+ }
+
+ // Processes any mock chrome events. Events are strings separated by
+ // newlines sent to the event pipe. We could check if the pipe fired in
+ // the watcher, but it's less code to just do a non-blocking read.
+ fn process_chrome_events(&mut self, _watcher: &mut FileWatcher) -> Result<(i32, i32)> {
+ let mut buf = [0u8; 4096];
+ let read_bytes = read_nonblocking_pipe(&mut self.fifo_in, &mut buf)?;
+ let events = str::from_utf8(&buf[..read_bytes])?.lines();
+ let mut tab_discard_count = 0;
+ let mut oom_kill_count = 0;
+ for event in events {
+ match event {
+ "tab-discard" => tab_discard_count += 1,
+ "oom-kill" => oom_kill_count += 1,
+ other => return Err(format!("unexpected mock event {:?}", other).into()),
+ };
+ }
+ Ok((tab_discard_count, oom_kill_count))
+ }
+}
+
+impl MockDbus {
+ fn new(fifo_path: &Path) -> Result<MockDbus> {
+ let fifo_in = OpenOptions::new()
+ .custom_flags(libc::O_NONBLOCK)
+ .read(true)
+ .open(&fifo_path)?;
+ let fds = vec![fifo_in.as_raw_fd()];
+ let fifo_out = OpenOptions::new()
+ .custom_flags(libc::O_NONBLOCK)
+ .write(true)
+ .open(&fifo_path)?;
+ Ok(MockDbus { fds, fifo_in, fifo_out: Some(fifo_out) })
+ }
+}
+
+pub fn test_loop(_always_poll_fast: bool, paths: &Paths) {
+ for test_desc in TEST_DESCRIPTORS.iter() {
+ // Every test run requires a (mock) restart of the daemon.
+ println!("--------------\nrunning test:\n{}", test_desc);
+ // Clean up log directory.
+ std::fs::remove_dir_all(&paths.log_directory).expect("cannot remove /var/log/memd");
+ std::fs::create_dir_all(&paths.log_directory).expect("cannot create /var/log/memd");
+
+ let events = events_from_test_descriptor(test_desc);
+ let mut dbus = Box::new(MockDbus::new(&paths.testing_root.join(MOCK_DBUS_FIFO_NAME))
+ .expect("cannot create mock dbus"));
+ let timer = Box::new(MockTimer::new(events, paths.clone(),
+ dbus.fifo_out.take().unwrap()));
+ let mut sampler = Sampler::new(false, &paths, timer, dbus);
+ loop {
+ // Alternate between slow and fast poll.
+ sampler.slow_poll().expect("slow poll error");
+ if sampler.quit_request {
+ break;
+ }
+ sampler.fast_poll().expect("fast poll error");
+ if sampler.quit_request {
+ break;
+ }
+ }
+ verify_test_results(test_desc, &paths.log_directory)
+ .expect(&format!("test:{}failed.", test_desc));
+ println!("test succeeded\n--------------");
+ }
+ teardown_test_environment(&paths);
+}
+
+// ================
+// Test Descriptors
+// ================
+//
+// Define events and expected result using "ASCII graphics".
+//
+// The top lines of the test descriptor (all lines except the last one) define
+// sequences of events. The last line describes the expected result.
+//
+// Events are single characters:
+//
+// M = start medium pressure (fast poll)
+// H = start high pressure (low-mem notification)
+// L = start low pressure (slow poll)
+// <digit> = tab discard
+// K = kernel OOM kill
+// k = chrome notification of OOM kill
+// ' ', . = nop (just wait 1 second)
+// | = ignored (no delay), cosmetic only
+//
+// - each character indicates a 1-second slot
+// - events (if any) happen at the beginning of their slot
+// - multiple events in the same slot are stacked vertically
+//
+// Example:
+//
+// ..H.1..L
+// 2
+//
+// means:
+// - wait 2 seconds
+// - signal high-memory pressure, wait 1 second
+// - wait 1 second
+// - signal two tab discard events (named 1 and 2), wait 1 second
+// - wait 2 more seconds
+// - return to low-memory pressure
+//
+// The last line describes the expected clip logs. Each log is identified by
+// one digit: 0 for memd.clip000.log, 1 for memd.clip001.log etc. The positions
+// of the digits correspond to the time span covered by each clip. So a clip
+// file whose description is 5 characters long is supposed to contain 5 seconds
+// worth of samples.
+//
+// For readability, the descriptor must start and end with newlines, which are
+// removed. Also, indentation (common all-space prefixes) is removed.
+
+const TEST_DESCRIPTORS: &[&str] = &[
+ // Very simple test: go from slow poll to fast poll and back. No clips
+ // are collected.
+ "
+ .M.L.
+ .....
+ ",
+
+ // Simple test: start fast poll, signal low-mem, signal tab discard.
+ "
+ .M...H..1.....L
+ ..00000000001..
+ ",
+ // Two full disjoint clips. Also tests kernel-reported and chrome-reported OOM
+ // kills.
+ "
+ .M......K.............k.....
+ ...0000000000....1111111111.
+ ",
+
+ // Test that clip collection continues for the time span of interest even if
+ // memory pressure returns quickly to a low level. Note that the
+ // medium-pressure event (M) is at t = 1s, but the fast poll starts at 2s
+ // (multiple of 2s slow-poll period).
+
+ "
+ .MH1L.....
+ ..000000..
+ ",
+
+ // Several discards, which result in three consecutive clips. Tab discards 1
+ // and 2 produce an 8-second clip because the first two seconds of data are
+ // missing. Also see the note above regarding fast poll start.
+ "
+ ...M.H12..|...3...6..|.7.....L
+ | 4 |
+ | 5 |
+ ....000000|0011111111|112222..
+ ",
+];
+
+fn trim_descriptor(descriptor: &str) -> Vec<Vec<u8>> {
+ // Remove vertical bars. Don't check for consistent use because it's easy
+ // enough to notice visually.
+ let barless_descriptor: String = descriptor.chars().filter(|c| *c != '|').collect();
+ // Split string into lines.
+ let all_lines: Vec<String> = barless_descriptor.split('\n').map(String::from).collect();
+ // A test descriptor must start and end with empty lines, and have at least
+ // one line of events, and exactly one line to describe the clip files.
+ assert!(all_lines.len() >= 4, "invalid test descriptor format");
+ // Remove first and last line.
+ let valid_lines = all_lines[1 .. all_lines.len()-1].to_vec();
+ // Find indentation amount. Unwrap() cannot fail because of previous assert.
+ let indent = valid_lines.iter().map(|s| s.len() - s.trim_left().len()).min().unwrap();
+ // Remove indentation.
+ let trimmed_lines: Vec<Vec<u8>> =
+ valid_lines.iter().map(|s| s[indent..].to_string().into_bytes()).collect();
+ trimmed_lines
+}
+
+fn events_from_test_descriptor(descriptor: &str) -> Vec<TestEvent> {
+ let all_descriptors = trim_descriptor(descriptor);
+ let event_sequences = &all_descriptors[..all_descriptors.len() - 1];
+ let max_length = event_sequences.iter().map(|d| d.len()).max().unwrap();
+ let mut events = vec![];
+ for i in 0 .. max_length {
+ for seq in event_sequences {
+ // Each character represents one second. Time unit is milliseconds.
+ let mut opt_type = None;
+ if i < seq.len() {
+ match seq[i] {
+ b'0' | b'1' | b'2' | b'3' | b'4' |
+ b'5' | b'6' | b'7' | b'8' | b'9'
+ => opt_type = Some(TestEventType::TabDiscard),
+ b'H' => opt_type = Some(TestEventType::EnterHighPressure),
+ b'M' => opt_type = Some(TestEventType::EnterMediumPressure),
+ b'L' => opt_type = Some(TestEventType::EnterLowPressure),
+ b'k' => opt_type = Some(TestEventType::OomKillBrowser),
+ b'K' => opt_type = Some(TestEventType::OomKillKernel),
+ b'.' | b' ' | b'|' => {},
+ x => panic!("unexpected character {} in descriptor '{}'", &x, descriptor),
+ }
+ }
+ if let Some(t) = opt_type {
+ events.push(TestEvent { time: i as i64 * 1000, event_type: t });
+ }
+ }
+ }
+ events
+}
+
+// Given a descriptor string for the expected clips, returns a vector of start
+// and end time of each clip.
+fn expected_clips(descriptor: &[u8]) -> Vec<(i64, i64)> {
+ let mut time = 0;
+ let mut clip_start_time = 0;
+ let mut previous_clip = b'0' - 1;
+ let mut previous_char = 0u8;
+ let mut clips = vec![];
+
+ for &c in descriptor {
+ if c != previous_char {
+ if (previous_char as char).is_digit(10) {
+ // End of clip.
+ clips.push((clip_start_time, time));
+ }
+ if (c as char).is_digit(10) {
+ // Start of clip.
+ clip_start_time = time;
+ assert_eq!(c, previous_clip + 1, "malformed clip descriptor");
+ previous_clip = c;
+ }
+ }
+ previous_char = c;
+ time += 1000;
+ }
+ clips
+}
+
+// Converts a string starting with a timestamp in seconds (#####.##, with two
+// decimal digits) to a timestamp in milliseconds.
+fn time_from_sample_string(line: &str) -> Result<i64> {
+ let mut tokens = line.split(|c: char| !c.is_digit(10));
+ let seconds = match tokens.next() {
+ Some(digits) => digits.parse::<i64>().unwrap(),
+ None => return Err("no digits in string".into())
+ };
+ let centiseconds = match tokens.next() {
+ Some(digits) => if digits.len() == 2 {
+ digits.parse::<i64>().unwrap()
+ } else {
+ return Err("expecting 2 decimals".into());
+ },
+ None => return Err("expecting at least two groups of digits".into()),
+ };
+ Ok(seconds * 1000 + centiseconds * 10)
+}
+
+macro_rules! assert_approx_eq {
+ ($x:expr, $y: expr, $tolerance: expr, $format:expr $(, $arg:expr)*) => {{
+ let x = $x;
+ let y = $y;
+ let tolerance = $tolerance;
+ let y_min = y - tolerance;
+ let y_max = y + tolerance;
+ assert!(x < y_max && x > y_min, $format $(, $arg)*);
+ }}
+}
+
+fn check_clip(clip_times: (i64, i64), clip_path: PathBuf, events: &Vec<TestEvent>)
+ -> Result<()> {
+ let clip_name = clip_path.to_string_lossy();
+ let mut clip_file = File::open(&clip_path)?;
+ let mut file_content = String::new();
+ clip_file.read_to_string(&mut file_content)?;
+ let lines = file_content.lines().collect::<Vec<&str>>();
+ // First line is time stamp. Second line is field names. Check count of
+ // field names and field values in the third line (don't bother to check
+ // the other lines).
+ let name_count = lines[1].split_whitespace().count();
+ let value_count = lines[2].split_whitespace().count();
+ assert_eq!(name_count, value_count);
+
+ // Check first and last time stamps.
+ let start_time = time_from_sample_string(&lines[2]).expect("cannot parse first timestamp");
+ let end_time = time_from_sample_string(&lines[lines.len() - 1])
+ .expect("cannot parse last timestamp");
+ let expected_start_time = clip_times.0;
+ let expected_end_time = clip_times.1;
+ // Milliseconds of slack allowed on start/stop times. We allow one full
+ // fast poll period to take care of edge cases. The specs don't need to be
+ // tight here because it doesn't matter if we collect one fewer sample (or
+ // an extra one) at each end.
+ let slack_ms = 101i64;
+ assert_approx_eq!(start_time, expected_start_time, slack_ms,
+ "unexpected start time for {}", clip_name);
+ assert_approx_eq!(end_time, expected_end_time, slack_ms,
+ "unexpected end time for {}", clip_name);
+
+ // Check sample count.
+ let expected_sample_count_from_events: usize = events.iter()
+ .map(|e| if e.time <= start_time || e.time > end_time {
+ 0
+ } else {
+ match e.event_type {
+ // OomKillKernel generates two samples.
+ TestEventType::OomKillKernel => 2,
+ // These generate 0 samples.
+ TestEventType::EnterLowPressure |
+ TestEventType::EnterMediumPressure => 0,
+ _ => 1,
+ }
+ })
+ .sum();
+ // We include samples both at the beginning and end of the range, so we
+ // need to add 1. Note that here we use the actual sample times, not the
+ // expected times.
+ let expected_sample_count_from_timer = ((end_time - start_time) / 100) as usize + 1;
+ let expected_sample_count =
+ expected_sample_count_from_events + expected_sample_count_from_timer;
+ let sample_count = lines.len() - 2;
+ assert_eq!(sample_count, expected_sample_count, "unexpected sample count for {}", clip_name);
+ Ok(())
+}
+
+fn verify_test_results(descriptor: &str, log_directory: &PathBuf) -> Result<()> {
+ let all_descriptors = trim_descriptor(descriptor);
+ let result_descriptor = &all_descriptors[all_descriptors.len() - 1];
+ let clips = expected_clips(result_descriptor);
+ let events = events_from_test_descriptor(descriptor);
+
+ // Check that there are no more clips than expected.
+ let files_count = std::fs::read_dir(log_directory)?.count();
+ assert_eq!(clips.len() + 1, files_count, "wrong number of clip files");
+
+ let mut clip_number = 0;
+ for clip in clips {
+ let clip_path = log_directory.join(format!("memd.clip{:03}.log", clip_number));
+ check_clip(clip, clip_path, &events)?;
+ clip_number += 1;
+ }
+ Ok(())
+}
+
+fn create_dir_all(path: &Path) -> Result<()> {
+ let result = std::fs::create_dir_all(path);
+ match result {
+ Ok(_) => Ok(()),
+ Err(e) => Err(format!("create_dir_all: {}: {:?}", path.to_string_lossy(), e).into())
+ }
+}
+
+fn teardown_test_environment (paths: &Paths) {
+ if let Err(e) = std::fs::remove_dir_all(&paths.testing_root) {
+ info!("teardown: could not remove {}: {:?}", paths.testing_root.to_str().unwrap(), e);
+ }
+}
+
+pub fn setup_test_environment(paths: &Paths) {
+ std::fs::create_dir(&paths.testing_root).
+ expect(&format!("cannot create {}", paths.testing_root.to_str().unwrap()));
+ mkfifo(&paths.testing_root.join(MOCK_DBUS_FIFO_NAME))
+ .expect("failed to make mock dbus fifo");
+ create_dir_all(paths.vmstat.parent().unwrap())
+ .expect("cannot create /proc");
+ create_dir_all(paths.available.parent().unwrap())
+ .expect("cannot create ../chromeos-low-mem");
+ create_dir_all(paths.trace_pipe.parent().unwrap())
+ .expect("cannot create ../tracing");
+ let sys_vm = paths.testing_root.join("proc/sys/vm");
+ create_dir_all(&sys_vm).expect("cannot create /proc/sys/vm");
+ create_dir_all(paths.low_mem_device.parent().unwrap()).expect("cannot create /dev");
+
+ let vmstat_content = include_str!("vmstat_content");
+ let zoneinfo_content = include_str!("zoneinfo_content");
+ print_to_path!(&paths.vmstat, "{}", vmstat_content).expect("cannot initialize vmstat");
+ print_to_path!(&paths.zoneinfo, "{}", zoneinfo_content).expect("cannot initialize zoneinfo");
+ print_to_path!(&paths.available, "{}\n", LOW_MEM_HIGH_AVAILABLE)
+ .expect("cannot initialize available");
+ print_to_path!(&paths.runnables, "0.16 0.18 0.22 4/981 8504")
+ .expect("cannot initialize runnables");
+ print_to_path!(&paths.tracing_enabled, "1\n").expect("cannot initialize tracing_enabled");
+ print_to_path!(&paths.tracing_on, "1\n").expect("cannot initialize tracing_on");
+ print_to_path!(&paths.set_ftrace_filter, "").expect("cannot initialize set_ftrace_filter");
+ print_to_path!(&paths.current_tracer, "").expect("cannot initialize current_tracer");
+ print_to_path!(&paths.low_mem_margin, "{}", LOW_MEM_MARGIN)
+ .expect("cannot initialize low_mem_margin");
+ mkfifo(&paths.trace_pipe).expect("could not make mock trace_pipe");
+
+ print_to_path!(sys_vm.join("min_filelist_kbytes"), "100000\n")
+ .expect("cannot initialize min_filelist_kbytes");
+ print_to_path!(sys_vm.join("min_free_kbytes"), "80000\n")
+ .expect("cannot initialize min_free_kbytes");
+ print_to_path!(sys_vm.join("extra_free_kbytes"), "60000\n")
+ .expect("cannot initialize extra_free_kbytes");
+
+ mkfifo(&paths.low_mem_device).expect("could not make mock low-mem device");
+}
diff --git a/metrics/memd/src/vmstat_content b/metrics/memd/src/vmstat_content
new file mode 100644
index 0000000..b42e9cf
--- /dev/null
+++ b/metrics/memd/src/vmstat_content
@@ -0,0 +1,104 @@
+nr_free_pages 50489
+nr_alloc_batch 3326
+nr_inactive_anon 48591
+nr_active_anon 145673
+nr_inactive_file 49597
+nr_active_file 50199
+nr_unevictable 0
+nr_mlock 0
+nr_anon_pages 180171
+nr_mapped 63194
+nr_file_pages 149074
+nr_dirty 136
+nr_writeback 0
+nr_slab_reclaimable 10484
+nr_slab_unreclaimable 16134
+nr_page_table_pages 9255
+nr_kernel_stack 979
+nr_unstable 0
+nr_bounce 0
+nr_vmscan_write 60544150
+nr_vmscan_immediate_reclaim 29587
+nr_writeback_temp 0
+nr_isolated_anon 0
+nr_isolated_file 0
+nr_shmem 8982
+nr_dirtied 3909911
+nr_written 64426230
+nr_pages_scanned 88
+workingset_refault 16082484
+workingset_activate 1186339
+workingset_nodereclaim 0
+nr_anon_transparent_hugepages 2
+nr_free_cma 0
+nr_dirty_threshold 68740
+nr_dirty_background_threshold 5728
+pgpgin 41070493
+pgpgout 16914684
+pswpin 59158894
+pswpout 60513438
+pgalloc_dma 3247
+pgalloc_dma32 131210265
+pgalloc_normal 0
+pgalloc_movable 0
+pgfree 140534607
+pgactivate 70428610
+pgdeactivate 79401090
+pgfault 110047495
+pgmajfault 52561387
+pgmajfault_s 97569
+pgmajfault_a 52314090
+pgmajfault_f 149728
+pgrefill_dma 4749
+pgrefill_dma32 79973171
+pgrefill_normal 0
+pgrefill_movable 0
+pgsteal_kswapd_dma 112
+pgsteal_kswapd_dma32 62450553
+pgsteal_kswapd_normal 0
+pgsteal_kswapd_movable 0
+pgsteal_direct_dma 0
+pgsteal_direct_dma32 18534641
+pgsteal_direct_normal 0
+pgsteal_direct_movable 0
+pgscan_kswapd_dma 4776
+pgscan_kswapd_dma32 126413953
+pgscan_kswapd_normal 0
+pgscan_kswapd_movable 0
+pgscan_direct_dma 11
+pgscan_direct_dma32 36676518
+pgscan_direct_normal 0
+pgscan_direct_movable 0
+pgscan_direct_throttle 0
+pginodesteal 3302
+slabs_scanned 24535326
+kswapd_inodesteal 237022
+kswapd_low_wmark_hit_quickly 24998
+kswapd_high_wmark_hit_quickly 40300
+pageoutrun 87752
+allocstall 376923
+pgrotated 247987
+drop_pagecache 0
+drop_slab 0
+pgmigrate_success 8814551
+pgmigrate_fail 94348
+compact_migrate_scanned 86192323
+compact_free_scanned 1982032044
+compact_isolated 19084449
+compact_stall 20280
+compact_fail 17323
+compact_success 2957
+unevictable_pgs_culled 0
+unevictable_pgs_scanned 0
+unevictable_pgs_rescued 0
+unevictable_pgs_mlocked 0
+unevictable_pgs_munlocked 0
+unevictable_pgs_cleared 0
+unevictable_pgs_stranded 0
+thp_fault_alloc 15
+thp_fault_fallback 0
+thp_collapse_alloc 7894
+thp_collapse_alloc_failed 4784
+thp_split 13
+thp_zero_page_alloc 0
+thp_zero_page_alloc_failed 0
diff --git a/metrics/memd/src/zoneinfo_content b/metrics/memd/src/zoneinfo_content
new file mode 100644
index 0000000..3e6a858
--- /dev/null
+++ b/metrics/memd/src/zoneinfo_content
@@ -0,0 +1,114 @@
+Node 0, zone DMA
+ pages free 1756
+ min 179
+ low 223
+ high 268
+ scanned 0
+ spanned 4095
+ present 3999
+ managed 3977
+ nr_free_pages 1756
+ nr_alloc_batch 45
+ nr_inactive_anon 0
+ nr_active_anon 0
+ nr_inactive_file 238
+ nr_active_file 377
+ nr_unevictable 0
+ nr_mlock 0
+ nr_anon_pages 0
+ nr_mapped 284
+ nr_file_pages 615
+ nr_dirty 0
+ nr_writeback 0
+ nr_slab_reclaimable 23
+ nr_slab_unreclaimable 22
+ nr_page_table_pages 2
+ nr_kernel_stack 0
+ nr_unstable 0
+ nr_bounce 0
+ nr_vmscan_write 112
+ nr_vmscan_immediate_reclaim 0
+ nr_writeback_temp 0
+ nr_isolated_anon 0
+ nr_isolated_file 0
+ nr_shmem 0
+ nr_dirtied 352
+ nr_written 464
+ nr_pages_scanned 0
+ workingset_refault 0
+ workingset_activate 0
+ workingset_nodereclaim 0
+ nr_anon_transparent_hugepages 0
+ nr_free_cma 0
+ protection: (0, 1928, 1928, 1928)
+ pagesets
+ cpu: 0
+ count: 0
+ high: 0
+ batch: 1
+ vm stats threshold: 4
+ cpu: 1
+ count: 0
+ high: 0
+ batch: 1
+ vm stats threshold: 4
+ all_unreclaimable: 1
+ start_pfn: 1
+ inactive_ratio: 1
+Node 0, zone DMA32
+ pages free 56774
+ min 22348
+ low 27935
+ high 33522
+ scanned 0
+ spanned 506914
+ present 506658
+ managed 494688
+ nr_free_pages 56774
+ nr_alloc_batch 3023
+ nr_inactive_anon 48059
+ nr_active_anon 138044
+ nr_inactive_file 47952
+ nr_active_file 49885
+ nr_unevictable 0
+ nr_mlock 0
+ nr_anon_pages 170349
+ nr_mapped 62610
+ nr_file_pages 147990
+ nr_dirty 231
+ nr_writeback 0
+ nr_slab_reclaimable 10414
+ nr_slab_unreclaimable 16112
+ nr_page_table_pages 9231
+ nr_kernel_stack 981
+ nr_unstable 0
+ nr_bounce 0
+ nr_vmscan_write 60554686
+ nr_vmscan_immediate_reclaim 29587
+ nr_writeback_temp 0
+ nr_isolated_anon 0
+ nr_isolated_file 0
+ nr_shmem 8982
+ nr_dirtied 3918108
+ nr_written 64443856
+ nr_pages_scanned 0
+ workingset_refault 16084171
+ workingset_activate 1186426
+ workingset_nodereclaim 0
+ nr_anon_transparent_hugepages 2
+ nr_free_cma 0
+ protection: (0, 0, 0, 0)
+ pagesets
+ cpu: 0
+ count: 135
+ high: 186
+ batch: 31
+ vm stats threshold: 20
+ cpu: 1
+ count: 166
+ high: 186
+ batch: 31
+ vm stats threshold: 20
+ all_unreclaimable: 0
+ start_pfn: 4096
+ inactive_ratio: 3