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