blob: d5ff7d6d2aed7a2e4665dddb644f87a0732568d1 [file]
/*
* Copyright (C) 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "src/trace_processor/trace_summary/summarizer.h"
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "perfetto/base/logging.h"
#include "perfetto/base/status.h"
#include "perfetto/base/time.h"
#include "perfetto/ext/base/flat_hash_map.h"
#include "perfetto/ext/base/fnv_hash.h"
#include "perfetto/protozero/scattered_heap_buffer.h"
#include "perfetto/trace_processor/trace_processor.h"
#include "src/trace_processor/perfetto_sql/generator/structured_query_generator.h"
#include "src/trace_processor/util/descriptors.h"
#include "src/trace_processor/util/protozero_to_text.h"
#include "protos/perfetto/perfetto_sql/structured_query.pbzero.h"
#include "protos/perfetto/trace_summary/file.pbzero.h"
namespace perfetto::trace_processor {
// Base class destructor (must be defined for the interface).
Summarizer::~Summarizer() = default;
namespace summary {
namespace {
using perfetto_sql::generator::StructuredQueryGenerator;
using TraceSummarySpec = protos::pbzero::TraceSummarySpec;
using PerfettoSqlStructuredQuery = protos::pbzero::PerfettoSqlStructuredQuery;
// Creates a table-source structured query that references a materialized table.
// This is used for unchanged queries to avoid re-running their full SQL.
std::vector<uint8_t> CreateTableSourceQuery(
const std::string& query_id,
const std::string& table_name,
const std::vector<std::string>& columns) {
protozero::HeapBuffered<PerfettoSqlStructuredQuery> sq;
sq->set_id(query_id);
auto* table = sq->set_table();
table->set_table_name(table_name);
for (const auto& col : columns) {
table->add_column_names(col);
}
return sq.SerializeAsArray();
}
// Recursively extracts all inner_query_id fields from a structured query proto.
// This is needed because queries can embed other queries (e.g.,
// join.left_query, filter_to_intervals.base), and those embedded queries may
// reference other queries by ID that need to be tracked as dependencies.
void ExtractInnerQueryIds(const uint8_t* data,
size_t size,
std::vector<std::string>& out_ids) {
PerfettoSqlStructuredQuery::Decoder query(data, size);
// Check top-level inner_query_id.
if (query.has_inner_query_id()) {
out_ids.push_back(query.inner_query_id().ToStdString());
}
// Check embedded inner_query (recursively).
if (query.has_inner_query()) {
auto inner = query.inner_query();
ExtractInnerQueryIds(inner.data, inner.size, out_ids);
}
// Check interval_intersect.base and interval_intersect.interval_intersect[].
if (query.has_interval_intersect()) {
auto ii = query.interval_intersect();
using II = PerfettoSqlStructuredQuery::IntervalIntersect;
II::Decoder ii_dec(ii.data, ii.size);
if (ii_dec.has_base()) {
auto base = ii_dec.base();
ExtractInnerQueryIds(base.data, base.size, out_ids);
}
for (auto it = ii_dec.interval_intersect(); it; ++it) {
ExtractInnerQueryIds(it->data(), it->size(), out_ids);
}
}
// Check experimental_filter_to_intervals.base and .intervals.
if (query.has_experimental_filter_to_intervals()) {
auto fti = query.experimental_filter_to_intervals();
using FTI = PerfettoSqlStructuredQuery::ExperimentalFilterToIntervals;
FTI::Decoder fti_dec(fti.data, fti.size);
if (fti_dec.has_base()) {
auto base = fti_dec.base();
ExtractInnerQueryIds(base.data, base.size, out_ids);
}
if (fti_dec.has_intervals()) {
auto intervals = fti_dec.intervals();
ExtractInnerQueryIds(intervals.data, intervals.size, out_ids);
}
}
// Check experimental_join.left_query and .right_query.
if (query.has_experimental_join()) {
auto join = query.experimental_join();
using Join = PerfettoSqlStructuredQuery::ExperimentalJoin;
Join::Decoder join_dec(join.data, join.size);
if (join_dec.has_left_query()) {
auto left = join_dec.left_query();
ExtractInnerQueryIds(left.data, left.size, out_ids);
}
if (join_dec.has_right_query()) {
auto right = join_dec.right_query();
ExtractInnerQueryIds(right.data, right.size, out_ids);
}
}
// Check experimental_union.queries[].
if (query.has_experimental_union()) {
auto un = query.experimental_union();
using Union = PerfettoSqlStructuredQuery::ExperimentalUnion;
Union::Decoder un_dec(un.data, un.size);
for (auto it = un_dec.queries(); it; ++it) {
ExtractInnerQueryIds(it->data(), it->size(), out_ids);
}
}
// Check experimental_add_columns.core_query and .input_query.
if (query.has_experimental_add_columns()) {
auto ac = query.experimental_add_columns();
using AC = PerfettoSqlStructuredQuery::ExperimentalAddColumns;
AC::Decoder ac_dec(ac.data, ac.size);
if (ac_dec.has_core_query()) {
auto core = ac_dec.core_query();
ExtractInnerQueryIds(core.data, core.size, out_ids);
}
if (ac_dec.has_input_query()) {
auto input = ac_dec.input_query();
ExtractInnerQueryIds(input.data, input.size, out_ids);
}
}
// Check experimental_create_slices.starts_query and .ends_query.
if (query.has_experimental_create_slices()) {
auto cs = query.experimental_create_slices();
using CS = PerfettoSqlStructuredQuery::ExperimentalCreateSlices;
CS::Decoder cs_dec(cs.data, cs.size);
if (cs_dec.has_starts_query()) {
auto starts = cs_dec.starts_query();
ExtractInnerQueryIds(starts.data, starts.size, out_ids);
}
if (cs_dec.has_ends_query()) {
auto ends = cs_dec.ends_query();
ExtractInnerQueryIds(ends.data, ends.size, out_ids);
}
}
// Check experimental_counter_intervals.input_query.
if (query.has_experimental_counter_intervals()) {
auto ci = query.experimental_counter_intervals();
using CI = PerfettoSqlStructuredQuery::ExperimentalCounterIntervals;
CI::Decoder ci_dec(ci.data, ci.size);
if (ci_dec.has_input_query()) {
auto input = ci_dec.input_query();
ExtractInnerQueryIds(input.data, input.size, out_ids);
}
}
// Check experimental_filter_in.base and .match_values.
if (query.has_experimental_filter_in()) {
auto fi = query.experimental_filter_in();
using FI = PerfettoSqlStructuredQuery::ExperimentalFilterIn;
FI::Decoder fi_dec(fi.data, fi.size);
if (fi_dec.has_base()) {
auto base = fi_dec.base();
ExtractInnerQueryIds(base.data, base.size, out_ids);
}
if (fi_dec.has_match_values()) {
auto match_values = fi_dec.match_values();
ExtractInnerQueryIds(match_values.data, match_values.size, out_ids);
}
}
// Check sql.dependencies[].query (recursively).
if (query.has_sql()) {
auto sql = query.sql();
using Sql = PerfettoSqlStructuredQuery::Sql;
Sql::Decoder sql_dec(sql.data, sql.size);
for (auto it = sql_dec.dependencies(); it; ++it) {
Sql::Dependency::Decoder dep_dec(it->data(), it->size());
if (dep_dec.has_query()) {
auto dep_query = dep_dec.query();
ExtractInnerQueryIds(dep_query.data, dep_query.size, out_ids);
}
}
}
}
} // namespace
// static
bool SummarizerImpl::ShouldUseView(const uint8_t* data, size_t size) {
PerfettoSqlStructuredQuery::Decoder query(data, size);
if (query.has_group_by() || query.has_order_by()) {
return false;
}
// Simple pass-through source types where preserving index visibility is
// beneficial. All other source types (sql, embedded inner_query,
// interval_intersect, etc.) default to materialized TABLE.
return query.has_table() || query.has_simple_slices() ||
query.has_inner_query_id();
}
SummarizerImpl::SummarizerImpl(TraceProcessor* tp,
DescriptorPool* descriptor_pool,
std::string id)
: tp_(tp), descriptor_pool_(descriptor_pool), id_(std::move(id)) {}
SummarizerImpl::~SummarizerImpl() {
DropAll();
}
std::string SummarizerImpl::ComputeProtoHash(const uint8_t* data, size_t size) {
base::FnvHasher hasher;
hasher.Update(reinterpret_cast<const char*>(data), size);
return std::to_string(hasher.digest());
}
base::Status SummarizerImpl::UpdateSpec(const uint8_t* spec_data,
size_t spec_size,
SummarizerUpdateSpecResult* result) {
TraceSummarySpec::Decoder spec_decoder(spec_data, spec_size);
// Track which query IDs are in the new spec.
base::FlatHashMap<std::string, bool> new_query_ids;
// Parse all queries from the spec and store them.
for (auto it = spec_decoder.query(); it; ++it) {
protozero::ProtoDecoder decoder(it->data(), it->size());
auto id_field =
decoder.FindField(PerfettoSqlStructuredQuery::kIdFieldNumber);
if (!id_field) {
return base::ErrStatus(
"Query missing required 'id' field: all queries must have an id");
}
std::string query_id = id_field.as_std_string();
std::string proto_hash = ComputeProtoHash(it->data(), it->size());
// Extract all inner_query_id dependencies (including nested ones).
std::vector<std::string> inner_query_ids;
ExtractInnerQueryIds(it->data(), it->size(), inner_query_ids);
new_query_ids.Insert(query_id, true);
// Check if this query already exists and is unchanged.
QueryState* existing = query_states_.Find(query_id);
if (existing && existing->proto_hash == proto_hash &&
!existing->needs_materialization && !existing->error) {
// Query is unchanged and already materialized - keep it.
// Re-populate proto_data because it was cleared after materialization
// (to save memory), but we may need it again if a dependency changes
// and this query needs to be re-materialized transitively.
existing->proto_data.assign(it->data(), it->data() + it->size());
existing->inner_query_ids = std::move(inner_query_ids);
continue;
}
// Query is new or changed - store it for lazy materialization.
QueryState state;
state.proto_hash = proto_hash;
state.proto_data.assign(it->data(), it->data() + it->size());
state.inner_query_ids = std::move(inner_query_ids);
state.needs_materialization = true;
// If existing, defer dropping the old table until new materialization.
// This prevents race conditions where in-flight queries against the old
// table fail with "no such table" errors.
if (existing && !existing->table_name.empty()) {
state.old_table_name = existing->table_name;
state.old_is_view = existing->is_view;
}
query_states_[query_id] = std::move(state);
}
// Drop tables for queries that are no longer in the spec (auto-drop).
std::vector<std::string> to_remove;
for (auto state_it = query_states_.GetIterator(); state_it; ++state_it) {
if (!new_query_ids.Find(state_it.key())) {
// Query was removed - drop its table/view.
if (!state_it.value().table_name.empty()) {
DropTableOrView(state_it.value().table_name, state_it.value().is_view);
}
// Record the drop in the result.
SummarizerUpdateSpecResult::QuerySyncInfo sync_info;
sync_info.query_id = state_it.key();
sync_info.was_dropped = true;
result->queries.push_back(std::move(sync_info));
to_remove.push_back(state_it.key());
}
}
for (const auto& key : to_remove) {
query_states_.Erase(key);
}
// Mark dependencies as needing re-materialization if their dependency
// changed. Repeat until no more changes (handles transitive dependencies).
bool changes_made = true;
while (changes_made) {
changes_made = false;
for (auto state_it = query_states_.GetIterator(); state_it; ++state_it) {
QueryState& state = state_it.value();
if (state.needs_materialization) {
continue; // Already marked.
}
// Check all dependencies (including nested ones from embedded queries).
for (const auto& dep_id : state.inner_query_ids) {
QueryState* dep = query_states_.Find(dep_id);
if (dep && dep->needs_materialization) {
// Dependency needs re-materialization, so does this query.
state.needs_materialization = true;
// Defer dropping the old table/view until new materialization.
if (!state.table_name.empty()) {
state.old_table_name = state.table_name;
state.old_is_view = state.is_view;
state.table_name.clear();
}
changes_made = true;
break; // Already marked, no need to check other deps.
}
}
}
}
// Report status for all queries (no materialization yet - that's lazy).
for (auto state_it = query_states_.GetIterator(); state_it; ++state_it) {
SummarizerUpdateSpecResult::QuerySyncInfo sync_info;
sync_info.query_id = state_it.key();
sync_info.was_updated = state_it.value().needs_materialization;
result->queries.push_back(std::move(sync_info));
}
return base::OkStatus();
}
std::vector<std::string> SummarizerImpl::CollectDependencies(
const std::string& query_id) {
std::vector<std::string> deps;
base::FlatHashMap<std::string, bool> visited;
// Walk up the dependency chain and collect in reverse order.
std::vector<std::string> stack;
stack.push_back(query_id);
while (!stack.empty()) {
std::string current = stack.back();
stack.pop_back();
if (visited.Find(current)) {
continue;
}
visited.Insert(current, true);
QueryState* state = query_states_.Find(current);
if (!state) {
continue;
}
// Add all dependencies (so they get materialized before this query).
for (const auto& dep_id : state->inner_query_ids) {
stack.push_back(dep_id);
}
deps.push_back(current);
}
// Reverse to get dependencies before dependents.
std::reverse(deps.begin(), deps.end());
return deps;
}
base::Status SummarizerImpl::PrepareGenerator(
StructuredQueryGenerator& generator,
std::vector<std::vector<uint8_t>>& table_source_protos) {
// Add ALL queries from query_states_ to the generator.
// For already-materialized queries, use table-source to avoid re-running SQL.
for (auto it = query_states_.GetIterator(); it; ++it) {
const std::string& dep_id = it.key();
const QueryState& dep_state = it.value();
if (!dep_state.needs_materialization && !dep_state.table_name.empty()) {
// Already materialized - use table-source query.
table_source_protos.push_back(CreateTableSourceQuery(
dep_id, dep_state.table_name, dep_state.columns));
const auto& proto = table_source_protos.back();
auto add_result = generator.AddQuery(proto.data(), proto.size());
if (!add_result.ok()) {
return base::ErrStatus("Failed to add table-source query for '%s': %s",
dep_id.c_str(), add_result.status().c_message());
}
} else {
// Not materialized - use full proto.
auto add_result = generator.AddQuery(dep_state.proto_data.data(),
dep_state.proto_data.size());
if (!add_result.ok()) {
return base::ErrStatus("Failed to add query '%s': %s", dep_id.c_str(),
add_result.status().c_message());
}
}
}
// Compute and execute modules/preambles ONCE.
auto modules = generator.ComputeReferencedModules();
auto preambles = generator.ComputePreambles();
// Include referenced modules (skip if already included).
for (const auto& module : modules) {
if (included_modules_.Find(module)) {
continue;
}
auto mod_it = tp_->ExecuteQuery("INCLUDE PERFETTO MODULE " + module);
while (mod_it.Next()) {
}
if (!mod_it.Status().ok()) {
return base::ErrStatus("Failed to include module '%s': %s",
module.c_str(), mod_it.Status().c_message());
}
included_modules_.Insert(module, true);
}
// Execute preambles.
for (const auto& preamble : preambles) {
auto preamble_it = tp_->ExecuteQuery(preamble);
while (preamble_it.Next()) {
}
if (!preamble_it.Status().ok()) {
return base::ErrStatus("Failed to execute preamble: %s",
preamble_it.Status().c_message());
}
}
return base::OkStatus();
}
base::Status SummarizerImpl::MaterializeQuery(
const std::string& query_id,
QueryState& state,
StructuredQueryGenerator& generator) {
// Generate SQL for this query.
// Use inline_shared_queries=true so that shared queries (referenced via
// inner_query_id) are included as CTEs in the generated SQL rather than being
// expected to exist as external tables.
auto sql_result =
generator.Generate(state.proto_data.data(), state.proto_data.size(),
/*inline_shared_queries=*/true);
if (!sql_result.ok()) {
state.error = "Failed to generate SQL for query '" + query_id +
"': " + sql_result.status().message();
state.needs_materialization = false; // Don't retry.
return sql_result.status();
}
const std::string& query_sql = *sql_result;
// Build complete runnable SQL with includes and preambles (for display).
// Get these from the generator (already computed during PrepareGenerator).
auto modules = generator.ComputeReferencedModules();
auto preambles = generator.ComputePreambles();
std::string complete_sql;
for (const auto& module : modules) {
complete_sql += "INCLUDE PERFETTO MODULE " + module + ";\n";
}
for (const auto& preamble : preambles) {
complete_sql += preamble + "\n";
}
if (!complete_sql.empty()) {
complete_sql += "\n"; // Extra newline before main query.
}
complete_sql += query_sql;
state.sql = complete_sql;
// Generate textproto representation (if descriptor pool is available).
if (descriptor_pool_) {
state.textproto = protozero_to_text::ProtozeroToText(
*descriptor_pool_, ".perfetto.protos.PerfettoSqlStructuredQuery",
protozero::ConstBytes{state.proto_data.data(), state.proto_data.size()},
protozero_to_text::kIncludeNewLines);
}
// Note: Standalone SQL generation is deferred to Query() time via
// GenerateStandaloneSql(). This avoids O(N²) work during batch
// materialization since each call would otherwise iterate all queries.
// Generate a new name. Include the summarizer id so that multiple
// summarizer instances can coexist without name collisions.
std::string table_name =
"_exp_mat_" + id_ + "_" + std::to_string(next_materialized_id_++);
// Decide whether to create a VIEW (preserves source table indexes for
// downstream joins) or a TABLE (materializes data for caching).
bool use_view =
ShouldUseView(state.proto_data.data(), state.proto_data.size());
// Track timing. For tables, we measure only the CREATE TABLE AS SELECT
// (which executes the query and writes results). For views, CREATE VIEW is
// near-instant, so we measure the COUNT(*) query which forces execution.
auto start_time = base::GetWallTimeNs();
// Create as VIEW or TABLE.
std::string create_sql;
if (use_view) {
create_sql = "CREATE PERFETTO VIEW " + table_name + " AS " + query_sql;
} else {
create_sql = "CREATE PERFETTO TABLE " + table_name + " AS " + query_sql;
}
auto create_it = tp_->ExecuteQuery(create_sql);
while (create_it.Next()) {
}
if (!create_it.Status().ok()) {
state.error = create_it.Status().message();
state.needs_materialization = false; // Don't retry.
return create_it.Status();
}
// For tables, capture timing now (CREATE TABLE AS already executed the
// query). For views, we defer until after COUNT(*) which forces execution.
if (!use_view) {
auto end_time = base::GetWallTimeNs();
state.duration_ms =
static_cast<double>((end_time - start_time).count()) / 1e6;
}
// Get column information from the created table/view.
auto schema_it =
tp_->ExecuteQuery("SELECT * FROM " + table_name + " LIMIT 0");
uint32_t col_count = schema_it.ColumnCount();
state.columns.clear();
for (uint32_t i = 0; i < col_count; ++i) {
state.columns.push_back(schema_it.GetColumnName(i));
}
while (schema_it.Next()) {
}
if (!schema_it.Status().ok()) {
state.error = schema_it.Status().message();
state.needs_materialization = false;
return schema_it.Status();
}
// Get row count. For views this executes the underlying query.
auto count_it = tp_->ExecuteQuery("SELECT COUNT(*) FROM " + table_name);
if (count_it.Next()) {
state.row_count = count_it.Get(0).AsLong();
}
if (!count_it.Status().ok()) {
state.error = count_it.Status().message();
state.needs_materialization = false;
return count_it.Status();
}
// For views, capture timing after COUNT(*) which forces query execution.
if (use_view) {
auto end_time = base::GetWallTimeNs();
state.duration_ms =
static_cast<double>((end_time - start_time).count()) / 1e6;
}
state.table_name = table_name;
state.is_view = use_view;
state.error = std::nullopt;
state.needs_materialization = false;
// Note: We intentionally keep proto_data after materialization.
// It's needed when generating standalone_sql for dependent queries that
// are materialized later in the same batch. The memory cost is acceptable
// because the client re-sends the full spec on each sync anyway.
// Now that the new table/view is created, drop the old one if it exists.
// This deferred drop prevents race conditions where in-flight queries
// against the old table would fail with "no such table" errors.
if (!state.old_table_name.empty()) {
DropTableOrView(state.old_table_name, state.old_is_view);
state.old_table_name.clear();
state.old_is_view = false;
}
return base::OkStatus();
}
void SummarizerImpl::GenerateStandaloneSql(QueryState& state) {
// Skip if already generated.
if (!state.standalone_sql.empty()) {
return;
}
// Fallback to execution SQL if proto_data is missing.
if (state.proto_data.empty()) {
state.standalone_sql = state.sql;
return;
}
// Generate standalone SQL using original proto data for all queries
// (no table-source substitutions). This produces SQL that can be copied
// and run anywhere without depending on materialized tables.
StructuredQueryGenerator standalone_generator;
for (auto it = query_states_.GetIterator(); it; ++it) {
const QueryState& dep_state = it.value();
if (dep_state.proto_data.empty()) {
continue; // Skip queries without proto data.
}
auto add_result = standalone_generator.AddQuery(
dep_state.proto_data.data(), dep_state.proto_data.size());
if (!add_result.ok()) {
// If we can't add a query, fall back to the execution SQL.
state.standalone_sql = state.sql;
return;
}
}
auto standalone_sql_result = standalone_generator.Generate(
state.proto_data.data(), state.proto_data.size(),
/*inline_shared_queries=*/true);
if (!standalone_sql_result.ok()) {
// Fall back to execution SQL if generation fails.
state.standalone_sql = state.sql;
return;
}
// Build complete standalone SQL with includes and preambles.
auto standalone_modules = standalone_generator.ComputeReferencedModules();
auto standalone_preambles = standalone_generator.ComputePreambles();
std::string standalone_complete;
for (const auto& module : standalone_modules) {
standalone_complete += "INCLUDE PERFETTO MODULE " + module + ";\n";
}
for (const auto& preamble : standalone_preambles) {
standalone_complete += preamble + "\n";
}
if (!standalone_complete.empty()) {
standalone_complete += "\n";
}
standalone_complete += *standalone_sql_result;
state.standalone_sql = standalone_complete;
}
void SummarizerImpl::DropTableOrView(const std::string& name, bool is_view) {
std::string sql = is_view ? ("DROP VIEW IF EXISTS " + name)
: ("DROP TABLE IF EXISTS " + name);
auto drop_it = tp_->ExecuteQuery(sql);
while (drop_it.Next()) {
}
// Drop failures are non-fatal: the table/view may still be referenced by
// in-flight queries. Log to aid debugging but don't propagate the error.
if (!drop_it.Status().ok()) {
PERFETTO_DLOG("Failed to drop %s '%s': %s", is_view ? "view" : "table",
name.c_str(), drop_it.Status().c_message());
}
}
void SummarizerImpl::DropAll() {
for (auto it = query_states_.GetIterator(); it; ++it) {
if (!it.value().table_name.empty()) {
DropTableOrView(it.value().table_name, it.value().is_view);
}
// Also drop old tables/views that haven't been cleaned up yet (e.g., if
// UpdateSpec() marked a query for re-materialization but Query() was
// never called to complete the swap).
if (!it.value().old_table_name.empty()) {
DropTableOrView(it.value().old_table_name, it.value().old_is_view);
}
}
query_states_.Clear();
}
base::Status SummarizerImpl::Query(const std::string& query_id,
SummarizerQueryResult* result) {
QueryState* state = query_states_.Find(query_id);
if (!state) {
result->exists = false;
return base::OkStatus();
}
result->exists = true;
// Lazy materialization: materialize if needed.
if (state->needs_materialization) {
// Prepare the generator ONCE (adds all queries, executes
// modules/preambles).
StructuredQueryGenerator generator;
std::vector<std::vector<uint8_t>> table_source_protos;
auto prepare_status = PrepareGenerator(generator, table_source_protos);
if (!prepare_status.ok()) {
state->error = prepare_status.message();
return prepare_status;
}
// Materialize dependencies first.
auto deps = CollectDependencies(query_id);
for (const auto& dep_id : deps) {
if (dep_id == query_id) {
continue; // Handle the target query last.
}
QueryState* dep_state = query_states_.Find(dep_id);
if (dep_state && dep_state->needs_materialization) {
auto status = MaterializeQuery(dep_id, *dep_state, generator);
if (!status.ok()) {
// Dependency failed - propagate error.
state->error =
"Dependency '" + dep_id + "' failed: " + status.message();
state->needs_materialization = false;
}
}
}
// Now materialize the target query (if dependencies succeeded).
if (!state->error) {
auto status = MaterializeQuery(query_id, *state, generator);
if (!status.ok()) {
return status;
}
}
}
// If there's a stored error from a previous failed materialization, return
// it.
if (state->error) {
return base::ErrStatus("%s", state->error->c_str());
}
// Generate standalone SQL lazily (only when Query() is called for this node).
// This avoids O(N²) work during batch materialization.
GenerateStandaloneSql(*state);
result->table_name = state->table_name;
result->is_view = state->is_view;
result->row_count = state->row_count;
result->columns = state->columns;
result->duration_ms = state->duration_ms;
result->sql = state->sql;
result->textproto = state->textproto;
result->standalone_sql = state->standalone_sql;
return base::OkStatus();
}
} // namespace summary
} // namespace perfetto::trace_processor