| /* | |
| * 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/shell/query_subcommand.h" | |
| #include <algorithm> | |
| #include <cstdint> | |
| #include <memory> | |
| #include <string> | |
| #include <string_view> | |
| #include <vector> | |
| #include "perfetto/base/build_config.h" | |
| #include "perfetto/base/logging.h" | |
| #include "perfetto/base/status.h" | |
| #include "perfetto/base/time.h" | |
| #include "perfetto/ext/base/file_utils.h" | |
| #include "perfetto/ext/base/status_macros.h" | |
| #include "perfetto/ext/base/string_utils.h" | |
| #include "perfetto/ext/base/utils.h" | |
| #include "perfetto/trace_processor/summarizer.h" | |
| #include "perfetto/trace_processor/trace_processor.h" | |
| #include "src/protozero/text_to_proto/text_to_proto.h" | |
| #include "src/trace_processor/shell/common_flags.h" | |
| #include "src/trace_processor/shell/interactive.h" | |
| #include "src/trace_processor/shell/metatrace.h" | |
| #include "src/trace_processor/shell/query.h" | |
| #include "src/trace_processor/shell/subcommand.h" | |
| #include "src/trace_processor/trace_summary/trace_summary.descriptor.h" | |
| #if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) | |
| #include <unistd.h> // For STDIN_FILENO. | |
| #endif | |
| #if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) && !defined(STDIN_FILENO) | |
| #define STDIN_FILENO 0 | |
| #endif | |
| #if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) || \ | |
| PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) || \ | |
| PERFETTO_BUILDFLAG(PERFETTO_OS_FREEBSD) || \ | |
| PERFETTO_BUILDFLAG(PERFETTO_OS_APPLE) | |
| #define PERFETTO_HAS_SIGNAL_H() 1 | |
| #include <signal.h> | |
| #else | |
| #define PERFETTO_HAS_SIGNAL_H() 0 | |
| #endif | |
| namespace perfetto::trace_processor::shell { | |
| namespace { | |
| // Returns true if the file at |path| with |content| should be treated as | |
| // textproto rather than binary proto. Uses the same heuristic as the | |
| // classic codepath: .pb → binary, .textproto → text, otherwise | |
| // content-sniff the first 128 bytes (all printable/whitespace → text). | |
| bool IsTextproto(const std::string& path, const std::string& content) { | |
| if (base::EndsWith(path, ".pb")) { | |
| return false; | |
| } | |
| if (base::EndsWith(path, ".textproto")) { | |
| return true; | |
| } | |
| std::string_view prefix(content.c_str(), | |
| std::min<size_t>(content.size(), 128)); | |
| return std::all_of(prefix.begin(), prefix.end(), | |
| [](char c) { return std::isspace(c) || std::isprint(c); }); | |
| } | |
| // Reads a TraceSummarySpec file and passes it to |summarizer|, converting | |
| // textproto to binary proto when necessary. | |
| base::Status LoadSpecIntoSummarizer(Summarizer* summarizer, | |
| const std::string& path) { | |
| std::string content; | |
| if (!base::ReadFile(path, &content)) { | |
| return base::ErrStatus("Unable to read spec file %s", path.c_str()); | |
| } | |
| const uint8_t* spec_data; | |
| size_t spec_size; | |
| std::vector<uint8_t> binary_proto; | |
| if (IsTextproto(path, content)) { | |
| ASSIGN_OR_RETURN(binary_proto, | |
| protozero::TextToProto(kTraceSummaryDescriptor.data(), | |
| kTraceSummaryDescriptor.size(), | |
| ".perfetto.protos.TraceSummarySpec", | |
| "-", std::string_view(content))); | |
| spec_data = binary_proto.data(); | |
| spec_size = binary_proto.size(); | |
| } else { | |
| spec_data = reinterpret_cast<const uint8_t*>(content.data()); | |
| spec_size = content.size(); | |
| } | |
| SummarizerUpdateSpecResult update_result; | |
| RETURN_IF_ERROR(summarizer->UpdateSpec(spec_data, spec_size, &update_result)); | |
| for (const auto& q : update_result.queries) { | |
| if (q.error.has_value()) { | |
| return base::ErrStatus("Error in query '%s' from spec '%s': %s", | |
| q.query_id.c_str(), path.c_str(), | |
| q.error->c_str()); | |
| } | |
| } | |
| return base::OkStatus(); | |
| } | |
| } // namespace | |
| const char* QuerySubcommand::name() const { | |
| return "query"; | |
| } | |
| const char* QuerySubcommand::description() const { | |
| return "Load a trace and run a SQL query."; | |
| } | |
| const char* QuerySubcommand::usage_args() const { | |
| return "<trace_file> [SQL]"; | |
| } | |
| const char* QuerySubcommand::detailed_help() const { | |
| return R"(Run one or more SQL queries against a loaded trace file and print results. | |
| SQL can be provided in three ways: | |
| 1. Positional argument: tp query trace.pb "SELECT ts FROM slice LIMIT 10" | |
| 2. From a file: tp query -f queries.sql trace.pb | |
| 3. From stdin: cat q.sql | tp query trace.pb | |
| Multiple semicolon-separated statements are supported: every statement's | |
| result set is printed as CSV, with consecutive result sets separated by a | |
| single blank line. Use -i to drop into an interactive shell after the | |
| queries complete. | |
| Advanced (for debugging/testing structured queries): | |
| --structured-query-id ID --summary-spec FILE [...] | |
| Executes a single structured query by ID from the given summary spec | |
| files. The spec files replace -f/stdin/positional SQL. Output is the | |
| query result table.)"; | |
| } | |
| std::vector<FlagSpec> QuerySubcommand::GetFlags() { | |
| return { | |
| StringFlag("query-file", 'f', "FILE", | |
| "Read SQL from FILE (use '-' for stdin).", &query_file_), | |
| StringFlag("structured-query-id", '\0', "ID", | |
| "[Advanced] Run a single structured query by ID.", | |
| &structured_query_id_), | |
| {"summary-spec", '\0', true, "FILE", | |
| "[Advanced] Summary spec file for structured queries (repeatable).", | |
| [this](const char* v) { structured_query_specs_.emplace_back(v); }}, | |
| BoolFlag("interactive", 'i', "Start interactive shell after query.", | |
| &interactive_), | |
| BoolFlag("wide", 'W', "Double column width for output.", &wide_), | |
| StringFlag("perf-file", '\0', "FILE", "Write perf timing data to FILE.", | |
| &perf_file_), | |
| }; | |
| } | |
| base::Status QuerySubcommand::Run(const SubcommandContext& ctx) { | |
| RETURN_IF_ERROR(RejectExtraPositionals(ctx, "query", 2)); | |
| // With --remote, the trace is already loaded server-side, so there is no | |
| // trace-file positional: the first positional (if any) is the SQL. | |
| std::string trace_file; | |
| size_t sql_pos = 0; | |
| RETURN_IF_ERROR(ResolveTraceFileArg(ctx, "query", &trace_file, &sql_pos)); | |
| // Advanced: structured query mode. | |
| if (!structured_query_id_.empty()) { | |
| return RunStructuredQuery(ctx, trace_file); | |
| } | |
| // Determine SQL source: | |
| // 1. Positional: query trace.pb "SELECT ..." | |
| // 2. File: query -f file.sql trace.pb | |
| // 3. Stdin flag: query -f - trace.pb | |
| // 4. Stdin pipe: query trace.pb < file.sql | |
| std::string sql; | |
| bool read_stdin = | |
| query_file_ == "-" || (query_file_.empty() && !base::IsTty(STDIN_FILENO)); | |
| if (ctx.positional_args.size() > sql_pos) { | |
| sql = ctx.positional_args[sql_pos]; | |
| } else if (read_stdin) { | |
| if (!base::ReadFileDescriptor(STDIN_FILENO, &sql)) | |
| return base::ErrStatus("query: failed to read SQL from stdin"); | |
| query_file_.clear(); | |
| } | |
| if (sql.empty() && query_file_.empty()) { | |
| return base::ErrStatus( | |
| "query: no SQL provided. Use positional arg, -f FILE, or pipe to " | |
| "stdin."); | |
| } | |
| base::TimeNanos t_load{}; | |
| ASSIGN_OR_RETURN(auto tp, CreateTraceProcessor(*ctx.global, ctx.platform, | |
| trace_file, &t_load)); | |
| // One-shot `query TRACE` calls re-parse the trace every time. Scripts and | |
| // coding agents rarely read --help but do read stderr, so when parsing | |
| // was slow enough to matter tell them, once per call, how to keep the | |
| // trace loaded across queries. Measured on agents: this line alone moved | |
| // warm-session use from 0% to >80% of runs; longer guides were not read. | |
| constexpr double kSlowParseSeconds = 1.0; | |
| double load_s = static_cast<double>(t_load.count()) / 1e9; | |
| if (ctx.global->remote_addr.empty() && !interactive_ && | |
| load_s >= kSlowParseSeconds) { | |
| fprintf( | |
| stderr, | |
| "Tip: parsing took %.1fs and `query TRACE` re-parses on every call. " | |
| "To run many queries, load once and reuse the session:\n" | |
| " trace_processor server unix --name S --daemonize %s\n" | |
| " trace_processor query --remote S \"SELECT ...\"\n" | |
| " trace_processor server kill S\n" | |
| "Schema discovery and PerfettoSQL tips: `trace_processor help " | |
| "agent`.\n", | |
| load_s, trace_file.c_str()); | |
| } | |
| if (!query_file_.empty()) { | |
| if (!base::ReadFile(query_file_, &sql)) { | |
| return base::ErrStatus("query: unable to read file '%s'", | |
| query_file_.c_str()); | |
| } | |
| } | |
| PERFETTO_CHECK(!sql.empty()); | |
| #if PERFETTO_HAS_SIGNAL_H() | |
| static TraceProcessor* g_tp_for_signal_handler = tp.get(); | |
| signal(SIGINT, [](int) { g_tp_for_signal_handler->InterruptQuery(); }); | |
| #endif | |
| base::TimeNanos t_query_start = base::GetWallTimeNs(); | |
| auto status = RunQueries(tp.get(), sql, true); | |
| if (!status.ok()) { | |
| MaybeWriteMetatrace(tp.get(), ctx.global->metatrace_path); | |
| return status; | |
| } | |
| base::TimeNanos t_query = base::GetWallTimeNs() - t_query_start; | |
| if (!perf_file_.empty()) { | |
| RETURN_IF_ERROR(PrintPerfFile(perf_file_, t_load, t_query)); | |
| } | |
| if (interactive_) { | |
| RETURN_IF_ERROR(StartInteractiveShell( | |
| tp.get(), | |
| InteractiveOptions{ | |
| wide_ ? 40u : 20u, MetricV1OutputFormat::kNone, {}, {}, nullptr})); | |
| } | |
| RETURN_IF_ERROR(MaybeWriteMetatrace(tp.get(), ctx.global->metatrace_path)); | |
| return base::OkStatus(); | |
| } | |
| base::Status QuerySubcommand::RunStructuredQuery( | |
| const SubcommandContext& ctx, | |
| const std::string& trace_file) { | |
| if (structured_query_specs_.empty()) { | |
| return base::ErrStatus( | |
| "query: --structured-query-id requires at least one --summary-spec"); | |
| } | |
| base::TimeNanos t_load{}; | |
| ASSIGN_OR_RETURN(auto tp, CreateTraceProcessor(*ctx.global, ctx.platform, | |
| trace_file, &t_load)); | |
| std::unique_ptr<Summarizer> summarizer; | |
| RETURN_IF_ERROR(tp->CreateSummarizer(&summarizer)); | |
| for (const auto& path : structured_query_specs_) { | |
| RETURN_IF_ERROR(LoadSpecIntoSummarizer(summarizer.get(), path)); | |
| } | |
| base::TimeNanos t_query_start = base::GetWallTimeNs(); | |
| SummarizerQueryResult query_result; | |
| RETURN_IF_ERROR(summarizer->Query(structured_query_id_, &query_result)); | |
| if (!query_result.exists) { | |
| return base::ErrStatus( | |
| "Structured query ID '%s' not found in the provided spec files", | |
| structured_query_id_.c_str()); | |
| } | |
| RETURN_IF_ERROR( | |
| RunQueries(tp.get(), "SELECT * FROM " + query_result.table_name, true)); | |
| base::TimeNanos t_query = base::GetWallTimeNs() - t_query_start; | |
| if (!perf_file_.empty()) { | |
| RETURN_IF_ERROR(PrintPerfFile(perf_file_, t_load, t_query)); | |
| } | |
| RETURN_IF_ERROR(MaybeWriteMetatrace(tp.get(), ctx.global->metatrace_path)); | |
| return base::OkStatus(); | |
| } | |
| } // namespace perfetto::trace_processor::shell |