| /* | |
| * 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/interactive.h" | |
| #include <cerrno> | |
| #include <cinttypes> | |
| #include <cstdint> | |
| #include <cstdio> | |
| #include <cstdlib> | |
| #include <cstring> | |
| #include <memory> | |
| #include <optional> | |
| #include <string> | |
| #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/scoped_file.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" | |
| #include "src/trace_processor/shell/metrics.h" | |
| #include "src/trace_processor/shell/query.h" | |
| #include "src/trace_processor/shell/shell_utils.h" | |
| #if PERFETTO_BUILDFLAG(PERFETTO_TP_LINENOISE) | |
| #include <linenoise.h> | |
| #include <pwd.h> | |
| #include <sys/types.h> | |
| #endif | |
| namespace perfetto::trace_processor { | |
| namespace { | |
| #if PERFETTO_BUILDFLAG(PERFETTO_TP_LINENOISE) | |
| bool EnsureDir(const std::string& path) { | |
| return base::Mkdir(path) || errno == EEXIST; | |
| } | |
| bool EnsureFile(const std::string& path) { | |
| return base::OpenFile(path, O_RDONLY | O_CREAT, 0644).get() != -1; | |
| } | |
| std::string GetConfigPath() { | |
| const char* homedir = getenv("HOME"); | |
| #if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) || \ | |
| PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) || \ | |
| PERFETTO_BUILDFLAG(PERFETTO_OS_FREEBSD) || \ | |
| PERFETTO_BUILDFLAG(PERFETTO_OS_APPLE) | |
| if (homedir == nullptr) | |
| homedir = getpwuid(getuid())->pw_dir; | |
| #elif PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) | |
| if (homedir == nullptr) | |
| homedir = getenv("USERPROFILE"); | |
| #endif | |
| if (homedir == nullptr) | |
| return ""; | |
| return std::string(homedir) + "/.config"; | |
| } | |
| std::string GetPerfettoPath() { | |
| std::string config = GetConfigPath(); | |
| if (config.empty()) | |
| return ""; | |
| return config + "/perfetto"; | |
| } | |
| std::string GetHistoryPath() { | |
| std::string perfetto = GetPerfettoPath(); | |
| if (perfetto.empty()) | |
| return ""; | |
| return perfetto + "/.trace_processor_shell_history"; | |
| } | |
| void SetupLineEditor() { | |
| linenoiseSetMultiLine(true); | |
| linenoiseHistorySetMaxLen(1000); | |
| bool success = !GetHistoryPath().empty(); | |
| success = success && EnsureDir(GetConfigPath()); | |
| success = success && EnsureDir(GetPerfettoPath()); | |
| success = success && EnsureFile(GetHistoryPath()); | |
| success = success && linenoiseHistoryLoad(GetHistoryPath().c_str()) != -1; | |
| if (!success) { | |
| PERFETTO_PLOG("Could not load history from %s", GetHistoryPath().c_str()); | |
| } | |
| } | |
| struct LineDeleter { | |
| void operator()(char* p) const { | |
| linenoiseHistoryAdd(p); | |
| linenoiseHistorySave(GetHistoryPath().c_str()); | |
| linenoiseFree(p); | |
| } | |
| }; | |
| using ScopedLine = std::unique_ptr<char, LineDeleter>; | |
| ScopedLine GetLine(const char* prompt) { | |
| errno = 0; | |
| auto line = ScopedLine(linenoise(prompt)); | |
| // linenoise returns a nullptr both for CTRL-C and CTRL-D, however in the | |
| // former case it sets errno to EAGAIN. | |
| // If the user press CTRL-C return "" instead of nullptr. We don't want the | |
| // main loop to quit in that case as that is inconsistent with the behavior | |
| // "CTRL-C interrupts the current query" and frustrating when hitting that | |
| // a split second after the query is done. | |
| if (!line && errno == EAGAIN) | |
| return ScopedLine(strdup("")); | |
| return line; | |
| } | |
| #else | |
| void SetupLineEditor() {} | |
| using ScopedLine = std::unique_ptr<char>; | |
| ScopedLine GetLine(const char* prompt) { | |
| printf("\r%80s\r%s", "", prompt); | |
| fflush(stdout); | |
| ScopedLine line(new char[1024]); | |
| if (!fgets(line.get(), 1024 - 1, stdin)) | |
| return nullptr; | |
| if (strlen(line.get()) > 0) | |
| line.get()[strlen(line.get()) - 1] = 0; | |
| return line; | |
| } | |
| #endif // PERFETTO_TP_LINENOISE | |
| void PrintQueryResultInteractively(Iterator* it, | |
| base::TimeNanos t_start, | |
| uint32_t column_width) { | |
| base::TimeNanos t_end = base::GetWallTimeNs(); | |
| for (uint32_t rows = 0; it->Next(); rows++) { | |
| if (rows % 32 == 0) { | |
| if (rows == 0) { | |
| t_end = base::GetWallTimeNs(); | |
| } else { | |
| fprintf(stderr, "...\nType 'q' to stop, Enter for more records: "); | |
| fflush(stderr); | |
| char input[32]; | |
| if (!fgets(input, sizeof(input) - 1, stdin)) | |
| exit(0); | |
| if (input[0] == 'q') | |
| break; | |
| } | |
| for (uint32_t i = 0; i < it->ColumnCount(); i++) | |
| printf("%-*.*s ", column_width, column_width, | |
| it->GetColumnName(i).c_str()); | |
| printf("\n"); | |
| std::string divider(column_width, '-'); | |
| for (uint32_t i = 0; i < it->ColumnCount(); i++) { | |
| printf("%-*s ", column_width, divider.c_str()); | |
| } | |
| printf("\n"); | |
| } | |
| for (uint32_t c = 0; c < it->ColumnCount(); c++) { | |
| auto value = it->Get(c); | |
| switch (value.type) { | |
| case SqlValue::Type::kNull: | |
| printf("%-*s", column_width, "[NULL]"); | |
| break; | |
| case SqlValue::Type::kDouble: | |
| printf("%*f", column_width, value.double_value); | |
| break; | |
| case SqlValue::Type::kLong: | |
| printf("%*" PRIi64, column_width, value.long_value); | |
| break; | |
| case SqlValue::Type::kString: | |
| printf("%-*.*s", column_width, column_width, value.string_value); | |
| break; | |
| case SqlValue::Type::kBytes: | |
| printf("%-*s", column_width, "<raw bytes>"); | |
| break; | |
| } | |
| printf(" "); | |
| } | |
| printf("\n"); | |
| } | |
| base::Status status = it->Status(); | |
| if (!status.ok()) { | |
| fprintf(stderr, "%s\n", status.c_message()); | |
| } | |
| printf("\nQuery executed in %.3f ms\n\n", | |
| static_cast<double>((t_end - t_start).count()) / 1E6); | |
| } | |
| void PrintShellUsage() { | |
| PERFETTO_ELOG(R"( | |
| Available commands: | |
| .quit, .q Exit the shell. | |
| .help This text. | |
| .dump FILE Export the trace as a sqlite database. | |
| .read FILE Executes the queries in the FILE. | |
| .reset Destroys all tables/view created by the user. | |
| .load-metrics-sql Reloads SQL from extension and custom metric paths | |
| specified in command line args. | |
| .run-metrics Runs metrics specified in command line args | |
| and prints the result. | |
| .width WIDTH Changes the column width of interactive query | |
| output. | |
| )"); | |
| } | |
| } // namespace | |
| base::Status StartInteractiveShell(TraceProcessor* trace_processor, | |
| const InteractiveOptions& options) { | |
| SetupLineEditor(); | |
| uint32_t column_width = options.column_width; | |
| for (;;) { | |
| ScopedLine line = GetLine("> "); | |
| if (!line) | |
| break; | |
| if (strcmp(line.get(), "") == 0) { | |
| printf("If you want to quit either type .q or press CTRL-D (EOF)\n"); | |
| continue; | |
| } | |
| if (line.get()[0] == '.') { | |
| char command[32] = {}; | |
| char arg[1024] = {}; | |
| sscanf(line.get() + 1, "%31s %1023s", command, arg); | |
| if (strcmp(command, "quit") == 0 || strcmp(command, "q") == 0) { | |
| break; | |
| } | |
| if (strcmp(command, "help") == 0) { | |
| PrintShellUsage(); | |
| } else if (strcmp(command, "dump") == 0 && strlen(arg)) { | |
| if (!ExportTraceToDatabase(trace_processor, arg).ok()) | |
| PERFETTO_ELOG("Database export failed"); | |
| } else if (strcmp(command, "reset") == 0) { | |
| trace_processor->RestoreInitialTables(); | |
| } else if (strcmp(command, "read") == 0 && strlen(arg)) { | |
| base::Status status = RunQueriesFromFile(trace_processor, arg, true); | |
| if (!status.ok()) { | |
| PERFETTO_ELOG("%s", status.c_message()); | |
| } | |
| } else if (strcmp(command, "width") == 0 && strlen(arg)) { | |
| std::optional<uint32_t> width = base::CStringToUInt32(arg); | |
| if (!width) { | |
| PERFETTO_ELOG("Invalid column width specified"); | |
| continue; | |
| } | |
| column_width = *width; | |
| } else if (strcmp(command, "load-metrics-sql") == 0) { | |
| base::Status status = LoadMetricsAndExtensionsSql( | |
| trace_processor, options.metrics, options.extensions); | |
| if (!status.ok()) { | |
| PERFETTO_ELOG("%s", status.c_message()); | |
| } | |
| } else if (strcmp(command, "run-metrics") == 0) { | |
| if (options.metrics.empty()) { | |
| PERFETTO_ELOG("No metrics specified on command line"); | |
| continue; | |
| } | |
| base::Status status = RunMetrics(trace_processor, options.metrics, | |
| options.metric_v1_format); | |
| if (!status.ok()) { | |
| fprintf(stderr, "%s\n", status.c_message()); | |
| } | |
| } else { | |
| PrintShellUsage(); | |
| } | |
| continue; | |
| } | |
| base::TimeNanos t_start = base::GetWallTimeNs(); | |
| auto it = trace_processor->ExecuteQuery(line.get()); | |
| PrintQueryResultInteractively(&it, t_start, column_width); | |
| } | |
| return base::OkStatus(); | |
| } | |
| } // namespace perfetto::trace_processor |