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 {