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/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.