blob: 2b072f731c9b6a5c94959fc27c4c4407a735efe8 [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 <string>
#include <vector>
#include "perfetto/protozero/scattered_heap_buffer.h"
#include "perfetto/trace_processor/trace_processor.h"
#include "protos/perfetto/perfetto_sql/structured_query.pbzero.h"
#include "protos/perfetto/trace_summary/file.pbzero.h"
#include "src/base/test/status_matchers.h"
#include "test/gtest_and_gmock.h"
namespace perfetto::trace_processor::summary {
namespace {
using ::testing::HasSubstr;
// Import public types from parent namespace.
using ::perfetto::trace_processor::Summarizer;
using ::perfetto::trace_processor::SummarizerQueryResult;
using ::perfetto::trace_processor::SummarizerUpdateSpecResult;
using QuerySyncInfo = SummarizerUpdateSpecResult::QuerySyncInfo;
class SummarizerTest : public ::testing::Test {
protected:
void SetUp() override {
tp_ = TraceProcessor::CreateInstance(Config{});
// NotifyEndOfFile() without loading a trace is intentional - it initializes
// TP's internal state (tables, functions) so we can execute queries against
// it. The Summarizer doesn't need trace data, just a working SQL engine.
tp_->NotifyEndOfFile();
summarizer_ =
std::make_unique<SummarizerImpl>(tp_.get(), nullptr, "test_id");
}
// Helper to create a TraceSummarySpec with queries.
std::vector<uint8_t> CreateSpec(
const std::vector<std::pair<std::string, std::string>>& queries) {
protozero::HeapBuffered<protos::pbzero::TraceSummarySpec> spec;
for (const auto& [id, sql] : queries) {
auto* query = spec->add_query();
query->set_id(id);
auto* sql_source = query->set_sql();
sql_source->set_sql(sql);
sql_source->add_column_names("value");
}
return spec.SerializeAsArray();
}
// Helper to create a spec for chained queries A -> B -> C.
std::vector<uint8_t> CreateChainedSpec(const std::string& sql_a) {
protozero::HeapBuffered<protos::pbzero::TraceSummarySpec> spec;
// Query A: standalone query.
{
auto* query = spec->add_query();
query->set_id("A");
auto* sql_source = query->set_sql();
sql_source->set_sql(sql_a);
sql_source->add_column_names("value");
}
// Query B: depends on A via inner_query_id.
{
auto* query = spec->add_query();
query->set_id("B");
query->set_inner_query_id("A");
// Apply a filter to make it different from A.
auto* filter = query->add_filters();
filter->set_column_name("value");
filter->set_op(
protos::pbzero::PerfettoSqlStructuredQuery::Filter::GREATER_THAN);
filter->add_int64_rhs(0);
}
// Query C: depends on B via inner_query_id.
{
auto* query = spec->add_query();
query->set_id("C");
query->set_inner_query_id("B");
}
return spec.SerializeAsArray();
}
std::unique_ptr<TraceProcessor> tp_;
std::unique_ptr<SummarizerImpl> summarizer_;
};
TEST_F(SummarizerTest, BasicMaterialization) {
auto spec_data = CreateSpec({{"test_query", "SELECT 42 as value"}});
SummarizerUpdateSpecResult result;
ASSERT_OK(
summarizer_->UpdateSpec(spec_data.data(), spec_data.size(), &result));
// Should have one query result.
ASSERT_EQ(result.queries.size(), 1u);
EXPECT_EQ(result.queries[0].query_id, "test_query");
EXPECT_TRUE(result.queries[0].was_updated);
EXPECT_FALSE(result.queries[0].was_dropped);
EXPECT_FALSE(result.queries[0].error.has_value());
// Should be able to fetch the result.
SummarizerQueryResult info;
ASSERT_OK(summarizer_->Query("test_query", &info));
EXPECT_TRUE(info.exists);
EXPECT_FALSE(info.table_name.empty());
EXPECT_EQ(info.row_count, 1);
ASSERT_EQ(info.columns.size(), 1u);
EXPECT_EQ(info.columns[0], "value");
}
TEST_F(SummarizerTest, UnchangedQueryNotRematerialized) {
auto spec_data = CreateSpec({{"test_query", "SELECT 42 as value"}});
// First update - should materialize.
SummarizerUpdateSpecResult result1;
ASSERT_OK(
summarizer_->UpdateSpec(spec_data.data(), spec_data.size(), &result1));
ASSERT_EQ(result1.queries.size(), 1u);
EXPECT_TRUE(result1.queries[0].was_updated);
SummarizerQueryResult info1;
ASSERT_OK(summarizer_->Query("test_query", &info1));
std::string first_table_name = info1.table_name;
// Second update with same spec - should NOT rematerialize.
SummarizerUpdateSpecResult result2;
ASSERT_OK(
summarizer_->UpdateSpec(spec_data.data(), spec_data.size(), &result2));
ASSERT_EQ(result2.queries.size(), 1u);
EXPECT_FALSE(result2.queries[0].was_updated);
SummarizerQueryResult info2;
ASSERT_OK(summarizer_->Query("test_query", &info2));
EXPECT_EQ(info2.table_name, first_table_name);
}
TEST_F(SummarizerTest, ChangedQueryRematerialized) {
auto spec_data1 = CreateSpec({{"test_query", "SELECT 42 as value"}});
// First update.
SummarizerUpdateSpecResult result1;
ASSERT_OK(
summarizer_->UpdateSpec(spec_data1.data(), spec_data1.size(), &result1));
ASSERT_EQ(result1.queries.size(), 1u);
EXPECT_TRUE(result1.queries[0].was_updated);
SummarizerQueryResult info1;
ASSERT_OK(summarizer_->Query("test_query", &info1));
std::string first_table_name = info1.table_name;
// Second update with changed SQL.
auto spec_data2 = CreateSpec({{"test_query", "SELECT 100 as value"}});
SummarizerUpdateSpecResult result2;
ASSERT_OK(
summarizer_->UpdateSpec(spec_data2.data(), spec_data2.size(), &result2));
ASSERT_EQ(result2.queries.size(), 1u);
EXPECT_TRUE(result2.queries[0].was_updated);
SummarizerQueryResult info2;
ASSERT_OK(summarizer_->Query("test_query", &info2));
// Table name should be different (new materialization).
EXPECT_NE(info2.table_name, first_table_name);
}
TEST_F(SummarizerTest, AutoDropWhenQueryRemoved) {
auto spec_data1 = CreateSpec(
{{"query_a", "SELECT 1 as value"}, {"query_b", "SELECT 2 as value"}});
// First update with both queries.
SummarizerUpdateSpecResult result1;
ASSERT_OK(
summarizer_->UpdateSpec(spec_data1.data(), spec_data1.size(), &result1));
ASSERT_EQ(result1.queries.size(), 2u);
// Second update with only query_a.
auto spec_data2 = CreateSpec({{"query_a", "SELECT 1 as value"}});
SummarizerUpdateSpecResult result2;
ASSERT_OK(
summarizer_->UpdateSpec(spec_data2.data(), spec_data2.size(), &result2));
// Should have 2 entries: query_a (unchanged) and query_b (dropped).
ASSERT_EQ(result2.queries.size(), 2u);
bool found_query_a = false;
bool found_query_b_dropped = false;
for (const auto& info : result2.queries) {
if (info.query_id == "query_a") {
found_query_a = true;
EXPECT_FALSE(info.was_dropped);
}
if (info.query_id == "query_b") {
found_query_b_dropped = true;
EXPECT_TRUE(info.was_dropped);
}
}
EXPECT_TRUE(found_query_a);
EXPECT_TRUE(found_query_b_dropped);
// query_b should no longer exist.
SummarizerQueryResult info_b;
ASSERT_OK(summarizer_->Query("query_b", &info_b));
EXPECT_FALSE(info_b.exists);
}
TEST_F(SummarizerTest, ChainedQueriesWithOptimization) {
// Create a chain: A -> B -> C.
// A is a standalone query.
// B references A via inner_query_id and adds a filter.
// C references B via inner_query_id.
auto spec_data =
CreateChainedSpec("SELECT 1 as value UNION ALL SELECT 2 as value");
// First update - all should be materialized.
SummarizerUpdateSpecResult result1;
ASSERT_OK(
summarizer_->UpdateSpec(spec_data.data(), spec_data.size(), &result1));
ASSERT_EQ(result1.queries.size(), 3u);
for (const auto& info : result1.queries) {
EXPECT_TRUE(info.was_updated)
<< "Query " << info.query_id << " should be materialized on first run";
}
// Get the table names.
SummarizerQueryResult info_a1;
ASSERT_OK(summarizer_->Query("A", &info_a1));
SummarizerQueryResult info_b1;
ASSERT_OK(summarizer_->Query("B", &info_b1));
SummarizerQueryResult info_c1;
ASSERT_OK(summarizer_->Query("C", &info_c1));
ASSERT_TRUE(info_a1.exists);
ASSERT_TRUE(info_b1.exists);
ASSERT_TRUE(info_c1.exists);
// Second update with same spec - none should be rematerialized.
SummarizerUpdateSpecResult result2;
ASSERT_OK(
summarizer_->UpdateSpec(spec_data.data(), spec_data.size(), &result2));
ASSERT_EQ(result2.queries.size(), 3u);
for (const auto& info : result2.queries) {
EXPECT_FALSE(info.was_updated)
<< "Query " << info.query_id
<< " should NOT be rematerialized on second run";
}
// Table names should be the same.
SummarizerQueryResult info_a2;
ASSERT_OK(summarizer_->Query("A", &info_a2));
SummarizerQueryResult info_b2;
ASSERT_OK(summarizer_->Query("B", &info_b2));
SummarizerQueryResult info_c2;
ASSERT_OK(summarizer_->Query("C", &info_c2));
EXPECT_EQ(info_a2.table_name, info_a1.table_name);
EXPECT_EQ(info_b2.table_name, info_b1.table_name);
EXPECT_EQ(info_c2.table_name, info_c1.table_name);
}
TEST_F(SummarizerTest, ChainedQueriesPartialRematerialization) {
// Create initial chain: A -> B -> C.
protozero::HeapBuffered<protos::pbzero::TraceSummarySpec> spec1_proto;
// Query A: SELECT 1, 2, 3.
{
auto* query = spec1_proto->add_query();
query->set_id("A");
auto* sql_source = query->set_sql();
sql_source->set_sql(
"SELECT 1 as value UNION ALL SELECT 2 UNION ALL SELECT 3");
sql_source->add_column_names("value");
}
// Query B: filter A to value > 1.
{
auto* query = spec1_proto->add_query();
query->set_id("B");
query->set_inner_query_id("A");
auto* filter = query->add_filters();
filter->set_column_name("value");
filter->set_op(
protos::pbzero::PerfettoSqlStructuredQuery::Filter::GREATER_THAN);
filter->add_int64_rhs(1);
}
// Query C: references B.
{
auto* query = spec1_proto->add_query();
query->set_id("C");
query->set_inner_query_id("B");
}
auto spec1_data = spec1_proto.SerializeAsArray();
// First update.
SummarizerUpdateSpecResult result1;
ASSERT_OK(
summarizer_->UpdateSpec(spec1_data.data(), spec1_data.size(), &result1));
ASSERT_EQ(result1.queries.size(), 3u);
SummarizerQueryResult info_a1;
ASSERT_OK(summarizer_->Query("A", &info_a1));
SummarizerQueryResult info_b1;
ASSERT_OK(summarizer_->Query("B", &info_b1));
SummarizerQueryResult info_c1;
ASSERT_OK(summarizer_->Query("C", &info_c1));
ASSERT_TRUE(info_a1.exists);
ASSERT_TRUE(info_b1.exists);
ASSERT_TRUE(info_c1.exists);
// Modify B's filter (change > 1 to > 2) and re-sync.
// A should stay the same, B and C should be rematerialized.
protozero::HeapBuffered<protos::pbzero::TraceSummarySpec> spec2_proto;
// Query A: unchanged.
{
auto* query = spec2_proto->add_query();
query->set_id("A");
auto* sql_source = query->set_sql();
sql_source->set_sql(
"SELECT 1 as value UNION ALL SELECT 2 UNION ALL SELECT 3");
sql_source->add_column_names("value");
}
// Query B: filter changed to value > 2.
{
auto* query = spec2_proto->add_query();
query->set_id("B");
query->set_inner_query_id("A");
auto* filter = query->add_filters();
filter->set_column_name("value");
filter->set_op(
protos::pbzero::PerfettoSqlStructuredQuery::Filter::GREATER_THAN);
filter->add_int64_rhs(2); // Changed from 1 to 2.
}
// Query C: references B.
{
auto* query = spec2_proto->add_query();
query->set_id("C");
query->set_inner_query_id("B");
}
auto spec2_data = spec2_proto.SerializeAsArray();
SummarizerUpdateSpecResult result2;
ASSERT_OK(
summarizer_->UpdateSpec(spec2_data.data(), spec2_data.size(), &result2));
ASSERT_EQ(result2.queries.size(), 3u);
// Check what was updated.
bool a_updated = false, b_updated = false, c_updated = false;
for (const auto& info : result2.queries) {
if (info.query_id == "A")
a_updated = info.was_updated;
if (info.query_id == "B")
b_updated = info.was_updated;
if (info.query_id == "C")
c_updated = info.was_updated;
}
// A should NOT be updated (optimization: table-source query used).
EXPECT_FALSE(a_updated) << "A should NOT be rematerialized";
// B should be updated (its filter changed).
EXPECT_TRUE(b_updated) << "B should be rematerialized (filter changed)";
// C should be updated (its dependency B changed).
EXPECT_TRUE(c_updated) << "C should be rematerialized (B changed)";
// Verify A's table name is preserved.
SummarizerQueryResult info_a2;
ASSERT_OK(summarizer_->Query("A", &info_a2));
EXPECT_EQ(info_a2.table_name, info_a1.table_name);
// B and C should have new table names.
SummarizerQueryResult info_b2;
ASSERT_OK(summarizer_->Query("B", &info_b2));
SummarizerQueryResult info_c2;
ASSERT_OK(summarizer_->Query("C", &info_c2));
EXPECT_NE(info_b2.table_name, info_b1.table_name);
EXPECT_NE(info_c2.table_name, info_c1.table_name);
// Verify the row counts are correct.
// B now has filter > 2, so only value 3 should remain (1 row).
EXPECT_EQ(info_b2.row_count, 1);
EXPECT_EQ(info_c2.row_count, 1);
}
TEST_F(SummarizerTest, OldTableAccessibleUntilNewMaterialization) {
// This test verifies that the old materialized table is NOT dropped
// immediately when UpdateSpec() detects a change. The old table should remain
// accessible until Query() creates the new table. This prevents race
// conditions where in-flight queries fail with "no such table" errors.
auto spec_data1 = CreateSpec({{"test_query", "SELECT 42 as value"}});
// First update and materialize.
SummarizerUpdateSpecResult result1;
ASSERT_OK(
summarizer_->UpdateSpec(spec_data1.data(), spec_data1.size(), &result1));
SummarizerQueryResult info1;
ASSERT_OK(summarizer_->Query("test_query", &info1));
ASSERT_TRUE(info1.exists);
std::string old_table_name = info1.table_name;
// Verify old table is accessible.
{
auto check1 = tp_->ExecuteQuery("SELECT COUNT(*) FROM " + old_table_name);
ASSERT_TRUE(check1.Next());
EXPECT_EQ(check1.Get(0).AsLong(), 1);
while (check1.Next()) {
} // Fully consume iterator.
}
// Update with changed SQL - this marks the query for re-materialization
// but should NOT drop the old table yet.
auto spec_data2 = CreateSpec({{"test_query", "SELECT 100 as value"}});
SummarizerUpdateSpecResult result2;
ASSERT_OK(
summarizer_->UpdateSpec(spec_data2.data(), spec_data2.size(), &result2));
ASSERT_EQ(result2.queries.size(), 1u);
EXPECT_TRUE(
result2.queries[0].was_updated); // Marked for re-materialization.
// CRITICAL: Old table should still be accessible after UpdateSpec().
// This is the key behavior that prevents race conditions.
{
auto check2 = tp_->ExecuteQuery("SELECT COUNT(*) FROM " + old_table_name);
ASSERT_TRUE(check2.Next())
<< "Old table should still exist after UpdateSpec()";
EXPECT_EQ(check2.Get(0).AsLong(), 1);
} // Iterator goes out of scope, releasing any locks.
// Now fetch the result, which triggers materialization of the new table.
SummarizerQueryResult info2;
ASSERT_OK(summarizer_->Query("test_query", &info2));
ASSERT_TRUE(info2.exists);
EXPECT_NE(info2.table_name, old_table_name); // New table created.
// After Query(), old table should be dropped automatically.
auto check3 = tp_->ExecuteQuery("SELECT COUNT(*) FROM " + old_table_name);
check3.Next(); // Execute the query.
// Query should fail because table was dropped.
EXPECT_FALSE(check3.Status().ok())
<< "Old table should be dropped after new materialization: "
<< check3.Status().message();
}
// Tests dependency propagation for queries with nested embedded queries.
// This simulates the FilterDuring scenario where the main query embeds
// references to other queries via inner_query fields.
//
// The test verifies that when A changes, C is marked for re-materialization
// even though C's inner_query_id reference to A is nested inside an embedded
// query (not at the top level of C's proto).
TEST_F(SummarizerTest, NestedEmbeddedQueryDependencyPropagation) {
// Create three queries:
// A: Source data
// B: Another source
// C: Has an embedded inner_query that references A via inner_query_id
// (simulating how FilterDuring, Join, etc. embed references)
// First spec: A and B are simple queries, C embeds a reference to A.
protozero::HeapBuffered<protos::pbzero::TraceSummarySpec> spec1_proto;
// Query A: Simple SELECT.
{
auto* query = spec1_proto->add_query();
query->set_id("A");
auto* sql_source = query->set_sql();
sql_source->set_sql("SELECT 1 as value UNION ALL SELECT 2 as value");
sql_source->add_column_names("value");
}
// Query B: Simple SELECT.
{
auto* query = spec1_proto->add_query();
query->set_id("B");
auto* sql_source = query->set_sql();
sql_source->set_sql("SELECT 10 as other");
sql_source->add_column_names("other");
}
// Query C: Uses inner_query with an embedded query that has inner_query_id.
// This creates a nested dependency: C -> (embedded) -> A.
{
auto* query = spec1_proto->add_query();
query->set_id("C");
// Embed a query that references A via inner_query_id.
auto* inner = query->set_inner_query();
inner->set_id("C_inner");
inner->set_inner_query_id("A");
// Apply a trivial filter to make it different from A.
auto* filter = inner->add_filters();
filter->set_column_name("value");
filter->set_op(
protos::pbzero::PerfettoSqlStructuredQuery::Filter::GREATER_THAN);
filter->add_int64_rhs(0);
}
auto spec1_data = spec1_proto.SerializeAsArray();
// First update.
SummarizerUpdateSpecResult result1;
ASSERT_OK(
summarizer_->UpdateSpec(spec1_data.data(), spec1_data.size(), &result1));
ASSERT_EQ(result1.queries.size(), 3u);
// Materialize all queries.
SummarizerQueryResult info_a1;
ASSERT_OK(summarizer_->Query("A", &info_a1));
SummarizerQueryResult info_b1;
ASSERT_OK(summarizer_->Query("B", &info_b1));
SummarizerQueryResult info_c1;
ASSERT_OK(summarizer_->Query("C", &info_c1));
ASSERT_TRUE(info_a1.exists);
ASSERT_TRUE(info_b1.exists);
ASSERT_TRUE(info_c1.exists);
EXPECT_EQ(info_a1.row_count, 2); // 1 and 2.
EXPECT_EQ(info_c1.row_count, 2); // Both > 0.
// Second spec: Modify A (the nested dependency of C).
// C should be marked for rematerialization even though its direct proto
// hasn't changed - the change was in a nested inner_query_id reference.
protozero::HeapBuffered<protos::pbzero::TraceSummarySpec> spec2_proto;
// Query A: CHANGED - now returns only 1 row.
{
auto* query = spec2_proto->add_query();
query->set_id("A");
auto* sql_source = query->set_sql();
sql_source->set_sql("SELECT 1 as value"); // Only 1 row now.
sql_source->add_column_names("value");
}
// Query B: unchanged.
{
auto* query = spec2_proto->add_query();
query->set_id("B");
auto* sql_source = query->set_sql();
sql_source->set_sql("SELECT 10 as other");
sql_source->add_column_names("other");
}
// Query C: unchanged proto, but depends on A which changed.
{
auto* query = spec2_proto->add_query();
query->set_id("C");
auto* inner = query->set_inner_query();
inner->set_id("C_inner");
inner->set_inner_query_id("A");
auto* filter = inner->add_filters();
filter->set_column_name("value");
filter->set_op(
protos::pbzero::PerfettoSqlStructuredQuery::Filter::GREATER_THAN);
filter->add_int64_rhs(0);
}
auto spec2_data = spec2_proto.SerializeAsArray();
SummarizerUpdateSpecResult result2;
ASSERT_OK(
summarizer_->UpdateSpec(spec2_data.data(), spec2_data.size(), &result2));
ASSERT_EQ(result2.queries.size(), 3u);
// Check what was updated.
bool a_updated = false, b_updated = false, c_updated = false;
for (const auto& info : result2.queries) {
if (info.query_id == "A")
a_updated = info.was_updated;
if (info.query_id == "B")
b_updated = info.was_updated;
if (info.query_id == "C")
c_updated = info.was_updated;
}
// A should be updated (its SQL changed).
EXPECT_TRUE(a_updated) << "A should be rematerialized (SQL changed)";
// B should NOT be updated (unchanged).
EXPECT_FALSE(b_updated) << "B should NOT be rematerialized";
// CRITICAL: C should be updated because its nested dependency A changed.
// This is the key test - before the fix, C would NOT be updated because
// the dependency propagation only checked top-level inner_query_id.
EXPECT_TRUE(c_updated)
<< "C should be rematerialized (nested dependency A changed)";
// Verify the results reflect the change.
SummarizerQueryResult info_a2;
ASSERT_OK(summarizer_->Query("A", &info_a2));
SummarizerQueryResult info_c2;
ASSERT_OK(summarizer_->Query("C", &info_c2));
// A now has 1 row.
EXPECT_EQ(info_a2.row_count, 1);
// C should also reflect A's change (1 row now, since only value=1 > 0).
EXPECT_EQ(info_c2.row_count, 1);
}
TEST_F(SummarizerTest, MultipleSummarizersCoexist) {
// Two summarizers created via the public API should be able to materialize
// the same query without table name collisions. The ids are auto-generated
// internally by TraceProcessor.
std::unique_ptr<Summarizer> s1;
std::unique_ptr<Summarizer> s2;
ASSERT_OK(tp_->CreateSummarizer(&s1));
ASSERT_OK(tp_->CreateSummarizer(&s2));
auto spec_data = CreateSpec({{"q", "SELECT 1 as value"}});
SummarizerUpdateSpecResult r1;
ASSERT_OK(s1->UpdateSpec(spec_data.data(), spec_data.size(), &r1));
ASSERT_EQ(r1.queries.size(), 1u);
SummarizerUpdateSpecResult r2;
ASSERT_OK(s2->UpdateSpec(spec_data.data(), spec_data.size(), &r2));
ASSERT_EQ(r2.queries.size(), 1u);
// Trigger actual materialization for both — if table names collided, the
// CREATE TABLE would fail with a "table already exists" error.
SummarizerQueryResult info1;
ASSERT_OK(s1->Query("q", &info1));
ASSERT_TRUE(info1.exists);
SummarizerQueryResult info2;
ASSERT_OK(s2->Query("q", &info2));
ASSERT_TRUE(info2.exists);
// Table names must differ (namespaced by auto-generated summarizer id).
EXPECT_NE(info1.table_name, info2.table_name);
}
TEST_F(SummarizerTest, TableNameContainsSummarizerId) {
// Verify that the materialized table name includes the summarizer id.
// The test fixture creates a SummarizerImpl with id "test_id".
auto spec_data = CreateSpec({{"q", "SELECT 1 as value"}});
SummarizerUpdateSpecResult result;
ASSERT_OK(
summarizer_->UpdateSpec(spec_data.data(), spec_data.size(), &result));
SummarizerQueryResult info;
ASSERT_OK(summarizer_->Query("q", &info));
ASSERT_TRUE(info.exists);
EXPECT_THAT(info.table_name, HasSubstr("test_id"));
}
TEST_F(SummarizerTest, DestructorCleansUpOldTableName) {
// Verify that destroying a summarizer between UpdateSpec (which sets
// old_table_name) and Query (which would normally clean it up) still
// drops the old table.
auto spec_data1 = CreateSpec({{"q", "SELECT 42 as value"}});
SummarizerUpdateSpecResult result1;
ASSERT_OK(
summarizer_->UpdateSpec(spec_data1.data(), spec_data1.size(), &result1));
SummarizerQueryResult info1;
ASSERT_OK(summarizer_->Query("q", &info1));
ASSERT_TRUE(info1.exists);
std::string old_table = info1.table_name;
// Update with changed SQL — sets old_table_name but does NOT call Query().
auto spec_data2 = CreateSpec({{"q", "SELECT 100 as value"}});
SummarizerUpdateSpecResult result2;
ASSERT_OK(
summarizer_->UpdateSpec(spec_data2.data(), spec_data2.size(), &result2));
// Verify old table still exists before destruction.
{
auto check = tp_->ExecuteQuery("SELECT COUNT(*) FROM " + old_table);
ASSERT_TRUE(check.Next());
while (check.Next()) {
}
}
// Destroy the summarizer WITHOUT calling Query().
summarizer_.reset();
// Old table should be dropped (no new table was created since Query() was
// never called after the second UpdateSpec).
auto check = tp_->ExecuteQuery("SELECT COUNT(*) FROM " + old_table);
check.Next(); // Execute the query.
EXPECT_FALSE(check.Status().ok())
<< "old_table_name should be dropped on destruction";
}
TEST_F(SummarizerTest, SimpleTableSourceCreatesView) {
// A query with a simple table source (no aggregation, no joins) should be
// created as a VIEW instead of a TABLE, preserving source table indexes.
protozero::HeapBuffered<protos::pbzero::TraceSummarySpec> spec;
{
auto* query = spec->add_query();
query->set_id("table_query");
auto* table = query->set_table();
table->set_table_name("slice");
table->add_column_names("id");
table->add_column_names("ts");
table->add_column_names("dur");
}
auto spec_data = spec.SerializeAsArray();
SummarizerUpdateSpecResult result;
ASSERT_OK(
summarizer_->UpdateSpec(spec_data.data(), spec_data.size(), &result));
SummarizerQueryResult info;
ASSERT_OK(summarizer_->Query("table_query", &info));
ASSERT_TRUE(info.exists);
EXPECT_FALSE(info.table_name.empty());
EXPECT_TRUE(info.is_view);
}
TEST_F(SummarizerTest, SqlSourceMaterializesAsTable) {
// A query with SQL source should always be materialized as a TABLE.
auto spec_data = CreateSpec({{"sql_query", "SELECT 42 as value"}});
SummarizerUpdateSpecResult result;
ASSERT_OK(
summarizer_->UpdateSpec(spec_data.data(), spec_data.size(), &result));
SummarizerQueryResult info;
ASSERT_OK(summarizer_->Query("sql_query", &info));
ASSERT_TRUE(info.exists);
EXPECT_FALSE(info.table_name.empty());
EXPECT_FALSE(info.is_view);
}
TEST_F(SummarizerTest, TableSourceWithGroupByMaterializesAsTable) {
// A query with GROUP BY should always be materialized, even if source is
// a simple table.
protozero::HeapBuffered<protos::pbzero::TraceSummarySpec> spec;
{
auto* query = spec->add_query();
query->set_id("agg_query");
auto* table = query->set_table();
table->set_table_name("slice");
table->add_column_names("id");
table->add_column_names("ts");
auto* group_by = query->set_group_by();
group_by->add_column_names("ts");
}
auto spec_data = spec.SerializeAsArray();
SummarizerUpdateSpecResult result;
ASSERT_OK(
summarizer_->UpdateSpec(spec_data.data(), spec_data.size(), &result));
SummarizerQueryResult info;
ASSERT_OK(summarizer_->Query("agg_query", &info));
ASSERT_TRUE(info.exists);
EXPECT_FALSE(info.table_name.empty());
EXPECT_FALSE(info.is_view);
}
TEST_F(SummarizerTest, ViewQueryableWithPagination) {
// Verify that a view can be queried with LIMIT/OFFSET (as the UI does for
// DataGrid pagination).
protozero::HeapBuffered<protos::pbzero::TraceSummarySpec> spec;
{
auto* query = spec->add_query();
query->set_id("view_query");
auto* table = query->set_table();
table->set_table_name("slice");
table->add_column_names("id");
table->add_column_names("ts");
table->add_column_names("dur");
}
auto spec_data = spec.SerializeAsArray();
SummarizerUpdateSpecResult result;
ASSERT_OK(
summarizer_->UpdateSpec(spec_data.data(), spec_data.size(), &result));
SummarizerQueryResult info;
ASSERT_OK(summarizer_->Query("view_query", &info));
ASSERT_TRUE(info.exists);
EXPECT_TRUE(info.is_view);
// Query the view with LIMIT/OFFSET (simulates DataGrid pagination).
auto paginated = tp_->ExecuteQuery("SELECT * FROM " + info.table_name +
" LIMIT 10 OFFSET 0");
// Verify the query succeeds and returns columns.
EXPECT_GT(paginated.ColumnCount(), 0u);
while (paginated.Next()) {
}
EXPECT_TRUE(paginated.Status().ok());
}
TEST_F(SummarizerTest, ViewRematerializedOnChange) {
// Verify that a view is properly dropped and recreated when its source
// table changes.
protozero::HeapBuffered<protos::pbzero::TraceSummarySpec> spec1;
{
auto* query = spec1->add_query();
query->set_id("v");
auto* table = query->set_table();
table->set_table_name("slice");
table->add_column_names("id");
table->add_column_names("ts");
}
auto spec_data1 = spec1.SerializeAsArray();
SummarizerUpdateSpecResult result1;
ASSERT_OK(
summarizer_->UpdateSpec(spec_data1.data(), spec_data1.size(), &result1));
SummarizerQueryResult info1;
ASSERT_OK(summarizer_->Query("v", &info1));
ASSERT_TRUE(info1.exists);
EXPECT_TRUE(info1.is_view);
std::string old_view = info1.table_name;
// Change the query (add a column).
protozero::HeapBuffered<protos::pbzero::TraceSummarySpec> spec2;
{
auto* query = spec2->add_query();
query->set_id("v");
auto* table = query->set_table();
table->set_table_name("slice");
table->add_column_names("id");
table->add_column_names("ts");
table->add_column_names("dur");
}
auto spec_data2 = spec2.SerializeAsArray();
SummarizerUpdateSpecResult result2;
ASSERT_OK(
summarizer_->UpdateSpec(spec_data2.data(), spec_data2.size(), &result2));
SummarizerQueryResult info2;
ASSERT_OK(summarizer_->Query("v", &info2));
ASSERT_TRUE(info2.exists);
// Should have a new name.
EXPECT_NE(info2.table_name, old_view);
// Old view should be dropped.
auto check = tp_->ExecuteQuery("SELECT * FROM " + old_view + " LIMIT 0");
check.Next();
EXPECT_FALSE(check.Status().ok())
<< "Old view should be dropped after rematerialization";
}
TEST_F(SummarizerTest, DestructorCleansUpViews) {
// Verify that destroying a summarizer cleans up views (not just tables).
protozero::HeapBuffered<protos::pbzero::TraceSummarySpec> spec;
{
auto* query = spec->add_query();
query->set_id("v");
auto* table = query->set_table();
table->set_table_name("slice");
table->add_column_names("id");
table->add_column_names("ts");
}
auto spec_data = spec.SerializeAsArray();
SummarizerUpdateSpecResult result;
ASSERT_OK(
summarizer_->UpdateSpec(spec_data.data(), spec_data.size(), &result));
SummarizerQueryResult info;
ASSERT_OK(summarizer_->Query("v", &info));
ASSERT_TRUE(info.exists);
EXPECT_TRUE(info.is_view);
std::string view_name = info.table_name;
// Verify view exists before destruction.
{
auto check = tp_->ExecuteQuery("SELECT * FROM " + view_name + " LIMIT 0");
while (check.Next()) {
}
EXPECT_TRUE(check.Status().ok());
}
// Destroy the summarizer.
summarizer_.reset();
// View should be dropped.
auto check = tp_->ExecuteQuery("SELECT * FROM " + view_name + " LIMIT 0");
check.Next();
EXPECT_FALSE(check.Status().ok()) << "View should be dropped on destruction";
}
TEST_F(SummarizerTest, ChainedViewsQueryCorrectly) {
// Test a chain where B and C are views referencing A (a table).
// CreateChainedSpec builds: A = SQL source, B = inner_query_id("A") with
// a "value > 0" filter, C = inner_query_id("B") with no filter.
// Verify the whole chain produces correct results when C is queried.
auto spec_data =
CreateChainedSpec("SELECT 1 as value UNION ALL SELECT 2 as value");
SummarizerUpdateSpecResult result;
ASSERT_OK(
summarizer_->UpdateSpec(spec_data.data(), spec_data.size(), &result));
// Query all three to verify types.
SummarizerQueryResult info_a;
ASSERT_OK(summarizer_->Query("A", &info_a));
ASSERT_TRUE(info_a.exists);
EXPECT_FALSE(info_a.is_view) << "A has SQL source, should be a table";
SummarizerQueryResult info_b;
ASSERT_OK(summarizer_->Query("B", &info_b));
ASSERT_TRUE(info_b.exists);
EXPECT_TRUE(info_b.is_view) << "B is inner_query_id, should be a view";
SummarizerQueryResult info_c;
ASSERT_OK(summarizer_->Query("C", &info_c));
ASSERT_TRUE(info_c.exists);
EXPECT_TRUE(info_c.is_view) << "C is inner_query_id, should be a view";
// B filters value > 0, so both rows (1 and 2) should pass through.
// C has no additional filter, so it should have the same rows as B.
EXPECT_EQ(info_c.row_count, 2);
// Verify C's view resolves to the correct values.
auto query_c = tp_->ExecuteQuery("SELECT value FROM " + info_c.table_name +
" ORDER BY value");
ASSERT_TRUE(query_c.Next());
EXPECT_EQ(query_c.Get(0).AsLong(), 1);
ASSERT_TRUE(query_c.Next());
EXPECT_EQ(query_c.Get(0).AsLong(), 2);
EXPECT_FALSE(query_c.Next());
ASSERT_OK(query_c.Status());
}
TEST_F(SummarizerTest, QuerySwitchesFromTableToView) {
// A query that initially has GROUP BY (→ table) then changes to not have
// GROUP BY (→ view). The old TABLE should be dropped even though the new
// one is a VIEW.
protozero::HeapBuffered<protos::pbzero::TraceSummarySpec> spec1;
{
auto* query = spec1->add_query();
query->set_id("q");
auto* table = query->set_table();
table->set_table_name("slice");
table->add_column_names("ts");
auto* group_by = query->set_group_by();
group_by->add_column_names("ts");
}
auto spec_data1 = spec1.SerializeAsArray();
SummarizerUpdateSpecResult result1;
ASSERT_OK(
summarizer_->UpdateSpec(spec_data1.data(), spec_data1.size(), &result1));
SummarizerQueryResult info1;
ASSERT_OK(summarizer_->Query("q", &info1));
ASSERT_TRUE(info1.exists);
EXPECT_FALSE(info1.is_view) << "GROUP BY query should be a table";
std::string old_name = info1.table_name;
// Change to no GROUP BY (should become a view).
protozero::HeapBuffered<protos::pbzero::TraceSummarySpec> spec2;
{
auto* query = spec2->add_query();
query->set_id("q");
auto* table = query->set_table();
table->set_table_name("slice");
table->add_column_names("ts");
}
auto spec_data2 = spec2.SerializeAsArray();
SummarizerUpdateSpecResult result2;
ASSERT_OK(
summarizer_->UpdateSpec(spec_data2.data(), spec_data2.size(), &result2));
SummarizerQueryResult info2;
ASSERT_OK(summarizer_->Query("q", &info2));
ASSERT_TRUE(info2.exists);
EXPECT_TRUE(info2.is_view) << "Without GROUP BY should be a view";
EXPECT_NE(info2.table_name, old_name);
// Old table should be dropped.
auto check = tp_->ExecuteQuery("SELECT * FROM " + old_name + " LIMIT 0");
check.Next();
EXPECT_FALSE(check.Status().ok())
<< "Old TABLE should be dropped when query switches to VIEW";
}
TEST_F(SummarizerTest, InnerQueryIdSourceCreatesView) {
// An inner_query_id source (referencing another query) should create a view.
protozero::HeapBuffered<protos::pbzero::TraceSummarySpec> spec;
{
auto* base_query = spec->add_query();
base_query->set_id("base");
auto* sql_source = base_query->set_sql();
sql_source->set_sql("SELECT 1 as value");
sql_source->add_column_names("value");
}
{
auto* ref_query = spec->add_query();
ref_query->set_id("ref");
ref_query->set_inner_query_id("base");
}
auto spec_data = spec.SerializeAsArray();
SummarizerUpdateSpecResult result;
ASSERT_OK(
summarizer_->UpdateSpec(spec_data.data(), spec_data.size(), &result));
SummarizerQueryResult base_info;
ASSERT_OK(summarizer_->Query("base", &base_info));
EXPECT_FALSE(base_info.is_view) << "SQL source should be a table";
SummarizerQueryResult ref_info;
ASSERT_OK(summarizer_->Query("ref", &ref_info));
ASSERT_TRUE(ref_info.exists);
EXPECT_FALSE(ref_info.table_name.empty());
EXPECT_TRUE(ref_info.is_view) << "inner_query_id source should be a view";
}
TEST_F(SummarizerTest, TableSourceWithOrderByMaterializesAsTable) {
// A query with ORDER BY should always be materialized, even if source is
// a simple table, since re-sorting on every downstream query is wasteful.
protozero::HeapBuffered<protos::pbzero::TraceSummarySpec> spec;
{
auto* query = spec->add_query();
query->set_id("ordered_query");
auto* table = query->set_table();
table->set_table_name("slice");
table->add_column_names("id");
table->add_column_names("ts");
auto* order_by = query->set_order_by();
auto* ordering = order_by->add_ordering_specs();
ordering->set_column_name("ts");
}
auto spec_data = spec.SerializeAsArray();
SummarizerUpdateSpecResult result;
ASSERT_OK(
summarizer_->UpdateSpec(spec_data.data(), spec_data.size(), &result));
SummarizerQueryResult info;
ASSERT_OK(summarizer_->Query("ordered_query", &info));
ASSERT_TRUE(info.exists);
EXPECT_FALSE(info.table_name.empty());
EXPECT_FALSE(info.is_view) << "ORDER BY query should be a table";
}
TEST_F(SummarizerTest, InnerQuerySourceMaterializesAsTable) {
// An embedded inner_query source can be arbitrarily complex, so it should
// always be materialized as a TABLE (not a VIEW).
protozero::HeapBuffered<protos::pbzero::TraceSummarySpec> spec;
{
auto* query = spec->add_query();
query->set_id("nested");
auto* inner = query->set_inner_query();
auto* table = inner->set_table();
table->set_table_name("slice");
table->add_column_names("id");
table->add_column_names("ts");
}
auto spec_data = spec.SerializeAsArray();
SummarizerUpdateSpecResult result;
ASSERT_OK(
summarizer_->UpdateSpec(spec_data.data(), spec_data.size(), &result));
SummarizerQueryResult info;
ASSERT_OK(summarizer_->Query("nested", &info));
ASSERT_TRUE(info.exists);
EXPECT_FALSE(info.table_name.empty());
EXPECT_FALSE(info.is_view) << "inner_query source should be a table";
}
TEST_F(SummarizerTest, SimpleSlicesSourceShouldUseView) {
// A query with a simple_slices source (no aggregation) should be classified
// as a view by ShouldUseView. We test the classification directly because
// simple_slices requires runtime PerfettoSQL modules that aren't available
// in the unit test environment.
protozero::HeapBuffered<protos::pbzero::PerfettoSqlStructuredQuery> query;
{
auto* slices = query->set_simple_slices();
slices->set_slice_name_glob("*");
}
auto query_data = query.SerializeAsArray();
EXPECT_TRUE(
SummarizerImpl::ShouldUseView(query_data.data(), query_data.size()))
<< "simple_slices source should use a view";
}
TEST_F(SummarizerTest, SimpleSlicesWithGroupByShouldNotUseView) {
// A simple_slices source with GROUP BY should NOT be a view.
protozero::HeapBuffered<protos::pbzero::PerfettoSqlStructuredQuery> query;
{
auto* slices = query->set_simple_slices();
slices->set_slice_name_glob("*");
auto* group_by = query->set_group_by();
group_by->add_column_names("slice_name");
}
auto query_data = query.SerializeAsArray();
EXPECT_FALSE(
SummarizerImpl::ShouldUseView(query_data.data(), query_data.size()))
<< "simple_slices with GROUP BY should materialize as a table";
}
} // namespace
} // namespace perfetto::trace_processor::summary