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