tp: add explicit debuginfod lookup for native symbols Resolve missing native symbols by build ID without requiring local binaries. Try local sources first, then validated cache entries and ordered debuginfod servers. Use curl with configurable connection and stall timeouts, and publish downloaded files atomically. Require --debuginfod to enable lookups. Add URL and cache path overrides, warn about configured URLs when disabled, and filter LLVM's environment to prevent implicit downloads or option overrides. Report aggregates, verbose lookup details, and optional terminal progress. Document the workflow separately from the shared CLI reference.
diff --git a/Android.bp b/Android.bp index 62233d8..e4bb3ff 100644 --- a/Android.bp +++ b/Android.bp
@@ -22124,6 +22124,7 @@ srcs: [ "src/trace_processor/util/symbolizer/breakpad_parser.cc", "src/trace_processor/util/symbolizer/breakpad_symbolizer.cc", + "src/trace_processor/util/symbolizer/debuginfod.cc", "src/trace_processor/util/symbolizer/filesystem_posix.cc", "src/trace_processor/util/symbolizer/filesystem_windows.cc", "src/trace_processor/util/symbolizer/local_symbolizer.cc",
diff --git a/BUILD b/BUILD index 3addae2..8b7692f 100644 --- a/BUILD +++ b/BUILD
@@ -5946,6 +5946,7 @@ srcs = [ "src/trace_processor/util/symbolizer/breakpad_parser.cc", "src/trace_processor/util/symbolizer/breakpad_symbolizer.cc", + "src/trace_processor/util/symbolizer/debuginfod.cc", "src/trace_processor/util/symbolizer/filesystem_posix.cc", "src/trace_processor/util/symbolizer/filesystem_windows.cc", "src/trace_processor/util/symbolizer/local_symbolizer.cc", @@ -5960,6 +5961,7 @@ ":include_perfetto_public_base", "src/trace_processor/util/symbolizer/breakpad_parser.h", "src/trace_processor/util/symbolizer/breakpad_symbolizer.h", + "src/trace_processor/util/symbolizer/debuginfod.h", "src/trace_processor/util/symbolizer/elf.h", "src/trace_processor/util/symbolizer/filesystem.h", "src/trace_processor/util/symbolizer/local_symbolizer.h",
diff --git a/docs/learning-more/symbolization.md b/docs/learning-more/symbolization.md index e13785d..165a6bf 100644 --- a/docs/learning-more/symbolization.md +++ b/docs/learning-more/symbolization.md
@@ -14,6 +14,26 @@ R8/ProGuard (e.g. `fsd.a`) back to the original identifiers, using the `mapping.txt` produced at build time. +## Fetch debug files using debuginfod + +If you have a trace with native build IDs and access to a debuginfod server, +you can create a symbolized bundle without supplying local binaries. Install +`curl` and `llvm-symbolizer`, then run: + +```sh +trace_processor bundle --debuginfod \ + --debuginfod-urls "https://your-debuginfod-server.example" \ + input.pftrace output.tar +``` + +If `DEBUGINFOD_URLS` is already configured, only `--debuginfod` is needed. +Open the resulting bundle in the UI. Check the reported unresolved-frame count; +use `--verbose` to investigate unsuccessful lookups. Downloads are cached for +later runs. Use `--debuginfod-cache-path PATH` to choose a different cache. + +See the [CLI reference](/docs/reference/trace-processor-cli.md#debuginfod) for +precedence, timeouts, cache layout, and output controls. + ## Which workflow do you need? {#which-workflow} Match your trace to one of the categories below and follow the link. Picking the
diff --git a/docs/reference/trace-processor-cli.md b/docs/reference/trace-processor-cli.md index 81ad4f2..131e49d 100644 --- a/docs/reference/trace-processor-cli.md +++ b/docs/reference/trace-processor-cli.md
@@ -62,6 +62,63 @@ machine-readable `server unix` startup record are command results and remain visible in quiet mode. +## Debuginfod {#debuginfod} + +Native symbolization can fetch debug files using build IDs from the trace; +local binaries are not required. These controls apply to trace loading (for +example `query` and `server`), `bundle`, `util symbolize`, and `convert profile`. +They also work in the classic interface and in `traceconv`'s corresponding +commands. Remote sessions use the server's symbolization configuration; pass +`--debuginfod` when starting the server, rather than together with `--remote`. + +| Flag | Meaning | Default | +| --- | --- | --- | +| `--debuginfod` | Enable debuginfod cache lookup and downloads | Disabled | +| `--debuginfod-urls URLS` | Quoted, whitespace-separated HTTP(S) server roots | `DEBUGINFOD_URLS` | +| `--debuginfod-cache-path PATH` | Directory for downloaded debug files | `DEBUGINFOD_CACHE_PATH`, then the platform cache below | +| `--debuginfod-connect-timeout SECONDS` | Connection timeout per request | `5` | +| `--debuginfod-stall-timeout SECONDS` | Abort a transfer averaging less than one byte per second for this long | `10` | + +URL and cache flags replace their environment defaults, including when given +an empty value. Enabling debuginfod requires at least one server URL and a +nonempty cache path. Timeouts must be positive whole seconds. There is no +whole-transfer deadline: a large download can continue while it makes progress. + +Setting URLs alone does not enable downloads. Configured URLs produce a warning +when debuginfod is disabled, including with `--quiet`. The CLI removes +`DEBUGINFOD_URLS` and `LLVM_SYMBOLIZER_OPTS` from the LLVM child's environment; +`LLVM_SYMBOLIZER_OPTS`, when set, produces a warning that it is ignored. LLVM +therefore cannot independently opt into downloads or change the output format. +Other elfutils debuginfod environment controls are not interpreted by this CLI. + +Local native and Breakpad sources run first. Only unresolved addresses reach +debuginfod. Each missing build ID is looked up once per invocation, using the +cache first, then servers in the specified order. Requests are sequential. +The first valid file with the requested build ID is used. Requests use +`SERVER/buildid/HEX_BUILD_ID/debuginfo`; missing build IDs cannot be fetched. + +Downloads require `curl` and symbolization requires `llvm-symbolizer`, both on +`PATH`. Curl handles HTTP(S) redirects and honors its proxy and TLS environment +settings. Its configuration file (`.curlrc`) is disabled so it cannot override +these output and timeout controls. Server roots cannot include a query or +fragment. Downloads and redirects are restricted to HTTP(S). + +On POSIX systems the default cache is +`$XDG_CACHE_HOME/debuginfod_client`, or `$HOME/.cache/debuginfod_client` when +`XDG_CACHE_HOME` is unset. On Windows it is +`%LOCALAPPDATA%/debuginfod_client`. Entries are stored as +`HEX_BUILD_ID/debuginfo`. Files are checked against the requested build ID +before use. Downloads are published atomically after validation; failed +requests do not publish partial files. Abrupt termination may leave a temporary +file. There is no automatic cache eviction; remove entries to reclaim space. + +Normal output reports cache hits, downloads, and unavailable build IDs alongside +the frame totals. Verbose output adds cache paths, servers, and download errors. +TTY progress identifies the build ID being fetched; `--no-progress` disables +that display. Quiet suppresses routine output while retaining unresolved-frame +warnings. A failed download leaves the affected addresses unresolved; it does +not discard other symbols or prevent creation of a partial bundle. + ## Color environment variables | Environment | Behavior |
diff --git a/src/trace_processor/shell/BUILD.gn b/src/trace_processor/shell/BUILD.gn index 8cfce59..362d983 100644 --- a/src/trace_processor/shell/BUILD.gn +++ b/src/trace_processor/shell/BUILD.gn
@@ -143,6 +143,7 @@ "../../../protos/perfetto/trace/ftrace:cpp", "../../../protos/perfetto/trace/interned_data:cpp", "../../../protos/perfetto/trace/profiling:cpp", + "../../../test:test_helper", "../../base:test_support", ] }
diff --git a/src/trace_processor/shell/bundle_integrationtest.cc b/src/trace_processor/shell/bundle_integrationtest.cc index 9de6227..0f80006 100644 --- a/src/trace_processor/shell/bundle_integrationtest.cc +++ b/src/trace_processor/shell/bundle_integrationtest.cc
@@ -38,6 +38,7 @@ #include "protos/perfetto/trace/trace_packet.gen.h" #include "src/base/test/utils.h" #include "test/gtest_and_gmock.h" +#include "test/test_helper.h" #if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) #include <unistd.h> @@ -105,6 +106,8 @@ class TraceconvShellBundleTest : public ::testing::Test { protected: void SetUp() override { + base::UnsetEnv("DEBUGINFOD_URLS"); + base::UnsetEnv("LLVM_SYMBOLIZER_OPTS"); input_trace_ = base::GetTestDataPath( "test/data/heapprofd_standalone_client_example-trace"); output_path_ = output_file_.path(); @@ -129,6 +132,7 @@ return names; } + TestEnvCleaner env_{"DEBUGINFOD_URLS", "LLVM_SYMBOLIZER_OPTS"}; base::TempDir temp_dir_ = base::TempDir::Create(); base::TempFile output_file_ = base::TempFile::Create(); std::string input_trace_;
diff --git a/src/trace_processor/shell/bundle_subcommand.cc b/src/trace_processor/shell/bundle_subcommand.cc index 4d3dbe3..47a7478 100644 --- a/src/trace_processor/shell/bundle_subcommand.cc +++ b/src/trace_processor/shell/bundle_subcommand.cc
@@ -175,6 +175,8 @@ } trace_to_text::BundleContext context; + if (ctx.global) + context.debuginfod = ctx.global->debuginfod; if (!symbol_paths_.empty()) context.symbol_paths = base::SplitString(symbol_paths_, ","); for (const std::string& map : proguard_maps_) {
diff --git a/src/trace_processor/shell/common_flags.cc b/src/trace_processor/shell/common_flags.cc index 085817d..1752bd3 100644 --- a/src/trace_processor/shell/common_flags.cc +++ b/src/trace_processor/shell/common_flags.cc
@@ -131,6 +131,28 @@ "no-progress", '\0', "Disable live progress (summaries and warnings are still printed).", &opts->no_progress)); + flags.push_back(BoolFlag( + "debuginfod", '\0', + "Download missing native debug files by build ID (requires curl).", + &opts->debuginfod_options.enabled)); + flags.push_back( + {"debuginfod-urls", '\0', true, "URLS", + "Space-separated server URLs; overrides DEBUGINFOD_URLS.", + [opts](const char* value) { opts->debuginfod_options.urls = value; }}); + flags.push_back({"debuginfod-cache-path", '\0', true, "PATH", + "Cache directory; overrides DEBUGINFOD_CACHE_PATH.", + [opts](const char* value) { + opts->debuginfod_options.cache_path = value; + }}); + flags.push_back( + StringFlag("debuginfod-connect-timeout", '\0', "SECONDS", + "Connection timeout in positive whole seconds (default: 5).", + &opts->debuginfod_options.connect_timeout)); + flags.push_back(StringFlag( + "debuginfod-stall-timeout", '\0', "SECONDS", + "Abort transfers below 1 byte/second for this long (default: 10).", + &opts->debuginfod_options.stall_timeout)); + flags.push_back(BoolFlag("full-sort", '\0', "Forces full sort ignoring windowing.", &opts->force_full_sort)); @@ -442,7 +464,8 @@ TraceProcessorShell_PlatformInterface* platform, const std::string& trace_file, bool no_progress, - bool quiet) { + bool quiet, + const profiling::DebuginfodConfig& debuginfod) { base::TimeNanos t_load_start = base::GetWallTimeNs(); double size_mb = 0; base::ProgressReporter progress(!no_progress && !quiet); @@ -476,6 +499,8 @@ } profiling::SymbolizerConfig sym_config; + sym_config.debuginfod = debuginfod; + sym_config.progress = !no_progress && !quiet; const char* mode = getenv("PERFETTO_SYMBOLIZER_MODE"); std::vector<std::string> paths = profiling::GetPerfettoBinaryPath(); if (mode && std::string_view(mode) == "find") { @@ -484,7 +509,7 @@ sym_config.index_symbol_paths = std::move(paths); } if (!sym_config.index_symbol_paths.empty() || - !sym_config.find_symbol_paths.empty()) { + !sym_config.find_symbol_paths.empty() || !debuginfod.urls.empty()) { if (is_proto_trace) { tp->Flush(); auto sym_result = profiling::SymbolizeDatabaseAndLog( @@ -575,6 +600,7 @@ bool is_set; const char* flag; } incompatible[] = { + {!opts.debuginfod.urls.empty(), "--debuginfod"}, {opts.force_full_sort, "--full-sort"}, {opts.no_ftrace_raw, "--no-ftrace-raw"}, {opts.analyze_trace_proto_content, "--analyze-trace-proto-content"}, @@ -629,9 +655,10 @@ } ASSIGN_OR_RETURN(Config config, BuildConfig(opts, platform)); ASSIGN_OR_RETURN(auto tp, SetupTraceProcessor(opts, config, platform)); - ASSIGN_OR_RETURN(base::TimeNanos t_load, - LoadTraceFile(tp.get(), platform, trace_file, - opts.no_progress, opts.quiet)); + ASSIGN_OR_RETURN( + base::TimeNanos t_load, + LoadTraceFile(tp.get(), platform, trace_file, opts.no_progress, + opts.quiet, opts.debuginfod)); if (t_load_out) *t_load_out = t_load; return std::move(tp);
diff --git a/src/trace_processor/shell/common_flags.h b/src/trace_processor/shell/common_flags.h index c2e2116..b2ce611 100644 --- a/src/trace_processor/shell/common_flags.h +++ b/src/trace_processor/shell/common_flags.h
@@ -32,6 +32,7 @@ #include "perfetto/trace_processor/trace_processor.h" #include "src/trace_processor/shell/metrics.h" #include "src/trace_processor/shell/subcommand.h" +#include "src/trace_processor/util/symbolizer/debuginfod.h" namespace perfetto::trace_processor { class TraceProcessorShell_PlatformInterface; @@ -41,6 +42,8 @@ // Options shared across all subcommands (trace loading, metatrace, dev, etc.). struct GlobalOptions { + profiling::DebuginfodOptions debuginfod_options; + profiling::DebuginfodConfig debuginfod; std::string trace_file; // If non-empty, trace-consuming subcommands run against a remote warm session @@ -117,7 +120,8 @@ TraceProcessorShell_PlatformInterface* platform, const std::string& trace_file, bool no_progress = false, - bool quiet = false); + bool quiet = false, + const profiling::DebuginfodConfig& debuginfod = {}); // Resolves the trace-file positional argument for a trace-consuming subcommand, // accounting for --remote. In --remote mode the trace is already loaded
diff --git a/src/trace_processor/shell/convert_subcommand.cc b/src/trace_processor/shell/convert_subcommand.cc index 1fbf8f1..7f60e47 100644 --- a/src/trace_processor/shell/convert_subcommand.cc +++ b/src/trace_processor/shell/convert_subcommand.cc
@@ -199,7 +199,7 @@ } RETURN_IF_ERROR(trace_to_text::TraceToProfile( input, pid, timestamps, !no_annotations_, output_dir_, profile_type, - verbose_, no_progress, quiet)); + verbose_, no_progress, quiet, ctx.global->debuginfod)); } else { // firefox RETURN_IF_ERROR( trace_to_text::TraceToFirefoxProfile(input, output, no_progress));
diff --git a/src/trace_processor/shell/export_subcommand.cc b/src/trace_processor/shell/export_subcommand.cc index af99cc9..40c6876 100644 --- a/src/trace_processor/shell/export_subcommand.cc +++ b/src/trace_processor/shell/export_subcommand.cc
@@ -93,7 +93,8 @@ ASSIGN_OR_RETURN(auto tp, SetupTraceProcessor(*ctx.global, config, ctx.platform)); RETURN_IF_ERROR(LoadTraceFile(tp.get(), ctx.platform, trace_file, - ctx.global->no_progress, ctx.global->quiet) + ctx.global->no_progress, ctx.global->quiet, + ctx.global->debuginfod) .status()); TraceProcessor::ExportFormat export_format;
diff --git a/src/trace_processor/shell/server_subcommand.cc b/src/trace_processor/shell/server_subcommand.cc index 8ef1257..dce626f 100644 --- a/src/trace_processor/shell/server_subcommand.cc +++ b/src/trace_processor/shell/server_subcommand.cc
@@ -257,7 +257,8 @@ if (!trace_file.empty()) { ASSIGN_OR_RETURN(auto t_load, LoadTraceFile(tp.get(), ctx.platform, trace_file, - ctx.global->no_progress, ctx.global->quiet)); + ctx.global->no_progress, ctx.global->quiet, + ctx.global->debuginfod)); base::ignore_result(t_load); }
diff --git a/src/trace_processor/shell/util_subcommand.cc b/src/trace_processor/shell/util_subcommand.cc index 8cd85e8..e14e020 100644 --- a/src/trace_processor/shell/util_subcommand.cc +++ b/src/trace_processor/shell/util_subcommand.cc
@@ -232,7 +232,7 @@ if (util == "symbolize") { RETURN_IF_ERROR(trace_to_text::SymbolizeProfile( input, output, verbose_, ctx.global && ctx.global->no_progress, - ctx.global && ctx.global->quiet)); + ctx.global && ctx.global->quiet, ctx.global->debuginfod)); } else if (util == "deobfuscate") { RETURN_IF_ERROR(trace_to_text::DeobfuscateProfile(input, output)); } else if (util == "decompress_packets") {
diff --git a/src/trace_processor/trace_processor_shell.cc b/src/trace_processor/trace_processor_shell.cc index 80cd3ad..beba64d 100644 --- a/src/trace_processor/trace_processor_shell.cc +++ b/src/trace_processor/trace_processor_shell.cc
@@ -114,6 +114,7 @@ std::string query_file_path; bool quiet = false; + profiling::DebuginfodOptions debuginfod; std::string query_string; std::vector<std::string> sql_package_paths; std::vector<std::string> override_sql_package_paths; @@ -297,6 +298,13 @@ executing some other commands (-q, -Q, --run-metrics, --summary). +Symbolization: + --debuginfod Download missing debug files by build ID. + --debuginfod-urls URLS Space-separated HTTP(S) servers; + overrides DEBUGINFOD_URLS. + --debuginfod-cache-path PATH Overrides DEBUGINFOD_CACHE_PATH. + --debuginfod-connect-timeout SECONDS Connection timeout (default: 5). + --debuginfod-stall-timeout SECONDS Low-speed timeout (default: 10). Output: --quiet Suppress routine status messages, retaining results, warnings, and errors. @@ -527,6 +535,11 @@ OPT_HELP_CLASSIC, OPT_QUIET, + OPT_DEBUGINFOD, + OPT_DEBUGINFOD_URLS, + OPT_DEBUGINFOD_CACHE_PATH, + OPT_DEBUGINFOD_CONNECT_TIMEOUT, + OPT_DEBUGINFOD_STALL_TIMEOUT, }; constexpr char kShortOptions[] = "hvWiDdm:p:q:Q:e:"; @@ -535,6 +548,14 @@ {"help", no_argument, nullptr, 'h'}, {"help-classic", no_argument, nullptr, OPT_HELP_CLASSIC}, {"quiet", no_argument, nullptr, OPT_QUIET}, + {"debuginfod", no_argument, nullptr, OPT_DEBUGINFOD}, + {"debuginfod-urls", required_argument, nullptr, OPT_DEBUGINFOD_URLS}, + {"debuginfod-cache-path", required_argument, nullptr, + OPT_DEBUGINFOD_CACHE_PATH}, + {"debuginfod-connect-timeout", required_argument, nullptr, + OPT_DEBUGINFOD_CONNECT_TIMEOUT}, + {"debuginfod-stall-timeout", required_argument, nullptr, + OPT_DEBUGINFOD_STALL_TIMEOUT}, {"version", no_argument, nullptr, 'v'}, {"httpd", no_argument, nullptr, 'D'}, @@ -626,6 +647,27 @@ continue; } + if (option == OPT_DEBUGINFOD) { + command_line_options.debuginfod.enabled = true; + continue; + } + if (option == OPT_DEBUGINFOD_URLS) { + command_line_options.debuginfod.urls = optarg; + continue; + } + if (option == OPT_DEBUGINFOD_CACHE_PATH) { + command_line_options.debuginfod.cache_path = optarg; + continue; + } + if (option == OPT_DEBUGINFOD_CONNECT_TIMEOUT) { + command_line_options.debuginfod.connect_timeout = optarg; + continue; + } + if (option == OPT_DEBUGINFOD_STALL_TIMEOUT) { + command_line_options.debuginfod.stall_timeout = optarg; + continue; + } + if (option == 'Q') { command_line_options.query_string = optarg; continue; @@ -1031,6 +1073,11 @@ protos::pbzero::TRACE_PROCESSOR_CURRENT_API_VERSION); return base::OkStatus(); } + std::string warnings; + RETURN_IF_ERROR(profiling::ResolveDebuginfodOptions( + global.debuginfod_options, &global.debuginfod, &warnings)); + if (!warnings.empty()) + fprintf(stderr, "%s", warnings.c_str()); // Parse metric extensions and populate their descriptor pool. The // pool is always created (built-in metrics need it for output @@ -1064,6 +1111,20 @@ // Forward global flags. auto add_global_flags = [&]() { + if (options.debuginfod.enabled) + args.emplace_back("--debuginfod"); + if (options.debuginfod.urls) { + args.emplace_back("--debuginfod-urls"); + args.push_back(*options.debuginfod.urls); + } + if (options.debuginfod.cache_path) { + args.emplace_back("--debuginfod-cache-path"); + args.push_back(*options.debuginfod.cache_path); + } + args.emplace_back("--debuginfod-connect-timeout"); + args.push_back(options.debuginfod.connect_timeout); + args.emplace_back("--debuginfod-stall-timeout"); + args.push_back(options.debuginfod.stall_timeout); if (options.quiet) args.emplace_back("--quiet"); if (options.force_full_sort)
diff --git a/src/trace_processor/util/symbolizer/BUILD.gn b/src/trace_processor/util/symbolizer/BUILD.gn index b84a965..4269fb5 100644 --- a/src/trace_processor/util/symbolizer/BUILD.gn +++ b/src/trace_processor/util/symbolizer/BUILD.gn
@@ -24,6 +24,8 @@ "breakpad_parser.h", "breakpad_symbolizer.cc", "breakpad_symbolizer.h", + "debuginfod.cc", + "debuginfod.h", "elf.h", "filesystem.h", "filesystem_posix.cc",
diff --git a/src/trace_processor/util/symbolizer/debuginfod.cc b/src/trace_processor/util/symbolizer/debuginfod.cc new file mode 100644 index 0000000..72ed52d --- /dev/null +++ b/src/trace_processor/util/symbolizer/debuginfod.cc
@@ -0,0 +1,262 @@ +/* + * 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/util/symbolizer/debuginfod.h" + +#include <cstdlib> +#include <map> +#include <sstream> +#include <utility> + +#include "perfetto/base/build_config.h" +#include "perfetto/ext/base/atomic_file.h" +#include "perfetto/ext/base/file_utils.h" +#include "perfetto/ext/base/progress_reporter.h" +#include "perfetto/ext/base/string_utils.h" +#include "perfetto/ext/base/subprocess.h" +#include "src/trace_processor/util/symbolizer/local_symbolizer.h" + +namespace perfetto::profiling { +namespace { + +std::string EnvironmentValue(const char* name) { + const char* value = getenv(name); + return value ? value : ""; +} + +#if PERFETTO_BUILDFLAG(PERFETTO_LOCAL_SYMBOLIZER) +bool MakeCacheDirectory(const std::string& path) { + if (base::DirectoryExists(path)) + return true; + if (path.empty()) + return false; + std::string parent = base::Dirname(path); + if (parent != path && !MakeCacheDirectory(parent)) + return false; + return base::Mkdir(path, 0700) || base::DirectoryExists(path); +} + +class DebuginfodBinaryFinder : public BinaryFinder { + public: + DebuginfodBinaryFinder(const DebuginfodConfig& config, + bool progress, + DebuginfodStats* stats) + : config_(config), progress_(progress), stats_(stats) {} + + BinaryLookupResult FindBinary(const std::string&, + const std::string& build_id) override { + auto entry = results_.emplace(build_id, BinaryLookupResult{}); + if (!entry.second) + return entry.first->second; + auto& result = entry.first->second; + if (build_id.empty()) + return result; + const std::string hex = base::ToHex(build_id); + const std::string directory = config_.cache_path + "/" + hex; + const std::string path = directory + "/debuginfo"; + BinaryPathError error; + result.binary = FindBinaryFile(path, build_id, &error); + if (result.binary) { + ++stats_->cache_hits; + result.attempts.push_back({path, BinaryPathError::kOk}); + stats_->details += " " + hex + ": cache hit " + path + "\n"; + return result; + } + if (!MakeCacheDirectory(directory)) { + ++stats_->failures; + stats_->details += + " " + hex + ": cannot create cache directory " + directory + "\n"; + return result; + } + for (const auto& server : config_.urls) { + const std::string url = server + "/buildid/" + hex + "/debuginfo"; + base::AtomicFile output(path); + auto status = output.Open(); + if (!status.ok()) { + stats_->details += " " + hex + ": " + status.message() + "\n"; + break; + } + base::ProgressReporter progress(progress_); + progress.Update("Debuginfod: fetching build ID " + hex); + base::Subprocess curl; + // Disable curlrc and URL globbing. Keep proxy/TLS environment support, + // but let the CLI own output, protocols, and timeout policy. + curl.args.exec_cmd = {"curl", + "--disable", + "--globoff", + "--fail", + "--silent", + "--show-error", + "--location", + "--proto", + "=http,https", + "--proto-redir", + "=http,https", + "--connect-timeout", + std::to_string(config_.connect_timeout_seconds), + "--speed-limit", + "1", + "--speed-time", + std::to_string(config_.stall_timeout_seconds), + "--output", + output.temp_path(), + "--url", + url}; + curl.args.stdin_mode = base::Subprocess::InputMode::kDevNull; + curl.args.stdout_mode = base::Subprocess::OutputMode::kDevNull; + curl.args.stderr_mode = base::Subprocess::OutputMode::kBuffer; + bool ok = curl.Call(); + progress.Clear(); + if (!ok) { + if (curl.returncode() == 127 || curl.returncode() == 128) { + stats_->warnings = + "Cannot run curl; install curl and ensure it is " + "on PATH.\n"; + break; + } + stats_->details += " " + hex + ": curl exit " + + std::to_string(curl.returncode()) + " from " + + server + ": " + base::TrimWhitespace(curl.output()) + + "\n"; + continue; + } + result.binary = FindBinaryFile(output.temp_path(), build_id, &error); + if (!result.binary) { + result.attempts.push_back({server, error}); + stats_->details += + " " + hex + ": invalid debug file from " + server + "\n"; + continue; + } + status = std::move(output).Commit(); + if (!status.ok()) { + result.binary.reset(); + stats_->details += " " + hex + ": " + status.message() + "\n"; + break; + } + result.binary->file_name = path; + result.attempts.push_back({path, BinaryPathError::kOk}); + ++stats_->downloads; + stats_->details += + " " + hex + ": downloaded from " + server + " to " + path + "\n"; + return result; + } + ++stats_->failures; + return result; + } + + private: + DebuginfodConfig config_; + bool progress_; + DebuginfodStats* stats_; + std::map<std::string, BinaryLookupResult> results_; +}; +#endif + +} // namespace + +base::Status ResolveDebuginfodOptions(const DebuginfodOptions& options, + DebuginfodConfig* config, + std::string* warnings) { + if (!EnvironmentValue("LLVM_SYMBOLIZER_OPTS").empty()) + *warnings += + "LLVM_SYMBOLIZER_OPTS is ignored; Perfetto controls " + "llvm-symbolizer options.\n"; + const std::string env_urls = EnvironmentValue("DEBUGINFOD_URLS"); + if (!options.enabled) { + if (!env_urls.empty() || options.urls.has_value()) { + *warnings += options.urls ? "--debuginfod-urls is set but ignored; " + : "DEBUGINFOD_URLS is set but ignored; "; + *warnings += "pass --debuginfod to enable downloads.\n"; + } + return base::OkStatus(); + } +#if !PERFETTO_BUILDFLAG(PERFETTO_LOCAL_SYMBOLIZER) + return base::ErrStatus( + "this build does not support debuginfod symbolization"); +#endif + std::istringstream urls(options.urls.value_or(env_urls)); + for (std::string url; urls >> url;) { + while (!url.empty() && url.back() == '/') + url.pop_back(); + if ((!base::StartsWith(url, "https://") && + !base::StartsWith(url, "http://")) || + url.find_first_of("?#") != std::string::npos) + return base::ErrStatus("debuginfod URLs must be HTTP(S) server roots"); + config->urls.push_back(std::move(url)); + } + if (config->urls.empty()) + return base::ErrStatus( + "--debuginfod requires --debuginfod-urls or " + "DEBUGINFOD_URLS"); + auto connect = base::StringToUInt32(options.connect_timeout); + auto stall = base::StringToUInt32(options.stall_timeout); + if (!connect || !*connect || !stall || !*stall || + options.connect_timeout.find_first_not_of("0123456789") != + std::string::npos || + options.stall_timeout.find_first_not_of("0123456789") != + std::string::npos) + return base::ErrStatus( + "debuginfod timeouts must be positive whole seconds"); + config->connect_timeout_seconds = *connect; + config->stall_timeout_seconds = *stall; + config->cache_path = + options.cache_path.value_or(EnvironmentValue("DEBUGINFOD_CACHE_PATH")); + if (!options.cache_path && config->cache_path.empty()) { +#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) + std::string root = EnvironmentValue("LOCALAPPDATA"); + if (!root.empty()) + config->cache_path = root + "/debuginfod_client"; +#else + std::string root = EnvironmentValue("XDG_CACHE_HOME"); + if (root.empty()) { + root = EnvironmentValue("HOME"); + if (!root.empty()) + root += "/.cache"; + } + if (!root.empty()) + config->cache_path = root + "/debuginfod_client"; +#endif + } + if (config->cache_path.empty()) + return base::ErrStatus( + "cannot determine debuginfod cache directory; " + "pass --debuginfod-cache-path"); + return base::OkStatus(); +} + +std::unique_ptr<Symbolizer> CreateDebuginfodSymbolizer( + const DebuginfodConfig& config, + bool progress, + DebuginfodStats* stats) { +#if PERFETTO_BUILDFLAG(PERFETTO_LOCAL_SYMBOLIZER) + if (!CanRunLlvmSymbolizer()) { + stats->warnings = + "Cannot run llvm-symbolizer; install it and ensure it " + "is on PATH to use debuginfod.\n"; + return nullptr; + } + return std::make_unique<LocalSymbolizer>( + std::make_unique<DebuginfodBinaryFinder>(config, progress, stats), + /*use_kernel_paths=*/false); +#else + (void)config; + (void)progress; + (void)stats; + return nullptr; +#endif +} + +} // namespace perfetto::profiling
diff --git a/src/trace_processor/util/symbolizer/debuginfod.h b/src/trace_processor/util/symbolizer/debuginfod.h new file mode 100644 index 0000000..2c56d68 --- /dev/null +++ b/src/trace_processor/util/symbolizer/debuginfod.h
@@ -0,0 +1,67 @@ +/* + * 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. + */ + +#ifndef SRC_TRACE_PROCESSOR_UTIL_SYMBOLIZER_DEBUGINFOD_H_ +#define SRC_TRACE_PROCESSOR_UTIL_SYMBOLIZER_DEBUGINFOD_H_ + +#include <cstdint> +#include <memory> +#include <optional> +#include <string> +#include <vector> + +#include "perfetto/base/status.h" +#include "src/trace_processor/util/symbolizer/symbolizer.h" + +namespace perfetto::profiling { + +// Unset values use the corresponding environment variable. Explicit empty +// values override the environment too. Merely configuring a URL never opts in. +struct DebuginfodOptions { + bool enabled = false; + std::optional<std::string> urls; + std::optional<std::string> cache_path; + std::string connect_timeout = "5"; + std::string stall_timeout = "10"; +}; + +struct DebuginfodConfig { + std::vector<std::string> urls; + std::string cache_path; + uint32_t connect_timeout_seconds = 5; + uint32_t stall_timeout_seconds = 10; +}; + +// Resolves environment defaults and validates configuration. Warnings are +// returned separately so callers can print them even in quiet mode. +base::Status ResolveDebuginfodOptions(const DebuginfodOptions&, + DebuginfodConfig*, + std::string* warnings); + +struct DebuginfodStats { + uint32_t cache_hits = 0; + uint32_t downloads = 0; + uint32_t failures = 0; + std::string details; + std::string warnings; +}; + +std::unique_ptr<Symbolizer> CreateDebuginfodSymbolizer(const DebuginfodConfig&, + bool progress, + DebuginfodStats*); + +} // namespace perfetto::profiling +#endif // SRC_TRACE_PROCESSOR_UTIL_SYMBOLIZER_DEBUGINFOD_H_
diff --git a/src/trace_processor/util/symbolizer/local_symbolizer.cc b/src/trace_processor/util/symbolizer/local_symbolizer.cc index cb1ef3e..2f4bc08 100644 --- a/src/trace_processor/util/symbolizer/local_symbolizer.cc +++ b/src/trace_processor/util/symbolizer/local_symbolizer.cc
@@ -900,6 +900,12 @@ }); } +std::optional<FoundBinary> FindBinaryFile(const std::string& path, + const std::string& build_id, + BinaryPathError* error) { + return IsCorrectFile(path, build_id, error); +} + BinaryFinder::~BinaryFinder() = default; LocalBinaryIndexer::LocalBinaryIndexer( @@ -964,13 +970,37 @@ LocalBinaryFinder::~LocalBinaryFinder() = default; +bool CanRunLlvmSymbolizer() { +#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) + std::vector<std::string> args = {"--version"}; +#else + std::vector<std::string> args = {"llvm-symbolizer", "--version"}; +#endif + // Use the same filtered child environment as the actual symbolizer. + Subprocess process(kDefaultSymbolizer, std::move(args), + {"DEBUGINFOD_URLS", "LLVM_SYMBOLIZER_OPTS"}); + std::string version; + char buffer[1024]; + for (;;) { + int64_t size = process.Read(buffer, sizeof(buffer)); + if (size <= 0) + break; + version.append(buffer, static_cast<size_t>(size)); + } + return version.find("LLVM") != std::string::npos; +} + LLVMSymbolizerProcess::LLVMSymbolizerProcess(const std::string& symbolizer_path) : #if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) - subprocess_(symbolizer_path, {"--output-style=JSON"}) { + subprocess_(symbolizer_path, + {"--output-style=JSON"}, + {"DEBUGINFOD_URLS", "LLVM_SYMBOLIZER_OPTS"}) { } #else - subprocess_(symbolizer_path, {"llvm-symbolizer", "--output-style=JSON"}) { + subprocess_(symbolizer_path, + {"llvm-symbolizer", "--output-style=JSON"}, + {"DEBUGINFOD_URLS", "LLVM_SYMBOLIZER_OPTS"}) { } #endif @@ -1081,10 +1111,9 @@ bool is_kernel = base::StartsWith(mapping.name, "[kernel.kallsyms]"); std::optional<FoundBinary> binary; std::vector<BinaryPathAttempt> binary_attempts; - if (is_kernel) { - if (env.os_release) { + if (is_kernel && use_kernel_paths_) { + if (env.os_release) binary = FindKernelBinary(*env.os_release, binary_attempts); - } } else { BinaryLookupResult lookup = finder_->FindBinary(mapping.name, mapping.build_id); @@ -1111,6 +1140,7 @@ } SymbolizeResult result; + result.attempts = std::move(attempts); result.frames.reserve(addresses.size()); for (uint64_t address : addresses) { result.frames.emplace_back(llvm_symbolizer_.Symbolize( @@ -1120,11 +1150,16 @@ } LocalSymbolizer::LocalSymbolizer(const std::string& symbolizer_path, - std::unique_ptr<BinaryFinder> finder) - : llvm_symbolizer_(symbolizer_path), finder_(std::move(finder)) {} + std::unique_ptr<BinaryFinder> finder, + bool use_kernel_paths) + : llvm_symbolizer_(symbolizer_path), + finder_(std::move(finder)), + use_kernel_paths_(use_kernel_paths) {} -LocalSymbolizer::LocalSymbolizer(std::unique_ptr<BinaryFinder> finder) - : LocalSymbolizer(kDefaultSymbolizer, std::move(finder)) {} +LocalSymbolizer::LocalSymbolizer(std::unique_ptr<BinaryFinder> finder, + bool use_kernel_paths) + : LocalSymbolizer(kDefaultSymbolizer, std::move(finder), use_kernel_paths) { +} LocalSymbolizer::~LocalSymbolizer() = default;
diff --git a/src/trace_processor/util/symbolizer/local_symbolizer.h b/src/trace_processor/util/symbolizer/local_symbolizer.h index 377e1fd..f72a7a3 100644 --- a/src/trace_processor/util/symbolizer/local_symbolizer.h +++ b/src/trace_processor/util/symbolizer/local_symbolizer.h
@@ -82,6 +82,10 @@ bool ok() const { return binary.has_value(); } }; +std::optional<FoundBinary> FindBinaryFile(const std::string& path, + const std::string& build_id, + BinaryPathError* error); + class BinaryFinder { public: virtual ~BinaryFinder(); @@ -118,6 +122,8 @@ std::map<std::string, BinaryLookupResult> cache_; }; +bool CanRunLlvmSymbolizer(); + class LLVMSymbolizerProcess { public: explicit LLVMSymbolizerProcess(const std::string& symbolizer_path); @@ -132,9 +138,11 @@ class LocalSymbolizer : public Symbolizer { public: LocalSymbolizer(const std::string& symbolizer_path, - std::unique_ptr<BinaryFinder> finder); + std::unique_ptr<BinaryFinder> finder, + bool use_kernel_paths = true); - explicit LocalSymbolizer(std::unique_ptr<BinaryFinder> finder); + explicit LocalSymbolizer(std::unique_ptr<BinaryFinder> finder, + bool use_kernel_paths = true); SymbolizeResult Symbolize(const Environment& env, const UnsymbolizedMapping& mapping, @@ -145,6 +153,8 @@ private: LLVMSymbolizerProcess llvm_symbolizer_; std::unique_ptr<BinaryFinder> finder_; + // Remote lookup uses build IDs instead of searching host kernel paths. + bool use_kernel_paths_; }; std::unique_ptr<Symbolizer> MaybeLocalSymbolizer(
diff --git a/src/trace_processor/util/symbolizer/subprocess.h b/src/trace_processor/util/symbolizer/subprocess.h index 0590ff7..7109548 100644 --- a/src/trace_processor/util/symbolizer/subprocess.h +++ b/src/trace_processor/util/symbolizer/subprocess.h
@@ -35,7 +35,9 @@ class Subprocess { public: - Subprocess(const std::string& file, std::vector<std::string> args); + Subprocess(const std::string& file, + std::vector<std::string> args, + const std::vector<std::string>& excluded_env = {}); ~Subprocess(); int64_t Write(const char* buffer, size_t size);
diff --git a/src/trace_processor/util/symbolizer/subprocess_posix.cc b/src/trace_processor/util/symbolizer/subprocess_posix.cc index 1e04e4d..2838cf7 100644 --- a/src/trace_processor/util/symbolizer/subprocess_posix.cc +++ b/src/trace_processor/util/symbolizer/subprocess_posix.cc
@@ -22,13 +22,19 @@ #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> +#include <algorithm> +#include <cstring> #include "perfetto/ext/base/utils.h" +extern "C" char** environ; + namespace perfetto { namespace profiling { -Subprocess::Subprocess(const std::string& file, std::vector<std::string> args) +Subprocess::Subprocess(const std::string& file, + std::vector<std::string> args, + const std::vector<std::string>& excluded_env) : input_pipe_(base::Pipe::Create(base::Pipe::kBothBlock)), output_pipe_(base::Pipe::Create(base::Pipe::kBothBlock)) { std::vector<char*> c_str_args; @@ -37,7 +43,19 @@ c_str_args.push_back(&(arg[0])); c_str_args.push_back(nullptr); + // Build the environment before fork; the child only redirects pointers. + std::vector<char*> environment; + for (char** entry = environ; *entry; ++entry) { + auto excluded = [entry](const std::string& name) { + return strncmp(*entry, name.c_str(), name.size()) == 0 && + (*entry)[name.size()] == '='; + }; + if (std::none_of(excluded_env.begin(), excluded_env.end(), excluded)) + environment.push_back(*entry); + } + environment.push_back(nullptr); if ((pid_ = fork()) == 0) { + environ = environment.data(); // Child PERFETTO_CHECK(dup2(*input_pipe_.rd, STDIN_FILENO) != -1); PERFETTO_CHECK(dup2(*output_pipe_.wr, STDOUT_FILENO) != -1);
diff --git a/src/trace_processor/util/symbolizer/subprocess_windows.cc b/src/trace_processor/util/symbolizer/subprocess_windows.cc index 3e3a3c4..08b89a6 100644 --- a/src/trace_processor/util/symbolizer/subprocess_windows.cc +++ b/src/trace_processor/util/symbolizer/subprocess_windows.cc
@@ -19,6 +19,7 @@ #if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) +#include <algorithm> #include <sstream> #include <string> @@ -29,7 +30,9 @@ namespace perfetto { namespace profiling { -Subprocess::Subprocess(const std::string& file, std::vector<std::string> args) { +Subprocess::Subprocess(const std::string& file, + std::vector<std::string> args, + const std::vector<std::string>& excluded_env) { std::stringstream cmd; cmd << file; for (auto arg : args) { @@ -66,14 +69,33 @@ start_info.hStdInput = child_pipe_in_read_; start_info.dwFlags |= STARTF_USESTDHANDLES; + std::vector<char> environment; + LPCH inherited = GetEnvironmentStringsA(); + if (!inherited) { + PERFETTO_ELOG("Failed to read process environment"); + return; + } + for (const char* entry = inherited; *entry; entry += strlen(entry) + 1) { + auto excluded = [entry](const std::string& name) { + return _strnicmp(entry, name.c_str(), name.size()) == 0 && + entry[name.size()] == '='; + }; + if (std::none_of(excluded_env.begin(), excluded_env.end(), excluded)) + environment.insert(environment.end(), entry, entry + strlen(entry) + 1); + } + FreeEnvironmentStringsA(inherited); + environment.push_back('\0'); + if (environment.size() == 1) + environment.push_back('\0'); + // Create the child process. success = CreateProcessA(nullptr, &(cmd.str()[0]), // command line nullptr, // process security attributes - nullptr, // primary thread security attributes - TRUE, // handles are inherited - 0, // creation flags - nullptr, // use parent's environment + nullptr, // primary thread security attributes + TRUE, // handles are inherited + 0, // creation flags + environment.data(), // filtered environment nullptr, // use parent's current directory &start_info, // STARTUPINFO pointer &proc_info); // receives PROCESS_INFORMATION
diff --git a/src/trace_processor/util/symbolizer/symbolize_database.cc b/src/trace_processor/util/symbolizer/symbolize_database.cc index da692a8..352298a 100644 --- a/src/trace_processor/util/symbolizer/symbolize_database.cc +++ b/src/trace_processor/util/symbolizer/symbolize_database.cc
@@ -497,7 +497,8 @@ bool has_any_paths = !config.index_symbol_paths.empty() || !config.symbol_files.empty() || - !config.find_symbol_paths.empty() || !config.breakpad_paths.empty(); + !config.find_symbol_paths.empty() || !config.breakpad_paths.empty() || + !config.debuginfod.urls.empty(); if (!has_any_paths) { result.error = SymbolizerError::kSymbolizerNotAvailable; result.error_details = @@ -519,6 +520,18 @@ SymbolizePendingAddresses(groups, env, &symbolizer, &mappings, &result.symbols); } + bool unresolved = false; + for (const auto& mapping : mappings) { + for (const auto& address : mapping.second.addresses) + unresolved |= !address.second.resolved; + } + if (unresolved && !config.debuginfod.urls.empty()) { + auto symbolizer = CreateDebuginfodSymbolizer( + config.debuginfod, config.progress, &result.debuginfod); + if (symbolizer) + SymbolizePendingAddresses(groups, env, symbolizer.get(), &mappings, + &result.symbols); + } CollectResults(mappings, &result); result.error = SymbolizerError::kOk; @@ -569,6 +582,16 @@ " could not be symbolized"); } summary += ".\n"; + const auto& downloads = result.debuginfod; + summary += downloads.warnings; + if (downloads.cache_hits || downloads.downloads || downloads.failures) { + summary += "Debuginfod: " + std::to_string(downloads.cache_hits) + + " cache hits, " + std::to_string(downloads.downloads) + + " downloaded, " + std::to_string(downloads.failures) + + " unavailable.\n"; + if (verbose) + summary += downloads.details; + } if (!result.error_details.empty()) summary += result.error_details + "\n";
diff --git a/src/trace_processor/util/symbolizer/symbolize_database.h b/src/trace_processor/util/symbolizer/symbolize_database.h index 02299a9..2e2f985 100644 --- a/src/trace_processor/util/symbolizer/symbolize_database.h +++ b/src/trace_processor/util/symbolizer/symbolize_database.h
@@ -22,6 +22,7 @@ #include <utility> #include <vector> +#include "src/trace_processor/util/symbolizer/debuginfod.h" #include "src/trace_processor/util/symbolizer/symbolizer.h" namespace perfetto::trace_processor { @@ -40,6 +41,8 @@ // Configuration for symbolization. struct SymbolizerConfig { + DebuginfodConfig debuginfod; + bool progress = false; // Directories to search using "index" mode (builds an index by build ID). // Faster for repeated lookups. std::vector<std::string> index_symbol_paths; @@ -94,6 +97,7 @@ // Result of symbolization operation. struct SymbolizerResult { + DebuginfodStats debuginfod; SymbolizerError error = SymbolizerError::kOk; // Machine-readable details about the error (e.g., missing path).
diff --git a/src/trace_processor/util/symbolizer/symbolize_database_unittest.cc b/src/trace_processor/util/symbolizer/symbolize_database_unittest.cc index 6763650..b2d08e4 100644 --- a/src/trace_processor/util/symbolizer/symbolize_database_unittest.cc +++ b/src/trace_processor/util/symbolizer/symbolize_database_unittest.cc
@@ -22,6 +22,7 @@ #include <string> #include <vector> +#include "perfetto/base/build_config.h" #include "perfetto/ext/base/file_utils.h" #include "perfetto/ext/base/string_utils.h" #include "perfetto/ext/base/temp_file.h" @@ -98,6 +99,63 @@ static_cast<ssize_t>(contents.size())); } +#if PERFETTO_BUILDFLAG(PERFETTO_LOCAL_SYMBOLIZER) +TEST(DebuginfodOptionsTest, ExplicitValuesAndWhitespace) { + DebuginfodOptions options; + options.enabled = true; + options.urls = " https://one.example/\t\nhttp://two.example/debug/ "; + options.cache_path = "/chosen/cache"; + options.connect_timeout = "2"; + options.stall_timeout = "3"; + DebuginfodConfig config; + std::string warnings; + ASSERT_TRUE(ResolveDebuginfodOptions(options, &config, &warnings).ok()); + EXPECT_THAT(config.urls, testing::ElementsAre("https://one.example", + "http://two.example/debug")); + EXPECT_EQ(config.cache_path, "/chosen/cache"); + EXPECT_EQ(config.connect_timeout_seconds, 2u); + EXPECT_EQ(config.stall_timeout_seconds, 3u); +} + +#endif + +TEST(DebuginfodOptionsTest, ConfigurationDoesNotEnableDownloads) { + DebuginfodOptions options; + options.urls = "https://one.example"; + options.cache_path = "/chosen/cache"; + DebuginfodConfig config; + std::string warnings; + ASSERT_TRUE(ResolveDebuginfodOptions(options, &config, &warnings).ok()); + EXPECT_TRUE(config.urls.empty()); + EXPECT_THAT(warnings, testing::HasSubstr("--debuginfod")); +} + +TEST(DebuginfodOptionsTest, RejectsEmptyOverridesAndInvalidValues) { + DebuginfodOptions options; + options.enabled = true; + options.urls = ""; + options.cache_path = "/chosen/cache"; + DebuginfodConfig config; + std::string warnings; + EXPECT_FALSE(ResolveDebuginfodOptions(options, &config, &warnings).ok()); + for (const char* url : + {"file:///tmp/debug", "https://host?q=x", "https://host#fragment"}) { + config = {}; + options.urls = url; + EXPECT_FALSE(ResolveDebuginfodOptions(options, &config, &warnings).ok()); + } + options.urls = "https://one.example"; + for (const char* timeout : {"0", "-1", "1.5", "bad", "4294967296"}) { + config = {}; + options.connect_timeout = timeout; + EXPECT_FALSE(ResolveDebuginfodOptions(options, &config, &warnings).ok()); + } + config = {}; + options.connect_timeout = "5"; + options.cache_path = ""; + EXPECT_FALSE(ResolveDebuginfodOptions(options, &config, &warnings).ok()); +} + TEST(SymbolizeDatabaseTest, CoalescesEquivalentMappingsAndAddresses) { protos::gen::Trace trace; AddProfile(&trace, 1, 0x100000, 0, {0x10, 0x20});
diff --git a/src/trace_processor/util/trace_enrichment/trace_enrichment.cc b/src/trace_processor/util/trace_enrichment/trace_enrichment.cc index 3e017a5..129413c 100644 --- a/src/trace_processor/util/trace_enrichment/trace_enrichment.cc +++ b/src/trace_processor/util/trace_enrichment/trace_enrichment.cc
@@ -184,6 +184,8 @@ // === Native Symbolization === { profiling::SymbolizerConfig sym_config; + sym_config.debuginfod = config.debuginfod; + sym_config.progress = config.progress; // Start with explicit paths from config. sym_config.index_symbol_paths = config.symbol_paths;
diff --git a/src/trace_processor/util/trace_enrichment/trace_enrichment.h b/src/trace_processor/util/trace_enrichment/trace_enrichment.h index 6633386..8f65ee3 100644 --- a/src/trace_processor/util/trace_enrichment/trace_enrichment.h +++ b/src/trace_processor/util/trace_enrichment/trace_enrichment.h
@@ -19,6 +19,7 @@ #include <string> #include <vector> +#include "src/trace_processor/util/symbolizer/debuginfod.h" namespace perfetto::trace_processor { class TraceProcessor; @@ -30,6 +31,8 @@ // Users should provide explicit paths or set environment variables. // If auto-discovery is enabled, well-known locations are also searched. struct EnrichmentConfig { + bool progress = false; + profiling::DebuginfodConfig debuginfod; // Explicit paths to search for native symbols (highest priority). // These paths are also searched for breakpad symbol files. std::vector<std::string> symbol_paths;
diff --git a/src/traceconv/symbolize_profile.cc b/src/traceconv/symbolize_profile.cc index 6a1d7b0..4f1239a 100644 --- a/src/traceconv/symbolize_profile.cc +++ b/src/traceconv/symbolize_profile.cc
@@ -35,8 +35,11 @@ std::ostream* output, bool verbose, bool no_progress, - bool quiet) { + bool quiet, + const profiling::DebuginfodConfig& debuginfod) { profiling::SymbolizerConfig sym_config; + sym_config.debuginfod = debuginfod; + sym_config.progress = !no_progress && !quiet; const char* breakpad_dir = getenv("BREAKPAD_SYMBOL_DIR"); if (breakpad_dir != nullptr) { @@ -53,12 +56,13 @@ if (sym_config.index_symbol_paths.empty() && sym_config.find_symbol_paths.empty() && - sym_config.breakpad_paths.empty()) { + sym_config.breakpad_paths.empty() && debuginfod.urls.empty()) { return base::ErrStatus( "no symbol paths configured: set the PERFETTO_BINARY_PATH " "environment variable to a colon-separated list of directories " "containing the unstripped binaries (or BREAKPAD_SYMBOL_DIR for " - "Breakpad symbol files) and try again"); + "Breakpad symbol files), or pass --debuginfod with " + "--debuginfod-urls to download debug files by build ID"); } trace_processor::Config config;
diff --git a/src/traceconv/symbolize_profile.h b/src/traceconv/symbolize_profile.h index 9987bb5..cec2f3f 100644 --- a/src/traceconv/symbolize_profile.h +++ b/src/traceconv/symbolize_profile.h
@@ -20,15 +20,18 @@ #include <iostream> #include "perfetto/base/status.h" +#include "src/trace_processor/util/symbolizer/debuginfod.h" namespace perfetto { namespace trace_to_text { -base::Status SymbolizeProfile(std::istream* input, - std::ostream* output, - bool verbose, - bool no_progress = false, - bool quiet = false); +base::Status SymbolizeProfile( + std::istream* input, + std::ostream* output, + bool verbose, + bool no_progress = false, + bool quiet = false, + const profiling::DebuginfodConfig& debuginfod = {}); } // namespace trace_to_text } // namespace perfetto
diff --git a/src/traceconv/trace_to_bundle.cc b/src/traceconv/trace_to_bundle.cc index d1f97cb..74146c2 100644 --- a/src/traceconv/trace_to_bundle.cc +++ b/src/traceconv/trace_to_bundle.cc
@@ -93,6 +93,8 @@ // Build enrichment configuration from context. trace_processor::util::EnrichmentConfig enrich_config; + enrich_config.debuginfod = context.debuginfod; + enrich_config.progress = !context.no_progress && !context.quiet; enrich_config.symbol_paths = context.symbol_paths; enrich_config.no_auto_symbol_paths = context.no_auto_symbol_paths; enrich_config.no_auto_proguard_maps = context.no_auto_proguard_maps;
diff --git a/src/traceconv/trace_to_bundle.h b/src/traceconv/trace_to_bundle.h index c21fdce..6aac2b8 100644 --- a/src/traceconv/trace_to_bundle.h +++ b/src/traceconv/trace_to_bundle.h
@@ -19,6 +19,7 @@ #include <string> #include <vector> +#include "src/trace_processor/util/symbolizer/debuginfod.h" #include "perfetto/base/status.h" @@ -32,6 +33,7 @@ // Context structure for bundle configuration struct BundleContext { + profiling::DebuginfodConfig debuginfod; // Additional paths to search for symbols (beyond automatic discovery) std::vector<std::string> symbol_paths;
diff --git a/src/traceconv/trace_to_profile.cc b/src/traceconv/trace_to_profile.cc index c5cbdcf..59cd01a 100644 --- a/src/traceconv/trace_to_profile.cc +++ b/src/traceconv/trace_to_profile.cc
@@ -59,11 +59,15 @@ void MaybeSymbolize(trace_processor::TraceProcessor* tp, bool verbose, - bool quiet) { + bool quiet, + bool no_progress, + const profiling::DebuginfodConfig& debuginfod) { profiling::SymbolizerConfig sym_config; + sym_config.debuginfod = debuginfod; + sym_config.progress = !no_progress && !quiet; const char* mode = getenv("PERFETTO_SYMBOLIZER_MODE"); std::vector<std::string> paths = profiling::GetPerfettoBinaryPath(); - if (paths.empty()) { + if (paths.empty() && debuginfod.urls.empty()) { return; } if (mode && std::string_view(mode) == "find") { @@ -175,7 +179,8 @@ std::optional<ConversionMode> explicit_mode, bool verbose, bool no_progress, - bool quiet) { + bool quiet, + const profiling::DebuginfodConfig& debuginfod) { // Pre-parse trace. trace_processor::Config config; std::unique_ptr<trace_processor::TraceProcessor> tp = @@ -222,7 +227,7 @@ } // Add symbolisation and deobfuscation packets. - MaybeSymbolize(tp.get(), verbose, quiet); + MaybeSymbolize(tp.get(), verbose, quiet, no_progress, debuginfod); MaybeDeobfuscate(tp.get()); if (auto status = tp->NotifyEndOfFile(); !status.ok()) { return base::ErrStatus("failed to finalize trace: %s", status.c_message());
diff --git a/src/traceconv/trace_to_profile.h b/src/traceconv/trace_to_profile.h index 9b728e8..fa356dc 100644 --- a/src/traceconv/trace_to_profile.h +++ b/src/traceconv/trace_to_profile.h
@@ -25,6 +25,7 @@ #include "perfetto/base/status.h" #include "perfetto/profiling/pprof_builder.h" +#include "src/trace_processor/util/symbolizer/debuginfod.h" namespace perfetto { namespace trace_to_text { @@ -37,7 +38,8 @@ std::optional<ConversionMode> conversion_mode, bool verbose, bool no_progress = false, - bool quiet = false); + bool quiet = false, + const profiling::DebuginfodConfig& debuginfod = {}); } // namespace trace_to_text } // namespace perfetto
diff --git a/src/traceconv/traceconv.cc b/src/traceconv/traceconv.cc index 675e74a..1e2b5f3 100644 --- a/src/traceconv/traceconv.cc +++ b/src/traceconv/traceconv.cc
@@ -122,6 +122,12 @@ discovery (e.g. Gradle project layout) --quiet, -q Suppress routine status messages --no-progress Disable live progress + --debuginfod Download missing debug files by build ID + --debuginfod-urls URLS Space-separated servers; overrides + DEBUGINFOD_URLS (requires --debuginfod) + --debuginfod-cache-path PATH Overrides DEBUGINFOD_CACHE_PATH + --debuginfod-connect-timeout SEC Connection timeout (default: 5 seconds) + --debuginfod-stall-timeout SEC Low-speed timeout (default: 10 seconds) --verbose Print more detailed output binary Converts text proto to binary format @@ -185,6 +191,8 @@ bool verbose = false; bool no_progress = false; bool quiet = false; + profiling::DebuginfodOptions debuginfod_options; + profiling::DebuginfodConfig debuginfod; bool skip_unknown_fields = false; std::string output_dir; for (int i = 1; i < argc; i++) { @@ -226,6 +234,25 @@ } else if (i < argc && strcmp(argv[i], "--symbol-paths") == 0) { i++; symbol_paths = base::SplitString(argv[i], ","); + } else if (strcmp(argv[i], "--debuginfod") == 0) { + debuginfod_options.enabled = true; + } else if (strcmp(argv[i], "--debuginfod-urls") == 0 || + strcmp(argv[i], "--debuginfod-cache-path") == 0 || + strcmp(argv[i], "--debuginfod-connect-timeout") == 0 || + strcmp(argv[i], "--debuginfod-stall-timeout") == 0) { + std::string flag = argv[i]; + if (++i >= argc) { + PERFETTO_ELOG("%s requires an argument", flag.c_str()); + return 1; + } + if (flag == "--debuginfod-urls") + debuginfod_options.urls = argv[i]; + else if (flag == "--debuginfod-cache-path") + debuginfod_options.cache_path = argv[i]; + else if (flag == "--debuginfod-connect-timeout") + debuginfod_options.connect_timeout = argv[i]; + else + debuginfod_options.stall_timeout = argv[i]; } else if (strcmp(argv[i], "--quiet") == 0 || strcmp(argv[i], "-q") == 0) { quiet = true; } else if (strcmp(argv[i], "--no-progress") == 0) { @@ -269,6 +296,16 @@ if (positional_args.empty()) return Usage(argv[0]); + std::string warnings; + auto debuginfod_status = profiling::ResolveDebuginfodOptions( + debuginfod_options, &debuginfod, &warnings); + if (!warnings.empty()) + fprintf(stderr, "%s", warnings.c_str()); + if (!debuginfod_status.ok()) { + PERFETTO_ELOG("%s", debuginfod_status.c_message()); + return 1; + } + std::istream* input_stream; std::ifstream file_istream; if (positional_args.size() > 1) { @@ -386,7 +423,7 @@ } return ToExitCode(trace_to_text::TraceToProfile( input_stream, pid, timestamps, !profile_no_annotations, output_dir, - profile_type, verbose, no_progress, quiet)); + profile_type, verbose, no_progress, quiet, debuginfod)); } if (format == "java_heap_profile") { @@ -394,12 +431,12 @@ return ToExitCode(trace_to_text::TraceToProfile( input_stream, pid, timestamps, !profile_no_annotations, output_dir, trace_to_text::ConversionMode::kJavaHeapProfile, verbose, no_progress, - quiet)); + quiet, debuginfod)); } if (format == "symbolize") return ToExitCode(trace_to_text::SymbolizeProfile( - input_stream, output_stream, verbose, no_progress, quiet)); + input_stream, output_stream, verbose, no_progress, quiet, debuginfod)); if (format == "deobfuscate") return ToExitCode( @@ -442,6 +479,7 @@ } trace_to_text::BundleContext context; + context.debuginfod = debuginfod; context.symbol_paths = symbol_paths; context.proguard_maps = std::move(proguard_maps); context.no_auto_symbol_paths = no_auto_symbol_paths;
diff --git a/test/debuginfod_integrationtest.py b/test/debuginfod_integrationtest.py new file mode 100644 index 0000000..2f84a9f --- /dev/null +++ b/test/debuginfod_integrationtest.py
@@ -0,0 +1,297 @@ +#!/usr/bin/env python3 +# 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. +"""Local HTTP integration tests. Run with --shell PATH_TO_TRACE_PROCESSOR_SHELL. + +Requires curl, llvm-symbolizer, and the downloaded test_symbolizer_binary +fixture. No public server is contacted. +""" + +import argparse +import http.server +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import threading +import time +import unittest + +ROOT = Path(__file__).resolve().parents[1] +BUILD_ID = 'f7558cfad3e9e2ff6cafcb0fd8442a322210ba6e' +SHELL = None + + +class DebuginfodTest(unittest.TestCase): + + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.cache = self.root / 'cache' + self.requests = [] + binary = (ROOT / 'test/data/test_symbolizer_binary').read_bytes() + requests = self.requests + + class Handler(http.server.BaseHTTPRequestHandler): + + def log_message(self, *_): + pass + + def do_GET(self): + requests.append(self.path) + if self.path.startswith('/missing/'): + self.send_error(404) + return + if self.path.startswith('/slow/'): + time.sleep(2) + if self.path.startswith('/redirect/'): + self.send_response(302) + self.send_header('Location', self.path.replace('/redirect/', '/ok/')) + self.end_headers() + return + data = b'not an ELF file' if self.path.startswith('/bad/') else binary + if self.path.startswith('/wrong/'): + data = binary.replace(bytes.fromhex(BUILD_ID), b'\x00' * 20) + self.send_response(200) + self.send_header('Content-Length', str(len(data))) + self.end_headers() + try: + self.wfile.write(data) + except (BrokenPipeError, ConnectionResetError): + pass + + self.server = http.server.ThreadingHTTPServer(('127.0.0.1', 0), Handler) + self.addCleanup(self.server.server_close) + self.addCleanup(self.server.shutdown) + thread = threading.Thread(target=self.server.serve_forever, daemon=True) + thread.start() + self.url = 'http://127.0.0.1:%d' % self.server.server_port + self.env = dict(os.environ) + for name in ('DEBUGINFOD_URLS', 'DEBUGINFOD_CACHE_PATH', + 'LLVM_SYMBOLIZER_OPTS', 'PERFETTO_BINARY_PATH', + 'BREAKPAD_SYMBOL_DIR'): + self.env.pop(name, None) + self.env['NO_PROXY'] = '127.0.0.1' + self.env['no_proxy'] = '127.0.0.1' + build_id = ''.join('\\%03o' % byte for byte in bytes.fromhex(BUILD_ID)) + source = self.root / 'trace.textproto' + source.write_text('''packet { clock_snapshot { + clocks { clock_id: 3 timestamp: 0 } + clocks { clock_id: 5 timestamp: 0 } + clocks { clock_id: 6 timestamp: 0 } + } } + packet { + trusted_packet_sequence_id: 1 + incremental_state_cleared: true + thread_descriptor { pid: 1 tid: 1 } + interned_data { + build_ids { iid: 1 str: "%s" } + mapping_paths { iid: 1 str: "remote-only" } + mappings { iid: 1 build_id: 1 path_string_ids: 1 + start: 1048576 end: 1114112 load_bias: 0 exact_offset: 4096 } + frames { iid: 1 mapping_id: 1 rel_pc: 4400 } + callstacks { iid: 1 frame_ids: 1 } + } + } + packet { + trusted_packet_sequence_id: 1 + streaming_profile_packet { callstack_iid: 1 timestamp_delta_us: 1 } + }''' % build_id) + self.trace = self.root / 'trace.pftrace' + self.run_shell('util', 'text_to_binary', str(source), str(self.trace)) + + def run_shell(self, *args, success=True): + result = subprocess.run([SHELL, *args], + env=self.env, + capture_output=True, + timeout=15) + if success: + self.assertEqual(result.returncode, 0, result.stderr.decode()) + else: + self.assertNotEqual(result.returncode, 0) + return result + + def bundle(self, *args): + return self.run_shell('bundle', '--no-auto-symbol-paths', + '--no-auto-proguard-maps', '--debuginfod-cache-path', + str(self.cache), *args, str(self.trace), + str(self.root / 'bundle.tar')) + + def assert_symbolized(self): + result = self.run_shell('query', '-q', str(self.root / 'bundle.tar'), + 'SELECT name FROM stack_profile_symbol') + self.assertIn(b'TestFunctionToSymbolize', result.stdout) + + def test_remote_only_and_cache_reuse(self): + result = self.bundle('--debuginfod', '--debuginfod-urls', self.url + '/ok') + self.assertIn(b'1 downloaded', result.stderr) + self.assert_symbolized() + self.assertEqual(self.requests, ['/ok/buildid/' + BUILD_ID + '/debuginfo']) + self.requests.clear() + result = self.bundle('--debuginfod', '--debuginfod-urls', self.url + '/ok') + self.assertIn(b'1 cache hits', result.stderr) + self.assertEqual(self.requests, []) + + def test_disabled_warns_even_when_quiet(self): + self.env['DEBUGINFOD_URLS'] = self.url + '/ok' + result = self.bundle('-q') + self.assertIn(b'ignored', result.stderr) + self.assertIn(b'--debuginfod', result.stderr) + self.assertEqual(self.requests, []) + self.assertFalse(self.cache.exists()) + + def test_cli_overrides_environment_and_llvm_options(self): + self.env['DEBUGINFOD_URLS'] = self.url + '/missing' + self.env['DEBUGINFOD_CACHE_PATH'] = str(self.root / 'wrong-cache') + self.env[ + 'LLVM_SYMBOLIZER_OPTS'] = '--output-style=GNU --debuginfod --invalid-option' + result = self.bundle('--debuginfod', '--debuginfod-urls', + self.url + '/redirect') + self.assertIn(b'LLVM_SYMBOLIZER_OPTS is ignored', result.stderr) + self.assert_symbolized() + self.assertFalse((self.root / 'wrong-cache').exists()) + self.assertFalse(any('/missing/' in path for path in self.requests)) + + def test_invalid_response_falls_back(self): + result = self.bundle('--debuginfod', '--debuginfod-urls', + self.url + '/bad\t' + self.url + '/ok', '--verbose') + self.assertIn(b'invalid debug file', result.stderr) + self.assert_symbolized() + self.assertEqual(len(self.requests), 2) + self.assertEqual(list(self.cache.rglob('*.tmp.*')), []) + + def test_wrong_build_id_falls_back(self): + self.bundle('--debuginfod', '--debuginfod-urls', + self.url + '/wrong ' + self.url + '/ok') + self.assert_symbolized() + self.assertEqual(len(self.requests), 2) + + def test_invalid_cache_is_replaced(self): + entry = self.cache / BUILD_ID / 'debuginfo' + entry.parent.mkdir(parents=True) + entry.write_bytes(b'incomplete') + self.bundle('--debuginfod', '--debuginfod-urls', self.url + '/ok') + self.assert_symbolized() + self.assertEqual(len(self.requests), 1) + self.assertEqual(entry.read_bytes(), + (ROOT / 'test/data/test_symbolizer_binary').read_bytes()) + + def test_curl_config_cannot_add_requests(self): + (self.root / '.curlrc').write_text('url = "' + self.url + '/missing"') + self.env['CURL_HOME'] = str(self.root) + self.bundle('--debuginfod', '--debuginfod-urls', self.url + '/ok') + self.assert_symbolized() + self.assertEqual(self.requests, ['/ok/buildid/' + BUILD_ID + '/debuginfo']) + + def test_failed_download_does_not_publish_cache_entry(self): + result = self.bundle('--debuginfod', '--debuginfod-urls', self.url + '/bad') + self.assertIn(b'1 unavailable', result.stderr) + self.assertFalse((self.cache / BUILD_ID / 'debuginfo').exists()) + self.assertEqual(list(self.cache.rglob('*.tmp.*')), []) + + def test_local_result_prevents_download(self): + local = self.root / 'local' + local.mkdir() + shutil.copy(ROOT / 'test/data/test_symbolizer_binary', local / 'binary') + self.bundle('--debuginfod', '--debuginfod-urls', self.url + '/ok', + '--symbol-paths', str(local)) + self.assert_symbolized() + self.assertEqual(self.requests, []) + + def test_stall_timeout_falls_back(self): + result = self.bundle('--debuginfod', '--debuginfod-stall-timeout', '1', + '--debuginfod-urls', + self.url + '/slow ' + self.url + '/ok', '--verbose') + self.assertIn(b'curl exit 28', result.stderr) + self.assert_symbolized() + + @unittest.skipUnless(hasattr(os, 'openpty'), 'requires a pseudo-terminal') + def test_progress_controls(self): + self.env['TERM'] = 'xterm' + for flags in ([], ['--no-progress'], ['-q']): + shutil.rmtree(self.cache, ignore_errors=True) + master, slave = os.openpty() + try: + result = subprocess.run([ + SHELL, 'bundle', '--no-auto-symbol-paths', + '--no-auto-proguard-maps', '--debuginfod', '--debuginfod-urls', + self.url + '/ok', '--debuginfod-cache-path', + str(self.cache), *flags, + str(self.trace), + str(self.root / 'bundle.tar') + ], + env=self.env, + stdout=subprocess.PIPE, + stderr=slave, + timeout=15) + os.close(slave) + slave = None + output = b'' + while True: + try: + data = os.read(master, 4096) + except OSError: + break + if not data: + break + output += data + self.assertEqual(result.returncode, 0, output.decode()) + self.assertEqual(b'Debuginfod: fetching build ID' in output, not flags) + if flags == ['-q']: + self.assertEqual(output, b'') + finally: + os.close(master) + if slave is not None: + os.close(slave) + + def test_symbolize_utility_and_traceconv(self): + flags = [ + '--debuginfod', '--debuginfod-urls', self.url + '/ok', + '--debuginfod-cache-path', + str(self.cache), '--quiet' + ] + result = self.run_shell('util', 'symbolize', *flags, str(self.trace)) + self.assertIn(b'TestFunctionToSymbolize', result.stdout) + self.assertEqual(result.stderr, b'') + result = subprocess.run([ + str(Path(SHELL).with_name('traceconv')), 'symbolize', *flags, + str(self.trace) + ], + env=self.env, + capture_output=True, + timeout=15) + self.assertEqual(result.returncode, 0, result.stderr.decode()) + self.assertIn(b'TestFunctionToSymbolize', result.stdout) + self.assertEqual(result.stderr, b'') + + def test_query_and_classic_interface_without_local_binaries(self): + for args in [('query', '-q', str(self.trace), + 'SELECT name FROM stack_profile_symbol'), + ('--quiet', '-Q', 'SELECT name FROM stack_profile_symbol', + str(self.trace))]: + result = self.run_shell(*args, '--debuginfod', '--debuginfod-urls', + self.url + '/ok', '--debuginfod-cache-path', + str(self.cache)) + self.assertIn(b'TestFunctionToSymbolize', result.stdout) + self.assertEqual(result.stderr, b'', result.stderr.decode()) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--shell', required=True) + args, remaining = parser.parse_known_args() + SHELL = str(Path(args.shell).resolve()) + unittest.main(argv=[__file__, *remaining])
diff --git a/test/trace_processor_shell_integrationtest.cc b/test/trace_processor_shell_integrationtest.cc index c34e105..dfd06cf 100644 --- a/test/trace_processor_shell_integrationtest.cc +++ b/test/trace_processor_shell_integrationtest.cc
@@ -354,7 +354,8 @@ base::Subprocess process({ShellPath(), "--stdiod"}); process.args.stdin_mode = base::Subprocess::InputMode::kBuffer; process.args.stdout_mode = base::Subprocess::OutputMode::kBuffer; - process.args.stderr_mode = base::Subprocess::OutputMode::kBuffer; + // Keep diagnostics separate from the binary RPC response. + process.args.stderr_mode = base::Subprocess::OutputMode::kInherit; process.args.input = req.SerializeAsString(); process.Start(); ASSERT_TRUE(process.Wait(kDefaultTestTimeoutMs)); @@ -907,7 +908,8 @@ base::Subprocess process({ShellPath(), "server", "stdio"}); process.args.stdin_mode = base::Subprocess::InputMode::kBuffer; process.args.stdout_mode = base::Subprocess::OutputMode::kBuffer; - process.args.stderr_mode = base::Subprocess::OutputMode::kBuffer; + // Keep diagnostics separate from the binary RPC response. + process.args.stderr_mode = base::Subprocess::OutputMode::kInherit; process.args.input = req.SerializeAsString(); process.Start(); @@ -1189,7 +1191,8 @@ {ShellPath(), "--stdiod", "--metric-extension", ext_arg}); process.args.stdin_mode = base::Subprocess::InputMode::kBuffer; process.args.stdout_mode = base::Subprocess::OutputMode::kBuffer; - process.args.stderr_mode = base::Subprocess::OutputMode::kBuffer; + // Keep diagnostics separate from the binary RPC response. + process.args.stderr_mode = base::Subprocess::OutputMode::kInherit; process.args.input = req.SerializeAsString(); process.Start(); ASSERT_TRUE(process.Wait(kDefaultTestTimeoutMs)); @@ -1251,7 +1254,8 @@ {ShellPath(), "--stdiod", "--add-sql-package", pkg_arg}); process.args.stdin_mode = base::Subprocess::InputMode::kBuffer; process.args.stdout_mode = base::Subprocess::OutputMode::kBuffer; - process.args.stderr_mode = base::Subprocess::OutputMode::kBuffer; + // Keep diagnostics separate from the binary RPC response. + process.args.stderr_mode = base::Subprocess::OutputMode::kInherit; process.args.input = req.SerializeAsString(); process.Start(); ASSERT_TRUE(process.Wait(kDefaultTestTimeoutMs));