Merge "tp: fix invalid use and memory leak" into main
diff --git a/Android.bp b/Android.bp
index 9bc3137..d43bcb7 100644
--- a/Android.bp
+++ b/Android.bp
@@ -12852,6 +12852,7 @@
     srcs: [
         "src/trace_redaction/build_timeline.cc",
         "src/trace_redaction/filter_ftrace_using_allowlist.cc",
+        "src/trace_redaction/filter_print_events.cc",
         "src/trace_redaction/filter_sched_waking_events.cc",
         "src/trace_redaction/find_package_uid.cc",
         "src/trace_redaction/optimize_timeline.cc",
@@ -12861,6 +12862,7 @@
         "src/trace_redaction/prune_package_list.cc",
         "src/trace_redaction/redact_sched_switch.cc",
         "src/trace_redaction/scrub_ftrace_events.cc",
+        "src/trace_redaction/scrub_process_stats.cc",
         "src/trace_redaction/scrub_process_trees.cc",
         "src/trace_redaction/scrub_task_rename.cc",
         "src/trace_redaction/scrub_trace_packet.cc",
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc
index 266245c..4d514ab 100644
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc
+++ b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc
@@ -180,9 +180,10 @@
     PERFETTO_FATAL("Failed to initialize perfetto_tables: %s", errmsg_raw);
   }
 
-  engine_->RegisterVirtualTableModule<RuntimeTableFunction>(
-      "runtime_table_function", this,
-      SqliteTableLegacy::TableType::kExplicitCreate, false);
+  auto ctx = std::make_unique<RuntimeTableFunctionModule::Context>();
+  runtime_table_fn_context_ = ctx.get();
+  engine_->RegisterVirtualTableModule<RuntimeTableFunctionModule>(
+      "runtime_table_function", std::move(ctx));
   auto context = std::make_unique<DbSqliteTable::Context>(
       [this](const std::string& name) {
         auto* table = runtime_tables_.Find(name);
@@ -202,7 +203,6 @@
   // Destroying the sqlite engine should also destroy all the created table
   // functions.
   engine_.reset();
-  PERFETTO_CHECK(runtime_table_fn_states_.size() == 0);
   PERFETTO_CHECK(runtime_tables_.size() == 0);
 }
 
@@ -620,8 +620,9 @@
                                    cf.sql);
   }
 
-  std::unique_ptr<RuntimeTableFunction::State> state(
-      new RuntimeTableFunction::State{cf.sql, cf.prototype, {}, std::nullopt});
+  auto state = std::make_unique<RuntimeTableFunctionModule::State>(
+      RuntimeTableFunctionModule::State{
+          this, cf.sql, cf.prototype, {}, std::nullopt});
 
   // Parse the return type into a enum format.
   {
@@ -699,36 +700,44 @@
           state->return_values[i].name().c_str());
     }
   }
-  state->reusable_stmt = std::move(stmt);
+  state->temporary_create_stmt = std::move(stmt);
 
   // TODO(lalitm): this suffers the same non-atomic DROP/CREATE problem as
   // CREATE PERFETTO TABLE implementation above: see the comment there for
   // more info on this.
-  std::string fn_name = state->prototype.function_name;
-  std::string lower_name = base::ToLower(state->prototype.function_name);
-  if (runtime_table_fn_states_.Find(lower_name)) {
-    if (!cf.replace) {
-      return base::ErrStatus("Table function named %s already exists",
-                             state->prototype.function_name.c_str());
-    }
-    // This will cause |OnTableFunctionDestroyed| below to be executed.
-    base::StackString<1024> drop("DROP TABLE %s",
+  if (cf.replace) {
+    base::StackString<1024> drop("DROP TABLE IF EXISTS %s",
                                  state->prototype.function_name.c_str());
     auto res = Execute(
         SqlSource::FromTraceProcessorImplementation(drop.ToStdString()));
     RETURN_IF_ERROR(res.status());
   }
 
-  auto it_and_inserted =
-      runtime_table_fn_states_.Insert(lower_name, std::move(state));
-  PERFETTO_CHECK(it_and_inserted.second);
-
   base::StackString<1024> create(
-      "CREATE VIRTUAL TABLE %s USING runtime_table_function", fn_name.c_str());
-  return Execute(cf.sql.RewriteAllIgnoreExisting(
-                     SqlSource::FromTraceProcessorImplementation(
-                         create.ToStdString())))
-      .status();
+      "CREATE VIRTUAL TABLE %s USING runtime_table_function",
+      state->prototype.function_name.c_str());
+
+  // Make sure we didn't accidentally leak a state from a previous function
+  // creation.
+  PERFETTO_CHECK(!runtime_table_fn_context_->temporary_create_state);
+
+  // Move the state into the context so that it will be picked up in xCreate
+  // of RuntimeTableFunctionModule.
+  runtime_table_fn_context_->temporary_create_state = std::move(state);
+  auto status = Execute(cf.sql.RewriteAllIgnoreExisting(
+                            SqlSource::FromTraceProcessorImplementation(
+                                create.ToStdString())))
+                    .status();
+
+  // If an error happened, it's possible that the state was not picked up.
+  // Therefore, always reset the state just in case. OTOH if the creation
+  // succeeded, the state should always have been captured.
+  if (status.ok()) {
+    PERFETTO_CHECK(!runtime_table_fn_context_->temporary_create_state);
+  } else {
+    runtime_table_fn_context_->temporary_create_state.reset();
+  }
+  return status;
 }
 
 base::Status PerfettoSqlEngine::ExecuteCreateMacro(
@@ -779,18 +788,6 @@
   return base::OkStatus();
 }
 
-RuntimeTableFunction::State* PerfettoSqlEngine::GetRuntimeTableFunctionState(
-    const std::string& name) const {
-  auto* it = runtime_table_fn_states_.Find(base::ToLower(name));
-  PERFETTO_CHECK(it);
-  return it->get();
-}
-
-void PerfettoSqlEngine::OnRuntimeTableFunctionDestroyed(
-    const std::string& name) {
-  PERFETTO_CHECK(runtime_table_fn_states_.Erase(base::ToLower(name)));
-}
-
 base::StatusOr<std::vector<std::string>>
 PerfettoSqlEngine::GetColumnNamesFromSelectStatement(
     const SqliteEngine::PreparedStatement& stmt,
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h
index ef5358d..cb7930b 100644
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h
+++ b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h
@@ -172,13 +172,6 @@
   // Registers a trace processor C++ table function with SQLite.
   void RegisterStaticTableFunction(std::unique_ptr<StaticTableFunction> fn);
 
-  // Returns the state for the given table function.
-  RuntimeTableFunction::State* GetRuntimeTableFunctionState(
-      const std::string&) const;
-
-  // Should be called when a table function is destroyed.
-  void OnRuntimeTableFunctionDestroyed(const std::string&);
-
   SqliteEngine* sqlite_engine() { return engine_.get(); }
 
   // Makes new SQL module available to import.
@@ -276,8 +269,7 @@
   uint64_t static_window_function_count_ = 0;
   uint64_t runtime_function_count_ = 0;
 
-  base::FlatHashMap<std::string, std::unique_ptr<RuntimeTableFunction::State>>
-      runtime_table_fn_states_;
+  RuntimeTableFunctionModule::Context* runtime_table_fn_context_ = nullptr;
   base::FlatHashMap<std::string, const Table*> static_tables_;
   base::FlatHashMap<std::string, std::unique_ptr<RuntimeTable>> runtime_tables_;
   base::FlatHashMap<std::string, sql_modules::RegisteredModule> modules_;
diff --git a/src/trace_processor/perfetto_sql/engine/runtime_table_function.cc b/src/trace_processor/perfetto_sql/engine/runtime_table_function.cc
index 3d0af93..12faa70 100644
--- a/src/trace_processor/perfetto_sql/engine/runtime_table_function.cc
+++ b/src/trace_processor/perfetto_sql/engine/runtime_table_function.cc
@@ -16,15 +16,28 @@
 
 #include "src/trace_processor/perfetto_sql/engine/runtime_table_function.h"
 
+#include <sqlite3.h>
+#include <cstddef>
+#include <cstdint>
+#include <memory>
 #include <optional>
+#include <string>
 #include <utility>
+#include <vector>
 
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/string_utils.h"
+#include "perfetto/public/compiler.h"
+#include "src/trace_processor/perfetto_sql/engine/function_util.h"
 #include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
 #include "src/trace_processor/sqlite/bindings/sqlite_result.h"
-#include "src/trace_processor/util/status_macros.h"
+#include "src/trace_processor/sqlite/module_lifecycle_manager.h"
+#include "src/trace_processor/sqlite/sqlite_utils.h"
+#include "src/trace_processor/tp_metatrace.h"
+#include "src/trace_processor/util/sql_argument.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 namespace {
 
@@ -33,217 +46,226 @@
   sqlite3_clear_bindings(stmt);
 }
 
-}  // namespace
-
-RuntimeTableFunction::RuntimeTableFunction(sqlite3*, PerfettoSqlEngine* engine)
-    : engine_(engine) {}
-
-RuntimeTableFunction::~RuntimeTableFunction() {
-  engine_->OnRuntimeTableFunctionDestroyed(name());
-}
-
-base::Status RuntimeTableFunction::Init(int,
-                                        const char* const*,
-                                        Schema* schema) {
-  state_ = engine_->GetRuntimeTableFunctionState(name());
-
-  // Now we've parsed prototype and return values, create the schema.
-  *schema = CreateSchema();
-  return base::OkStatus();
-}
-
-SqliteTableLegacy::Schema RuntimeTableFunction::CreateSchema() {
-  std::vector<Column> columns;
-  for (size_t i = 0; i < state_->return_values.size(); ++i) {
-    const auto& ret = state_->return_values[i];
-    columns.push_back(Column(columns.size(), ret.name().ToStdString(),
+auto CreateTableStrFromState(RuntimeTableFunctionModule::State* state) {
+  std::vector<std::string> columns;
+  columns.reserve(state->return_values.size());
+  for (const auto& ret : state->return_values) {
+    columns.emplace_back(ret.name().ToStdString() + " " +
+                         sqlite::utils::SqlValueTypeToString(
                              sql_argument::TypeToSqlValueType(ret.type())));
   }
-  for (size_t i = 0; i < state_->prototype.arguments.size(); ++i) {
-    const auto& arg = state_->prototype.arguments[i];
-
+  for (const auto& arg : state->prototype.arguments) {
     // Add the "in_" prefix to every argument param to avoid clashes between the
     // output and input parameters.
-    columns.push_back(Column(columns.size(), "in_" + arg.name().ToStdString(),
-                             sql_argument::TypeToSqlValueType(arg.type()),
-                             true));
+    columns.emplace_back("in_" + arg.name().ToStdString() + " " +
+                         sqlite::utils::SqlValueTypeToString(
+                             sql_argument::TypeToSqlValueType(arg.type())) +
+                         " HIDDEN");
   }
+  columns.emplace_back("_primary_key BIGINT HIDDEN");
 
-  std::vector<size_t> primary_keys;
-
-  // Add the "primary key" column. SQLite requires that we provide a column
-  // which is non-null and unique. Unfortunately, we have no restrictions on
-  // the subqueries so we cannot rely on this constraint being held there.
-  // Therefore, we create a "primary key" column which exists purely for SQLite
-  // primary key purposes and is equal to the row number.
-  columns.push_back(
-      Column(columns.size(), "_primary_key", SqlValue::kLong, true));
-  primary_keys.emplace_back(columns.size() - 1);
-
-  return SqliteTableLegacy::Schema(std::move(columns), std::move(primary_keys));
+  std::string cols = base::Join(columns, ",");
+  return base::StackString<1024>(
+      R"(CREATE TABLE x(%s, PRIMARY KEY(_primary_key)) WITHOUT ROWID)",
+      cols.c_str());
 }
 
-std::unique_ptr<SqliteTableLegacy::BaseCursor>
-RuntimeTableFunction::CreateCursor() {
-  return std::unique_ptr<Cursor>(new Cursor(this, state_));
+}  // namespace
+
+int RuntimeTableFunctionModule::Create(sqlite3* db,
+                                       void* ctx,
+                                       int,
+                                       const char* const* argv,
+                                       sqlite3_vtab** vtab,
+                                       char**) {
+  auto* context = GetContext(ctx);
+  auto state = std::move(context->temporary_create_state);
+
+  auto create_table_str = CreateTableStrFromState(state.get());
+  if (int ret = sqlite3_declare_vtab(db, create_table_str.c_str());
+      ret != SQLITE_OK) {
+    return ret;
+  }
+  std::unique_ptr<Vtab> res = std::make_unique<Vtab>();
+  res->reusable_stmt = std::move(state->temporary_create_stmt);
+  state->temporary_create_stmt = std::nullopt;
+  res->state = context->manager.OnCreate(argv, std::move(state));
+  *vtab = res.release();
+  return SQLITE_OK;
 }
 
-int RuntimeTableFunction::BestIndex(const QueryConstraints& qc,
-                                    BestIndexInfo* info) {
-  // Only accept constraint sets where every input parameter has a value.
-  size_t seen_argument_constraints = 0;
-  for (size_t i = 0; i < qc.constraints().size(); ++i) {
-    const auto& cs = qc.constraints()[i];
-    seen_argument_constraints +=
-        state_->IsArgumentColumn(static_cast<size_t>(cs.column));
+int RuntimeTableFunctionModule::Destroy(sqlite3_vtab* vtab) {
+  std::unique_ptr<Vtab> tab(GetVtab(vtab));
+  sqlite::ModuleStateManager<RuntimeTableFunctionModule>::OnDestroy(tab->state);
+  return SQLITE_OK;
+}
+
+int RuntimeTableFunctionModule::Connect(sqlite3* db,
+                                        void* ctx,
+                                        int,
+                                        const char* const*,
+                                        sqlite3_vtab** vtab,
+                                        char** argv) {
+  auto* context = GetContext(ctx);
+
+  std::unique_ptr<Vtab> res = std::make_unique<Vtab>();
+  res->state = context->manager.OnConnect(argv);
+
+  auto create_table_str = CreateTableStrFromState(
+      sqlite::ModuleStateManager<RuntimeTableFunctionModule>::GetState(
+          res->state));
+  if (int ret = sqlite3_declare_vtab(db, create_table_str.c_str());
+      ret != SQLITE_OK) {
+    // If the registration happens to fail, make sure to disconnect the state
+    // again.
+    sqlite::ModuleStateManager<RuntimeTableFunctionModule>::OnDisconnect(
+        res->state);
+    return ret;
   }
-  if (seen_argument_constraints < state_->prototype.arguments.size())
+  *vtab = res.release();
+  return SQLITE_OK;
+}
+
+int RuntimeTableFunctionModule::Disconnect(sqlite3_vtab* vtab) {
+  std::unique_ptr<Vtab> tab(GetVtab(vtab));
+  sqlite::ModuleStateManager<RuntimeTableFunctionModule>::OnDisconnect(
+      tab->state);
+  return SQLITE_OK;
+}
+
+int RuntimeTableFunctionModule::BestIndex(sqlite3_vtab* tab,
+                                          sqlite3_index_info* info) {
+  auto* t = GetVtab(tab);
+  auto* s = sqlite::ModuleStateManager<RuntimeTableFunctionModule>::GetState(
+      t->state);
+
+  // Don't deal with any constraints on the output parameters for simplicty.
+  // TODO(lalitm): reconsider this decision to allow more efficient queries:
+  // we would need to wrap the query in a SELECT * FROM (...) WHERE constraint
+  // like we do for SPAN JOIN.
+  base::Status status = sqlite::utils::ValidateFunctionArguments(
+      info, s->prototype.arguments.size(),
+      [s](size_t c) { return s->IsArgumentColumn(c); });
+  if (!status.ok()) {
     return SQLITE_CONSTRAINT;
-
-  for (size_t i = 0; i < info->sqlite_omit_constraint.size(); ++i) {
-    size_t col = static_cast<size_t>(qc.constraints()[i].column);
-    if (state_->IsArgumentColumn(col)) {
-      info->sqlite_omit_constraint[i] = true;
-    }
   }
   return SQLITE_OK;
 }
 
-RuntimeTableFunction::Cursor::Cursor(RuntimeTableFunction* table, State* state)
-    : SqliteTableLegacy::BaseCursor(table), table_(table), state_(state) {
-  if (state->reusable_stmt) {
-    stmt_ = std::move(state->reusable_stmt);
-    state->reusable_stmt = std::nullopt;
-    return_stmt_to_state_ = true;
+int RuntimeTableFunctionModule::Open(sqlite3_vtab* tab,
+                                     sqlite3_vtab_cursor** cursor) {
+  auto* t = GetVtab(tab);
+  std::unique_ptr<Cursor> c = std::make_unique<Cursor>();
+  if (t->reusable_stmt) {
+    c->stmt = std::move(t->reusable_stmt);
+    t->reusable_stmt = std::nullopt;
   }
+  *cursor = c.release();
+  return SQLITE_OK;
 }
 
-RuntimeTableFunction::Cursor::~Cursor() {
-  if (return_stmt_to_state_) {
-    ResetStatement(stmt_->sqlite_stmt());
-    state_->reusable_stmt = std::move(stmt_);
+int RuntimeTableFunctionModule::Close(sqlite3_vtab_cursor* cursor) {
+  std::unique_ptr<Cursor> c(GetCursor(cursor));
+  auto* t = GetVtab(c->pVtab);
+  if (!t->reusable_stmt && c->stmt) {
+    ResetStatement(c->stmt->sqlite_stmt());
+    t->reusable_stmt = std::move(c->stmt);
   }
+  return SQLITE_OK;
 }
 
-base::Status RuntimeTableFunction::Cursor::Filter(const QueryConstraints& qc,
-                                                  sqlite3_value** argv,
-                                                  FilterHistory) {
+int RuntimeTableFunctionModule::Filter(sqlite3_vtab_cursor* cur,
+                                       int,
+                                       const char*,
+                                       int argc,
+                                       sqlite3_value** argv) {
+  auto* c = GetCursor(cur);
+  auto* t = GetVtab(cur->pVtab);
+  auto* s = sqlite::ModuleStateManager<RuntimeTableFunctionModule>::GetState(
+      t->state);
+
+  PERFETTO_CHECK(static_cast<size_t>(argc) == s->prototype.arguments.size());
   PERFETTO_TP_TRACE(metatrace::Category::FUNCTION_CALL, "TABLE_FUNCTION_CALL",
-                    [this](metatrace::Record* r) {
-                      r->AddArg("Function",
-                                state_->prototype.function_name.c_str());
+                    [s](metatrace::Record* r) {
+                      r->AddArg("Function", s->prototype.function_name.c_str());
                     });
 
-  auto col_to_arg_idx = [this](int col) {
-    return static_cast<uint32_t>(col) -
-           static_cast<uint32_t>(state_->return_values.size());
-  };
-
-  size_t seen_argument_constraints = 0;
-  for (size_t i = 0; i < qc.constraints().size(); ++i) {
-    const auto& cs = qc.constraints()[i];
-
-    // Only consider argument columns (i.e. input parameters) as we're
-    // delegating the rest to SQLite.
-    if (!state_->IsArgumentColumn(static_cast<size_t>(cs.column)))
-      continue;
-
-    // We only support equality constraints as we're expecting "input arguments"
-    // to our "function".
-    if (!sqlite::utils::IsOpEq(cs.op)) {
-      return base::ErrStatus("%s: non-equality constraint passed",
-                             state_->prototype.function_name.c_str());
-    }
-
-    const auto& arg = state_->prototype.arguments[col_to_arg_idx(cs.column)];
-    base::Status status = sqlite::utils::TypeCheckSqliteValue(
-        argv[i], sql_argument::TypeToSqlValueType(arg.type()),
-        sql_argument::TypeToHumanFriendlyString(arg.type()));
-    if (!status.ok()) {
-      return base::ErrStatus("%s: argument %s (index %zu) %s",
-                             state_->prototype.function_name.c_str(),
-                             arg.name().c_str(), i, status.c_message());
-    }
-
-    seen_argument_constraints++;
-  }
-
-  // Verify that we saw one valid constraint for every input argument.
-  if (seen_argument_constraints < state_->prototype.arguments.size()) {
-    return base::ErrStatus(
-        "%s: missing value for input argument. Saw %zu arguments but expected "
-        "%zu",
-        state_->prototype.function_name.c_str(), seen_argument_constraints,
-        state_->prototype.arguments.size());
-  }
-
   // Prepare the SQL definition as a statement using SQLite.
   // TODO(lalitm): measure and implement whether it would be a good idea to
   // forward constraints here when we build the nested query.
-  if (stmt_) {
+  if (c->stmt) {
     // Filter can be called multiple times for the same cursor, so if we
     // already have a statement, reset and reuse it. Otherwise, create a
     // new one.
-    ResetStatement(stmt_->sqlite_stmt());
+    ResetStatement(c->stmt->sqlite_stmt());
   } else {
-    auto stmt = table_->engine_->sqlite_engine()->PrepareStatement(
-        state_->sql_defn_str);
-    RETURN_IF_ERROR(stmt.status());
-    stmt_ = std::move(stmt);
+    auto stmt = s->engine->sqlite_engine()->PrepareStatement(s->sql_defn_str);
+    c->stmt = std::move(stmt);
+    if (const auto& status = c->stmt->status(); !status.ok()) {
+      return sqlite::utils::SetError(t, status.c_message());
+    }
   }
 
   // Bind all the arguments to the appropriate places in the function.
-  for (size_t i = 0; i < qc.constraints().size(); ++i) {
-    const auto& cs = qc.constraints()[i];
-
-    // Don't deal with any constraints on the output parameters for simplicty.
-    // TODO(lalitm): reconsider this decision to allow more efficient queries:
-    // we would need to wrap the query in a SELECT * FROM (...) WHERE constraint
-    // like we do for SPAN JOIN.
-    if (!state_->IsArgumentColumn(static_cast<size_t>(cs.column)))
-      continue;
-
-    uint32_t index = col_to_arg_idx(cs.column);
-    PERFETTO_DCHECK(index < state_->prototype.arguments.size());
-
-    const auto& arg = state_->prototype.arguments[index];
-    auto status = MaybeBindArgument(
-        stmt_->sqlite_stmt(), state_->prototype.function_name, arg, argv[i]);
-    RETURN_IF_ERROR(status);
+  for (uint32_t i = 0; i < static_cast<uint32_t>(argc); ++i) {
+    const auto& arg = s->prototype.arguments[i];
+    base::Status status = MaybeBindArgument(
+        c->stmt->sqlite_stmt(), s->prototype.function_name, arg, argv[i]);
+    if (!status.ok()) {
+      return sqlite::utils::SetError(t, status.c_message());
+    }
   }
 
   // Reset the next call count - this is necessary because the same cursor
   // can be used for multiple filter operations.
-  next_call_count_ = 0;
-  return Next();
+  c->next_call_count = 0;
+  return Next(cur);
 }
 
-base::Status RuntimeTableFunction::Cursor::Next() {
-  is_eof_ = !stmt_->Step();
-  next_call_count_++;
-  return stmt_->status();
+int RuntimeTableFunctionModule::Next(sqlite3_vtab_cursor* cur) {
+  auto* c = GetCursor(cur);
+  c->is_eof = !c->stmt->Step();
+  c->next_call_count++;
+  if (const auto& status = c->stmt->status(); !status.ok()) {
+    return sqlite::utils::SetError(cur->pVtab, status.c_message());
+  }
+  return SQLITE_OK;
 }
 
-bool RuntimeTableFunction::Cursor::Eof() {
-  return is_eof_;
+int RuntimeTableFunctionModule::Eof(sqlite3_vtab_cursor* cur) {
+  return GetCursor(cur)->is_eof;
 }
 
-base::Status RuntimeTableFunction::Cursor::Column(sqlite3_context* ctx, int i) {
-  size_t idx = static_cast<size_t>(i);
-  if (state_->IsReturnValueColumn(idx)) {
-    sqlite::result::Value(ctx, sqlite3_column_value(stmt_->sqlite_stmt(), i));
-  } else if (state_->IsArgumentColumn(idx)) {
+int RuntimeTableFunctionModule::Column(sqlite3_vtab_cursor* cur,
+                                       sqlite3_context* ctx,
+                                       int N) {
+  auto* c = GetCursor(cur);
+  auto* t = GetVtab(cur->pVtab);
+  auto* s = sqlite::ModuleStateManager<RuntimeTableFunctionModule>::GetState(
+      t->state);
+
+  auto idx = static_cast<size_t>(N);
+  if (PERFETTO_LIKELY(s->IsReturnValueColumn(idx))) {
+    sqlite::result::Value(ctx, sqlite3_column_value(c->stmt->sqlite_stmt(), N));
+    return SQLITE_OK;
+  }
+
+  if (PERFETTO_LIKELY(s->IsArgumentColumn(idx))) {
     // TODO(lalitm): it may be more appropriate to keep a note of the arguments
     // which we passed in and return them here. Not doing this to because it
     // doesn't seem necessary for any useful thing but something which may need
     // to be changed in the future.
     sqlite::result::Null(ctx);
-  } else {
-    PERFETTO_DCHECK(state_->IsPrimaryKeyColumn(idx));
-    sqlite::result::Long(ctx, next_call_count_);
+    return SQLITE_OK;
   }
-  return base::OkStatus();
+
+  PERFETTO_DCHECK(s->IsPrimaryKeyColumn(idx));
+  sqlite::result::Long(ctx, c->next_call_count);
+  return SQLITE_OK;
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+int RuntimeTableFunctionModule::Rowid(sqlite3_vtab_cursor*, sqlite_int64*) {
+  return SQLITE_ERROR;
+}
+
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/perfetto_sql/engine/runtime_table_function.h b/src/trace_processor/perfetto_sql/engine/runtime_table_function.h
index 80edf72..a027e02 100644
--- a/src/trace_processor/perfetto_sql/engine/runtime_table_function.h
+++ b/src/trace_processor/perfetto_sql/engine/runtime_table_function.h
@@ -17,31 +17,37 @@
 #ifndef SRC_TRACE_PROCESSOR_PERFETTO_SQL_ENGINE_RUNTIME_TABLE_FUNCTION_H_
 #define SRC_TRACE_PROCESSOR_PERFETTO_SQL_ENGINE_RUNTIME_TABLE_FUNCTION_H_
 
+#include <cstddef>
+#include <cstdint>
+#include <memory>
 #include <optional>
+#include <string>
+#include <vector>
 
+#include "perfetto/base/logging.h"
 #include "src/trace_processor/perfetto_sql/engine/function_util.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_module.h"
+#include "src/trace_processor/sqlite/module_lifecycle_manager.h"
+#include "src/trace_processor/sqlite/sql_source.h"
 #include "src/trace_processor/sqlite/sqlite_engine.h"
+#include "src/trace_processor/util/sql_argument.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 class PerfettoSqlEngine;
 
 // The implementation of the SqliteTableLegacy interface for table functions
 // defined at runtime using SQL.
-class RuntimeTableFunction final
-    : public TypedSqliteTable<RuntimeTableFunction, PerfettoSqlEngine*> {
- public:
-  // The state of this function. This is separated from |RuntimeTableFunction|
-  // because |RuntimeTableFunction| is owned by Sqlite while |State| is owned by
-  // PerfettoSqlEngine.
+struct RuntimeTableFunctionModule
+    : public sqlite::Module<RuntimeTableFunctionModule> {
   struct State {
+    PerfettoSqlEngine* engine;
     SqlSource sql_defn_str;
 
     FunctionPrototype prototype;
     std::vector<sql_argument::ArgumentDefinition> return_values;
 
-    std::optional<SqliteEngine::PreparedStatement> reusable_stmt;
+    std::optional<SqliteEngine::PreparedStatement> temporary_create_stmt;
 
     bool IsReturnValueColumn(size_t i) const {
       PERFETTO_DCHECK(i < TotalColumnCount());
@@ -65,44 +71,55 @@
              kPrimaryKeyColumns;
     }
   };
-  class Cursor final : public SqliteTableLegacy::BaseCursor {
-   public:
-    explicit Cursor(RuntimeTableFunction* table, State* state);
-    ~Cursor() final;
-
-    base::Status Filter(const QueryConstraints& qc,
-                        sqlite3_value**,
-                        FilterHistory);
-    base::Status Next();
-    bool Eof();
-    base::Status Column(sqlite3_context* context, int N);
-
-   private:
-    RuntimeTableFunction* table_ = nullptr;
-    State* state_ = nullptr;
-
-    std::optional<SqliteEngine::PreparedStatement> stmt_;
-    bool return_stmt_to_state_ = false;
-
-    bool is_eof_ = false;
-    int next_call_count_ = 0;
+  struct Context {
+    std::unique_ptr<State> temporary_create_state;
+    sqlite::ModuleStateManager<RuntimeTableFunctionModule> manager;
+  };
+  struct Vtab : sqlite::Module<RuntimeTableFunctionModule>::Vtab {
+    sqlite::ModuleStateManager<RuntimeTableFunctionModule>::PerVtabState* state;
+    std::optional<SqliteEngine::PreparedStatement> reusable_stmt;
+  };
+  struct Cursor : sqlite::Module<RuntimeTableFunctionModule>::Cursor {
+    std::optional<SqliteEngine::PreparedStatement> stmt;
+    bool is_eof = false;
+    int next_call_count = 0;
   };
 
-  RuntimeTableFunction(sqlite3*, PerfettoSqlEngine*);
-  ~RuntimeTableFunction() final;
+  static constexpr bool kSupportsWrites = false;
+  static constexpr bool kDoesOverloadFunctions = false;
 
-  base::Status Init(int argc, const char* const* argv, Schema*) final;
-  std::unique_ptr<SqliteTableLegacy::BaseCursor> CreateCursor() final;
-  int BestIndex(const QueryConstraints& qc, BestIndexInfo* info) final;
+  static int Create(sqlite3*,
+                    void*,
+                    int,
+                    const char* const*,
+                    sqlite3_vtab**,
+                    char**);
+  static int Destroy(sqlite3_vtab*);
 
- private:
-  Schema CreateSchema();
+  static int Connect(sqlite3*,
+                     void*,
+                     int,
+                     const char* const*,
+                     sqlite3_vtab**,
+                     char**);
+  static int Disconnect(sqlite3_vtab*);
 
-  PerfettoSqlEngine* engine_ = nullptr;
-  State* state_ = nullptr;
+  static int BestIndex(sqlite3_vtab*, sqlite3_index_info*);
+
+  static int Open(sqlite3_vtab*, sqlite3_vtab_cursor**);
+  static int Close(sqlite3_vtab_cursor*);
+
+  static int Filter(sqlite3_vtab_cursor*,
+                    int,
+                    const char*,
+                    int,
+                    sqlite3_value**);
+  static int Next(sqlite3_vtab_cursor*);
+  static int Eof(sqlite3_vtab_cursor*);
+  static int Column(sqlite3_vtab_cursor*, sqlite3_context*, int);
+  static int Rowid(sqlite3_vtab_cursor*, sqlite_int64*);
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_ENGINE_RUNTIME_TABLE_FUNCTION_H_
diff --git a/src/trace_processor/sqlite/sqlite_utils.h b/src/trace_processor/sqlite/sqlite_utils.h
index ba1eed6..bb95e2e 100644
--- a/src/trace_processor/sqlite/sqlite_utils.h
+++ b/src/trace_processor/sqlite/sqlite_utils.h
@@ -18,10 +18,13 @@
 #define SRC_TRACE_PROCESSOR_SQLITE_SQLITE_UTILS_H_
 
 #include <sqlite3.h>
+#include <algorithm>
 #include <bitset>
 #include <cstddef>
 #include <cstdint>
 #include <cstring>
+#include <functional>
+#include <initializer_list>
 #include <optional>
 #include <string>
 #include <utility>
@@ -156,6 +159,54 @@
                                 status.c_message()));
 }
 
+// For a given |sqlite3_index_info| struct received in a BestIndex call, returns
+// whether all |arg_count| arguments (with |is_arg_column| indicating whether a
+// given column is a function argument) have exactly one equaltiy constraint
+// associated with them.
+//
+// If so, the associated constraint is omitted and the argvIndex is mapped to
+// the corresponding argument's index.
+inline base::Status ValidateFunctionArguments(
+    sqlite3_index_info* info,
+    size_t arg_count,
+    const std::function<bool(size_t)>& is_arg_column) {
+  std::vector<bool> present;
+  size_t present_count = 0;
+  for (int i = 0; i < info->nConstraint; ++i) {
+    const auto& in = info->aConstraint[i];
+    if (!in.usable) {
+      continue;
+    }
+    auto cs_col = static_cast<size_t>(in.iColumn);
+    if (!is_arg_column(cs_col)) {
+      continue;
+    }
+    if (!IsOpEq(in.op)) {
+      return base::ErrStatus(
+          "Unexpected non equality constraints for column %zu", cs_col);
+    }
+    if (cs_col >= present.size()) {
+      present.resize(cs_col + 1);
+    }
+    if (present[cs_col]) {
+      return base::ErrStatus("Unexpected multiple constraints for column %zu",
+                             cs_col);
+    }
+    present[cs_col] = true;
+    present_count++;
+
+    auto& out = info->aConstraintUsage[i];
+    out.argvIndex = static_cast<int>(present_count);
+    out.omit = true;
+  }
+  if (present_count != arg_count) {
+    return base::ErrStatus(
+        "Unexpected missing argument: expected %zu, actual %zu", arg_count,
+        present_count);
+  }
+  return base::OkStatus();
+}
+
 // Converts the given SqlValue type to the type string SQLite understands.
 inline std::string SqlValueTypeToString(SqlValue::Type type) {
   switch (type) {
diff --git a/src/trace_redaction/BUILD.gn b/src/trace_redaction/BUILD.gn
index f908e96..e4b35a1 100644
--- a/src/trace_redaction/BUILD.gn
+++ b/src/trace_redaction/BUILD.gn
@@ -32,6 +32,8 @@
     "build_timeline.h",
     "filter_ftrace_using_allowlist.cc",
     "filter_ftrace_using_allowlist.h",
+    "filter_print_events.cc",
+    "filter_print_events.h",
     "filter_sched_waking_events.cc",
     "filter_sched_waking_events.h",
     "find_package_uid.cc",
@@ -50,6 +52,8 @@
     "redact_sched_switch.h",
     "scrub_ftrace_events.cc",
     "scrub_ftrace_events.h",
+    "scrub_process_stats.cc",
+    "scrub_process_stats.h",
     "scrub_process_trees.cc",
     "scrub_process_trees.h",
     "scrub_task_rename.cc",
@@ -82,6 +86,7 @@
     "filter_sched_waking_events_integrationtest.cc",
     "redact_sched_switch_integrationtest.cc",
     "scrub_ftrace_events_integrationtest.cc",
+    "scrub_process_stats_integrationtest.cc",
     "scrub_process_trees_integrationtest.cc",
     "scrub_task_rename_integrationtest.cc",
     "trace_redaction_integration_fixture.cc",
diff --git a/src/trace_redaction/filter_ftrace_using_allowlist_integrationtest.cc b/src/trace_redaction/filter_ftrace_using_allowlist_integrationtest.cc
index c0abf3f..89946ae 100644
--- a/src/trace_redaction/filter_ftrace_using_allowlist_integrationtest.cc
+++ b/src/trace_redaction/filter_ftrace_using_allowlist_integrationtest.cc
@@ -170,6 +170,7 @@
   ASSERT_TRUE(events.count(protos::pbzero::FtraceEvent::kTimestampFieldNumber));
 
   // These are events.
+  ASSERT_TRUE(events.count(protos::pbzero::FtraceEvent::kPrintFieldNumber));
   ASSERT_TRUE(
       events.count(protos::pbzero::FtraceEvent::kCpuFrequencyFieldNumber));
   ASSERT_TRUE(events.count(protos::pbzero::FtraceEvent::kCpuIdleFieldNumber));
@@ -197,7 +198,6 @@
   // These are events.
   ASSERT_FALSE(
       events.count(protos::pbzero::FtraceEvent::kOomScoreAdjUpdateFieldNumber));
-  ASSERT_FALSE(events.count(protos::pbzero::FtraceEvent::kPrintFieldNumber));
   ASSERT_FALSE(
       events.count(protos::pbzero::FtraceEvent::kSchedProcessExitFieldNumber));
   ASSERT_FALSE(
diff --git a/src/trace_redaction/filter_print_events.cc b/src/trace_redaction/filter_print_events.cc
new file mode 100644
index 0000000..de0c952
--- /dev/null
+++ b/src/trace_redaction/filter_print_events.cc
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024 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_redaction/filter_print_events.h"
+
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+
+#include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.h"
+
+namespace perfetto::trace_redaction {
+
+base::Status FilterPrintEvents::VerifyContext(const Context& context) const {
+  if (!context.package_uid.has_value()) {
+    return base::ErrStatus("FilterPrintEvents: missing packet uid.");
+  }
+
+  if (!context.timeline) {
+    return base::ErrStatus("FilterPrintEvents: missing timeline.");
+  }
+
+  return base::OkStatus();
+}
+
+bool FilterPrintEvents::KeepEvent(const Context& context,
+                                  protozero::ConstBytes bytes) const {
+  PERFETTO_DCHECK(context.timeline);
+  PERFETTO_DCHECK(context.package_uid.has_value());
+
+  const auto* timeline = context.timeline.get();
+  auto package_uid = context.package_uid;
+
+  protozero::ProtoDecoder event(bytes);
+
+  // This is not a print packet. Keep the packet.
+  if (event.FindField(protos::pbzero::FtraceEvent::kPrintFieldNumber).valid()) {
+    return true;
+  }
+
+  auto time =
+      event.FindField(protos::pbzero::FtraceEvent::kTimestampFieldNumber);
+  auto pid = event.FindField(protos::pbzero::FtraceEvent::kPidFieldNumber);
+
+  // Pid + Time --> UID, if the uid matches the target package, keep the event.
+  return pid.valid() && time.valid() &&
+         timeline->Search(time.as_uint64(), pid.as_int32()).uid == package_uid;
+}
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/filter_print_events.h b/src/trace_redaction/filter_print_events.h
new file mode 100644
index 0000000..36ef92b
--- /dev/null
+++ b/src/trace_redaction/filter_print_events.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 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_REDACTION_FILTER_PRINT_EVENTS_H_
+#define SRC_TRACE_REDACTION_FILTER_PRINT_EVENTS_H_
+
+#include "perfetto/protozero/field.h"
+#include "src/trace_redaction/scrub_ftrace_events.h"
+#include "src/trace_redaction/trace_redaction_framework.h"
+
+namespace perfetto::trace_redaction {
+
+// event {
+//   timestamp: 6702093749982230
+//   pid: 7947                    <-- target
+//   print {
+//     buf: "B|7105|virtual void
+//     swappy::ChoreographerThread::onChoreographer()\n"
+//   }
+// }
+//
+// If the target pid doesn't belong to the target package (context.package_uid),
+// then the event will be marked as "don't keep".
+class FilterPrintEvents : public FtraceEventFilter {
+ public:
+  base::Status VerifyContext(const Context& context) const override;
+  bool KeepEvent(const Context& context,
+                 protozero::ConstBytes bytes) const override;
+};
+
+}  // namespace perfetto::trace_redaction
+
+#endif  // SRC_TRACE_REDACTION_FILTER_PRINT_EVENTS_H_
diff --git a/src/trace_redaction/main.cc b/src/trace_redaction/main.cc
index c0061b6..4620ed2 100644
--- a/src/trace_redaction/main.cc
+++ b/src/trace_redaction/main.cc
@@ -18,6 +18,7 @@
 #include "perfetto/base/status.h"
 #include "src/trace_redaction/build_timeline.h"
 #include "src/trace_redaction/filter_ftrace_using_allowlist.h"
+#include "src/trace_redaction/filter_print_events.h"
 #include "src/trace_redaction/filter_sched_waking_events.h"
 #include "src/trace_redaction/find_package_uid.h"
 #include "src/trace_redaction/optimize_timeline.h"
@@ -25,6 +26,7 @@
 #include "src/trace_redaction/prune_package_list.h"
 #include "src/trace_redaction/redact_sched_switch.h"
 #include "src/trace_redaction/scrub_ftrace_events.h"
+#include "src/trace_redaction/scrub_process_stats.h"
 #include "src/trace_redaction/scrub_process_trees.h"
 #include "src/trace_redaction/scrub_task_rename.h"
 #include "src/trace_redaction/scrub_trace_packet.h"
@@ -55,11 +57,13 @@
   // number of events they need to iterate over.
   auto scrub_ftrace_events = redactor.emplace_transform<ScrubFtraceEvents>();
   scrub_ftrace_events->emplace_back<FilterFtraceUsingAllowlist>();
+  scrub_ftrace_events->emplace_back<FilterPrintEvents>();
   scrub_ftrace_events->emplace_back<FilterSchedWakingEvents>();
 
   redactor.emplace_transform<ScrubProcessTrees>();
   redactor.emplace_transform<ScrubTaskRename>();
   redactor.emplace_transform<RedactSchedSwitch>();
+  redactor.emplace_transform<ScrubProcessStats>();
 
   Context context;
   context.package_name = package_name;
diff --git a/src/trace_redaction/populate_allow_lists.cc b/src/trace_redaction/populate_allow_lists.cc
index 2ba81b6..7a5b48a 100644
--- a/src/trace_redaction/populate_allow_lists.cc
+++ b/src/trace_redaction/populate_allow_lists.cc
@@ -77,6 +77,7 @@
       protos::pbzero::FtraceEvent::kIonBufferDestroyFieldNumber,
       protos::pbzero::FtraceEvent::kDmaHeapStatFieldNumber,
       protos::pbzero::FtraceEvent::kRssStatThrottledFieldNumber,
+      protos::pbzero::FtraceEvent::kPrintFieldNumber,
   };
 
   // TODO: Some ftrace fields should be retained, but they carry too much risk
diff --git a/src/trace_redaction/scrub_process_stats.cc b/src/trace_redaction/scrub_process_stats.cc
new file mode 100644
index 0000000..991c02f
--- /dev/null
+++ b/src/trace_redaction/scrub_process_stats.cc
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2024 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_redaction/scrub_process_stats.h"
+
+#include <string>
+
+#include "perfetto/base/status.h"
+#include "perfetto/protozero/field.h"
+#include "perfetto/protozero/scattered_heap_buffer.h"
+#include "protos/perfetto/trace/ps/process_stats.pbzero.h"
+#include "src/trace_redaction/proto_util.h"
+#include "src/trace_redaction/trace_redaction_framework.h"
+
+namespace perfetto::trace_redaction {
+
+base::Status ScrubProcessStats::Transform(const Context& context,
+                                          std::string* packet) const {
+  if (!context.package_uid.has_value()) {
+    return base::ErrStatus("FilterProcessStats: missing package uid.");
+  }
+
+  if (!context.timeline) {
+    return base::ErrStatus("FilterProcessStats: missing timeline.");
+  }
+
+  protozero::ProtoDecoder packet_decoder(*packet);
+
+  // Very few packets will have process stats. It's best to avoid
+  // reserialization whenever possible.
+  if (!packet_decoder
+           .FindField(protos::pbzero::TracePacket::kProcessStatsFieldNumber)
+           .valid()) {
+    return base::OkStatus();
+  }
+
+  protozero::HeapBuffered<protos::pbzero::TracePacket> message;
+
+  // TODO(vaage): Add primitive to drop all packets that don't have a
+  // timestamp, allowing all other packets assume there are timestamps.
+  auto time_field = packet_decoder.FindField(
+      protos::pbzero::TracePacket::kTimestampFieldNumber);
+  PERFETTO_DCHECK(time_field.valid());
+  auto time = time_field.as_uint64();
+
+  auto* timeline = context.timeline.get();
+  auto uid = context.package_uid.value();
+
+  for (auto packet_field = packet_decoder.ReadField(); packet_field.valid();
+       packet_field = packet_decoder.ReadField()) {
+    if (packet_field.id() !=
+        protos::pbzero::TracePacket::kProcessStatsFieldNumber) {
+      proto_util::AppendField(packet_field, message.get());
+      continue;
+    }
+
+    auto process_stats = std::move(packet_field);
+    protozero::ProtoDecoder process_stats_decoder(process_stats.as_bytes());
+
+    auto* process_stats_message = message->set_process_stats();
+
+    for (auto process_stats_field = process_stats_decoder.ReadField();
+         process_stats_field.valid();
+         process_stats_field = process_stats_decoder.ReadField()) {
+      bool keep_field;
+
+      if (process_stats_field.id() ==
+          protos::pbzero::ProcessStats::kProcessesFieldNumber) {
+        protozero::ProtoDecoder process_decoder(process_stats_field.as_bytes());
+        auto pid = process_decoder.FindField(
+            protos::pbzero::ProcessStats::Process::kPidFieldNumber);
+        keep_field =
+            pid.valid() && timeline->Search(time, pid.as_int32()).uid == uid;
+      } else {
+        keep_field = true;
+      }
+
+      if (keep_field) {
+        proto_util::AppendField(process_stats_field, process_stats_message);
+      }
+    }
+  }
+
+  packet->assign(message.SerializeAsString());
+
+  return base::OkStatus();
+}
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/scrub_process_stats.h b/src/trace_redaction/scrub_process_stats.h
new file mode 100644
index 0000000..99b6697
--- /dev/null
+++ b/src/trace_redaction/scrub_process_stats.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 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_REDACTION_SCRUB_PROCESS_STATS_H_
+#define SRC_TRACE_REDACTION_SCRUB_PROCESS_STATS_H_
+
+#include "src/trace_redaction/trace_redaction_framework.h"
+
+namespace perfetto::trace_redaction {
+
+class ScrubProcessStats : public TransformPrimitive {
+ public:
+  base::Status Transform(const Context& context,
+                         std::string* packet) const override;
+};
+
+}  // namespace perfetto::trace_redaction
+
+#endif  // SRC_TRACE_REDACTION_SCRUB_PROCESS_STATS_H_
diff --git a/src/trace_redaction/scrub_process_stats_integrationtest.cc b/src/trace_redaction/scrub_process_stats_integrationtest.cc
new file mode 100644
index 0000000..01c61b1
--- /dev/null
+++ b/src/trace_redaction/scrub_process_stats_integrationtest.cc
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2024 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 <cstdint>
+#include <string>
+
+#include "perfetto/base/status.h"
+#include "src/base/test/status_matchers.h"
+#include "src/trace_redaction/build_timeline.h"
+#include "src/trace_redaction/optimize_timeline.h"
+#include "src/trace_redaction/scrub_process_stats.h"
+#include "src/trace_redaction/trace_redaction_framework.h"
+#include "src/trace_redaction/trace_redaction_integration_fixture.h"
+#include "src/trace_redaction/trace_redactor.h"
+#include "test/gtest_and_gmock.h"
+
+#include "protos/perfetto/trace/ps/process_stats.pbzero.h"
+#include "protos/perfetto/trace/trace.pbzero.h"
+#include "protos/perfetto/trace/trace_packet.pbzero.h"
+
+namespace perfetto::trace_redaction {
+
+class ScrubProcessStatsTest : public testing::Test,
+                              protected TraceRedactionIntegrationFixure {
+ protected:
+  void SetUp() override {
+    trace_redactor()->emplace_collect<BuildTimeline>();
+    trace_redactor()->emplace_build<OptimizeTimeline>();
+    trace_redactor()->emplace_transform<ScrubProcessStats>();
+
+    // Package "com.Unity.com.unity.multiplayer.samples.coop";
+    context()->package_uid = 10252;
+  }
+
+  // Gets pids from all process_stats messages in the trace (bytes).
+  base::FlatSet<int32_t> GetAllPids(const std::string& bytes) const {
+    base::FlatSet<int32_t> pids;
+
+    protos::pbzero::Trace::Decoder decoder(bytes);
+
+    for (auto packet = decoder.packet(); packet; ++packet) {
+      protos::pbzero::TracePacket::Decoder trace_packet(packet->as_bytes());
+
+      if (!trace_packet.has_process_stats()) {
+        continue;
+      }
+
+      protos::pbzero::ProcessStats::Decoder process_stats(
+          trace_packet.process_stats());
+
+      for (auto process = process_stats.processes(); process; ++process) {
+        protos::pbzero::ProcessStats::Process::Decoder p(process->as_bytes());
+        PERFETTO_DCHECK(p.has_pid());
+        pids.insert(p.pid());
+      }
+    }
+
+    return pids;
+  }
+};
+
+// This test is a canary for changes to the test data. If the test data was to
+// change, every test in this file would fail.
+//
+//  SELECT DISTINCT pid
+//  FROM process
+//  WHERE upid IN (
+//    SELECT DISTINCT upid
+//    FROM counter
+//      JOIN process_counter_track ON counter.track_id=process_counter_track.id
+//    WHERE name!='oom_score_adj'
+//  )
+//  ORDER BY pid
+//
+//  NOTE: WHERE name!='oom_score_adj' is used because there are two sources for
+//  oom_score_adj values and we only want process stats here.
+TEST_F(ScrubProcessStatsTest, VerifyTraceStats) {
+  base::FlatSet<int32_t> expected = {
+      1,     578,   581,   696,   697,   698,   699,   700,   701,   704,
+      709,   710,   718,   728,   749,   750,   751,   752,   756,   760,
+      761,   762,   873,   874,   892,   1046,  1047,  1073,  1074,  1091,
+      1092,  1093,  1101,  1103,  1104,  1105,  1106,  1107,  1110,  1111,
+      1112,  1113,  1115,  1116,  1118,  1119,  1120,  1121,  1123,  1124,
+      1125,  1126,  1127,  1129,  1130,  1131,  1133,  1140,  1145,  1146,
+      1147,  1151,  1159,  1163,  1164,  1165,  1166,  1167,  1168,  1175,
+      1177,  1205,  1206,  1235,  1237,  1238,  1248,  1251,  1254,  1255,
+      1295,  1296,  1298,  1300,  1301,  1303,  1304,  1312,  1317,  1325,
+      1339,  1340,  1363,  1374,  1379,  1383,  1388,  1392,  1408,  1409,
+      1410,  1413,  1422,  1426,  1427,  1428,  1429,  1433,  1436,  1448,
+      1450,  1451,  1744,  1774,  1781,  1814,  2262,  2268,  2286,  2392,
+      2456,  2502,  2510,  2518,  2528,  2569,  3171,  3195,  3262,  3286,
+      3310,  3338,  3442,  3955,  4386,  4759,  5935,  6034,  6062,  6167,
+      6547,  6573,  6720,  6721,  6725,  6944,  6984,  7105,  7207,  7557,
+      7636,  7786,  7874,  7958,  7960,  7967,  15449, 15685, 15697, 16453,
+      19683, 21124, 21839, 23150, 23307, 23876, 24317, 25017, 25126, 25450,
+      25474, 27271, 30604, 32289,
+  };
+
+  auto original = LoadOriginal();
+  ASSERT_OK(original) << original.status().c_message();
+
+  auto actual = GetAllPids(*original);
+
+  for (auto pid : expected) {
+    ASSERT_TRUE(actual.count(pid))
+        << "pid " << pid << " was not found in the trace";
+  }
+
+  for (auto pid : actual) {
+    ASSERT_TRUE(expected.count(pid))
+        << "pid " << pid << " was found in the trace";
+  }
+}
+
+// Package name: "com.Unity.com.unity.multiplayer.samples.coop"
+// Package pid: 7105
+TEST_F(ScrubProcessStatsTest, OnlyKeepsStatsForPackage) {
+  auto result = Redact();
+  ASSERT_OK(result) << result.c_message();
+
+  auto redacted = LoadRedacted();
+  ASSERT_OK(redacted) << redacted.status().c_message();
+
+  auto actual = GetAllPids(*redacted);
+  ASSERT_EQ(actual.size(), 1u);
+  ASSERT_TRUE(actual.count(7105));
+}
+
+}  // namespace perfetto::trace_redaction
diff --git a/ui/src/assets/details.scss b/ui/src/assets/details.scss
index f3e529a..d09690a 100644
--- a/ui/src/assets/details.scss
+++ b/ui/src/assets/details.scss
@@ -173,43 +173,6 @@
         width: 50%;
       }
     }
-    &.flamegraph-profile {
-      display: flex;
-      justify-content: space-between;
-      align-content: center;
-      height: 30px;
-      padding: 0;
-      font-size: 12px;
-      * {
-        align-self: center;
-      }
-      .options {
-        display: inline-flex;
-        justify-content: space-around;
-      }
-      .details {
-        display: inline-flex;
-        justify-content: flex-end;
-      }
-      .title {
-        justify-self: start;
-        margin-left: 5px;
-        font-size: 14px;
-        margin-right: 10px;
-      }
-      .time {
-        justify-self: end;
-        margin-right: 10px;
-      }
-      .selected {
-        justify-self: end;
-        margin-right: 10px;
-        white-space: nowrap;
-        overflow: hidden;
-        text-overflow: ellipsis;
-        width: 200px;
-      }
-    }
   }
 
   table {
@@ -709,3 +672,35 @@
 .pf-noselection {
   height: 100%;
 }
+
+.flamegraph-profile {
+  height: 100%;
+  // This is required to position locally-scoped (i.e. non-full-screen) modal
+  // dialogs within the panel, as they use position: absolute.
+  position: relative;
+
+  .time {
+    justify-self: end;
+    margin-right: 10px;
+  }
+  .selected {
+    justify-self: end;
+    margin-right: 10px;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    width: 200px;
+  }
+  .flamegraph-content {
+    overflow: auto;
+    height: 100%;
+
+    .loading-container {
+      font-size: larger;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      height: 100%;
+    }
+  }
+}
diff --git a/ui/src/controller/flamegraph_controller.ts b/ui/src/controller/flamegraph_controller.ts
index a10252c..b1c00a4 100644
--- a/ui/src/controller/flamegraph_controller.ts
+++ b/ui/src/controller/flamegraph_controller.ts
@@ -300,6 +300,7 @@
         await this.args.engine.query(`select value from stats
        where severity = 'error' and name = 'heap_graph_non_finalized_graph'`)
       ).firstRow({value: NUM}).value > 0;
+    flamegraphDetails.graphLoading = false;
     publishFlamegraphDetails(flamegraphDetails);
   }
 
@@ -317,8 +318,10 @@
     if (this.flamegraphDatasets.has(key)) {
       currentData = this.flamegraphDatasets.get(key)!;
     } else {
-      // TODO(b/330703412): Show loading state.
-
+      publishFlamegraphDetails({
+        ...globals.flamegraphDetails,
+        graphLoading: true,
+      });
       // Collecting data for drawing flamegraph for selected profile.
       // Data needs to be in following format:
       // id, name, parent_id, depth, total_size
diff --git a/ui/src/frontend/flamegraph_panel.ts b/ui/src/frontend/flamegraph_panel.ts
index fea7fb2..0f4cba2 100644
--- a/ui/src/frontend/flamegraph_panel.ts
+++ b/ui/src/frontend/flamegraph_panel.ts
@@ -30,6 +30,8 @@
 import {Icon} from '../widgets/icon';
 import {Modal, ModalAttrs} from '../widgets/modal';
 import {Popup} from '../widgets/popup';
+import {EmptyState} from '../widgets/empty_state';
+import {Spinner} from '../widgets/spinner';
 
 import {Flamegraph, NodeRendering} from './flamegraph';
 import {globals} from './globals';
@@ -37,7 +39,9 @@
 import {Router} from './router';
 import {getCurrentTrace} from './sidebar';
 import {convertTraceToPprofAndDownload} from './trace_converter';
+import {ButtonBar} from '../widgets/button';
 import {DurationWidget} from './widgets/duration';
+import {DetailsShell} from '../widgets/details_shell';
 
 const HEADER_HEIGHT = 30;
 
@@ -90,33 +94,31 @@
         ? this.flamegraph.getHeight() + HEADER_HEIGHT
         : 0;
       return m(
-        '.details-panel',
+        '.flamegraph-profile',
         this.maybeShowModal(flamegraphDetails.graphIncomplete),
         m(
-          '.details-panel-heading.flamegraph-profile',
-          {onclick: (e: MouseEvent) => e.stopPropagation()},
-          [
-            m('div.options', [
-              m(
-                'div.title',
-                this.getTitle(),
-                this.profileType === ProfileType.MIXED_HEAP_PROFILE &&
+          DetailsShell,
+          {
+            fillParent: true,
+            title: m(
+              'div.title',
+              this.getTitle(),
+              this.profileType === ProfileType.MIXED_HEAP_PROFILE &&
+                m(
+                  Popup,
+                  {
+                    trigger: m(Icon, {icon: 'warning'}),
+                  },
                   m(
-                    Popup,
-                    {
-                      trigger: m(Icon, {icon: 'warning'}),
-                    },
-                    m(
-                      '',
-                      {style: {width: '300px'}},
-                      'This is a mixed java/native heap profile, free()s are not visualized. To visualize free()s, remove "all_heaps: true" from the config.',
-                    ),
+                    '',
+                    {style: {width: '300px'}},
+                    'This is a mixed java/native heap profile, free()s are not visualized. To visualize free()s, remove "all_heaps: true" from the config.',
                   ),
-                ':',
-              ),
-              this.getViewingOptionButtons(),
-            ]),
-            m('div.details', [
+                ),
+              ':',
+            ),
+            description: this.getViewingOptionButtons(),
+            buttons: [
               m(
                 'div.selected',
                 `Selected function: ${toSelectedCallsite(
@@ -145,23 +147,39 @@
                     this.downloadPprof();
                   },
                 }),
-            ]),
-          ],
+            ],
+          },
+          m(
+            '.flamegraph-content',
+            flamegraphDetails.graphLoading
+              ? m(
+                  '.loading-container',
+                  m(
+                    EmptyState,
+                    {
+                      icon: 'bar_chart',
+                      title: 'Computing graph ...',
+                      className: 'flamegraph-loading',
+                    },
+                    m(Spinner, {easing: true}),
+                  ),
+                )
+              : m(`canvas[ref=canvas]`, {
+                  style: `height:${height}px; width:100%`,
+                  onmousemove: (e: MouseEvent) => {
+                    const {offsetX, offsetY} = e;
+                    this.onMouseMove({x: offsetX, y: offsetY});
+                  },
+                  onmouseout: () => {
+                    this.onMouseOut();
+                  },
+                  onclick: (e: MouseEvent) => {
+                    const {offsetX, offsetY} = e;
+                    this.onMouseClick({x: offsetX, y: offsetY});
+                  },
+                }),
+          ),
         ),
-        m(`canvas[ref=canvas]`, {
-          style: `height:${height}px; width:100%`,
-          onmousemove: (e: MouseEvent) => {
-            const {offsetX, offsetY} = e;
-            this.onMouseMove({x: offsetX, y: offsetY});
-          },
-          onmouseout: () => {
-            this.onMouseOut();
-          },
-          onclick: (e: MouseEvent) => {
-            const {offsetX, offsetY} = e;
-            this.onMouseClick({x: offsetX, y: offsetY});
-          },
-        }),
       );
     } else {
       return m(
@@ -260,7 +278,7 @@
 
   getViewingOptionButtons(): m.Children {
     return m(
-      'div',
+      ButtonBar,
       ...FlamegraphDetailsPanel.selectViewingOptions(
         assertExists(this.profileType),
       ),
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index 1af1399..5cecd7e 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -160,6 +160,8 @@
   // When heap_graph_non_finalized_graph has a count >0, we mark the graph
   // as incomplete.
   graphIncomplete?: boolean;
+  // About to show a new graph whose data is not ready yet.
+  graphLoading?: boolean;
 }
 
 export interface CpuProfileDetails {