| /* | |
| * 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 <cstdio> | |
| #include <memory> | |
| #include <optional> | |
| #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/agent_mode.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/result_formatter.h" | |
| #include "src/trace_processor/shell/shell_utils.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(); | |
| } | |
| // Runs |sql|, printing every result set. On error, records which statement | |
| // failed so that it can be reported (see kFailedStatementPayload). | |
| base::Status RunAndPrint(TraceProcessor* tp, | |
| const std::string& sql, | |
| const ResultFormatOptions& format, | |
| const GlobalOptions& global) { | |
| uint32_t statement = 0; | |
| base::Status status = RunQueriesAndPrintResult(tp, sql, format, stdout, | |
| global.quiet, &statement); | |
| if (!status.ok() && statement > 0) { | |
| status.SetPayload(kFailedStatementPayload, std::to_string(statement)); | |
| } | |
| return status; | |
| } | |
| } // 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, with consecutive result sets separated by a single | |
| blank line. Use -i to drop into an interactive shell after the queries | |
| complete. | |
| Output is CSV by default. --format markdown prints compact markdown tables: | |
| results over --max-rows rows (default 1000) or --max-bytes bytes (default | |
| 10000) show only their first and last rows plus a footer with the row count, | |
| and cells are cut at --max-cell-width characters (default 500). CSV has no | |
| limits by default; if any are passed, results over them are cut to their | |
| first rows and a note is printed to stderr. | |
| Agent mode is on when run by a coding agent (detected from its environment, | |
| e.g. CLAUDECODE or AI_AGENT) with stdout not a terminal and no --format; | |
| --agent / --no-agent force it on or off. In agent mode, results default to | |
| markdown and errors are printed as JSON. | |
| 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_), | |
| RetiredWideFlag(), | |
| StringFlag("perf-file", '\0', "FILE", "Write perf timing data to FILE.", | |
| &perf_file_), | |
| StringFlag("format", '\0', "FMT", | |
| "Output format: csv (default) or markdown.", &format_), | |
| BoolFlag("agent", '\0', "Force agent mode on (see above).", &agent_), | |
| BoolFlag("no-agent", '\0', "Force agent mode off.", &no_agent_), | |
| StringFlag("max-rows", '\0', "N", | |
| "Rows printed in full; larger results are cut. 0 for no " | |
| "limit.", | |
| &max_rows_), | |
| StringFlag("max-bytes", '\0', "N", | |
| "Like --max-rows but for bytes of output.", &max_bytes_), | |
| StringFlag("max-cell-width", '\0', "N", | |
| "Characters printed per cell. 0 for no limit.", | |
| &max_cell_width_), | |
| }; | |
| } | |
| base::StatusOr<ResultFormatOptions> QuerySubcommand::ResolveFormat( | |
| const AgentMode& agent_mode) { | |
| ResultFormatOptions options = | |
| agent_mode.enabled ? DefaultMarkdownOptions() : ResultFormatOptions(); | |
| if (format_ == "csv") { | |
| options = ResultFormatOptions(); | |
| } else if (format_ == "markdown") { | |
| options = DefaultMarkdownOptions(); | |
| } else if (!format_.empty()) { | |
| return base::ErrStatus("query: unknown --format '%s' (csv or markdown)", | |
| format_.c_str()); | |
| } | |
| struct { | |
| const char* flag; | |
| const std::string& value; | |
| uint64_t* target; | |
| } limits[] = { | |
| {"--max-rows", max_rows_, &options.max_rows}, | |
| {"--max-bytes", max_bytes_, &options.max_bytes}, | |
| {"--max-cell-width", max_cell_width_, &options.max_cell_chars}, | |
| }; | |
| for (const auto& limit : limits) { | |
| if (limit.value.empty()) { | |
| continue; | |
| } | |
| std::optional<uint64_t> v = base::StringToUInt64(limit.value); | |
| if (!v) { | |
| return base::ErrStatus("query: %s expects a number, got '%s'", limit.flag, | |
| limit.value.c_str()); | |
| } | |
| *limit.target = *v; | |
| } | |
| return options; | |
| } | |
| base::Status QuerySubcommand::Run(const SubcommandContext& ctx) { | |
| RETURN_IF_ERROR(RejectExtraPositionals(ctx, "query", 2)); | |
| if (agent_ && no_agent_) { | |
| return base::ErrStatus("query: --agent and --no-agent are exclusive"); | |
| } | |
| std::optional<bool> force_agent; | |
| if (agent_ || no_agent_) { | |
| force_agent = agent_; | |
| } | |
| const bool stdout_is_tty = base::IsTty(stdout); | |
| AgentMode agent_mode = | |
| DecideAgentMode(force_agent, !format_.empty(), stdout_is_tty); | |
| const bool quiet = ctx.global->quiet; | |
| if (agent_mode.enabled && !quiet) { | |
| PrintAgentModeNotice(agent_mode); | |
| } | |
| base::Status status = ExecuteAndPrint(ctx, agent_mode); | |
| if (status.ok()) { | |
| return status; | |
| } | |
| // Results printed before the error (e.g. by earlier statements) must come | |
| // first when stdout and stderr go to the same place. | |
| fflush(stdout); | |
| if (agent_mode.enabled) { | |
| fprintf(stderr, "%s\n", FormatErrorAsJson(status).c_str()); | |
| } else if (!force_agent && format_.empty() && !stdout_is_tty && !quiet) { | |
| // Agents which don't set a known environment variable never get agent | |
| // mode automatically. Their stdout is redirected, so when a query fails | |
| // in that situation, point them at it. | |
| fprintf(stderr, "%s\n", status.c_message()); | |
| PrintAgentModeHint(); | |
| } else { | |
| return status; | |
| } | |
| status.SetPayload("perfetto.dev/has_printed_error", "1"); | |
| return status; | |
| } | |
| base::Status QuerySubcommand::ExecuteAndPrint(const SubcommandContext& ctx, | |
| const AgentMode& agent_mode) { | |
| ASSIGN_OR_RETURN(ResultFormatOptions format, ResolveFormat(agent_mode)); | |
| // 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, format); | |
| } | |
| // 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->quiet && ctx.global->remote_addr.empty() && !interactive_ && | |
| load_s >= kSlowParseSeconds) { | |
| PrintStatusLine( | |
| StatusLabelStyle::kHint, "Tip", | |
| base::StackString<64>("parsing took %.1fs", load_s).ToStdString() + | |
| " 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 " + | |
| trace_file + | |
| "\n" | |
| " trace_processor query --remote S \"SELECT ...\"\n" | |
| " trace_processor server kill S\n" | |
| "Scripts and AI agents: `trace_processor help agent` covers " | |
| "this, schema discovery and PerfettoSQL tips."); | |
| } | |
| 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 = RunAndPrint(tp.get(), sql, format, *ctx.global); | |
| 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{ctx.global->quiet})); | |
| } | |
| 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, | |
| const ResultFormatOptions& format) { | |
| 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(RunAndPrint(tp.get(), | |
| "SELECT * FROM " + query_result.table_name, | |
| format, *ctx.global)); | |
| 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 |