db: Track app launch history with sqlite db

Add support for sqlite3 database. Track the application launches'
metrics by inserting new rows into the database.

Test: manual (launch app and then check /data/misc/iorapd/sqlite.db)
Bug: 137786053
Change-Id: I0fa337926a4aad05495b00ebee6a0b3e815d6af7
diff --git a/Android.bp b/Android.bp
index 9363a2a..a4e79bd 100644
--- a/Android.bp
+++ b/Android.bp
@@ -127,10 +127,12 @@
     static_libs: [
         "libiorap-perfetto",
         "libiorap-prefetcher",
+        "libiorap-db",
     ],
     defaults: [
         "libiorap-perfetto-default-dependencies",
         "libiorap-prefetcher-default-dependencies",
+        "libiorap-db-default-dependencies",
     ],
     // Users of 'libiorap-manager' also need to include these defaults to avoid
     // linking errors.
@@ -519,3 +521,65 @@
   static_libs: [
   ],
 }
+
+// Static libraries cannot export their dependencies,
+// the current convention is to use an extra 'defaults' rule for statics
+// to bring in all the dependencies.
+cc_defaults {
+    name: "libiorap-db-default-dependencies",
+
+    defaults: [
+    ],
+
+    include_dirs: [],
+    static_libs: [
+    ],
+    shared_libs: [
+        "libsqlite",
+    ],
+}
+
+cc_library_static {
+    name: "libiorap-db",
+    defaults: [
+        "iorap-default-flags",
+        "iorap-default-dependencies",
+        "libiorap-db-default-dependencies",
+    ],
+
+    srcs: [
+        "src/db/**/*.cc",
+    ],
+}
+
+cc_binary {
+  name: "iorap.cmd.db",
+  defaults: [
+      "iorap-default-flags",
+      "iorap-default-dependencies",
+      "libiorap-db-default-dependencies",
+  ],
+  shared_libs: [],
+  include_dirs: [],
+  srcs: [
+      "src/db/**/*.cc",
+  ],
+  // Easier debugging. TODO: make a separate debug config.
+  // XX: Using -O0 seems to completely hide some errors.
+  cflags: ["-O2", "-UNDEBUG", "-DIORAP_DB_MAIN=1"],
+  sanitize: {
+    undefined: true,
+    all_undefined: true,
+    // Pretty print when ubsan detects a problem.
+    // Otherwise it just calls abort().
+
+/*
+    diag: {
+      undefined: true,
+    },
+    */ // don't use the diag when you want it to crash.
+  },
+
+  static_libs: [
+  ],
+}
diff --git a/src/db/app_component_name.h b/src/db/app_component_name.h
new file mode 100644
index 0000000..19ab4b2
--- /dev/null
+++ b/src/db/app_component_name.h
@@ -0,0 +1,120 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef IORAP_SRC_DB_APP_COMPONENT_NAME_H_
+#define IORAP_SRC_DB_APP_COMPONENT_NAME_H_
+
+namespace iorap::db {
+
+struct AppComponentName {
+  std::string package;
+  std::string activity_name;
+
+  static bool HasAppComponentName(const std::string& s) {
+    return s.find('/') != std::string::npos;
+  }
+
+  // "com.foo.bar/.A" -> {"com.foo.bar", ".A"}
+  static AppComponentName FromString(const std::string& s) {
+    constexpr const char delimiter = '/';
+
+    if (!HasAppComponentName(s)) {
+      return {std::move(s), ""};
+    }
+
+    std::string package = s.substr(0, s.find(delimiter));
+
+    std::string activity_name = s;
+    activity_name.erase(0, s.find(delimiter) + sizeof(delimiter));
+
+    return {std::move(package), std::move(activity_name)};
+  }
+
+  // {"com.foo.bar", ".A"} -> "com.foo.bar/.A"
+  std::string ToString() const {
+    return package + "/" + activity_name;
+  }
+
+  /*
+   * '/' is encoded into %2F
+   * '%' is encoded into %25
+   *
+   * This allows the component name to be be used as a file name
+   * ('/' is illegal due to being a path separator) with minimal
+   * munging.
+   */
+
+  // "com.foo.bar%2F.A%25" -> {"com.foo.bar", ".A%"}
+  static AppComponentName FromUrlEncodedString(const std::string& s) {
+    std::string cpy = s;
+    Replace(cpy, "%2F", "/");
+    Replace(cpy, "%25", "%");
+
+    return FromString(cpy);
+  }
+
+  // {"com.foo.bar", ".A%"} -> "com.foo.bar%2F.A%25"
+  std::string ToUrlEncodedString() const {
+    std::string s = ToString();
+    Replace(s, "%", "%25");
+    Replace(s, "/", "%2F");
+    return s;
+  }
+
+  /*
+   * '/' is encoded into @@
+   * '%' is encoded into ^^
+   *
+   * Two purpose:
+   * 1. This allows the package name to be used as a file name
+   * ('/' is illegal due to being a path separator) with minimal
+   * munging.
+   * 2. This allows the package name to be used in .mk file because
+   * '%' is a special char and cannot be easily escaped in Makefile.
+   *
+   * This is a workround for test purpose.
+   * Only package name is used because activity name varies on
+   * different testing framework.
+   * Hopefully, the double "@@" and "^^" are not used in other cases.
+   */
+  // {"com.foo.bar", ".A%"} -> "com.foo.bar"
+  std::string ToMakeFileSafeEncodedPkgString() const {
+    std::string s = package;
+    Replace(s, "/", "@@");
+    Replace(s, "%", "^^");
+    return s;
+  }
+
+ private:
+  static bool Replace(std::string& str, const std::string& from, const std::string& to) {
+    // TODO: call in a loop to replace all occurrences, not just the first one.
+    const size_t start_pos = str.find(from);
+    if (start_pos == std::string::npos) {
+      return false;
+    }
+
+    str.replace(start_pos, from.length(), to);
+
+    return true;
+}
+};
+
+inline std::ostream& operator<<(std::ostream& os, const AppComponentName& name) {
+  os << name.ToString();
+  return os;
+}
+
+}  // namespace iorap::db
+
+#endif  // IORAP_SRC_DB_APP_COMPONENT_NAME_H_
diff --git a/src/db/main.cc b/src/db/main.cc
new file mode 100644
index 0000000..7b64756
--- /dev/null
+++ b/src/db/main.cc
@@ -0,0 +1,228 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "common/debug.h"
+#include "common/loggers.h"
+#include "db/app_component_name.h"
+#include "db/models.h"
+
+#include <android-base/parseint.h>
+#include <android-base/logging.h>
+
+#include <iostream>
+#include <optional>
+#include <string_view>
+#include <string>
+#include <vector>
+
+#include <sqlite3.h>
+
+#include <signal.h>
+
+namespace iorap::db {
+
+void Usage(char** argv) {
+  std::cerr << "Usage: " << argv[0] << " <path-to-sqlite.db>" << std::endl;
+  std::cerr << "" << std::endl;
+  std::cerr << "  Interface with the iorap sqlite database and issue commands." << std::endl;
+  std::cerr << "" << std::endl;
+  std::cerr << "  Optional flags:" << std::endl;
+  std::cerr << "    --help,-h                  Print this Usage." << std::endl;
+  std::cerr << "    --register-raw-trace,-rrt  Register raw trace file path." << std::endl;
+  std::cerr << "    --register-compiled-trace,-rct  Register compiled trace file path." << std::endl;
+  std::cerr << "    --insert-component,-ic     Add component if it doesn't exist." << std::endl;
+  std::cerr << "    --initialize,-i            Initialize new database." << std::endl;
+  std::cerr << "    --rescan,-rs               Update all from canonical directories." << std::endl;
+  std::cerr << "    --prune,-pr                Remove any stale file paths." << std::endl;
+  std::cerr << "    --verbose,-v               Set verbosity (default off)." << std::endl;
+  std::cerr << "    --wait,-w                  Wait for key stroke before continuing (default off)." << std::endl;
+  exit(1);
+}
+
+void error_log_sqlite_callback(void *pArg, int iErrCode, const char *zMsg) {
+  LOG(ERROR) << "SQLite error (" << iErrCode << "): " << zMsg;
+}
+
+int Main(int argc, char** argv) {
+  // Go to system logcat + stderr when running from command line.
+  android::base::InitLogging(argv, iorap::common::StderrAndLogdLogger{android::base::SYSTEM});
+
+  bool wait_for_keystroke = false;
+  bool enable_verbose = false;
+
+  bool command_format_text = false; // false = binary.
+
+  int arg_input_fd = -1;
+  int arg_output_fd = -1;
+
+  std::vector<std::string> arg_input_filenames;
+  bool arg_use_sockets = false;
+
+  std::vector<std::pair<std::string,std::string>> arg_register_raw_trace;
+  std::vector<std::pair<std::string,std::string>> arg_register_compiled_trace;
+
+  std::vector<std::string> arg_insert_component;
+
+  bool arg_initialize = false;
+  bool arg_rescan = false;
+  bool arg_prune = false;
+
+  LOG(VERBOSE) << "argparse: argc=" << argc;
+
+  for (int arg = 1; arg < argc; ++arg) {
+    std::string argstr = argv[arg];
+    bool has_arg_next = (arg+1)<argc;
+    std::string arg_next = has_arg_next ? argv[arg+1] : "";
+
+    bool has_arg_next_next = (arg+2)<argc;
+    std::string arg_next_next = has_arg_next_next ? argv[arg+2] : "";
+
+    LOG(VERBOSE) << "argparse: argv[" << arg << "]=" << argstr;
+
+    if (argstr == "--help" || argstr == "-h") {
+      Usage(argv);
+    } else if (argstr == "--register-raw-trace" || argstr == "-rrt") {
+      if (!has_arg_next_next) {
+        LOG(ERROR) << "--register-raw-trace <component/name> <filepath>";
+        Usage(argv);
+      }
+      arg_register_raw_trace.push_back({arg_next, arg_next_next});
+    } else if (argstr == "--register-compiled-trace" || argstr == "-rct") {
+      if (!has_arg_next_next) {
+        LOG(ERROR) << "--register-compiled-trace <component/name> <filepath>";
+        Usage(argv);
+      }
+      arg_register_compiled_trace.push_back({arg_next, arg_next_next});
+    } else if (argstr == "--insert-component" || argstr == "-ic") {
+      if (!has_arg_next) {
+        LOG(ERROR) << "--insert-component <component/name>";
+        Usage(argv);
+      }
+      arg_insert_component.push_back(arg_next);
+    } else if (argstr == "--initialize" || argstr == "-i") {
+      arg_initialize = true;
+    } else if (argstr == "--rescan" || argstr == "-rs") {
+      arg_rescan = true;
+    } else if (argstr == "--prune" || argstr == "-pr") {
+      arg_prune = true;
+    } else if (argstr == "--verbose" || argstr == "-v") {
+      enable_verbose = true;
+    } else if (argstr == "--wait" || argstr == "-w") {
+      wait_for_keystroke = true;
+    } else {
+      arg_input_filenames.push_back(argstr);
+    }
+  }
+
+  if (arg_input_filenames.empty()) {
+    LOG(ERROR) << "Missing positional filename to a sqlite database.";
+    Usage(argv);
+  }
+
+  if (enable_verbose) {
+    android::base::SetMinimumLogSeverity(android::base::VERBOSE);
+
+    LOG(VERBOSE) << "Verbose check";
+    LOG(VERBOSE) << "Debug check: " << ::iorap::kIsDebugBuild;
+  } else {
+    android::base::SetMinimumLogSeverity(android::base::DEBUG);
+  }
+
+  LOG(VERBOSE) << "argparse: argc=" << argc;
+
+  for (int arg = 1; arg < argc; ++arg) {
+    std::string argstr = argv[arg];
+
+    LOG(VERBOSE) << "argparse: argv[" << arg << "]=" << argstr;
+  }
+
+  // Useful to attach a debugger...
+  // 1) $> iorap.cmd.readahead -w <args>
+  // 2) $> gdbclient <pid>
+  if (wait_for_keystroke) {
+    LOG(INFO) << "Self pid: " << getpid();
+
+    raise(SIGSTOP);
+    // LOG(INFO) << "Press any key to continue...";
+    // std::cin >> wait_for_keystroke;
+  }
+
+  // auto system_call = std::make_unique<SystemCallImpl>();
+  // TODO: mock readahead calls?
+  //
+  // Uncomment this if we want to leave the process around to inspect it from adb shell.
+  // sleep(100000);
+
+  int return_code = 0;
+
+  LOG(VERBOSE) << "Hello world";
+
+
+  do {
+    SchemaModel schema_model = SchemaModel::GetOrCreate(arg_input_filenames[0]);
+    DbHandle db = schema_model.db();
+    if (arg_initialize) {
+      // Drop tables and restart from scratch. All rows are effectively dropped.
+      schema_model.Reinitialize();
+    }
+
+    for (const auto& component_and_file_name : arg_register_raw_trace) {
+      AppComponentName component_name = AppComponentName::FromString(component_and_file_name.first);
+      const std::string& file_path = component_and_file_name.second;
+
+      LOG(VERBOSE) << "--register-raw-trace " << component_name << ", file_path: " << file_path;
+
+      std::optional<ActivityModel> activity =
+          ActivityModel::SelectOrInsert(db,
+                                        component_name.package,
+                                        /*version*/std::nullopt,
+                                        component_name.activity_name);
+      DCHECK(activity.has_value());
+      LOG(DEBUG) << "Component selected/inserted: " << *activity;
+    }
+
+    for (const std::string& component : arg_insert_component) {
+      AppComponentName component_name = AppComponentName::FromString(component);
+
+      LOG(VERBOSE) << "raw component: " << component;
+      LOG(VERBOSE) << "package: " << component_name.package;
+      LOG(VERBOSE) << "activity name: " << component_name.activity_name;
+
+      LOG(VERBOSE) << "--insert-component " << component_name;
+
+      std::optional<ActivityModel> activity =
+          ActivityModel::SelectOrInsert(db,
+                                        component_name.package,
+                                        /*version*/std::nullopt,
+                                        component_name.activity_name);
+
+      DCHECK(activity.has_value());
+      LOG(DEBUG) << "Component selected/inserted: " << *activity;
+    }
+  } while (false);
+
+  LOG(VERBOSE) << "main: Terminating";
+
+  // 0 -> successfully executed all commands.
+  // 1 -> failed along the way (#on_error and also see the error logs).
+  return return_code;
+}
+
+}  // namespace iorap::db
+
+#if defined(IORAP_DB_MAIN)
+int main(int argc, char** argv) {
+  return ::iorap::db::Main(argc, argv);
+}
+#endif  // IORAP_DB_MAIN
diff --git a/src/db/models.cc b/src/db/models.cc
new file mode 100644
index 0000000..851a061
--- /dev/null
+++ b/src/db/models.cc
@@ -0,0 +1,21 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "db/models.h"
+
+namespace iorap::db {
+
+std::optional<DbHandle> SchemaModel::s_singleton_;
+
+}  // namespace iorap::db
diff --git a/src/db/models.h b/src/db/models.h
new file mode 100644
index 0000000..ed6c16d
--- /dev/null
+++ b/src/db/models.h
@@ -0,0 +1,760 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef IORAP_SRC_DB_MODELS_H_
+#define IORAP_SRC_DB_MODELS_H_
+
+#include <android-base/logging.h>
+
+#include <optional>
+#include <ostream>
+#include <string>
+#include <sstream>
+#include <type_traits>
+
+#include <sqlite3.h>
+
+namespace iorap::db {
+
+struct SqliteDbDeleter {
+  void operator()(sqlite3* db) {
+    if (db != nullptr) {
+      LOG(VERBOSE) << "sqlite3_close for: " << db;
+      sqlite3_close(db);
+    }
+  }
+};
+
+class DbHandle {
+ public:
+  // Take over ownership of sqlite3 db.
+  explicit DbHandle(sqlite3* db)
+    : db_{std::shared_ptr<sqlite3>{db, SqliteDbDeleter{}}},
+      mutex_{std::make_shared<std::mutex>()} {
+  }
+
+  sqlite3* get() {
+    return db_.get();
+  }
+
+  operator sqlite3*() {
+    return db_.get();
+  }
+
+  std::mutex& mutex() {
+    return *mutex_.get();
+  }
+
+ private:
+  std::shared_ptr<sqlite3> db_;
+  std::shared_ptr<std::mutex> mutex_;
+};
+
+class ScopedLockDb {
+ public:
+  ScopedLockDb(std::mutex& mutex) : mutex(mutex) {
+    mutex.lock();
+  }
+
+  ScopedLockDb(DbHandle& handle) : ScopedLockDb(handle.mutex()) {
+  }
+
+  ~ScopedLockDb() {
+    mutex.unlock();
+  }
+ private:
+  std::mutex& mutex;
+};
+
+class DbStatement {
+ public:
+  template <typename ... Args>
+  static DbStatement Prepare(DbHandle db, const std::string& sql, Args&&... args) {
+    return Prepare(db, sql.c_str(), std::forward<Args>(args)...);
+  }
+
+  template <typename ... Args>
+  static DbStatement Prepare(DbHandle db, const char* sql, Args&&... args) {
+    DCHECK(db.get() != nullptr);
+    DCHECK(sql != nullptr);
+
+    // LOG(VERBOSE) << "Prepare DB=" << db.get();
+
+    sqlite3_stmt* stmt = nullptr;
+    int rc = sqlite3_prepare_v2(db.get(), sql, -1, /*out*/&stmt, nullptr);
+
+    DbStatement db_stmt{db, stmt};
+    DCHECK(db_stmt.CheckOk(rc)) << sql;
+    db_stmt.BindAll(std::forward<Args>(args)...);
+
+    return db_stmt;
+  }
+
+  DbStatement(DbHandle db, sqlite3_stmt* stmt) : db_(db), stmt_(stmt) {
+  }
+
+  sqlite3_stmt* get() {
+    return stmt_;
+  }
+
+  DbHandle db() {
+    return db_;
+  }
+
+  template <typename T, typename ... Args>
+  void BindAll(T&& arg, Args&&... args) {
+    Bind(std::forward<T>(arg));
+    BindAll(std::forward<Args>(args)...);
+  }
+
+  void BindAll() {}
+
+  template <typename T>
+  void Bind(const std::optional<T>& value) {
+    if (value) {
+      Bind(*value);
+    } else {
+      BindNull();
+    }
+  }
+
+  void Bind(bool value) {
+    CheckOk(sqlite3_bind_int(stmt_, bind_counter_++, value));
+  }
+
+  void Bind(int value) {
+    CheckOk(sqlite3_bind_int(stmt_, bind_counter_++, value));
+  }
+
+  void Bind(uint64_t value) {
+    CheckOk(sqlite3_bind_int64(stmt_, bind_counter_++, static_cast<int64_t>(value)));
+  }
+
+  void Bind(const char* value) {
+    if (value != nullptr) {
+      //sqlite3_bind_text(stmt_, /*index*/bind_counter_++, value, -1, SQLITE_STATIC);
+      CheckOk(sqlite3_bind_text(stmt_, /*index*/bind_counter_++, value, -1, SQLITE_TRANSIENT));
+    } else {
+      BindNull();
+    }
+  }
+
+  void Bind(const std::string& value) {
+    Bind(value.c_str());
+  }
+
+  template <typename E, typename = std::enable_if_t<std::is_enum_v<E>>>
+  void Bind(E value) {
+    Bind(static_cast<std::underlying_type_t<E>>(value));
+  }
+
+  void BindNull() {
+    CheckOk(sqlite3_bind_null(stmt_, bind_counter_++));
+  }
+
+  int Step() {
+    ++step_counter_;
+    return sqlite3_step(stmt_);
+  }
+
+  bool Step(int expected) {
+    int rc = Step();
+    if (rc != expected) {
+      LOG(ERROR) << "SQLite error: " << sqlite3_errmsg(db_.get());
+      return false;
+    }
+    return true;
+  }
+
+  template <typename T, typename ... Args>
+  void ColumnAll(T& first, Args&... rest) {
+    Column(first);
+    ColumnAll(rest...);
+  }
+
+  void ColumnAll() {}
+
+  template <typename T>
+  void Column(std::optional<T>& value) {
+    T tmp;
+    Column(/*out*/tmp);
+
+    if (!tmp) {  // disambiguate 0 and NULL
+      const unsigned char* text = sqlite3_column_text(stmt_, column_counter_ - 1);
+      if (text == nullptr) {
+        value = std::nullopt;
+        return;
+      }
+    }
+    value = std::move(tmp);
+  }
+
+  template <typename E, typename = std::enable_if_t<std::is_enum_v<E>>>
+  void Column(E& value) {
+    std::underlying_type_t<E> tmp;
+    Column(/*out*/tmp);
+    value = static_cast<E>(tmp);
+  }
+
+  void Column(bool& value) {
+    value = sqlite3_column_int(stmt_, column_counter_++);
+  }
+
+  void Column(int& value) {
+    value = sqlite3_column_int(stmt_, column_counter_++);
+  }
+
+  void Column(uint64_t& value) {
+    value = static_cast<uint64_t>(sqlite3_column_int64(stmt_, column_counter_++));
+  }
+
+  void Column(std::string& value) {
+    const unsigned char* text = sqlite3_column_text(stmt_, column_counter_++);
+    value = std::string{reinterpret_cast<const char*>(text)};
+  }
+
+  const char* ExpandedSql() {
+    char* p = sqlite3_expanded_sql(stmt_);
+    if (p == nullptr) {
+      return "(nullptr)";
+    }
+    return p;
+  }
+
+  const char* Sql() {
+    const char* p = sqlite3_sql(stmt_);
+    if (p == nullptr) {
+      return "(nullptr)";
+    }
+    return p;
+  }
+
+
+  DbStatement(DbStatement&& other)
+    : db_{other.db_}, stmt_{other.stmt_}, bind_counter_{other.bind_counter_},
+      step_counter_{other.step_counter_} {
+    other.db_ = DbHandle{nullptr};
+    other.stmt_ = nullptr;
+  }
+
+  ~DbStatement() {
+    if (stmt_ != nullptr) {
+      DCHECK_GT(step_counter_, 0) << " forgot to call Step()?";
+      sqlite3_finalize(stmt_);
+    }
+  }
+
+ private:
+  bool CheckOk(int rc, int expected = SQLITE_OK) {
+    if (rc != expected) {
+      LOG(ERROR) << "Got error for SQL query: '" << Sql() << "'"
+                 << ", expanded: '" << ExpandedSql() << "'";
+      LOG(ERROR) << "Failed SQLite api call (" << rc << "): " << sqlite3_errstr(rc);
+    }
+    return rc == expected;
+  }
+
+  DbHandle db_;
+  sqlite3_stmt* stmt_;
+  int bind_counter_ = 1;
+  int step_counter_ = 0;
+  int column_counter_ = 0;
+};
+
+class DbQueryBuilder {
+ public:
+  // Returns the row ID that was inserted last.
+  template <typename... Args>
+  static std::optional<int> Insert(DbHandle db, const std::string& sql, Args&&... args) {
+    ScopedLockDb lock{db};
+
+    sqlite3_int64 last_rowid = sqlite3_last_insert_rowid(db.get());
+    DbStatement stmt = DbStatement::Prepare(db, sql, std::forward<Args>(args)...);
+
+    if (!stmt.Step(SQLITE_DONE)) {
+      return std::nullopt;
+    }
+
+    last_rowid = sqlite3_last_insert_rowid(db.get());
+    DCHECK_GT(last_rowid, 0);
+
+    return static_cast<int>(last_rowid);
+  }
+
+  template <typename... Args>
+  static bool SelectOnce(DbStatement& stmt, Args&... args) {
+    int rc = stmt.Step();
+    switch (rc) {
+      case SQLITE_ROW:
+        stmt.ColumnAll(/*out*/args...);
+        return true;
+      case SQLITE_DONE:
+        return false;
+      default:
+        LOG(ERROR) << "Failed to step (" << rc << "): " << sqlite3_errmsg(stmt.db());
+        return false;
+    }
+  }
+};
+
+class Model {
+ public:
+  DbHandle db() const {
+    return db_;
+  }
+
+  Model(DbHandle db) : db_{db} {
+  }
+
+ private:
+  DbHandle db_;
+};
+
+class SchemaModel : public Model {
+ public:
+  static SchemaModel GetOrCreate(std::string location) {
+    int rc = sqlite3_config(SQLITE_CONFIG_LOG, ErrorLogCallback, /*data*/nullptr);
+
+    if (rc != SQLITE_OK) {
+      LOG(FATAL) << "Failed to configure logging";
+    }
+
+    sqlite3* db = nullptr;
+    if (location != ":memory:") {
+      // Try to open DB if it already exists.
+      rc = sqlite3_open_v2(location.c_str(), /*out*/&db, SQLITE_OPEN_READWRITE, /*vfs*/nullptr);
+
+      if (rc == SQLITE_OK) {
+        LOG(INFO) << "Opened existing database at '" << location << "'";
+        return SchemaModel{DbHandle{db}, location};
+      }
+    }
+
+    // Create a new DB if one didn't exist already.
+    rc = sqlite3_open(location.c_str(), /*out*/&db);
+
+    if (rc != SQLITE_OK) {
+      LOG(FATAL) << "Failed to open DB: " << sqlite3_errmsg(db);
+    }
+
+    SchemaModel schema{DbHandle{db}, location};
+    schema.Reinitialize();
+    // TODO: migrate versions upwards when we rev the schema version
+
+    int old_version = schema.Version();
+    LOG(VERBOSE) << "Loaded schema version: " << old_version;
+
+    return schema;
+  }
+
+  void MarkSingleton() {
+    s_singleton_ = db();
+  }
+
+  static DbHandle GetSingleton() {
+    DCHECK(s_singleton_.has_value());
+    return *s_singleton_;
+  }
+
+  void Reinitialize() {
+    const char* sql_to_initialize = R"SQLC0D3(
+        DROP TABLE IF EXISTS schema_versions;
+        DROP TABLE IF EXISTS packages;
+        DROP TABLE IF EXISTS activities;
+        DROP TABLE IF EXISTS app_launch_histories;
+        DROP TABLE IF EXISTS raw_traces;
+        DROP TABLE IF EXISTS prefetch_files;
+)SQLC0D3";
+    char* err_msg = nullptr;
+    int rc = sqlite3_exec(db().get(),
+                          sql_to_initialize,
+                          /*callback*/nullptr,
+                          /*arg*/0,
+                          /*out*/&err_msg);
+    if (rc != SQLITE_OK) {
+      LOG(FATAL) << "Failed to drop tables: " << err_msg ? err_msg : "nullptr";
+    }
+
+    CreateSchema();
+    LOG(INFO) << "Reinitialized database at '" << location_ << "'";
+  }
+
+  int Version() {
+    std::string query = "SELECT MAX(version) FROM schema_versions;";
+    DbStatement stmt = DbStatement::Prepare(db(), query);
+
+    int return_value = 0;
+    if (!DbQueryBuilder::SelectOnce(stmt, /*out*/return_value)) {
+      LOG(ERROR) << "Failed to query schema version";
+      return -1;
+    }
+
+    return return_value;
+  }
+
+ protected:
+  SchemaModel(DbHandle db, std::string location) : Model{db}, location_(location) {
+  }
+
+ private:
+  static std::optional<DbHandle> s_singleton_;
+
+  void CreateSchema() {
+    const char* sql_to_initialize = R"SQLC0D3(
+        CREATE TABLE schema_versions(
+            version INTEGER NOT NULL
+        );
+        INSERT INTO schema_versions VALUES(1);
+
+        CREATE TABLE packages(
+            id INTEGER NOT NULL,
+            name TEXT NOT NULL,
+            version INTEGER,
+
+            PRIMARY KEY(id)
+        );
+
+        CREATE TABLE activities(
+            id INTEGER NOT NULL,
+            name TEXT NOT NULL,
+            package_id INTEGER NOT NULL,
+
+            PRIMARY KEY(id),
+            FOREIGN KEY (package_id) REFERENCES packages (id)
+        );
+
+        CREATE TABLE app_launch_histories(
+            id INTEGER NOT NULL PRIMARY KEY,
+            activity_id INTEGER NOT NULL,
+            -- 1:Cold, 2:Warm, 3:Hot
+            temperature INTEGER CHECK (temperature IN (1, 2, 3)) NOT NULL,
+            trace_enabled INTEGER CHECK(trace_enabled in (TRUE, FALSE)) NOT NULL,
+            readahead_enabled INTEGER CHECK(trace_enabled in (TRUE, FALSE)) NOT NULL,
+            -- absolute timestamp since epoch
+            intent_started_ns INTEGER CHECK(intent_started_ns IS NULL or intent_started_ns >= 0),
+            -- absolute timestamp since epoch
+            total_time_ns INTEGER CHECK(total_time_ns IS NULL or total_time_ns >= 0),
+            -- absolute timestamp since epoch
+            report_fully_drawn_ns INTEGER CHECK(report_fully_drawn_ns IS NULL or report_fully_drawn_ns >= 0),
+
+            FOREIGN KEY (activity_id) REFERENCES activities (id)
+        );
+
+        CREATE TABLE raw_traces(
+            id INTEGER NOT NULL PRIMARY KEY,
+            history_id INTEGER NOT NULL,
+            file_path TEXT NOT NULL,
+
+            FOREIGN KEY (history_id) REFERENCES app_launch_histories (id)
+        );
+
+        CREATE TABLE prefetch_files(
+          id INTEGER NOT NULL PRIMARY KEY,
+          activity_id INTEGER NOT NULL,
+          file_path TEXT NOT NULL,
+
+          FOREIGN KEY (activity_id) REFERENCES activities (id)
+        );
+)SQLC0D3";
+
+    char* err_msg = nullptr;
+    int rc = sqlite3_exec(db().get(),
+                          sql_to_initialize,
+                          /*callback*/nullptr,
+                          /*arg*/0,
+                          /*out*/&err_msg);
+
+    if (rc != SQLITE_OK) {
+      LOG(FATAL) << "Failed to create tables: " << err_msg ? err_msg : "nullptr";
+    }
+  }
+
+  static void ErrorLogCallback(void *pArg, int iErrCode, const char *zMsg) {
+    LOG(ERROR) << "SQLite error (" << iErrCode << "): " << zMsg;
+  }
+
+  std::string location_;
+};
+
+class PackageModel : public Model {
+ protected:
+  PackageModel(DbHandle db) : Model{db} {
+  }
+
+ public:
+  static std::optional<PackageModel> SelectById(DbHandle db, int id) {
+    ScopedLockDb lock{db};
+    int original_id = id;
+
+    std::string query = "SELECT * FROM packages WHERE id = ?1 LIMIT 1;";
+    DbStatement stmt = DbStatement::Prepare(db, query, id);
+
+    PackageModel p{db};
+    if (!DbQueryBuilder::SelectOnce(stmt, p.id, p.name, p.version)) {
+      return std::nullopt;
+    }
+
+    return p;
+  }
+
+  static std::optional<PackageModel> SelectByName(DbHandle db, const char* name) {
+    ScopedLockDb lock{db};
+
+    std::string query = "SELECT * FROM packages WHERE name = ?1 LIMIT 1;";
+    DbStatement stmt = DbStatement::Prepare(db, query, name);
+
+    PackageModel p{db};
+    if (!DbQueryBuilder::SelectOnce(stmt, p.id, p.name, p.version)) {
+      return std::nullopt;
+    }
+
+    return p;
+  }
+
+  static std::optional<PackageModel> Insert(DbHandle db,
+                                            std::string name,
+                                            std::optional<int> version) {
+    const char* sql = "INSERT INTO packages (name, version) VALUES (?1, ?2);";
+
+    std::optional<int> inserted_row_id =
+        DbQueryBuilder::Insert(db, sql, name, version);
+    if (!inserted_row_id) {
+      return std::nullopt;
+    }
+
+    PackageModel p{db};
+    p.name = name;
+    p.version = version;
+    p.id = *inserted_row_id;
+
+    return p;
+  }
+
+  int id;
+  std::string name;
+  std::optional<int> version;
+};
+
+inline std::ostream& operator<<(std::ostream& os, const PackageModel& p) {
+  os << "PackageModel{id=" << p.id << ",name=" << p.name << ",";
+  os << "version=";
+  if (p.version) {
+    os << *p.version;
+  } else {
+    os << "(nullopt)";
+  }
+  os << "}";
+  return os;
+}
+
+class ActivityModel : public Model {
+ protected:
+  ActivityModel(DbHandle db) : Model{db} {
+  }
+
+ public:
+  static std::optional<ActivityModel> SelectById(DbHandle db, int id) {
+    ScopedLockDb lock{db};
+    int original_id = id;
+
+    std::string query = "SELECT * FROM activities WHERE id = ? LIMIT 1;";
+    DbStatement stmt = DbStatement::Prepare(db, query, id);
+
+    ActivityModel p{db};
+    if (!DbQueryBuilder::SelectOnce(stmt, p.id, p.name, p.package_id)) {
+      return std::nullopt;
+    }
+
+    return p;
+  }
+
+  static std::optional<ActivityModel> SelectByNameAndPackageId(DbHandle db,
+                                                               const char* name,
+                                                               int package_id) {
+    ScopedLockDb lock{db};
+
+    std::string query = "SELECT * FROM activities WHERE name = ? AND package_id = ? LIMIT 1;";
+    DbStatement stmt = DbStatement::Prepare(db, query, name, package_id);
+
+    ActivityModel p{db};
+    if (!DbQueryBuilder::SelectOnce(stmt, p.id, p.name, p.package_id)) {
+      return std::nullopt;
+    }
+
+    return p;
+  }
+
+  static std::optional<ActivityModel> Insert(DbHandle db,
+                                             std::string name,
+                                             int package_id) {
+    const char* sql = "INSERT INTO activities (name, package_id) VALUES (?1, ?2);";
+
+    std::optional<int> inserted_row_id =
+        DbQueryBuilder::Insert(db, sql, name, package_id);
+    if (!inserted_row_id) {
+      return std::nullopt;
+    }
+
+    ActivityModel p{db};
+    p.id = *inserted_row_id;
+    p.name = name;
+    p.package_id = package_id;
+
+    return p;
+  }
+
+  // Try to select by package_name+activity_name, otherwise insert into both tables.
+  // Package version is ignored for selects.
+  static std::optional<ActivityModel> SelectOrInsert(
+      DbHandle db,
+      std::string package_name,
+      std::optional<int> package_version,
+      std::string activity_name) {
+    std::optional<PackageModel> package = PackageModel::SelectByName(db, package_name.c_str());
+    if (!package) {
+      package = PackageModel::Insert(db, package_name, package_version);
+      DCHECK(package.has_value());
+    }
+
+    std::optional<ActivityModel> activity =
+        ActivityModel::SelectByNameAndPackageId(db,
+                                                activity_name.c_str(),
+                                                package->id);
+    if (!activity) {
+      activity = Insert(db, activity_name, package->id);
+      // XX: should we really return an optional here? This feels like it should never fail.
+    }
+
+    return activity;
+  }
+
+  int id;
+  std::string name;
+  int package_id;  // PackageModel::id
+};
+
+inline std::ostream& operator<<(std::ostream& os, const ActivityModel& p) {
+  os << "ActivityModel{id=" << p.id << ",name=" << p.name << ",";
+  os << "package_id=" << p.package_id << "}";
+  return os;
+}
+
+class AppLaunchHistoryModel : public Model {
+ protected:
+  AppLaunchHistoryModel(DbHandle db) : Model{db} {
+  }
+
+ public:
+  enum class Temperature : int32_t {
+    kUninitialized = -1,  // Note: Not a valid SQL value.
+    kCold = 1,
+    kWarm = 2,
+    kHot = 3,
+  };
+
+  static std::optional<AppLaunchHistoryModel> SelectById(DbHandle db, int id) {
+    ScopedLockDb lock{db};
+    int original_id = id;
+
+    std::string query = "SELECT * FROM app_launch_histories WHERE id = ? LIMIT 1;";
+    DbStatement stmt = DbStatement::Prepare(db, query, id);
+
+    AppLaunchHistoryModel p{db};
+    if (!DbQueryBuilder::SelectOnce(stmt,
+                                    p.id,
+                                    p.activity_id,
+                                    p.temperature,
+                                    p.trace_enabled,
+                                    p.readahead_enabled,
+                                    p.total_time_ns,
+                                    p.report_fully_drawn_ns)) {
+      return std::nullopt;
+    }
+
+    return p;
+  }
+
+  static std::optional<AppLaunchHistoryModel> Insert(DbHandle db,
+                                                     int activity_id,
+                                                     AppLaunchHistoryModel::Temperature temperature,
+                                                     bool trace_enabled,
+                                                     bool readahead_enabled,
+                                                     std::optional<uint64_t> total_time_ns,
+                                                     std::optional<uint64_t> report_fully_drawn_ns)
+  {
+    const char* sql = "INSERT INTO app_launch_histories (activity_id, temperature, trace_enabled, "
+                                                        "readahead_enabled, total_time_ns, "
+                                                        "report_fully_drawn_ns) "
+                      "VALUES (?1, ?2, ?3, ?4, ?5, ?6);";
+
+    std::optional<int> inserted_row_id =
+        DbQueryBuilder::Insert(db,
+                               sql,
+                               activity_id,
+                               temperature,
+                               trace_enabled,
+                               readahead_enabled,
+                               total_time_ns,
+                               report_fully_drawn_ns);
+    if (!inserted_row_id) {
+      return std::nullopt;
+    }
+
+    AppLaunchHistoryModel p{db};
+    p.id = *inserted_row_id;
+    p.activity_id = activity_id;
+    p.temperature = temperature;
+    p.trace_enabled = trace_enabled;
+    p.readahead_enabled = readahead_enabled;
+    p.total_time_ns = total_time_ns;
+    p.report_fully_drawn_ns = report_fully_drawn_ns;
+
+    return p;
+  }
+
+  int id;
+  int activity_id;  // ActivityModel::id
+  Temperature temperature = Temperature::kUninitialized;
+  bool trace_enabled;
+  bool readahead_enabled;
+  std::optional<uint64_t> total_time_ns;
+  std::optional<uint64_t> report_fully_drawn_ns;
+};
+
+inline std::ostream& operator<<(std::ostream& os, const AppLaunchHistoryModel& p) {
+  os << "AppLaunchHistoryModel{id=" << p.id << ","
+     << "activity_id=" << p.activity_id << ","
+     << "temperature=" << static_cast<int>(p.temperature) << ","
+     << "trace_enabled=" << p.trace_enabled << ","
+     << "readahead_enabled=" << p.readahead_enabled << ","
+     << "total_time_ns=";
+  if (p.total_time_ns) {
+    os << *p.total_time_ns;
+  } else {
+    os << "(nullopt)";
+  }
+  os << ",";
+  os << "report_fully_drawn_ns=";
+  if (p.report_fully_drawn_ns) {
+    os << *p.report_fully_drawn_ns;
+  } else {
+    os << "(nullopt)";
+  }
+  os << "}";
+  return os;
+}
+
+}  // namespace iorap::db
+
+#endif  // IORAP_SRC_DB_MODELS_H_
diff --git a/src/iorapd/main.cc b/src/iorapd/main.cc
index 3cfe38f..5c9f6ad 100644
--- a/src/iorapd/main.cc
+++ b/src/iorapd/main.cc
@@ -17,6 +17,7 @@
 #include "binder/iiorap_impl.h"
 #include "common/debug.h"
 #include "common/loggers.h"
+#include "db/models.h"
 #include "manager/event_manager.h"
 
 #include <android-base/logging.h>
@@ -37,10 +38,17 @@
   // Logs go to system logcat.
   android::base::InitLogging(argv, iorap::common::StderrAndLogdLogger{android::base::SYSTEM});
 
+  LOG(INFO) << kServiceName << " (the prefetchening) firing up";
   {
-    android::ScopedTrace trace_main{ATRACE_TAG_PACKAGE_MANAGER, "main"};
-    LOG(INFO) << kServiceName << " (the prefetchening) firing up";
+    android::ScopedTrace trace_db_init{ATRACE_TAG_PACKAGE_MANAGER, "IorapNativeService::db_init"};
+    iorap::db::SchemaModel db_schema =
+        iorap::db::SchemaModel::GetOrCreate(
+            android::base::GetProperty("iorapd.db.location",
+                                       "/data/misc/iorapd/sqlite.db"));
+    db_schema.MarkSingleton();
+  }
 
+  {
     android::ScopedTrace trace_start{ATRACE_TAG_PACKAGE_MANAGER, "IorapNativeService::start"};
 
     // TODO: use fruit for this DI.
diff --git a/src/manager/event_manager.cc b/src/manager/event_manager.cc
index 9976223..0dac5fd 100644
--- a/src/manager/event_manager.cc
+++ b/src/manager/event_manager.cc
@@ -16,6 +16,8 @@
 
 #include "common/debug.h"
 #include "common/expected.h"
+#include "db/app_component_name.h"
+#include "db/models.h"
 #include "manager/event_manager.h"
 #include "perfetto/rx_producer.h"
 #include "prefetcher/read_ahead.h"
@@ -26,6 +28,7 @@
 
 #include <atomic>
 #include <functional>
+#include <utils/Trace.h>
 
 using rxcpp::observe_on_one_worker;
 
@@ -39,98 +42,7 @@
 using perfetto::PerfettoStreamCommand;
 using perfetto::PerfettoTraceProto;
 
-struct AppComponentName {
-  std::string package;
-  std::string activity_name;
-
-  static bool HasAppComponentName(const std::string& s) {
-    return s.find('/') != std::string::npos;
-  }
-
-  // "com.foo.bar/.A" -> {"com.foo.bar", ".A"}
-  static AppComponentName FromString(const std::string& s) {
-    constexpr const char delimiter = '/';
-    std::string package = s.substr(0, delimiter);
-
-    std::string activity_name = s;
-    activity_name.erase(0, s.find(delimiter) + sizeof(delimiter));
-
-    return {std::move(package), std::move(activity_name)};
-  }
-
-  // {"com.foo.bar", ".A"} -> "com.foo.bar/.A"
-  std::string ToString() const {
-    return package + "/" + activity_name;
-  }
-
-  /*
-   * '/' is encoded into %2F
-   * '%' is encoded into %25
-   *
-   * This allows the component name to be be used as a file name
-   * ('/' is illegal due to being a path separator) with minimal
-   * munging.
-   */
-
-  // "com.foo.bar%2F.A%25" -> {"com.foo.bar", ".A%"}
-  static AppComponentName FromUrlEncodedString(const std::string& s) {
-    std::string cpy = s;
-    Replace(cpy, "%2F", "/");
-    Replace(cpy, "%25", "%");
-
-    return FromString(cpy);
-  }
-
-  // {"com.foo.bar", ".A%"} -> "com.foo.bar%2F.A%25"
-  std::string ToUrlEncodedString() const {
-    std::string s = ToString();
-    Replace(s, "%", "%25");
-    Replace(s, "/", "%2F");
-    return s;
-  }
-
-  /*
-   * '/' is encoded into @@
-   * '%' is encoded into ^^
-   *
-   * Two purpose:
-   * 1. This allows the pacakge name to be used as a file name
-   * ('/' is illegal due to being a path separator) with minimal
-   * munging.
-   * 2. This allows the package name to be used in .mk file because
-   * '%' is a special char and cannot be easily escaped in Makefile.
-   *
-   * This is a workround for test purpose.
-   * Only package name is used because activity name varies on
-   * diffferent testing framework.
-   * Hopefully, the double "@@" and "^^" are not used in other cases.
-   */
-  // {"com.foo.bar", ".A%"} -> "com.foo.bar"
-  std::string ToMakeFileSafeEncodedPkgString() const {
-    std::string s = package;
-    Replace(s, "/", "@@");
-    Replace(s, "%", "^^");
-    return s;
-  }
-
- private:
-  static bool Replace(std::string& str, const std::string& from, const std::string& to) {
-    // TODO: call in a loop to replace all occurrences, not just the first one.
-    const size_t start_pos = str.find(from);
-    if (start_pos == std::string::npos) {
-      return false;
-    }
-
-    str.replace(start_pos, from.length(), to);
-
-    return true;
-}
-};
-
-std::ostream& operator<<(std::ostream& os, const AppComponentName& name) {
-  os << name.ToString();
-  return os;
-}
+using db::AppComponentName;
 
 // Main logic of the #OnAppLaunchEvent scan method.
 //
@@ -144,6 +56,7 @@
   // Sequence ID is shared amongst the same app launch sequence,
   // but changes whenever a new app launch sequence begins.
   size_t sequence_id_ = static_cast<size_t>(-1);
+  std::optional<AppLaunchEvent::Temperature> temperature_;
 
   // labeled as 'shared' due to rx not being able to handle move-only objects.
   // lifetime: in practice equivalent to unique_ptr.
@@ -188,6 +101,9 @@
   void OnNewEvent(const AppLaunchEvent& event) {
     LOG(VERBOSE) << "AppLaunchEventState#OnNewEvent: " << event;
 
+    android::ScopedTrace trace_db_init{ATRACE_TAG_PACKAGE_MANAGER,
+                                       "IorapNativeService::OnAppLaunchEvent"};
+
     using Type = AppLaunchEvent::Type;
 
     DCHECK_GE(event.sequence_id, 0);
@@ -229,6 +145,7 @@
         // Restart tracing if the activity was unexpected.
 
         AppLaunchEvent::Temperature temperature = event.temperature;
+        temperature_ = temperature;
         if (temperature != AppLaunchEvent::Temperature::kCold) {
           LOG(DEBUG) << "AppLaunchEventState#OnNewEvent aborting trace due to non-cold temperature";
 
@@ -270,6 +187,7 @@
         break;
       }
       case Type::kActivityLaunchFinished:
+        RecordDbLaunchHistory();
         // Finish tracing and collect trace buffer.
         //
         // TODO: this happens automatically when perfetto finishes its
@@ -428,6 +346,48 @@
 
     // FIXME: how do we clear this vector?
   }
+
+  void RecordDbLaunchHistory() {
+    // TODO: deferred queue into a different lower priority thread.
+    if (!component_name_ || !temperature_) {
+      LOG(VERBOSE) << "Skip RecordDbLaunchHistory, no component name available.";
+      return;
+    }
+
+    android::ScopedTrace trace{ATRACE_TAG_PACKAGE_MANAGER,
+                               "IorapNativeService::RecordDbLaunchHistory"};
+    db::DbHandle db{db::SchemaModel::GetSingleton()};
+
+    using namespace iorap::db;
+
+    std::optional<ActivityModel> activity =
+        ActivityModel::SelectOrInsert(db,
+                                      component_name_->package,
+                                      /*version*/std::nullopt,
+                                      component_name_->activity_name);
+
+    if (!activity) {
+      LOG(WARNING) << "Failed to query activity row for : " << *component_name_;
+      return;
+    }
+
+    auto temp = static_cast<db::AppLaunchHistoryModel::Temperature>(*temperature_);
+
+    std::optional<AppLaunchHistoryModel> alh =
+        AppLaunchHistoryModel::Insert(db,
+                                      activity->id,
+                                      temp,
+                                      IsTracing(),
+                                      IsReadAhead(),
+                                      /*total_time_ns*/std::nullopt,
+                                      /*report_fully_drawn_ns*/std::nullopt);
+    if (!alh) {
+      LOG(WARNING) << "Failed to insert app_launch_histories row";
+      return;
+    }
+
+    LOG(VERBOSE) << "RecordDbLaunchHistory: " << *alh;
+  }
 };
 
 // Convert callback pattern into reactive pattern.