[memory] Added support for capturing and printing memory statistics.

A new command line program was created for this purpose,
called "mem". The supporting classes have been broken out
into a library to be used by this and memory_monitor.

In addition tests have been created for these functions.

CF-775

Change-Id: Ie10d74f65a067717f7317a2343810a739e481b24
Test: Included
diff --git a/garnet/bin/memory_monitor/BUILD.gn b/garnet/bin/memory_monitor/BUILD.gn
index 2883782b..12e110e 100644
--- a/garnet/bin/memory_monitor/BUILD.gn
+++ b/garnet/bin/memory_monitor/BUILD.gn
@@ -5,18 +5,59 @@
 import("//build/config.gni")
 import("//build/package.gni")
 
-source_set("lib") {
+source_set("mem_lib") {
+  sources = [
+    "capture.cc",
+    "capture.h",
+    "printer.cc",
+    "printer.h",
+    "summary.cc",
+    "summary.h",
+  ]
+
+  deps = [
+    "//garnet/public/lib/fsl",
+    "//src/lib/fxl",
+    "//zircon/public/fidl/fuchsia-sysinfo:fuchsia-sysinfo_c",
+    "//zircon/public/lib/task-utils",
+  ]
+}
+
+executable("mem_bin") {
+  output_name = "mem"
+  sources = [
+    "mem_main.cc",
+  ]
+  deps = [
+    ":mem_lib",
+    "//src/lib/fxl",
+    "//sdk/lib/sys/cpp",
+  ]
+}
+
+package("mem") {
+  deps = [
+    ":mem_bin",
+  ]
+  binaries = [
+    {
+      name = "mem"
+      shell = true
+    },
+  ]
+}
+
+source_set("mon_lib") {
   sources = [
     "monitor.cc",
     "monitor.h",
   ]
-
   public_deps = [
+    ":mem_lib",
     "//garnet/public/lib/inspect",
     "//sdk/fidl/fuchsia.memory",
     "//sdk/lib/sys/cpp",
   ]
-
   deps = [
     "//garnet/public/lib/fsl",
     "//src/lib/fxl",
@@ -26,14 +67,13 @@
   ]
 }
 
-executable("bin") {
+executable("mon_bin") {
   output_name = "memory_monitor"
-
   sources = [
-    "main.cc",
+    "mon_main.cc",
   ]
   deps = [
-    ":lib",
+    ":mon_lib",
     "//sdk/lib/sys/cpp",
     "//src/lib/fxl",
     "//zircon/public/lib/async-default",
@@ -44,11 +84,9 @@
 
 package("memory_monitor") {
   deps = [
-    ":bin",
+    ":mon_bin",
   ]
-
   binary = "memory_monitor"
-
   meta = [
     {
       path = rebase_path("meta/memory_monitor.cmx")
diff --git a/garnet/bin/memory_monitor/capture.cc b/garnet/bin/memory_monitor/capture.cc
new file mode 100644
index 0000000..9266c4d
--- /dev/null
+++ b/garnet/bin/memory_monitor/capture.cc
@@ -0,0 +1,158 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "garnet/bin/memory_monitor/capture.h"
+
+#include <fcntl.h>
+#include <fuchsia/sysinfo/c/fidl.h>
+#include <lib/zx/channel.h>
+#include <lib/fdio/fdio.h>
+#include <memory>
+#include <src/lib/fxl/logging.h>
+#include <task-utils/walker.h>
+#include <zircon/process.h>
+#include <zircon/status.h>
+
+namespace memory {
+namespace {
+
+zx_status_t get_root_resource(zx_handle_t* root_resource) {
+  const char* sysinfo = "/dev/misc/sysinfo";
+  int fd = open(sysinfo, O_RDWR);
+  if (fd < 0) {
+    FXL_LOG(ERROR) << "Cannot open sysinfo: " << strerror(errno);
+    return ZX_ERR_NOT_FOUND;
+  }
+
+  zx::channel channel;
+  zx_status_t status =
+      fdio_get_service_handle(fd, channel.reset_and_get_address());
+  if (status != ZX_OK) {
+    FXL_LOG(ERROR) << "Cannot obtain sysinfo channel: "
+                   << zx_status_get_string(status);
+    return status;
+  }
+
+  zx_status_t fidl_status = fuchsia_sysinfo_DeviceGetRootResource(
+      channel.get(), &status, root_resource);
+  if (fidl_status != ZX_OK) {
+    FXL_LOG(ERROR) << "Cannot obtain root resource: "
+                   << zx_status_get_string(fidl_status);
+    return fidl_status;
+  } else if (status != ZX_OK) {
+    FXL_LOG(ERROR) << "Cannot obtain root resource: "
+                   << zx_status_get_string(status);
+    return status;
+  }
+  return ZX_OK;
+}
+}  // namespace
+
+class Capture::ProcessGetter final : public TaskEnumerator {
+ public:
+  ProcessGetter(CaptureLevel level, zx_koid_t self_koid, Capture& capture)
+    : level_(level), self_koid_(self_koid), capture_(capture) {}
+
+  zx_status_t OnProcess(
+      int depth,
+      zx_handle_t handle,
+      zx_koid_t koid,
+      zx_koid_t parent_koid) override {
+    Process process = { .koid = koid };
+    zx_status_t s = zx_object_get_property(
+        handle, ZX_PROP_NAME, process.name, ZX_MAX_NAME_LEN);
+    if (s != ZX_OK) {
+      return s;
+    }
+    s = zx_object_get_info(handle, ZX_INFO_TASK_STATS,
+                           &process.stats, sizeof(process.stats),
+                           nullptr, nullptr);
+    if (s != ZX_OK) {
+      return s;
+    }
+
+    if (level_ == PROCESS) {
+      return ZX_OK;
+    }
+
+    if (koid == self_koid_) {
+      return ZX_OK;
+    }
+
+    size_t num_vmos;
+    s = zx_object_get_info(handle, ZX_INFO_PROCESS_VMOS,
+                           nullptr, 0, nullptr, &num_vmos);
+    if (s != ZX_OK) {
+      return s;
+    }
+    zx_info_vmo_t* vmos = new zx_info_vmo_t[num_vmos];
+    s = zx_object_get_info(handle, ZX_INFO_PROCESS_VMOS,
+                           vmos, num_vmos * sizeof(zx_info_vmo_t),
+                           &num_vmos, nullptr);
+    if (s != ZX_OK) {
+      delete[] vmos;
+      return s;
+    }
+    process.vmos.reserve(num_vmos);
+    for (size_t i = 0; i < num_vmos; i++) {
+      zx_koid_t vmo_koid = vmos[i].koid;
+      capture_.koid_to_vmo_.emplace(vmo_koid, vmos[i]);
+      process.vmos.push_back(vmo_koid);
+    }
+    delete[] vmos;
+    capture_.koid_to_process_.emplace(koid, process);
+    return ZX_OK;
+  }
+
+ private:
+  CaptureLevel level_;
+  zx_koid_t self_koid_;
+  Capture& capture_;
+
+  bool has_on_process() const final { return true; }
+};
+
+// static.
+zx_status_t Capture::GetCaptureState(CaptureState& state) {
+  zx_status_t err = get_root_resource(&state.root);
+  if (err != ZX_OK) {
+    return err;
+  }
+
+  zx_info_handle_basic_t info;
+  err = zx_object_get_info(zx_process_self(), ZX_INFO_HANDLE_BASIC,
+                           &info, sizeof(info), nullptr, nullptr);
+  if (err != ZX_OK) {
+    return err;
+  }
+
+  state.self_koid = info.koid;
+  return ZX_OK;
+}
+
+// static.
+zx_status_t Capture::GetCapture(
+    Capture& capture, const CaptureState& state, CaptureLevel level) {
+  capture.time_ = zx_clock_get_monotonic();
+  zx_status_t err = zx_object_get_info(
+      state.root, ZX_INFO_KMEM_STATS,
+      &capture.kmem_, sizeof(capture.kmem_),
+      NULL, NULL);
+  if (err != ZX_OK) {
+    return err;
+  }
+
+  if (level == KMEM) {
+    return ZX_OK;
+  }
+
+  ProcessGetter getter(level, state.self_koid, capture);
+  err = getter.WalkRootJobTree();
+  if (err != ZX_OK) {
+    return err;
+  }
+  return ZX_OK;
+}
+
+}  // namespace memory
diff --git a/garnet/bin/memory_monitor/capture.h b/garnet/bin/memory_monitor/capture.h
new file mode 100644
index 0000000..bda9fbf
--- /dev/null
+++ b/garnet/bin/memory_monitor/capture.h
@@ -0,0 +1,69 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GARNET_BIN_MEMORY_MONITOR_CAPTURE_H_
+#define GARNET_BIN_MEMORY_MONITOR_CAPTURE_H_
+
+#include <src/lib/fxl/macros.h>
+#include <lib/zx/time.h>
+#include <unordered_map>
+#include <vector>
+#include <zircon/syscalls.h>
+#include <zircon/types.h>
+
+namespace memory {
+
+struct Process {
+  zx_koid_t koid;
+  char name[ZX_MAX_NAME_LEN];
+  zx_info_task_stats_t stats;
+  std::vector<zx_koid_t> vmos;
+};
+
+typedef enum {KMEM, PROCESS, VMO} CaptureLevel;
+
+struct CaptureState {
+  zx_handle_t root;
+  zx_koid_t self_koid;
+};
+
+class Capture {
+ public:
+  static zx_status_t GetCaptureState(CaptureState& state);
+  static zx_status_t GetCapture(
+      Capture& capture, const CaptureState& state, CaptureLevel level);
+
+  zx_time_t time() const { return time_; };
+  const zx_info_kmem_stats_t& kmem() const { return kmem_; } ;
+
+  const std::unordered_map<zx_koid_t, const Process>& koid_to_process() const {
+    return koid_to_process_;
+  }
+
+  const std::unordered_map<zx_koid_t, const zx_info_vmo_t>& 
+      koid_to_vmo() const {
+    return koid_to_vmo_;
+  }
+
+  const Process& process_for_koid(zx_koid_t koid) const {
+    return koid_to_process_.at(koid);
+  }
+
+  const zx_info_vmo_t& vmo_for_koid(zx_koid_t koid) const {
+    return koid_to_vmo_.at(koid);
+  }
+
+ private:
+  zx_time_t time_;
+  zx_info_kmem_stats_t kmem_;
+  std::unordered_map<zx_koid_t, const Process> koid_to_process_;
+  std::unordered_map<zx_koid_t, const zx_info_vmo_t> koid_to_vmo_;
+
+  class ProcessGetter;
+  friend class TestUtils;
+};
+
+}  // namespace memory
+
+#endif  // GARNET_BIN_MEMORY_MONITOR_CAPTURE_H_
diff --git a/garnet/bin/memory_monitor/mem_main.cc b/garnet/bin/memory_monitor/mem_main.cc
new file mode 100644
index 0000000..b7d230b
--- /dev/null
+++ b/garnet/bin/memory_monitor/mem_main.cc
@@ -0,0 +1,102 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <iostream> 
+#include <src/lib/fxl/command_line.h>
+#include <src/lib/fxl/strings/string_number_conversions.h>
+#include <zircon/status.h>
+
+#include "garnet/bin/memory_monitor/capture.h"
+#include "garnet/bin/memory_monitor/summary.h"
+#include "garnet/bin/memory_monitor/printer.h"
+
+using namespace memory;
+
+int main(int argc, const char** argv) {
+  auto command_line = fxl::CommandLineFromArgcArgv(argc, argv);
+
+  CaptureState capture_state;
+  auto s = Capture::GetCaptureState(capture_state);
+  Printer printer(std::cout);
+
+  if (s != ZX_OK) {
+    std::cerr << "Error getting capture state: "
+              << zx_status_get_string(s);
+    return EXIT_FAILURE;
+  }
+  if (command_line.HasOption("summarize")) {
+    Capture capture;
+    s = Capture::GetCapture(capture, capture_state, VMO);
+    if (s != ZX_OK) {
+      std::cerr << "Error getting capture: " << zx_status_get_string(s);
+      return EXIT_FAILURE;
+    }
+    Summary summary(capture);
+    printer.PrintSummary(summary, VMO, SORTED);
+    return EXIT_SUCCESS;
+  }
+
+  if (command_line.HasOption("print")) {
+    Capture capture;
+    auto s = Capture::GetCapture(capture, capture_state, VMO);
+    if (s != ZX_OK) {
+      std::cerr << "Error getting capture: " << zx_status_get_string(s);
+      return EXIT_FAILURE;
+    }
+    printer.PrintCapture(capture, VMO, SORTED);
+    return EXIT_SUCCESS;
+  }
+
+  if (command_line.HasOption("output")) {
+    zx_koid_t pid = 0;
+    if (command_line.HasOption("pid")) {
+      std::string pid_value;
+      command_line.GetOptionValue("pid", &pid_value);
+      if (!fxl::StringToNumberWithError(pid_value, &pid)) {
+        std::cerr << "Invalid value for --pid: " << pid_value;
+        return EXIT_FAILURE;
+      }
+    }
+    if (!command_line.HasOption("repeat")) {
+      Capture capture;
+      auto s = Capture::GetCapture(capture, capture_state, VMO);
+      if (s != ZX_OK) {
+        std::cerr << "Error getting capture: " << zx_status_get_string(s);
+        return EXIT_FAILURE;
+      }
+      Summary summary(capture);
+      printer.OutputSummary(summary, UNSORTED, pid);
+      return EXIT_SUCCESS;
+    }
+    zx::time start = zx::clock::get_monotonic();
+    uint64_t repeat = 0;
+    std::string repeat_value;
+    command_line.GetOptionValue("repeat", &repeat_value);
+    if (!fxl::StringToNumberWithError(repeat_value, &repeat)) {
+      std::cerr << "Invalid value for --repeat: " << repeat_value;
+      return EXIT_FAILURE;
+    }
+    zx::duration repeat_secs = zx::sec(repeat);
+    int64_t i = 1;
+    do {
+      Capture capture;
+      auto s = Capture::GetCapture(capture, capture_state, VMO);
+      if (s != ZX_OK) {
+        std::cerr << "Error getting capture: " << zx_status_get_string(s);
+        return EXIT_FAILURE;
+      }
+      Summary summary(capture);
+      printer.OutputSummary(summary, UNSORTED, pid);
+      zx::time next = start + (repeat_secs * i);
+      if (next <= zx::clock::get_monotonic()) {
+        next = zx::clock::get_monotonic() + repeat_secs;
+      }
+      zx::nanosleep(next);
+      i++;
+    } while (true);
+    return EXIT_SUCCESS;
+  }
+
+  return EXIT_SUCCESS;
+}
diff --git a/garnet/bin/memory_monitor/main.cc b/garnet/bin/memory_monitor/mon_main.cc
similarity index 81%
rename from garnet/bin/memory_monitor/main.cc
rename to garnet/bin/memory_monitor/mon_main.cc
index f7a2655..5583438 100644
--- a/garnet/bin/memory_monitor/main.cc
+++ b/garnet/bin/memory_monitor/mon_main.cc
@@ -1,14 +1,14 @@
-// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
 #include <lib/async-loop/cpp/loop.h>
+#include <src/lib/fxl/command_line.h>
+#include <src/lib/fxl/log_settings_command_line.h>
+#include <src/lib/fxl/logging.h>
 #include <trace-provider/provider.h>
 
 #include "garnet/bin/memory_monitor/monitor.h"
-#include "src/lib/fxl/command_line.h"
-#include "src/lib/fxl/log_settings_command_line.h"
-#include "src/lib/fxl/logging.h"
 
 int main(int argc, const char** argv) {
   auto command_line = fxl::CommandLineFromArgcArgv(argc, argv);
diff --git a/garnet/bin/memory_monitor/monitor.cc b/garnet/bin/memory_monitor/monitor.cc
index 75c8ce8..34c587b 100644
--- a/garnet/bin/memory_monitor/monitor.cc
+++ b/garnet/bin/memory_monitor/monitor.cc
@@ -7,21 +7,20 @@
 #include <errno.h>
 #include <fcntl.h>
 #include <fuchsia/sysinfo/c/fidl.h>
+#include <iostream>
 #include <lib/async/cpp/task.h>
 #include <lib/async/cpp/time.h>
 #include <lib/async/default.h>
 #include <lib/fdio/directory.h>
 #include <lib/fdio/fd.h>
 #include <lib/fdio/fdio.h>
+#include <src/lib/fxl/command_line.h>
+#include <src/lib/fxl/logging.h>
+#include <src/lib/fxl/strings/string_number_conversions.h>
 #include <string.h>
 #include <trace/event.h>
 #include <zircon/status.h>
 
-#include <iostream>
-
-#include "src/lib/fxl/command_line.h"
-#include "src/lib/fxl/logging.h"
-#include "src/lib/fxl/strings/string_number_conversions.h"
 
 namespace memory {
 
@@ -29,37 +28,6 @@
 
 constexpr char kKstatsPathComponent[] = "kstats";
 
-zx_status_t get_root_resource(zx_handle_t* root_resource) {
-  const char* sysinfo = "/dev/misc/sysinfo";
-  int fd = open(sysinfo, O_RDWR);
-  if (fd < 0) {
-    FXL_LOG(ERROR) << "Cannot open sysinfo: " << strerror(errno);
-    return ZX_ERR_NOT_FOUND;
-  }
-
-  zx::channel channel;
-  zx_status_t status =
-      fdio_get_service_handle(fd, channel.reset_and_get_address());
-  if (status != ZX_OK) {
-    FXL_LOG(ERROR) << "Cannot obtain sysinfo channel: "
-                   << zx_status_get_string(status);
-    return status;
-  }
-
-  zx_status_t fidl_status = fuchsia_sysinfo_DeviceGetRootResource(
-      channel.get(), &status, root_resource);
-  if (fidl_status != ZX_OK) {
-    FXL_LOG(ERROR) << "Cannot obtain root resource: "
-                   << zx_status_get_string(fidl_status);
-    return fidl_status;
-  } else if (status != ZX_OK) {
-    FXL_LOG(ERROR) << "Cannot obtain root resource: "
-                   << zx_status_get_string(status);
-    return status;
-  }
-  return ZX_OK;
-}
-
 }  // namespace
 
 const char Monitor::kTraceName[] = "memory_monitor";
@@ -74,6 +42,13 @@
       dispatcher_(dispatcher),
       component_context_(std::move(context)),
       root_object_(component::ObjectDir::Make("root")) {
+  auto s = Capture::GetCaptureState(capture_state_);
+  if (s != ZX_OK) {
+    FXL_LOG(ERROR) << "Error getting capture state: "
+                   << zx_status_get_string(s);
+    exit(EXIT_FAILURE);
+  }
+
   component_context_->outgoing()->AddPublicService(bindings_.GetHandler(this));
   root_object_.set_children_callback(
       {kKstatsPathComponent},
@@ -85,11 +60,6 @@
       std::make_unique<vfs::Service>(
           inspect_bindings_.GetHandler(root_object_.object().get())));
 
-  auto status = get_root_resource(&root_);
-  if (status != ZX_OK) {
-    FXL_LOG(ERROR) << "Error getting root_resource: " << status;
-    exit(-1);
-  }
   if (command_line.HasOption("help")) {
     PrintHelp();
     exit(EXIT_SUCCESS);
@@ -102,7 +72,6 @@
       FXL_LOG(ERROR) << "Invalid value for delay: " << delay_as_string;
       exit(-1);
     }
-    printf("setting delay to %d\n", delay_as_int);
     delay_ = zx::msec(delay_as_int);
   }
   std::string prealloc_as_string;
@@ -141,17 +110,16 @@
 
   trace_observer_.Start(dispatcher_, [this] { UpdateState(); });
   if (logging_) {
-    zx_info_kmem_stats_t stats;
-    zx_status_t err = zx_object_get_info(root_, ZX_INFO_KMEM_STATS, &stats,
-                                         sizeof(stats), NULL, NULL);
-    if (err != ZX_OK) {
-      FXL_LOG(ERROR) << "ZX_INFO_KMEM_STATS returns "
-                     << zx_status_get_string(err);
-      exit(-1);
+    Capture capture;
+    auto s = Capture::GetCapture(capture, capture_state_, KMEM);
+    if (s != ZX_OK) {
+      FXL_LOG(ERROR) << "Error getting capture: " << zx_status_get_string(s);
+      exit(EXIT_FAILURE);
     }
-    FXL_LOG(INFO) << "Total: " << stats.total_bytes
-                  << " Wired: " << stats.wired_bytes
-                  << " Total Heap: " << stats.total_heap_bytes;
+    auto const& kmem = capture.kmem();
+    FXL_LOG(INFO) << "Total: " << kmem.total_bytes
+                  << " Wired: " << kmem.wired_bytes
+                  << " Total Heap: " << kmem.total_heap_bytes;
   }
   SampleAndPost();
 }
@@ -179,7 +147,7 @@
       std::remove_if(watchers_.begin(), watchers_.end(), predicate));
 }
 
-void Monitor::NotifyWatchers(zx_info_kmem_stats_t kmem_stats) {
+void Monitor::NotifyWatchers(const zx_info_kmem_stats_t& kmem_stats) {
   fuchsia::memory::Stats stats{
       .total_bytes = kmem_stats.total_bytes,
       .free_bytes = kmem_stats.free_bytes,
@@ -207,52 +175,50 @@
 
 void Monitor::Inspect(component::Object::ObjectVector* out_children) {
   auto kstats = component::ObjectDir::Make(kKstatsPathComponent);
-  zx_info_kmem_stats_t stats;
-  zx_status_t err = zx_object_get_info(root_, ZX_INFO_KMEM_STATS, &stats,
-                                       sizeof(stats), NULL, NULL);
-  if (err != ZX_OK) {
-    FXL_LOG(ERROR) << "ZX_INFO_KMEM_STATS returns "
-                   << zx_status_get_string(err);
-    return;
+  Capture capture;
+  auto s = Capture::GetCapture(capture, capture_state_, KMEM);
+  if (s != ZX_OK) {
+    FXL_LOG(ERROR) << "Error getting capture: " << zx_status_get_string(s);
   }
-  kstats.set_metric("total_bytes", component::UIntMetric(stats.total_bytes));
-  kstats.set_metric("free_bytes", component::UIntMetric(stats.free_bytes));
-  kstats.set_metric("wired_bytes", component::UIntMetric(stats.wired_bytes));
-  kstats.set_metric("total_heap_bytes",
-                    component::UIntMetric(stats.total_heap_bytes));
-  kstats.set_metric("vmo_bytes", component::UIntMetric(stats.vmo_bytes));
-  kstats.set_metric("mmu_overhead_bytes",
-                    component::UIntMetric(stats.mmu_overhead_bytes));
-  kstats.set_metric("ipc_bytes", component::UIntMetric(stats.ipc_bytes));
-  kstats.set_metric("other_bytes", component::UIntMetric(stats.other_bytes));
+  auto const& kmem = capture.kmem();
+
+  kstats.set_metric("total_bytes", component::UIntMetric(kmem.total_bytes));
+  kstats.set_metric("free_bytes", component::UIntMetric(kmem.free_bytes));
+  kstats.set_metric("wired_bytes", component::UIntMetric(kmem.wired_bytes));
+  kstats.set_metric(
+      "total_heap_bytes",component::UIntMetric(kmem.total_heap_bytes));
+  kstats.set_metric("vmo_bytes", component::UIntMetric(kmem.vmo_bytes));
+  kstats.set_metric(
+      "mmu_overhead_bytes", component::UIntMetric(kmem.mmu_overhead_bytes));
+  kstats.set_metric("ipc_bytes", component::UIntMetric(kmem.ipc_bytes));
+  kstats.set_metric("other_bytes", component::UIntMetric(kmem.other_bytes));
   out_children->push_back(kstats.object());
 }
 
 void Monitor::SampleAndPost() {
   if (logging_ || tracing_ || watchers_.size() > 0) {
-    zx_info_kmem_stats_t stats;
-    zx_status_t err = zx_object_get_info(root_, ZX_INFO_KMEM_STATS, &stats,
-                                         sizeof(stats), NULL, NULL);
-    if (err != ZX_OK) {
-      FXL_LOG(ERROR) << "ZX_INFO_KMEM_STATS returns "
-                     << zx_status_get_string(err);
+    Capture capture;
+    auto s = Capture::GetCapture(capture, capture_state_, KMEM);
+    if (s != ZX_OK) {
+      FXL_LOG(ERROR) << "Error getting capture: " << zx_status_get_string(s);
       return;
     }
+    auto const& kmem = capture.kmem();
     if (logging_) {
-      FXL_LOG(INFO) << "Free: " << stats.free_bytes
-                    << " Free Heap: " << stats.free_heap_bytes
-                    << " VMO: " << stats.vmo_bytes
-                    << " MMU: " << stats.mmu_overhead_bytes
-                    << " IPC: " << stats.ipc_bytes;
+      FXL_LOG(INFO) << "Free: " << kmem.free_bytes
+                    << " Free Heap: " << kmem.free_heap_bytes
+                    << " VMO: " << kmem.vmo_bytes
+                    << " MMU: " << kmem.mmu_overhead_bytes
+                    << " IPC: " << kmem.ipc_bytes;
     }
     if (tracing_) {
-      TRACE_COUNTER(kTraceName, "allocated", 0, "vmo", stats.vmo_bytes,
-                    "mmu_overhead", stats.mmu_overhead_bytes, "ipc",
-                    stats.ipc_bytes);
-      TRACE_COUNTER(kTraceName, "free", 0, "free", stats.free_bytes,
-                    "free_heap", stats.free_heap_bytes);
+      TRACE_COUNTER(kTraceName, "allocated", 0, "vmo", kmem.vmo_bytes,
+                    "mmu_overhead", kmem.mmu_overhead_bytes, "ipc",
+                    kmem.ipc_bytes);
+      TRACE_COUNTER(kTraceName, "free", 0, "free", kmem.free_bytes,
+                    "free_heap", kmem.free_heap_bytes);
     }
-    NotifyWatchers(stats);
+    NotifyWatchers(kmem);
     async::PostDelayedTask(
         dispatcher_, [this] { SampleAndPost(); }, delay_);
   }
@@ -263,17 +229,19 @@
     if (trace_is_category_enabled(kTraceName)) {
       FXL_LOG(INFO) << "Tracing started";
       if (!tracing_) {
-        zx_info_kmem_stats_t stats;
-        zx_status_t err = zx_object_get_info(root_, ZX_INFO_KMEM_STATS, &stats,
-                                             sizeof(stats), NULL, NULL);
-        if (err != ZX_OK) {
-          FXL_LOG(ERROR) << "ZX_INFO_KMEM_STATS returns "
-                         << zx_status_get_string(err);
+        Capture capture;
+        auto s = Capture::GetCapture(capture, capture_state_, KMEM);
+        if (s != ZX_OK) {
+          FXL_LOG(ERROR) << "Error getting capture: "
+                         << zx_status_get_string(s);
           return;
         }
-        TRACE_COUNTER(kTraceName, "fixed", 0, "total", stats.total_bytes,
-                      "wired", stats.wired_bytes, "total_heap",
-                      stats.total_heap_bytes);
+        auto const& kmem = capture.kmem();
+        TRACE_COUNTER(kTraceName,
+                      "fixed", 0,
+                      "total", kmem.total_bytes,
+                      "wired", kmem.wired_bytes,
+                      "total_heap", kmem.total_heap_bytes);
         tracing_ = true;
         if (!logging_) {
           SampleAndPost();
diff --git a/garnet/bin/memory_monitor/monitor.h b/garnet/bin/memory_monitor/monitor.h
index 55e39b7..da2d28f 100644
--- a/garnet/bin/memory_monitor/monitor.h
+++ b/garnet/bin/memory_monitor/monitor.h
@@ -1,4 +1,4 @@
-// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
@@ -11,13 +11,12 @@
 #include <lib/inspect/deprecated/object_dir.h>
 #include <lib/sys/cpp/component_context.h>
 #include <lib/zx/vmo.h>
+#include <memory>
+#include <src/lib/fxl/command_line.h>
 #include <trace/observer.h>
 #include <zircon/types.h>
 
-#include <memory>
-
-#include "src/lib/fxl/command_line.h"
-#include "src/lib/fxl/macros.h"
+#include "garnet/bin/memory_monitor/capture.h"
 
 namespace memory {
 
@@ -43,8 +42,9 @@
   // Destroys a watcher proxy (called upon a connection error).
   void ReleaseWatcher(fuchsia::memory::Watcher* watcher);
   // Alerts all watchers when an update has occurred.
-  void NotifyWatchers(zx_info_kmem_stats_t stats);
+  void NotifyWatchers(const zx_info_kmem_stats_t& stats);
 
+  CaptureState capture_state_;
   uint64_t prealloc_size_;
   zx::vmo prealloc_vmo_;
   bool logging_;
diff --git a/garnet/bin/memory_monitor/printer.cc b/garnet/bin/memory_monitor/printer.cc
new file mode 100644
index 0000000..638435e
--- /dev/null
+++ b/garnet/bin/memory_monitor/printer.cc
@@ -0,0 +1,203 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "garnet/bin/memory_monitor/printer.h"
+
+namespace memory {
+
+void Printer::PrintCapture(
+    const Capture& capture, CaptureLevel level, Sorted sorted) {
+  auto const& kmem = capture.kmem();
+  os_ << "K,"
+      << capture.time() << ","
+      << kmem.total_bytes << ","
+      << kmem.free_bytes << ","
+      << kmem.wired_bytes << ","
+      << kmem.total_heap_bytes  << ","
+      << kmem.free_heap_bytes << ","
+      << kmem.vmo_bytes << ","
+      << kmem.mmu_overhead_bytes << ","
+      << kmem.ipc_bytes << ","
+      << kmem.other_bytes << "\n";
+  if (level == KMEM) {
+    return;
+  }
+
+  auto const& koid_to_process = capture.koid_to_process();
+  std::vector<zx_koid_t> process_koids;
+  for (auto const& pair : koid_to_process) {
+    process_koids.push_back(pair.first);
+  }
+  if (sorted == SORTED) {
+    sort(process_koids.begin(), process_koids.end(),
+        [&koid_to_process](zx_koid_t a, zx_koid_t b) {
+          auto const& sa = koid_to_process.at(a).stats;
+          auto const& sb = koid_to_process.at(b).stats;
+          return sa.mem_private_bytes == sb.mem_private_bytes
+                  ? sa.mem_scaled_shared_bytes > sb.mem_scaled_shared_bytes
+                  : sa.mem_private_bytes > sb.mem_private_bytes;
+          });
+  }
+  for (auto const& koid : process_koids) {
+    auto const& p = koid_to_process.at(koid);
+    os_ << "P,"
+        << p.koid << ","
+        << p.name << ","
+        << p.stats.mem_mapped_bytes << ","
+        << p.stats.mem_private_bytes << ","
+        << p.stats.mem_shared_bytes << ","
+        << p.stats.mem_scaled_shared_bytes;
+    for (auto const& v: p.vmos) {
+      os_ << "," << v;
+    }
+    os_ << "\n";
+  }
+  if (level == PROCESS) {
+    return;
+  }
+
+  auto const& koid_to_vmo = capture.koid_to_vmo();
+  std::vector<zx_koid_t> vmo_koids;
+  for (auto const& pair : koid_to_vmo) {
+    vmo_koids.push_back(pair.first);
+  }
+  if (sorted == SORTED) {
+    sort(vmo_koids.begin(), vmo_koids.end(),
+        [&koid_to_vmo](zx_koid_t a, zx_koid_t b) {
+          auto const& sa = koid_to_vmo.at(a);
+          auto const& sb = koid_to_vmo.at(b);
+          return sa.committed_bytes > sb.committed_bytes;
+          });
+  }
+  for (auto const& koid : vmo_koids) {
+    auto const& v = koid_to_vmo.at(koid);
+    os_ << "V,"
+        << v.koid << ","
+        << v.name << ","
+        << v.size_bytes << ","
+        << v.parent_koid << ","
+        << v.committed_bytes << "\n";
+  }
+  os_ << std::flush;
+}
+
+void Printer::PrintSummary(
+    const Summary& summary, CaptureLevel level, Sorted sorted) {
+  auto& kstats = summary.kstats();
+  os_ << "Time: " << summary.time()
+      << " VMO: " << kstats.vmo_bytes
+      << " Free: " << kstats.free_bytes
+      << " Wired: " << kstats.wired_bytes
+      << " Heap: " << kstats.total_heap_bytes
+      << " Heap Free: " << kstats.free_heap_bytes
+      << " Overhead: " << kstats.mmu_overhead_bytes
+      << " Other: " << kstats.other_bytes << "\n";
+
+  if (level == KMEM) {
+    return;
+  } 
+
+  auto const& summaries = summary.process_summaries();
+  std::vector<ProcessSummary> sorted_summaries;
+  if (sorted == SORTED) {
+    sorted_summaries = summaries;
+    sort(sorted_summaries.begin(), sorted_summaries.end(),
+        [](ProcessSummary a, ProcessSummary b) {
+      return a.sizes().private_bytes > b.sizes().private_bytes;
+        });
+  }
+  for (auto const& s : sorted == SORTED ? sorted_summaries : summaries) {
+    os_ << s.name() << "<" << s.koid() << "> "
+        << s.sizes().private_bytes << " "
+        << s.sizes().scaled_bytes << " "
+        << s.sizes().total_bytes << "\n";
+    if (level == PROCESS) {
+      continue;
+    }
+    auto const& name_to_sizes = s.name_to_sizes();
+    std::vector<std::string> names;
+    for (auto const& pair : name_to_sizes) {
+      names.push_back(pair.first);
+    }
+    if (sorted == SORTED) {
+      sort(names.begin(), names.end(),
+          [&name_to_sizes](std::string a, std::string b) {
+            auto const& sa = name_to_sizes.at(a);
+            auto const& sb = name_to_sizes.at(b);
+            return sa.private_bytes == sb.private_bytes
+                    ? sa.scaled_bytes > sb.scaled_bytes
+                    : sa.private_bytes > sb.private_bytes;
+            });
+    }
+    for (auto const& name : names) {
+      auto const& sizes = name_to_sizes.at(name);
+      if (sizes.total_bytes == 0) {
+        continue;
+      }
+      os_ << " " << name
+          << " " << sizes.private_bytes
+          << " " << sizes.scaled_bytes
+          << " " << sizes.total_bytes << "\n";
+    }
+  }
+  os_ << std::flush;
+}
+
+void Printer::OutputSummary(
+    const Summary& summary, Sorted sorted, zx_koid_t pid) {
+  auto const& summaries = summary.process_summaries();
+  std::vector<ProcessSummary> sorted_summaries;
+  if (sorted == SORTED) {
+    sorted_summaries = summaries;
+    sort(sorted_summaries.begin(), sorted_summaries.end(),
+        [](ProcessSummary a, ProcessSummary b) {
+      return a.sizes().private_bytes > b.sizes().private_bytes;
+        });
+  }
+  for (auto const& s : sorted == SORTED ? sorted_summaries : summaries) {
+    if (pid != ZX_KOID_INVALID) {
+      if (s.koid() != pid) {
+        continue;
+      }
+      auto const& name_to_sizes = s.name_to_sizes();
+      std::vector<std::string> names;
+      for (auto const& pair : name_to_sizes) {
+        names.push_back(pair.first);
+      }
+      if (sorted == SORTED) {
+        sort(names.begin(), names.end(),
+            [&name_to_sizes](std::string a, std::string b) {
+              auto const& sa = name_to_sizes.at(a);
+              auto const& sb = name_to_sizes.at(b);
+              return sa.private_bytes == sb.private_bytes
+                      ? sa.scaled_bytes > sb.scaled_bytes
+                      : sa.private_bytes > sb.private_bytes;
+              });
+      }
+      for (auto const& name : names) {
+        auto const& sizes = name_to_sizes.at(name);
+        if (sizes.total_bytes == 0) {
+          continue;
+        }
+        os_ << summary.time() / 1000000000 << ","
+            << s.koid() << ","
+            << name << ","
+            << sizes.private_bytes << ","
+            << sizes.scaled_bytes << ","
+            << sizes.total_bytes << "\n";
+      }
+      continue;
+    }
+    auto sizes = s.sizes();
+    os_ << summary.time() / 1000000000 << ","
+        << s.koid() << ","
+        << s.name() << ","
+        << sizes.private_bytes << ","
+        << sizes.scaled_bytes << ","
+        << sizes.total_bytes << "\n";
+  }
+  os_ << std::flush;
+}
+
+}  // namespace memory
diff --git a/garnet/bin/memory_monitor/printer.h b/garnet/bin/memory_monitor/printer.h
new file mode 100644
index 0000000..d05a93d
--- /dev/null
+++ b/garnet/bin/memory_monitor/printer.h
@@ -0,0 +1,34 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GARNET_BIN_MEMORY_MONITOR_PRINTER_H_
+#define GARNET_BIN_MEMORY_MONITOR_PRINTER_H_
+
+#include <iostream>
+#include <src/lib/fxl/macros.h>
+#include <string>
+#include <sstream>
+
+#include "garnet/bin/memory_monitor/capture.h"
+#include "garnet/bin/memory_monitor/summary.h"
+
+namespace memory {
+
+enum Sorted { UNSORTED, SORTED };
+
+class Printer {
+ public:
+  Printer(std::ostream& os) : os_(os) {}
+  void PrintCapture(const Capture& capture, CaptureLevel level, Sorted sorted);
+  void PrintSummary(const Summary& summary, CaptureLevel level, Sorted sorted);
+  void OutputSummary(const Summary& summary, Sorted sorted, zx_koid_t pid);
+
+ private:
+  std::ostream& os_;
+  FXL_DISALLOW_COPY_AND_ASSIGN(Printer);
+};
+
+}  // namespace memory
+
+#endif  // GARNET_BIN_MEMORY_MONITOR_PRINTER_H_
diff --git a/garnet/bin/memory_monitor/summary.cc b/garnet/bin/memory_monitor/summary.cc
new file mode 100644
index 0000000..746aa3e
--- /dev/null
+++ b/garnet/bin/memory_monitor/summary.cc
@@ -0,0 +1,53 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "garnet/bin/memory_monitor/summary.h"
+
+namespace memory {
+
+Summary::Summary(const Capture& capture)
+    : time_(capture.time()), kstats_(capture.kmem()) {
+  std::unordered_map<zx_koid_t, std::unordered_set<zx_koid_t>>
+      vmo_to_processes;
+  for (auto const& pair : capture.koid_to_process()) {
+    auto process_koid = pair.first;
+    auto& process = pair.second;
+    ProcessSummary s(process_koid, process.name);
+    for (auto vmo_koid: process.vmos) {
+      do {
+        vmo_to_processes[vmo_koid].insert(process_koid);
+        s.vmos_.insert(vmo_koid);
+        auto const& vmo = capture.vmo_for_koid(vmo_koid);
+        vmo_koid = vmo.parent_koid;
+      } while (vmo_koid);
+    }
+    process_summaries_.push_back(s);
+  }
+
+  for (auto& s: process_summaries_) {
+    for (auto const& v : s.vmos_) {
+      auto const& vmo = capture.vmo_for_koid(v);
+      auto share_count = vmo_to_processes[v].size();
+      auto& name_sizes = s.name_to_sizes_[vmo.name];
+      name_sizes.total_bytes += vmo.committed_bytes;
+      s.sizes_.total_bytes += vmo.committed_bytes;
+      if (share_count == 1) {
+        name_sizes.private_bytes += vmo.committed_bytes;
+        s.sizes_.private_bytes += vmo.committed_bytes;
+        name_sizes.scaled_bytes += vmo.committed_bytes;
+        s.sizes_.scaled_bytes += vmo.committed_bytes;
+      } else {
+        auto scaled_bytes = vmo.committed_bytes / share_count;
+        name_sizes.scaled_bytes += scaled_bytes;
+        s.sizes_.scaled_bytes += scaled_bytes;
+      }
+    }
+  }
+}
+
+const Sizes& ProcessSummary::GetSizes(std::string name) const {
+  return name_to_sizes_.at(name);
+}
+
+}  // namespace memory
diff --git a/garnet/bin/memory_monitor/summary.h b/garnet/bin/memory_monitor/summary.h
new file mode 100644
index 0000000..3f20067
--- /dev/null
+++ b/garnet/bin/memory_monitor/summary.h
@@ -0,0 +1,66 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GARNET_BIN_MEMORY_MONITOR_SUMMARY_H_
+#define GARNET_BIN_MEMORY_MONITOR_SUMMARY_H_
+
+#include <string>
+#include <vector>
+#include <unordered_map>
+#include <unordered_set>
+#include <zircon/types.h>
+
+#include "garnet/bin/memory_monitor/capture.h"
+
+namespace memory {
+
+struct Sizes {
+  Sizes() : private_bytes(0), scaled_bytes(0), total_bytes(0) {}
+  uint64_t private_bytes;
+  uint64_t scaled_bytes;
+  uint64_t total_bytes;
+};
+
+class ProcessSummary {
+ public:
+  zx_koid_t koid() const { return koid_; }
+  std::string name() const { return name_; }
+  Sizes sizes() const { return sizes_; }
+  const std::unordered_map<std::string, Sizes>& name_to_sizes() const {
+    return name_to_sizes_;
+  }
+  const Sizes& GetSizes(std::string name) const;
+
+ private:
+  ProcessSummary(zx_koid_t koid, std::string name) : koid_(koid), name_(name) {}
+
+  zx_koid_t koid_;
+  std::string name_;
+  Sizes sizes_;
+  std::unordered_set<zx_koid_t> vmos_;
+  std::unordered_map<std::string, Sizes> name_to_sizes_;
+
+  friend class Summary;
+};
+
+class Summary {
+ public:
+  Summary(const Capture& capture);
+
+  void SortProcessSummaries();
+  zx_time_t time() const { return time_; }
+  const zx_info_kmem_stats_t& kstats() const { return kstats_; }
+  const std::vector<ProcessSummary>& process_summaries() const {
+    return process_summaries_;
+  }
+
+ private:
+  zx_time_t time_;
+  zx_info_kmem_stats_t kstats_;
+  std::vector<ProcessSummary> process_summaries_;
+};
+
+}  // namespace memory
+
+#endif  // GARNET_BIN_MEMORY_MONITOR_MEMORY_SUMMARY_H_
diff --git a/garnet/bin/memory_monitor/test/BUILD.gn b/garnet/bin/memory_monitor/test/BUILD.gn
index e7379a9..ee5a602 100644
--- a/garnet/bin/memory_monitor/test/BUILD.gn
+++ b/garnet/bin/memory_monitor/test/BUILD.gn
@@ -7,23 +7,22 @@
 
 group("test") {
   testonly = true
-  deps = [
-    ":memory_monitor_tests",
-    ":memory_monitor_unittests",
-  ]
+  deps = [":memory_monitor_tests"]
 }
 
 executable("memory_monitor_unittests") {
   testonly = true
-
   output_name = "memory_monitor_unittests"
-
   sources = [
     "monitor_unittest.cc",
+    "printer_unittest.cc",
+    "summary_unittest.cc",
+    "test_utils.cc",
+    "test_utils.h",
   ]
 
   deps = [
-    "//garnet/bin/memory_monitor:lib",
+    "//garnet/bin/memory_monitor:mon_lib",
     "//garnet/public/lib/gtest",
     "//sdk/lib/sys/cpp/testing:unit",
     "//src/lib/fxl/test:gtest_main",
@@ -31,9 +30,7 @@
 }
 
 test_package("memory_monitor_tests") {
-  deps = [
-    ":memory_monitor_unittests",
-  ]
+  deps = [":memory_monitor_unittests"]
 
   tests = [
     {
diff --git a/garnet/bin/memory_monitor/test/monitor_unittest.cc b/garnet/bin/memory_monitor/test/monitor_unittest.cc
index a127fd3..6b1bea0 100644
--- a/garnet/bin/memory_monitor/test/monitor_unittest.cc
+++ b/garnet/bin/memory_monitor/test/monitor_unittest.cc
@@ -1,14 +1,13 @@
-// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
 #include "garnet/bin/memory_monitor/monitor.h"
 
+#include <gtest/gtest.h>
 #include <lib/gtest/test_loop_fixture.h>
 #include <lib/sys/cpp/testing/component_context_provider.h>
 
-#include "gtest/gtest.h"
-
 namespace memory {
 namespace test {
 
diff --git a/garnet/bin/memory_monitor/test/printer_unittest.cc b/garnet/bin/memory_monitor/test/printer_unittest.cc
new file mode 100644
index 0000000..437e632
--- /dev/null
+++ b/garnet/bin/memory_monitor/test/printer_unittest.cc
@@ -0,0 +1,245 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <gtest/gtest.h>
+#include <lib/gtest/test_loop_fixture.h>
+#include <src/lib/fxl/strings/string_view.h>
+#include <src/lib/fxl/strings/split_string.h>
+
+#include "garnet/bin/memory_monitor/capture.h"
+#include "garnet/bin/memory_monitor/printer.h"
+#include "garnet/bin/memory_monitor/summary.h"
+#include "garnet/bin/memory_monitor/test/test_utils.h"
+
+namespace memory {
+namespace test {
+
+using PrinterUnitTest = gtest::TestLoopFixture;
+
+void ConfirmLines(std::ostringstream& oss,
+                  std::vector<std::string> expected_lines) {
+  SCOPED_TRACE("");
+  fxl::StringView lines_view(oss.str());
+  auto lines = fxl::SplitStringCopy(
+      lines_view, "\n", fxl::kKeepWhitespace, fxl::kSplitWantNonEmpty);
+  ASSERT_EQ(expected_lines.size(), lines.size());
+  for (size_t li = 0; li < expected_lines.size(); li++) {
+    SCOPED_TRACE(li);
+    std::string expected_line = expected_lines.at(li);
+    fxl::StringView line = lines.at(li);
+    EXPECT_STREQ(expected_line.c_str(), line.data());
+  }
+}
+
+TEST_F(PrinterUnitTest, PrintCaptureKMEM) {
+  Capture c;
+  TestUtils::CreateCapture(c, {
+    .time = 1234,
+    .kmem = {
+      .total_bytes = 300,
+      .free_bytes = 100,
+      .wired_bytes = 10,
+      .total_heap_bytes = 20,
+      .free_heap_bytes = 30,
+      .vmo_bytes = 40,
+      .mmu_overhead_bytes = 50,
+      .ipc_bytes = 60,
+      .other_bytes = 70
+    },
+    .vmos = {
+      {.koid = 1, .name = "v1", .committed_bytes = 100},
+    },
+    .processes = {
+      {.koid = 100, .name = "p1", .vmos = {1}},
+    },
+  });
+  std::ostringstream oss;
+  Printer p(oss);
+
+  p.PrintCapture(c, KMEM, SORTED);
+  ConfirmLines(oss, {
+    "K,1234,300,100,10,20,30,40,50,60,70"
+  });
+}
+
+TEST_F(PrinterUnitTest, PrintCapturePROCESS) {
+  Capture c;
+  TestUtils::CreateCapture(c, {
+    .time = 1234,
+    .kmem = {
+      .total_bytes = 300,
+      .free_bytes = 100,
+      .wired_bytes = 10,
+      .total_heap_bytes = 20,
+      .free_heap_bytes = 30,
+      .vmo_bytes = 40,
+      .mmu_overhead_bytes = 50,
+      .ipc_bytes = 60,
+      .other_bytes = 70
+    },
+    .vmos = {
+      {.koid = 1, .name = "v1", .committed_bytes = 100},
+    },
+    .processes = {
+      {.koid = 100, .name = "p1", .vmos = {1}, .stats = {10, 20, 30, 40}},
+    },
+  });
+  std::ostringstream oss;
+  Printer p(oss);
+
+  p.PrintCapture(c, PROCESS, SORTED);
+  ConfirmLines(oss, {
+    "K,1234,300,100,10,20,30,40,50,60,70",
+    "P,100,p1,10,20,30,40,1"
+  });
+}
+
+TEST_F(PrinterUnitTest, PrintCaptureVMO) {
+  Capture c;
+  TestUtils::CreateCapture(c, {
+    .time = 1234,
+    .kmem = {
+      .total_bytes = 300,
+      .free_bytes = 100,
+      .wired_bytes = 10,
+      .total_heap_bytes = 20,
+      .free_heap_bytes = 30,
+      .vmo_bytes = 40,
+      .mmu_overhead_bytes = 50,
+      .ipc_bytes = 60,
+      .other_bytes = 70,
+    },
+    .vmos = {
+      {
+        .koid = 1,
+        .name = "v1",
+        .size_bytes = 100,
+        .parent_koid = 200,
+        .committed_bytes = 300,
+      },
+    },
+    .processes = {
+      {.koid = 100, .name = "p1", .vmos = {1}, .stats = {10, 20, 30, 40}},
+    },
+  });
+  std::ostringstream oss;
+  Printer p(oss);
+
+  p.PrintCapture(c, VMO, SORTED);
+  ConfirmLines(oss, {
+    "K,1234,300,100,10,20,30,40,50,60,70",
+    "P,100,p1,10,20,30,40,1",
+    "V,1,v1,100,200,300",
+  });
+}
+
+TEST_F(PrinterUnitTest, OutputSummarySingle) {
+  Capture c;
+  TestUtils::CreateCapture(c, {
+    .time = 1234L * 1000000000L,
+    .vmos = {
+      {.koid = 1, .name = "v1", .committed_bytes = 100},
+    },
+    .processes = {
+      {.koid = 100, .name = "p1", .vmos = {1}},
+    },
+  });
+  Summary s(c);
+
+  std::ostringstream oss;
+  Printer p(oss);
+
+  p.OutputSummary(s, SORTED, ZX_KOID_INVALID);
+  ConfirmLines(oss, {
+    "1234,100,p1,100,100,100",
+  });
+
+  oss.str("");
+  p.OutputSummary(s, SORTED, 100);
+  ConfirmLines(oss, {
+    "1234,100,v1,100,100,100",
+  });
+}
+
+TEST_F(PrinterUnitTest, OutputSummaryDouble) {
+  Capture c;
+  TestUtils::CreateCapture(c, {
+    .time = 1234L * 1000000000L,
+    .vmos = {
+      {.koid = 1, .name = "v1", .committed_bytes = 100},
+      {.koid = 2, .name = "v2", .committed_bytes = 200},
+    },
+    .processes = {
+      {.koid = 100, .name = "p1", .vmos = {1}},
+      {.koid = 200, .name = "p2", .vmos = {2}},
+    },
+  });
+  Summary s(c);
+
+  std::ostringstream oss;
+  Printer p(oss);
+
+  p.OutputSummary(s, SORTED, ZX_KOID_INVALID);
+  ConfirmLines(oss, {
+    "1234,200,p2,200,200,200",
+    "1234,100,p1,100,100,100",
+  });
+
+  oss.str("");
+  p.OutputSummary(s, SORTED, 100);
+  ConfirmLines(oss, {
+    "1234,100,v1,100,100,100",
+  });
+
+  oss.str("");
+  p.OutputSummary(s, SORTED, 200);
+  ConfirmLines(oss, {
+    "1234,200,v2,200,200,200",
+  });
+}
+
+TEST_F(PrinterUnitTest, OutputSummaryShared) {
+  Capture c;
+  TestUtils::CreateCapture(c, {
+    .time = 1234L * 1000000000L,
+    .vmos = {
+      {.koid = 1, .name = "v1", .committed_bytes = 100},
+      {.koid = 2, .name = "v1", .committed_bytes = 100},
+      {.koid = 3, .name = "v1", .committed_bytes = 100},
+      {.koid = 4, .name = "v2", .committed_bytes = 100},
+      {.koid = 5, .name = "v3", .committed_bytes = 200},
+    },
+    .processes = {
+      {.koid = 100, .name = "p1", .vmos = {1,2,4}},
+      {.koid = 200, .name = "p2", .vmos = {2,3,5}},
+    },
+  });
+  Summary s(c);
+
+  std::ostringstream oss;
+  Printer p(oss);
+
+  p.OutputSummary(s, SORTED, ZX_KOID_INVALID);
+  ConfirmLines(oss, {
+    "1234,200,p2,300,350,400",
+    "1234,100,p1,200,250,300",
+  });
+
+  oss.str("");
+  p.OutputSummary(s, SORTED, 100);
+  ConfirmLines(oss, {
+    "1234,100,v1,100,150,200",
+    "1234,100,v2,100,100,100",
+  });
+
+  oss.str("");
+  p.OutputSummary(s, SORTED, 200);
+  ConfirmLines(oss, {
+    "1234,200,v3,200,200,200",
+    "1234,200,v1,100,150,200",
+  });
+}
+
+}  // namespace test
+}  // namespace memory
diff --git a/garnet/bin/memory_monitor/test/summary_unittest.cc b/garnet/bin/memory_monitor/test/summary_unittest.cc
new file mode 100644
index 0000000..555c9fc
--- /dev/null
+++ b/garnet/bin/memory_monitor/test/summary_unittest.cc
@@ -0,0 +1,286 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <gtest/gtest.h>
+#include <lib/gtest/test_loop_fixture.h>
+
+#include "garnet/bin/memory_monitor/capture.h"
+#include "garnet/bin/memory_monitor/summary.h"
+#include "garnet/bin/memory_monitor/test/test_utils.h"
+
+namespace memory {
+namespace test {
+
+using SummaryUnitTest = gtest::TestLoopFixture;
+
+TEST_F(SummaryUnitTest, Single) {
+  // One process, one vmo.
+  Capture c;
+  TestUtils::CreateCapture(c, {
+    .vmos = {
+      {.koid = 1, .name = "v1", .committed_bytes = 100},
+    },
+    .processes = {
+      {.koid = 1, .name = "p1", .vmos = {1}},
+    },
+  });
+  Summary s(c);
+  ASSERT_EQ(1U, s.process_summaries().size());
+
+  ProcessSummary ps = s.process_summaries().at(0);
+  EXPECT_EQ(1U, ps.koid());
+  EXPECT_STREQ("p1", ps.name().c_str());
+  Sizes sizes = ps.sizes();
+  EXPECT_EQ(100U, sizes.private_bytes);
+  EXPECT_EQ(100U, sizes.scaled_bytes);
+  EXPECT_EQ(100U, sizes.total_bytes);
+
+  EXPECT_EQ(1U, ps.name_to_sizes().size());
+  sizes = ps.GetSizes("v1");
+  EXPECT_EQ(100U, sizes.private_bytes);
+  EXPECT_EQ(100U, sizes.scaled_bytes);
+  EXPECT_EQ(100U, sizes.total_bytes);
+}
+
+TEST_F(SummaryUnitTest, TwoVmos) {
+  // One process, two vmos with same name.
+  Capture c;
+  TestUtils::CreateCapture(c, {
+    .vmos = {
+      {.koid = 1, .name = "v1", .committed_bytes = 100},
+      {.koid = 2, .name = "v1", .committed_bytes = 100},
+    },
+    .processes = {
+      {.koid = 1, .name = "p1", .vmos = {1, 2}},
+    },
+  });
+  Summary s(c);
+  ASSERT_EQ(1U, s.process_summaries().size());
+
+  ProcessSummary ps = s.process_summaries().at(0);
+  EXPECT_EQ(1U, ps.koid());
+  EXPECT_STREQ("p1", ps.name().c_str());
+  Sizes sizes = ps.sizes();
+  EXPECT_EQ(200U, sizes.private_bytes);
+  EXPECT_EQ(200U, sizes.scaled_bytes);
+  EXPECT_EQ(200U, sizes.total_bytes);
+
+  EXPECT_EQ(1U, ps.name_to_sizes().size());
+  sizes = ps.GetSizes("v1");
+  EXPECT_EQ(200U, sizes.private_bytes);
+  EXPECT_EQ(200U, sizes.scaled_bytes);
+  EXPECT_EQ(200U, sizes.total_bytes);
+}
+
+TEST_F(SummaryUnitTest, TwoVmoNames) {
+  // One process, two vmos with different names.
+  Capture c;
+  TestUtils::CreateCapture(c, {
+    .vmos = {
+      {.koid = 1, .name = "v1", .committed_bytes = 100},
+      {.koid = 2, .name = "v2", .committed_bytes = 100},
+    },
+    .processes = {
+      {.koid = 1, .name = "p1", .vmos = {1, 2}}
+    },
+  });
+  Summary s(c);
+  ASSERT_EQ(1U, s.process_summaries().size());
+
+  ProcessSummary ps = s.process_summaries().at(0);
+  EXPECT_EQ(1U, ps.koid());
+  EXPECT_STREQ("p1", ps.name().c_str());
+  Sizes sizes = ps.sizes();
+  EXPECT_EQ(200U, sizes.private_bytes);
+  EXPECT_EQ(200U, sizes.scaled_bytes);
+  EXPECT_EQ(200U, sizes.total_bytes);
+
+  EXPECT_EQ(2U, ps.name_to_sizes().size());
+  sizes = ps.GetSizes("v1");
+  EXPECT_EQ(100U, sizes.private_bytes);
+  EXPECT_EQ(100U, sizes.scaled_bytes);
+  EXPECT_EQ(100U, sizes.total_bytes);
+  sizes = ps.GetSizes("v2");
+  EXPECT_EQ(100U, sizes.private_bytes);
+  EXPECT_EQ(100U, sizes.scaled_bytes);
+  EXPECT_EQ(100U, sizes.total_bytes);
+}
+
+TEST_F(SummaryUnitTest, Parent) {
+  // One process, two vmos with different names, one is child.
+  Capture c;
+  TestUtils::CreateCapture(c, {
+    .vmos = {
+      {.koid = 1, .name = "v1", .committed_bytes = 100},
+      {.koid = 2, .name = "v2", .committed_bytes = 100, .parent_koid = 1},
+    },
+    .processes = {
+      {.koid = 1, .name = "p1", .vmos = {2}}
+    },
+  });
+  Summary s(c);
+  ASSERT_EQ(1U, s.process_summaries().size());
+
+  ProcessSummary ps = s.process_summaries().at(0);
+  EXPECT_EQ(1U, ps.koid());
+  EXPECT_STREQ("p1", ps.name().c_str());
+  Sizes sizes = ps.sizes();
+  EXPECT_EQ(200U, sizes.private_bytes);
+  EXPECT_EQ(200U, sizes.scaled_bytes);
+  EXPECT_EQ(200U, sizes.total_bytes);
+
+  EXPECT_EQ(2U, ps.name_to_sizes().size());
+  sizes = ps.GetSizes("v1");
+  EXPECT_EQ(100U, sizes.private_bytes);
+  EXPECT_EQ(100U, sizes.scaled_bytes);
+  EXPECT_EQ(100U, sizes.total_bytes);
+  sizes = ps.GetSizes("v2");
+  EXPECT_EQ(100U, sizes.private_bytes);
+  EXPECT_EQ(100U, sizes.scaled_bytes);
+  EXPECT_EQ(100U, sizes.total_bytes);
+}
+
+TEST_F(SummaryUnitTest, TwoProcesses) {
+  // Two processes, with different vmos.
+  Capture c;
+  TestUtils::CreateCapture(c, {
+    .vmos = {
+      {.koid = 1, .name = "v1", .committed_bytes = 100},
+      {.koid = 2, .name = "v2", .committed_bytes = 100},
+    },
+    .processes = {
+      {.koid = 1, .name = "p1", .vmos = {1}},
+      {.koid = 2, .name = "p2", .vmos = {2}},
+    }
+  });
+  Summary s(c);
+  auto process_summaries = TestUtils::GetProcessSummaries(s);
+  ASSERT_EQ(2U, process_summaries.size());
+
+  ProcessSummary ps = process_summaries.at(0);
+  EXPECT_EQ(1U, ps.koid());
+  EXPECT_STREQ("p1", ps.name().c_str());
+  Sizes sizes = ps.sizes();
+  EXPECT_EQ(100U, sizes.private_bytes);
+  EXPECT_EQ(100U, sizes.scaled_bytes);
+  EXPECT_EQ(100U, sizes.total_bytes);
+
+  EXPECT_EQ(1U, ps.name_to_sizes().size());
+  sizes = ps.GetSizes("v1");
+  EXPECT_EQ(100U, sizes.private_bytes);
+  EXPECT_EQ(100U, sizes.scaled_bytes);
+  EXPECT_EQ(100U, sizes.total_bytes);
+
+  ps = process_summaries.at(1);
+  EXPECT_EQ(2U, ps.koid());
+  EXPECT_STREQ("p2", ps.name().c_str());
+  sizes = ps.sizes();
+  EXPECT_EQ(100U, sizes.private_bytes);
+  EXPECT_EQ(100U, sizes.scaled_bytes);
+  EXPECT_EQ(100U, sizes.total_bytes);
+
+  EXPECT_EQ(1U, ps.name_to_sizes().size());
+  sizes = ps.GetSizes("v2");
+  EXPECT_EQ(100U, sizes.private_bytes);
+  EXPECT_EQ(100U, sizes.scaled_bytes);
+  EXPECT_EQ(100U, sizes.total_bytes);
+}
+
+TEST_F(SummaryUnitTest, TwoProcessesShared) {
+  // Two processes, with same vmos.
+  Capture c;
+  TestUtils::CreateCapture(c, {
+    .vmos = {
+      {.koid = 1, .name = "v1", .committed_bytes = 100},
+    },
+    .processes = {
+      {.koid = 1, .name = "p1", .vmos = {1}},
+      {.koid = 2, .name = "p2", .vmos = {1}},
+    }
+  });
+  Summary s(c);
+  auto process_summaries = TestUtils::GetProcessSummaries(s);
+  ASSERT_EQ(2U, process_summaries.size());
+
+  ProcessSummary ps = process_summaries.at(0);
+  EXPECT_EQ(1U, ps.koid());
+  EXPECT_STREQ("p1", ps.name().c_str());
+  Sizes sizes = ps.sizes();
+  EXPECT_EQ(0U, sizes.private_bytes);
+  EXPECT_EQ(50U, sizes.scaled_bytes);
+  EXPECT_EQ(100U, sizes.total_bytes);
+
+  EXPECT_EQ(1U, ps.name_to_sizes().size());
+  sizes = ps.GetSizes("v1");
+  EXPECT_EQ(0U, sizes.private_bytes);
+  EXPECT_EQ(50U, sizes.scaled_bytes);
+  EXPECT_EQ(100U, sizes.total_bytes);
+
+  ps = process_summaries.at(1);
+  EXPECT_EQ(2U, ps.koid());
+  EXPECT_STREQ("p2", ps.name().c_str());
+  sizes = ps.sizes();
+  EXPECT_EQ(0U, sizes.private_bytes);
+  EXPECT_EQ(50U, sizes.scaled_bytes);
+  EXPECT_EQ(100U, sizes.total_bytes);
+
+  EXPECT_EQ(1U, ps.name_to_sizes().size());
+  sizes = ps.GetSizes("v1");
+  EXPECT_EQ(0U, sizes.private_bytes);
+  EXPECT_EQ(50U, sizes.scaled_bytes);
+  EXPECT_EQ(100U, sizes.total_bytes);
+}
+
+TEST_F(SummaryUnitTest, TwoProcessesChild) {
+  // Two processes, with one vmo shared through parantage.
+  Capture c;
+  TestUtils::CreateCapture(c, {
+    .vmos = {
+      {.koid = 1, .name = "v1", .committed_bytes = 100},
+      {.koid = 2, .name = "v2", .committed_bytes = 100, .parent_koid = 1},
+    },
+    .processes = {
+      {.koid = 1, .name = "p1", .vmos = {1}},
+      {.koid = 2, .name = "p2", .vmos = {2}},
+    }
+  });
+  Summary s(c);
+  auto process_summaries = TestUtils::GetProcessSummaries(s);
+  ASSERT_EQ(2U, process_summaries.size());
+
+  ProcessSummary ps = process_summaries.at(0);
+  EXPECT_EQ(1U, ps.koid());
+  EXPECT_STREQ("p1", ps.name().c_str());
+  Sizes sizes = ps.sizes();
+  EXPECT_EQ(0U, sizes.private_bytes);
+  EXPECT_EQ(50U, sizes.scaled_bytes);
+  EXPECT_EQ(100U, sizes.total_bytes);
+
+  EXPECT_EQ(1U, ps.name_to_sizes().size());
+  sizes = ps.GetSizes("v1");
+  EXPECT_EQ(0U, sizes.private_bytes);
+  EXPECT_EQ(50U, sizes.scaled_bytes);
+  EXPECT_EQ(100U, sizes.total_bytes);
+
+  ps = process_summaries.at(1);
+  EXPECT_EQ(2U, ps.koid());
+  EXPECT_STREQ("p2", ps.name().c_str());
+  sizes = ps.sizes();
+  EXPECT_EQ(100U, sizes.private_bytes);
+  EXPECT_EQ(150U, sizes.scaled_bytes);
+  EXPECT_EQ(200U, sizes.total_bytes);
+
+  EXPECT_EQ(2U, ps.name_to_sizes().size());
+  sizes = ps.GetSizes("v1");
+  EXPECT_EQ(0U, sizes.private_bytes);
+  EXPECT_EQ(50U, sizes.scaled_bytes);
+  EXPECT_EQ(100U, sizes.total_bytes);
+  sizes = ps.GetSizes("v2");
+  EXPECT_EQ(100U, sizes.private_bytes);
+  EXPECT_EQ(100U, sizes.scaled_bytes);
+  EXPECT_EQ(100U, sizes.total_bytes);
+}
+
+}  // namespace test
+}  // namespace memory
diff --git a/garnet/bin/memory_monitor/test/test_utils.cc b/garnet/bin/memory_monitor/test/test_utils.cc
new file mode 100644
index 0000000..2bff0f6
--- /dev/null
+++ b/garnet/bin/memory_monitor/test/test_utils.cc
@@ -0,0 +1,32 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "garnet/bin/memory_monitor/test/test_utils.h"
+
+namespace memory {
+
+// static.
+void TestUtils::CreateCapture(memory::Capture& capture,
+                              const CaptureTemplate& t) {
+  capture.time_ = t.time;
+  capture.kmem_= t.kmem;
+  for (auto vmo : t.vmos) {
+    capture.koid_to_vmo_.emplace(vmo.koid, vmo);
+  }
+  for (auto process : t.processes) {
+    capture.koid_to_process_.emplace(process.koid, process);
+  }
+}
+
+// static.
+std::vector<ProcessSummary> TestUtils::GetProcessSummaries(
+    const Summary& summary) {
+  std::vector<ProcessSummary> summaries = summary.process_summaries();
+  sort(summaries.begin(), summaries.end(),
+       [](ProcessSummary a, ProcessSummary b) {
+    return a.koid() < b.koid();
+      });
+  return summaries;
+}
+}  // namespace memory
diff --git a/garnet/bin/memory_monitor/test/test_utils.h b/garnet/bin/memory_monitor/test/test_utils.h
new file mode 100644
index 0000000..d17d15d
--- /dev/null
+++ b/garnet/bin/memory_monitor/test/test_utils.h
@@ -0,0 +1,28 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <vector>
+
+#include "garnet/bin/memory_monitor/capture.h"
+#include "garnet/bin/memory_monitor/summary.h"
+
+namespace memory {
+
+struct CaptureTemplate {
+  zx_time_t time;
+  zx_info_kmem_stats_t kmem;
+  std::vector<zx_info_vmo_t> vmos;
+  std::vector<Process> processes;
+};
+
+class TestUtils {
+ public:
+  static void CreateCapture(memory::Capture& capture, const CaptureTemplate& t);
+
+  // Sorted by koid.
+  static std::vector<ProcessSummary> GetProcessSummaries(
+      const Summary& summary);
+};
+
+}  // namespace memory
diff --git a/garnet/packages/prod/BUILD.gn b/garnet/packages/prod/BUILD.gn
index a4b08c7..00fa99e 100644
--- a/garnet/packages/prod/BUILD.gn
+++ b/garnet/packages/prod/BUILD.gn
@@ -360,6 +360,7 @@
   testonly = true
   public_deps = [
     "//garnet/bin/memory_monitor",
+    "//garnet/bin/memory_monitor:mem",
     "//garnet/bin/memory_monitor:config",
     "//garnet/packages/config:services",
   ]