| /* |
| * 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.h" |
| |
| #include <chrono> |
| #include <cinttypes> |
| #include <cstdint> |
| #include <cstdio> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #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/scoped_file.h" |
| #include "perfetto/ext/base/status_macros.h" |
| #include "perfetto/ext/base/status_or.h" |
| #include "perfetto/ext/base/string_utils.h" |
| #include "perfetto/trace_processor/basic_types.h" |
| #include "perfetto/trace_processor/iterator.h" |
| #include "perfetto/trace_processor/trace_processor.h" |
| |
| namespace perfetto::trace_processor { |
| |
| base::StatusOr<QueryResult> ExtractQueryResult(Iterator* it, bool has_more) { |
| QueryResult result; |
| |
| for (uint32_t c = 0; c < it->ColumnCount(); c++) { |
| result.column_names.push_back(it->GetColumnName(c)); |
| } |
| |
| for (; has_more; has_more = it->Next()) { |
| std::vector<std::string> row; |
| for (uint32_t c = 0; c < it->ColumnCount(); c++) { |
| SqlValue value = it->Get(c); |
| std::string str_value; |
| switch (value.type) { |
| case SqlValue::Type::kNull: |
| str_value = "\"[NULL]\""; |
| break; |
| case SqlValue::Type::kDouble: |
| str_value = |
| base::StackString<256>("%f", value.double_value).ToStdString(); |
| break; |
| case SqlValue::Type::kLong: |
| str_value = base::StackString<256>("%" PRIi64, value.long_value) |
| .ToStdString(); |
| break; |
| case SqlValue::Type::kString: |
| str_value = '"' + std::string(value.string_value) + '"'; |
| break; |
| case SqlValue::Type::kBytes: |
| str_value = "\"<raw bytes>\""; |
| break; |
| } |
| |
| row.push_back(std::move(str_value)); |
| } |
| result.rows.push_back(std::move(row)); |
| } |
| RETURN_IF_ERROR(it->Status()); |
| return result; |
| } |
| |
| void PrintQueryResultAsCsv(const QueryResult& result, FILE* output) { |
| for (uint32_t c = 0; c < result.column_names.size(); c++) { |
| if (c > 0) |
| fprintf(output, ","); |
| fprintf(output, "\"%s\"", result.column_names[c].c_str()); |
| } |
| fprintf(output, "\n"); |
| |
| for (const auto& row : result.rows) { |
| for (uint32_t c = 0; c < result.column_names.size(); c++) { |
| if (c > 0) |
| fprintf(output, ","); |
| fprintf(output, "%s", row[c].c_str()); |
| } |
| fprintf(output, "\n"); |
| } |
| } |
| |
| base::Status RunQueriesWithoutOutput(TraceProcessor* trace_processor, |
| const std::string& sql_query) { |
| auto it = trace_processor->ExecuteQuery(sql_query); |
| if (it.StatementWithOutputCount() > 0) |
| return base::ErrStatus("Unexpected result from a query."); |
| |
| RETURN_IF_ERROR(it.Status()); |
| return it.Next() ? base::ErrStatus("Unexpected result from a query.") |
| : it.Status(); |
| } |
| |
| base::Status RunQueriesAndPrintResult(TraceProcessor* trace_processor, |
| const std::string& sql_query, |
| FILE* output) { |
| PERFETTO_DLOG("Executing query: %s", sql_query.c_str()); |
| |
| // Statements are executed one at a time and every statement's result set |
| // is printed as CSV, with consecutive result sets separated by a single |
| // blank line. Since our CSV writer quotes all strings, a blank line is |
| // unambiguously a boundary between result sets. Statements with no output |
| // print nothing, matching the sqlite3/duckdb shells. |
| std::chrono::nanoseconds exec_dur{0}; |
| uint32_t offset = 0; |
| bool executed_any_statement = false; |
| bool printed_any_result = false; |
| for (;;) { |
| auto query_start = std::chrono::steady_clock::now(); |
| std::optional<Iterator> it = |
| trace_processor->ExecuteNextStatement(sql_query, &offset); |
| if (!it.has_value()) { |
| break; |
| } |
| RETURN_IF_ERROR(it->Status()); |
| executed_any_statement = true; |
| |
| bool has_more = it->Next(); |
| RETURN_IF_ERROR(it->Status()); |
| |
| // Statements without a result set (e.g. CREATE TABLE) print nothing. |
| if (it->ColumnCount() == 0) { |
| PERFETTO_DCHECK(!has_more); |
| exec_dur += std::chrono::steady_clock::now() - query_start; |
| continue; |
| } |
| |
| // Statements with rows which nonetheless count as having no output are |
| // those whose output is explicitly ignored (a single column named |
| // `suppress_query_output`, void functions): step through them for their |
| // side effects but print nothing. |
| if (has_more && it->StatementWithOutputCount() == 0) { |
| for (; has_more; has_more = it->Next()) { |
| } |
| RETURN_IF_ERROR(it->Status()); |
| exec_dur += std::chrono::steady_clock::now() - query_start; |
| continue; |
| } |
| |
| // A zero-row result set still prints its header, with one exception: the |
| // `suppress_query_output` escape hatch must stay silent whether or not |
| // any row matched. (A zero-row void-function statement can't be detected |
| // here: its VOID marker lives on a row's value and there is no row.) |
| if (!has_more && it->ColumnCount() == 1 && |
| it->GetColumnName(0) == "suppress_query_output") { |
| exec_dur += std::chrono::steady_clock::now() - query_start; |
| continue; |
| } |
| |
| auto query_result = ExtractQueryResult(&*it, has_more); |
| RETURN_IF_ERROR(query_result.status()); |
| |
| // We want to include the query iteration time (as it's a part of |
| // executing SQL and can be non-trivial), and we want to exclude the time |
| // spent printing the result (which can be significant for large results), |
| // so we materialise the results first, then take the measurement, then |
| // print them. |
| exec_dur += std::chrono::steady_clock::now() - query_start; |
| |
| if (printed_any_result) { |
| fprintf(output, "\n"); |
| } |
| printed_any_result = true; |
| PrintQueryResultAsCsv(query_result.value(), output); |
| } |
| if (!executed_any_statement) { |
| return base::ErrStatus("No valid SQL to run"); |
| } |
| |
| PERFETTO_ILOG( |
| "Query execution time: %" PRIi64 " ms", |
| static_cast<int64_t>( |
| std::chrono::duration_cast<std::chrono::milliseconds>(exec_dur) |
| .count())); |
| return base::OkStatus(); |
| } |
| |
| base::Status PrintPerfFile(const std::string& perf_file_path, |
| base::TimeNanos t_load, |
| base::TimeNanos t_run) { |
| char buf[128]; |
| size_t count = base::SprintfTrunc(buf, sizeof(buf), "%" PRId64 ",%" PRId64, |
| static_cast<int64_t>(t_load.count()), |
| static_cast<int64_t>(t_run.count())); |
| if (count == 0) { |
| return base::ErrStatus("Failed to write perf data"); |
| } |
| |
| auto fd(base::OpenFile(perf_file_path, O_WRONLY | O_CREAT | O_TRUNC, 0666)); |
| if (!fd) { |
| return base::ErrStatus("Failed to open perf file"); |
| } |
| base::WriteAll(fd.get(), buf, count); |
| return base::OkStatus(); |
| } |
| |
| base::Status RunQueries(TraceProcessor* trace_processor, |
| const std::string& queries, |
| bool expect_output) { |
| if (expect_output) { |
| return RunQueriesAndPrintResult(trace_processor, queries, stdout); |
| } |
| return RunQueriesWithoutOutput(trace_processor, queries); |
| } |
| |
| base::Status RunQueriesFromFile(TraceProcessor* trace_processor, |
| const std::string& query_file_path, |
| bool expect_output) { |
| std::string queries; |
| if (!base::ReadFile(query_file_path, &queries)) { |
| return base::ErrStatus( |
| "Unable to read file %s. If you're passing an SQL query, did you mean " |
| "to use the -Q flag instead?", |
| query_file_path.c_str()); |
| } |
| return RunQueries(trace_processor, queries, expect_output); |
| } |
| |
| } // namespace perfetto::trace_processor |