trace_processor: thread table implementation
Fast filtering on Utid. Can be joined with the process table on Upid.
Also a few small changes to process table to be consistent with the
thread table.
Bug:80416541
Change-Id: Ie97f1997fef28d6af250aa8199b59aeebb5af8f1
diff --git a/src/trace_processor/BUILD.gn b/src/trace_processor/BUILD.gn
index bed05df..47995b6 100644
--- a/src/trace_processor/BUILD.gn
+++ b/src/trace_processor/BUILD.gn
@@ -43,6 +43,8 @@
"sched_slice_table.cc",
"sched_slice_table.h",
"scoped_db.h",
+ "thread_table.cc",
+ "thread_table.h",
"trace_database.cc",
"trace_database.h",
"trace_parser.cc",
@@ -67,6 +69,7 @@
"process_table_unittest.cc",
"query_constraints_unittest.cc",
"sched_slice_table_unittest.cc",
+ "thread_table_unittest.cc",
"trace_parser_unittest.cc",
"trace_storage_unittest.cc",
]
diff --git a/src/trace_processor/process_table.cc b/src/trace_processor/process_table.cc
index f8ad6c7..a4d6d48 100644
--- a/src/trace_processor/process_table.cc
+++ b/src/trace_processor/process_table.cc
@@ -147,11 +147,11 @@
int ProcessTable::Cursor::Column(sqlite3_context* context, int N) {
switch (N) {
case Column::kUpid: {
- sqlite3_result_int64(context, current_upid_);
+ sqlite3_result_int64(context, upid_filter_.current);
break;
}
case Column::kName: {
- auto process = storage_->GetProcess(current_upid_);
+ auto process = storage_->GetProcess(upid_filter_.current);
const auto& name = storage_->GetString(process.name_id);
sqlite3_result_text(context, name.c_str(),
static_cast<int>(name.length()), nullptr);
@@ -172,10 +172,10 @@
PERFETTO_DCHECK(qc.constraints().size() == static_cast<size_t>(argc));
- min_upid_ = 1;
- max_upid_ = static_cast<uint32_t>(storage_->process_count());
- desc_ = false;
- current_upid_ = min_upid_;
+ upid_filter_.min = 1;
+ upid_filter_.max = static_cast<uint32_t>(storage_->process_count());
+ upid_filter_.desc = false;
+ upid_filter_.current = upid_filter_.min;
for (size_t j = 0; j < qc.constraints().size(); j++) {
const auto& cs = qc.constraints()[j];
@@ -186,19 +186,22 @@
// constraints in the query. Everything between min and max (inclusive)
// will be returned.
if (IsOpGe(cs.op) || IsOpGt(cs.op)) {
- min_upid_ = IsOpGt(cs.op) ? constraint_upid + 1 : constraint_upid;
+ upid_filter_.min =
+ IsOpGt(cs.op) ? constraint_upid + 1 : constraint_upid;
} else if (IsOpLe(cs.op) || IsOpLt(cs.op)) {
- max_upid_ = IsOpLt(cs.op) ? constraint_upid - 1 : constraint_upid;
+ upid_filter_.max =
+ IsOpLt(cs.op) ? constraint_upid - 1 : constraint_upid;
} else if (IsOpEq(cs.op)) {
- min_upid_ = constraint_upid;
- max_upid_ = constraint_upid;
+ upid_filter_.min = constraint_upid;
+ upid_filter_.max = constraint_upid;
}
}
}
for (const auto& ob : qc.order_by()) {
if (ob.iColumn == Column::kUpid) {
- desc_ = ob.desc;
- current_upid_ = desc_ ? max_upid_ : min_upid_;
+ upid_filter_.desc = ob.desc;
+ upid_filter_.current =
+ upid_filter_.desc ? upid_filter_.max : upid_filter_.min;
}
}
@@ -206,10 +209,10 @@
}
int ProcessTable::Cursor::Next() {
- if (desc_) {
- --current_upid_;
+ if (upid_filter_.desc) {
+ --upid_filter_.current;
} else {
- ++current_upid_;
+ ++upid_filter_.current;
}
return SQLITE_OK;
}
@@ -219,7 +222,8 @@
}
int ProcessTable::Cursor::Eof() {
- return desc_ ? current_upid_ < min_upid_ : current_upid_ > max_upid_;
+ return upid_filter_.desc ? upid_filter_.current < upid_filter_.min
+ : upid_filter_.current > upid_filter_.max;
}
} // namespace trace_processor
diff --git a/src/trace_processor/process_table.h b/src/trace_processor/process_table.h
index 56829cd..b20b25d 100644
--- a/src/trace_processor/process_table.h
+++ b/src/trace_processor/process_table.h
@@ -58,11 +58,15 @@
private:
sqlite3_vtab_cursor base_; // Must be first.
+ struct UpidFilter {
+ TraceStorage::UniquePid min;
+ TraceStorage::UniquePid max;
+ TraceStorage::UniquePid current;
+ bool desc;
+ };
+
const TraceStorage* const storage_;
- TraceStorage::UniquePid min_upid_;
- TraceStorage::UniquePid max_upid_;
- TraceStorage::UniquePid current_upid_;
- bool desc_ = false;
+ UpidFilter upid_filter_;
};
static inline Cursor* AsCursor(sqlite3_vtab_cursor* cursor) {
diff --git a/src/trace_processor/process_table_unittest.cc b/src/trace_processor/process_table_unittest.cc
index 75d57af..e7f24ba 100644
--- a/src/trace_processor/process_table_unittest.cc
+++ b/src/trace_processor/process_table_unittest.cc
@@ -24,8 +24,6 @@
namespace trace_processor {
namespace {
-using Column = ProcessTable::Column;
-
class ProcessTableUnittest : public ::testing::Test {
public:
ProcessTableUnittest() {
diff --git a/src/trace_processor/thread_table.cc b/src/trace_processor/thread_table.cc
new file mode 100644
index 0000000..55fc769
--- /dev/null
+++ b/src/trace_processor/thread_table.cc
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2018 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 "src/trace_processor/thread_table.h"
+#include "src/trace_processor/query_constraints.h"
+
+#include "perfetto/base/logging.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+namespace {
+
+inline bool IsOpEq(int op) {
+ return op == SQLITE_INDEX_CONSTRAINT_EQ;
+}
+
+inline bool IsOpGe(int op) {
+ return op == SQLITE_INDEX_CONSTRAINT_GE;
+}
+
+inline bool IsOpGt(int op) {
+ return op == SQLITE_INDEX_CONSTRAINT_GT;
+}
+
+inline bool IsOpLe(int op) {
+ return op == SQLITE_INDEX_CONSTRAINT_LE;
+}
+
+inline bool IsOpLt(int op) {
+ return op == SQLITE_INDEX_CONSTRAINT_LT;
+}
+
+inline ThreadTable* AsTable(sqlite3_vtab* vtab) {
+ return reinterpret_cast<ThreadTable*>(vtab);
+}
+
+} // namespace
+
+ThreadTable::ThreadTable(const TraceStorage* storage) : storage_(storage) {
+ static_assert(offsetof(ThreadTable, base_) == 0,
+ "SQLite base class must be first member of the table");
+ memset(&base_, 0, sizeof(base_));
+}
+
+sqlite3_module ThreadTable::CreateModule() {
+ sqlite3_module module;
+ memset(&module, 0, sizeof(module));
+ module.xConnect = [](sqlite3* db, void* raw_args, int, const char* const*,
+ sqlite3_vtab** tab, char**) {
+ int res = sqlite3_declare_vtab(db,
+ "CREATE TABLE threads("
+ "utid UNSIGNED INT, "
+ "upid UNSIGNED INT, "
+ "name TEXT, "
+ "PRIMARY KEY(utid)"
+ ") WITHOUT ROWID;");
+ if (res != SQLITE_OK)
+ return res;
+ TraceStorage* storage = static_cast<TraceStorage*>(raw_args);
+ *tab = reinterpret_cast<sqlite3_vtab*>(new ThreadTable(storage));
+ return SQLITE_OK;
+ };
+ module.xBestIndex = [](sqlite3_vtab* t, sqlite3_index_info* i) {
+ return AsTable(t)->BestIndex(i);
+ };
+ module.xDisconnect = [](sqlite3_vtab* t) {
+ delete AsTable(t);
+ return SQLITE_OK;
+ };
+ module.xOpen = [](sqlite3_vtab* t, sqlite3_vtab_cursor** c) {
+ return AsTable(t)->Open(c);
+ };
+ module.xClose = [](sqlite3_vtab_cursor* c) {
+ delete AsCursor(c);
+ return SQLITE_OK;
+ };
+ module.xFilter = [](sqlite3_vtab_cursor* c, int i, const char* s, int a,
+ sqlite3_value** v) {
+ return AsCursor(c)->Filter(i, s, a, v);
+ };
+ module.xNext = [](sqlite3_vtab_cursor* c) { return AsCursor(c)->Next(); };
+ module.xEof = [](sqlite3_vtab_cursor* c) { return AsCursor(c)->Eof(); };
+ module.xColumn = [](sqlite3_vtab_cursor* c, sqlite3_context* a, int b) {
+ return AsCursor(c)->Column(a, b);
+ };
+ module.xRowid = [](sqlite3_vtab_cursor*, sqlite_int64*) {
+ return SQLITE_ERROR;
+ };
+ return module;
+}
+
+int ThreadTable::Open(sqlite3_vtab_cursor** ppCursor) {
+ *ppCursor = reinterpret_cast<sqlite3_vtab_cursor*>(new Cursor(storage_));
+ return SQLITE_OK;
+}
+
+// Called at least once but possibly many times before filtering things and is
+// the best time to keep track of constriants.
+int ThreadTable::BestIndex(sqlite3_index_info* idx) {
+ QueryConstraints qc;
+
+ for (int i = 0; i < idx->nOrderBy; i++) {
+ Column column = static_cast<Column>(idx->aOrderBy[i].iColumn);
+ unsigned char desc = idx->aOrderBy[i].desc;
+ qc.AddOrderBy(column, desc);
+ }
+ idx->orderByConsumed = true;
+
+ for (int i = 0; i < idx->nConstraint; i++) {
+ const auto& cs = idx->aConstraint[i];
+ if (!cs.usable)
+ continue;
+ qc.AddConstraint(cs.iColumn, cs.op);
+
+ idx->estimatedCost = cs.iColumn == Column::kUtid ? 10 : 100;
+
+ // argvIndex is 1-based so use the current size of the vector.
+ int argv_index = static_cast<int>(qc.constraints().size());
+ idx->aConstraintUsage[i].argvIndex = argv_index;
+ }
+
+ idx->idxStr = qc.ToNewSqlite3String().release();
+ idx->needToFreeIdxStr = true;
+
+ return SQLITE_OK;
+}
+
+ThreadTable::Cursor::Cursor(const TraceStorage* storage) : storage_(storage) {
+ static_assert(offsetof(Cursor, base_) == 0,
+ "SQLite base class must be first member of the cursor");
+ memset(&base_, 0, sizeof(base_));
+}
+
+int ThreadTable::Cursor::Column(sqlite3_context* context, int N) {
+ auto thread = storage_->GetThread(utid_filter_.current);
+ switch (N) {
+ case Column::kUtid: {
+ sqlite3_result_int64(context, utid_filter_.current);
+ break;
+ }
+ case Column::kUpid: {
+ sqlite3_result_int64(context, thread.upid);
+ break;
+ }
+ case Column::kName: {
+ const auto& name = storage_->GetString(thread.name_id);
+ sqlite3_result_text(context, name.c_str(),
+ static_cast<int>(name.length()), nullptr);
+ break;
+ }
+ default: {
+ PERFETTO_FATAL("Unknown column %d", N);
+ break;
+ }
+ }
+ return SQLITE_OK;
+}
+
+int ThreadTable::Cursor::Filter(int /*idxNum*/,
+ const char* idxStr,
+ int argc,
+ sqlite3_value** argv) {
+ QueryConstraints qc = QueryConstraints::FromString(idxStr);
+
+ PERFETTO_DCHECK(qc.constraints().size() == static_cast<size_t>(argc));
+
+ utid_filter_.min = 1;
+ utid_filter_.max = static_cast<uint32_t>(storage_->thread_count());
+ utid_filter_.desc = false;
+ utid_filter_.current = utid_filter_.min;
+
+ for (size_t j = 0; j < qc.constraints().size(); j++) {
+ const auto& cs = qc.constraints()[j];
+ if (cs.iColumn == Column::kUtid) {
+ TraceStorage::UniqueTid constraint_utid =
+ static_cast<TraceStorage::UniqueTid>(sqlite3_value_int(argv[j]));
+ // Filter the range of utids that we are interested in, based on the
+ // constraints in the query. Everything between min and max (inclusive)
+ // will be returned.
+ if (IsOpGe(cs.op) || IsOpGt(cs.op)) {
+ utid_filter_.min =
+ IsOpGt(cs.op) ? constraint_utid + 1 : constraint_utid;
+ } else if (IsOpLe(cs.op) || IsOpLt(cs.op)) {
+ utid_filter_.max =
+ IsOpLt(cs.op) ? constraint_utid - 1 : constraint_utid;
+ } else if (IsOpEq(cs.op)) {
+ utid_filter_.min = constraint_utid;
+ utid_filter_.max = constraint_utid;
+ }
+ }
+ }
+ for (const auto& ob : qc.order_by()) {
+ if (ob.iColumn == Column::kUtid) {
+ utid_filter_.desc = ob.desc;
+ utid_filter_.current =
+ utid_filter_.desc ? utid_filter_.max : utid_filter_.min;
+ }
+ }
+
+ return SQLITE_OK;
+}
+
+int ThreadTable::Cursor::Next() {
+ if (utid_filter_.desc) {
+ --utid_filter_.current;
+ } else {
+ ++utid_filter_.current;
+ }
+
+ return SQLITE_OK;
+}
+
+int ThreadTable::Cursor::Eof() {
+ return utid_filter_.desc ? utid_filter_.current < utid_filter_.min
+ : utid_filter_.current > utid_filter_.max;
+}
+} // namespace trace_processor
+} // namespace perfetto
diff --git a/src/trace_processor/thread_table.h b/src/trace_processor/thread_table.h
new file mode 100644
index 0000000..97c0b4e
--- /dev/null
+++ b/src/trace_processor/thread_table.h
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2018 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 SRC_TRACE_PROCESSOR_THREAD_TABLE_H_
+#define SRC_TRACE_PROCESSOR_THREAD_TABLE_H_
+
+#include <limits>
+#include <memory>
+
+#include "sqlite3.h"
+#include "src/trace_processor/trace_storage.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+// The implementation of the SQLite table containing each unique process with
+// the metadata for those processes.
+class ThreadTable {
+ public:
+ enum Column { kUtid = 0, kUpid = 1, kName = 2 };
+ struct OrderBy {
+ Column column = kUpid;
+ bool desc = false;
+ };
+
+ ThreadTable(const TraceStorage*);
+ static sqlite3_module CreateModule();
+
+ // Implementation for sqlite3_vtab.
+ int BestIndex(sqlite3_index_info*);
+ int Open(sqlite3_vtab_cursor**);
+
+ private:
+ using Constraint = sqlite3_index_info::sqlite3_index_constraint;
+
+ struct IndexInfo {
+ std::vector<OrderBy> order_by;
+ std::vector<Constraint> constraints;
+ };
+
+ class Cursor {
+ public:
+ Cursor(const TraceStorage*);
+
+ // Implementation of sqlite3_vtab_cursor.
+ int Filter(int idxNum, const char* idxStr, int argc, sqlite3_value** argv);
+ int Next();
+ int Eof();
+
+ int Column(sqlite3_context* context, int N);
+ int RowId(sqlite_int64* rowId);
+
+ private:
+ sqlite3_vtab_cursor base_; // Must be first.
+
+ struct UtidFilter {
+ TraceStorage::UniqueTid min;
+ TraceStorage::UniqueTid max;
+ TraceStorage::UniqueTid current;
+ bool desc;
+ };
+
+ const TraceStorage* const storage_;
+ UtidFilter utid_filter_;
+ };
+
+ static inline Cursor* AsCursor(sqlite3_vtab_cursor* cursor) {
+ return reinterpret_cast<Cursor*>(cursor);
+ }
+
+ sqlite3_vtab base_; // Must be first.
+ const TraceStorage* const storage_;
+};
+} // namespace trace_processor
+} // namespace perfetto
+
+#endif // SRC_TRACE_PROCESSOR_THREAD_TABLE_H_
diff --git a/src/trace_processor/thread_table_unittest.cc b/src/trace_processor/thread_table_unittest.cc
new file mode 100644
index 0000000..5b26822
--- /dev/null
+++ b/src/trace_processor/thread_table_unittest.cc
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2018 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 "src/trace_processor/thread_table.h"
+#include "src/trace_processor/process_table.h"
+#include "src/trace_processor/scoped_db.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+namespace perfetto {
+namespace trace_processor {
+namespace {
+
+class ThreadTableUnittest : public ::testing::Test {
+ public:
+ ThreadTableUnittest() {
+ sqlite3* db = nullptr;
+ PERFETTO_CHECK(sqlite3_open(":memory:", &db) == SQLITE_OK);
+ db_.reset(db);
+
+ static sqlite3_module t_module = ThreadTable::CreateModule();
+ sqlite3_create_module(*db_, "thread", &t_module,
+ static_cast<void*>(&storage_));
+ static sqlite3_module p_module = ProcessTable::CreateModule();
+ sqlite3_create_module(*db_, "process", &p_module,
+ static_cast<void*>(&storage_));
+ }
+
+ void PrepareValidStatement(const std::string& sql) {
+ int size = static_cast<int>(sql.size());
+ sqlite3_stmt* stmt;
+ ASSERT_EQ(sqlite3_prepare_v2(*db_, sql.c_str(), size, &stmt, nullptr),
+ SQLITE_OK);
+ stmt_.reset(stmt);
+ }
+
+ const char* GetColumnAsText(int colId) {
+ return reinterpret_cast<const char*>(sqlite3_column_text(*stmt_, colId));
+ }
+
+ protected:
+ TraceStorage storage_;
+ ScopedDb db_;
+ ScopedStmt stmt_;
+};
+
+TEST_F(ThreadTableUnittest, Select) {
+ uint32_t cpu = 3;
+ uint64_t timestamp = 100;
+ uint32_t pid_1 = 1;
+ uint32_t prev_state = 32;
+ static const char kCommProc1[] = "thread1";
+ static const char kCommProc2[] = "thread2";
+ uint32_t pid_2 = 4;
+
+ storage_.PushSchedSwitch(cpu, timestamp, pid_1, prev_state, kCommProc1,
+ sizeof(kCommProc1) - 1, pid_2);
+ storage_.PushSchedSwitch(cpu, timestamp + 1, pid_2, prev_state, kCommProc2,
+ sizeof(kCommProc2) - 1, pid_1);
+
+ storage_.PushProcess(2, "test", 4);
+ storage_.MatchThreadToProcess(1, 2);
+ PrepareValidStatement("SELECT utid, upid, name FROM thread");
+
+ ASSERT_EQ(sqlite3_step(*stmt_), SQLITE_ROW);
+ ASSERT_EQ(sqlite3_column_int(*stmt_, 0), 1 /* utid */);
+ ASSERT_EQ(sqlite3_column_int(*stmt_, 1), 1 /* upid */);
+ ASSERT_STREQ(GetColumnAsText(2), kCommProc1);
+
+ ASSERT_EQ(sqlite3_step(*stmt_), SQLITE_DONE);
+}
+
+TEST_F(ThreadTableUnittest, SelectWhere) {
+ uint32_t cpu = 3;
+ uint64_t timestamp = 100;
+ uint32_t pid_1 = 1;
+ uint32_t prev_state = 32;
+ static const char kCommProc1[] = "thread1";
+ static const char kCommProc2[] = "thread2";
+ uint32_t pid_2 = 4;
+
+ storage_.PushSchedSwitch(cpu, timestamp, pid_1, prev_state, kCommProc1,
+ sizeof(kCommProc1) - 1, pid_2);
+ storage_.PushSchedSwitch(cpu, timestamp + 1, pid_2, prev_state, kCommProc2,
+ sizeof(kCommProc2) - 1, pid_1);
+ storage_.PushSchedSwitch(cpu, timestamp + 2, pid_1, prev_state, kCommProc1,
+ sizeof(kCommProc1) - 1, pid_2);
+
+ storage_.PushProcess(2, "test", 4);
+ storage_.MatchThreadToProcess(1, 2);
+ storage_.MatchThreadToProcess(2, 2);
+ PrepareValidStatement("SELECT utid, upid, name FROM thread where utid = 1");
+
+ ASSERT_EQ(sqlite3_step(*stmt_), SQLITE_ROW);
+ ASSERT_EQ(sqlite3_column_int(*stmt_, 0), 1 /* utid */);
+ ASSERT_EQ(sqlite3_column_int(*stmt_, 1), 1 /* upid */);
+ ASSERT_STREQ(GetColumnAsText(2), kCommProc1);
+
+ ASSERT_EQ(sqlite3_step(*stmt_), SQLITE_DONE);
+}
+
+TEST_F(ThreadTableUnittest, JoinWithProcess) {
+ uint32_t cpu = 3;
+ uint64_t timestamp = 100;
+ uint32_t pid_1 = 1;
+ uint32_t prev_state = 32;
+ static const char kCommProc1[] = "thread1";
+ static const char kCommProc2[] = "thread2";
+ uint32_t pid_2 = 4;
+
+ storage_.PushSchedSwitch(cpu, timestamp, pid_1, prev_state, kCommProc1,
+ sizeof(kCommProc1) - 1, pid_2);
+ storage_.PushSchedSwitch(cpu, timestamp + 1, pid_2, prev_state, kCommProc2,
+ sizeof(kCommProc2) - 1, pid_1);
+
+ storage_.PushProcess(2, "test", 4);
+ storage_.PushProcess(3, "test1", 5);
+ storage_.MatchThreadToProcess(1, 2);
+ PrepareValidStatement(
+ "SELECT utid, thread.name, process.upid, process.name FROM thread INNER "
+ "JOIN process USING (upid)");
+
+ ASSERT_EQ(sqlite3_step(*stmt_), SQLITE_ROW);
+ ASSERT_EQ(sqlite3_column_int(*stmt_, 0), 1 /* utid */);
+ ASSERT_STREQ(GetColumnAsText(1), kCommProc1);
+
+ ASSERT_EQ(sqlite3_column_int(*stmt_, 2), 1 /* upid */);
+ ASSERT_STREQ(GetColumnAsText(3), "test");
+
+ ASSERT_EQ(sqlite3_step(*stmt_), SQLITE_DONE);
+}
+
+} // namespace
+} // namespace trace_processor
+} // namespace perfetto
diff --git a/src/trace_processor/trace_database.cc b/src/trace_processor/trace_database.cc
index 9d2e3c2..a95a784 100644
--- a/src/trace_processor/trace_database.cc
+++ b/src/trace_processor/trace_database.cc
@@ -39,6 +39,11 @@
static sqlite3_module p_module = ProcessTable::CreateModule();
sqlite3_create_module(*db_, "process", &p_module,
static_cast<void*>(&storage_));
+
+ // Setup the thread table.
+ static sqlite3_module t_module = ThreadTable::CreateModule();
+ sqlite3_create_module(*db_, "thread", &t_module,
+ static_cast<void*>(&storage_));
}
void TraceDatabase::LoadTrace(BlobReader* reader,
diff --git a/src/trace_processor/trace_database.h b/src/trace_processor/trace_database.h
index e1ef800..a0b81fb 100644
--- a/src/trace_processor/trace_database.h
+++ b/src/trace_processor/trace_database.h
@@ -26,6 +26,7 @@
#include "src/trace_processor/process_table.h"
#include "src/trace_processor/sched_slice_table.h"
#include "src/trace_processor/scoped_db.h"
+#include "src/trace_processor/thread_table.h"
#include "src/trace_processor/trace_parser.h"
#include "src/trace_processor/trace_storage.h"