tp: add tables and stdlib for bundled source and disassembly Groundwork for showing annotated source and disassembly for callstack samples in the UI (#7312): - SourceFile and ModuleDisassembly TracePacket fields carry a source file's contents and a module's per-function disassembly (address, bytes, text, static branch target, source line), keyed like ModuleSymbols so they can be bundled with a trace after recording. - The trace processor parses them into the source_file, disassembly_function and disassembly_instruction tables, dropping disassembly for modules absent from the trace (recorded in the disassembly_invalid_mapping_id stat). The standalone symbols importer also recognises these packets so bundle members load after the trace. - The callstacks.annotate stdlib module computes self and total (self plus callees) sample counts per callsite, per address and per source line, and joins a function's bundled instructions with its counts. The callstack forest and _callstacks_for_* macros expose mapping_id and rel_pc so tree nodes can be linked back to an address.
diff --git a/Android.bp b/Android.bp index fc2c2c2..16e7649 100644 --- a/Android.bp +++ b/Android.bp
@@ -19026,6 +19026,7 @@ filegroup { name: "perfetto_src_trace_processor_perfetto_sql_stdlib_callstacks_callstacks", srcs: [ + "src/trace_processor/perfetto_sql/stdlib/callstacks/annotate.sql", "src/trace_processor/perfetto_sql/stdlib/callstacks/stack_profile.sql", "src/trace_processor/perfetto_sql/stdlib/callstacks/symbolize.sql", ],
diff --git a/BUILD b/BUILD index e74a405..308d466 100644 --- a/BUILD +++ b/BUILD
@@ -4014,6 +4014,7 @@ perfetto_filegroup( name = "src_trace_processor_perfetto_sql_stdlib_callstacks_callstacks", srcs = [ + "src/trace_processor/perfetto_sql/stdlib/callstacks/annotate.sql", "src/trace_processor/perfetto_sql/stdlib/callstacks/stack_profile.sql", "src/trace_processor/perfetto_sql/stdlib/callstacks/symbolize.sql", ],
diff --git a/protos/perfetto/trace/profiling/profile_common.proto b/protos/perfetto/trace/profiling/profile_common.proto index 331a89a..068caba 100644 --- a/protos/perfetto/trace/profiling/profile_common.proto +++ b/protos/perfetto/trace/profiling/profile_common.proto
@@ -68,6 +68,84 @@ repeated AddressSymbols address_symbols = 3; } +// Contents of a source file referenced by symbolized frames. +// +// Emitted when a trace is bundled with symbols (`trace_processor bundle`) so +// that source code can be shown alongside profiling data without access to +// the original source tree. +message SourceFile { + // Path of the file exactly as it appears in the debug info, i.e. the same + // string as Line.source_file_name and Frame.source_path_iid. This is the key + // used to join the contents to symbolized frames. + optional string path = 1; + + // Raw contents of the file. + optional bytes contents = 2; +} + +// Disassembly of functions in a module. +// +// Emitted when a trace is bundled with symbols (`trace_processor bundle`) for +// functions that contain sampled addresses. Keyed by (path, build_id) in the +// same way as ModuleSymbols. +message ModuleDisassembly { + // Fully qualified path to the mapping. + // E.g. /system/lib64/libc.so. + optional string path = 1; + + // .note.gnu.build-id on Linux (not hex encoded). + // uuid on MacOS. + // Module GUID on Windows. + optional string build_id = 2; + + message Instruction { + // Address of the instruction relative to the start of the module. Uses the + // same convention as AddressSymbols.address and Frame.rel_pc. + optional uint64 address = 1; + + // Raw encoded bytes of the instruction. + optional bytes bytes = 2; + + // Rendered mnemonic and operands, e.g. "mov rax, qword ptr [rbp - 8]". + optional string text = 3; + + // For direct branches and calls, the module-relative address of the target + // instruction. Unset for instructions which do not transfer control or + // whose target is not statically known. + optional uint64 target_address = 4; + + // For branches and calls whose target lies outside this function, the + // name of the target symbol if known. + optional string target_symbol = 5; + + // Index into ModuleDisassembly.source_files of the source file this + // instruction was generated from, together with the line number. Unset + // when the module has no line information. + optional uint32 source_file_index = 6; + optional uint32 line_number = 7; + } + + message Function { + // Symbol name of the function. + optional string name = 1; + + // Module-relative address of the first instruction of the function. + optional uint64 start_address = 2; + + // Size of the function's code in bytes. + optional uint64 size = 3; + + // Instructions in address order. + repeated Instruction instructions = 4; + } + repeated Function functions = 3; + + // Source file paths referenced by Instruction.source_file_index. Paths use + // the same form as Line.source_file_name so they can be joined against + // SourceFile.path. + repeated string source_files = 4; +} + message Mapping { // Interning key. // Starts from 1, 0 is the same as "not set".
diff --git a/protos/perfetto/trace/trace_packet.proto b/protos/perfetto/trace/trace_packet.proto index 6ef6422..4886c45 100644 --- a/protos/perfetto/trace/trace_packet.proto +++ b/protos/perfetto/trace/trace_packet.proto
@@ -112,7 +112,7 @@ // See the [Buffers and Dataflow](/docs/concepts/buffers.md) doc for details. // // Next reserved id: 14 (up to 15). -// Next id: 139. +// Next id: 141. message TracePacket { // Encapsulates the state and configuration of the ProtoVM instances running // when the trace was snapshotted. This allows TP to re-instantiate the VMs @@ -236,6 +236,8 @@ // Only used in profile packets. ModuleSymbols module_symbols = 61; DeobfuscationMapping deobfuscation_mapping = 64; + SourceFile source_file = 139; + ModuleDisassembly module_disassembly = 140; // Deprecated, use TrackDescriptor instead. ProcessDescriptor process_descriptor = 43;
diff --git a/src/trace_processor/importers/proto/profile_module.cc b/src/trace_processor/importers/proto/profile_module.cc index 48a097e..cf6c679 100644 --- a/src/trace_processor/importers/proto/profile_module.cc +++ b/src/trace_processor/importers/proto/profile_module.cc
@@ -22,6 +22,7 @@ #include <vector> #include "perfetto/base/logging.h" +#include "perfetto/ext/base/string_utils.h" #include "perfetto/ext/base/string_view.h" #include "perfetto/ext/base/utils.h" #include "perfetto/protozero/field.h" @@ -121,6 +122,8 @@ RegisterForField(TracePacket::kPerfSampleFieldNumber); RegisterForField(TracePacket::kProfilePacketFieldNumber); RegisterForField(TracePacket::kModuleSymbolsFieldNumber); + RegisterForField(TracePacket::kSourceFileFieldNumber); + RegisterForField(TracePacket::kModuleDisassemblyFieldNumber); RegisterForField(TracePacket::kSmapsPacketFieldNumber); } @@ -149,6 +152,13 @@ case TracePacket::kModuleSymbolsFieldNumber: ParseModuleSymbols(args.field.Cast<TracePacket::kModuleSymbols>()); return; + case TracePacket::kSourceFileFieldNumber: + ParseSourceFile(args.field.Cast<TracePacket::kSourceFile>()); + return; + case TracePacket::kModuleDisassemblyFieldNumber: + ParseModuleDisassembly( + args.field.Cast<TracePacket::kModuleDisassembly>()); + return; case TracePacket::kSmapsPacketFieldNumber: ParseSmapsPacket(args.ts, args.field.Cast<TracePacket::kSmapsPacket>()); return; @@ -639,6 +649,86 @@ } } +void ProfileModule::ParseSourceFile(ConstBytes blob) { + protos::pbzero::SourceFile::Decoder file(blob.data, blob.size); + ConstBytes contents = file.contents(); + context_->storage->mutable_source_file_table()->Insert( + {context_->storage->InternString(file.path()), + context_->storage->InternString(base::StringView( + reinterpret_cast<const char*>(contents.data), contents.size))}); +} + +void ProfileModule::ParseModuleDisassembly(ConstBytes blob) { + protos::pbzero::ModuleDisassembly::Decoder disassembly(blob.data, blob.size); + std::optional<BuildId> build_id; + if (disassembly.build_id().size > 0) { + build_id = BuildId::FromRaw(disassembly.build_id()); + } + + // Only keep disassembly for modules which appear in the trace: without a + // mapping there is no rel_pc space to attribute samples in. + auto mappings = + context_->mapping_tracker->FindMappings(disassembly.path(), build_id); + if (mappings.empty()) { + context_->stats_tracker->IncrementStats( + stats::disassembly_invalid_mapping_id); + return; + } + + StringId path_id = context_->storage->InternString(disassembly.path()); + std::optional<StringId> build_id_id; + if (build_id) { + build_id_id = + context_->storage->InternString(base::StringView(build_id->ToHex())); + } + + std::vector<StringId> source_files; + for (auto it = disassembly.source_files(); it; ++it) { + source_files.push_back(context_->storage->InternString(*it)); + } + + auto* functions = context_->storage->mutable_disassembly_function_table(); + auto* instructions = + context_->storage->mutable_disassembly_instruction_table(); + for (auto fn_it = disassembly.functions(); fn_it; ++fn_it) { + protos::pbzero::ModuleDisassembly::Function::Decoder function(*fn_it); + auto function_id = + functions + ->Insert({path_id, build_id_id, + context_->storage->InternString(function.name()), + static_cast<int64_t>(function.start_address()), + static_cast<int64_t>(function.size())}) + .id; + for (auto insn_it = function.instructions(); insn_it; ++insn_it) { + protos::pbzero::ModuleDisassembly::Instruction::Decoder insn(*insn_it); + ConstBytes bytes = insn.bytes(); + std::optional<int64_t> target_rel_pc; + if (insn.has_target_address()) { + target_rel_pc = static_cast<int64_t>(insn.target_address()); + } + std::optional<StringId> target_symbol; + if (insn.has_target_symbol()) { + target_symbol = context_->storage->InternString(insn.target_symbol()); + } + std::optional<StringId> source_file; + if (insn.has_source_file_index() && + insn.source_file_index() < source_files.size()) { + source_file = source_files[insn.source_file_index()]; + } + std::optional<uint32_t> line_number; + if (source_file && insn.has_line_number()) { + line_number = insn.line_number(); + } + instructions->Insert( + {function_id, static_cast<int64_t>(insn.address()), + context_->storage->InternString(base::StringView(base::ToHex( + reinterpret_cast<const char*>(bytes.data), bytes.size))), + context_->storage->InternString(insn.text()), target_rel_pc, + target_symbol, source_file, line_number}); + } + } +} + void ProfileModule::ParseSmapsPacket(int64_t ts, ConstBytes blob) { protos::pbzero::SmapsPacket::Decoder sp(blob.data, blob.size); auto upid = context_->process_tracker->GetOrCreateProcess(sp.pid());
diff --git a/src/trace_processor/importers/proto/profile_module.h b/src/trace_processor/importers/proto/profile_module.h index c6bac5e..33bc151 100644 --- a/src/trace_processor/importers/proto/profile_module.h +++ b/src/trace_processor/importers/proto/profile_module.h
@@ -69,6 +69,11 @@ PacketSequenceStateGeneration*, protozero::ConstBytes); void ParseModuleSymbols(protozero::ConstBytes); + + // bundled source and disassembly: + void ParseSourceFile(protozero::ConstBytes); + void ParseModuleDisassembly(protozero::ConstBytes); + void ParseSmapsPacket(int64_t ts, protozero::ConstBytes); void ParsePackedSmaps(int64_t ts, UniquePid upid, protozero::ConstBytes);
diff --git a/src/trace_processor/importers/proto/proto_trace_reader.cc b/src/trace_processor/importers/proto/proto_trace_reader.cc index 4d5f798..e007018 100644 --- a/src/trace_processor/importers/proto/proto_trace_reader.cc +++ b/src/trace_processor/importers/proto/proto_trace_reader.cc
@@ -1355,7 +1355,16 @@ constexpr uint16_t kModuleSymbolsTag = protozero::proto_utils::MakeTagLengthDelimited( protos::pbzero::TracePacket::kModuleSymbolsFieldNumber); +constexpr uint16_t kSourceFileTag = + protozero::proto_utils::MakeTagLengthDelimited( + protos::pbzero::TracePacket::kSourceFileFieldNumber); +constexpr uint16_t kModuleDisassemblyTag = + protozero::proto_utils::MakeTagLengthDelimited( + protos::pbzero::TracePacket::kModuleDisassemblyFieldNumber); +// Whether the first packet of the trace carries data that is bundled with a +// trace after recording (symbols, source files, disassembly). Such packets +// back-patch existing rows, so they must be parsed after the main trace. bool IsProtoTraceWithSymbols(const uint8_t* ptr, size_t size) { const uint8_t* const end = ptr + size; @@ -1382,7 +1391,8 @@ return false; } - return tag == kModuleSymbolsTag; + return tag == kModuleSymbolsTag || tag == kSourceFileTag || + tag == kModuleDisassemblyTag; } // Perfetto proto trace.
diff --git a/src/trace_processor/perfetto_sql/stdlib/appleos/instruments/samples.sql b/src/trace_processor/perfetto_sql/stdlib/appleos/instruments/samples.sql index bd1ad69..f12bbde 100644 --- a/src/trace_processor/perfetto_sql/stdlib/appleos/instruments/samples.sql +++ b/src/trace_processor/perfetto_sql/stdlib/appleos/instruments/samples.sql
@@ -62,7 +62,15 @@ cumulative_count LONG ) AS -SELECT r.*, a.cumulative_count +SELECT + r.id, + r.parent_id, + r.name, + r.mapping_name, + r.source_file, + r.line_number, + r.self_count, + a.cumulative_count FROM _callstacks_self_to_cumulative!(( SELECT id, parent_id, self_count FROM _appleos_instruments_raw_callstacks
diff --git a/src/trace_processor/perfetto_sql/stdlib/callstacks/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/callstacks/BUILD.gn index 797e02a..277afc6 100644 --- a/src/trace_processor/perfetto_sql/stdlib/callstacks/BUILD.gn +++ b/src/trace_processor/perfetto_sql/stdlib/callstacks/BUILD.gn
@@ -16,6 +16,7 @@ perfetto_sql_source_set("callstacks") { sources = [ + "annotate.sql", "stack_profile.sql", "symbolize.sql", ]
diff --git a/src/trace_processor/perfetto_sql/stdlib/callstacks/annotate.sql b/src/trace_processor/perfetto_sql/stdlib/callstacks/annotate.sql new file mode 100644 index 0000000..233f1cc --- /dev/null +++ b/src/trace_processor/perfetto_sql/stdlib/callstacks/annotate.sql
@@ -0,0 +1,193 @@ +-- +-- Copyright 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 +-- +-- https://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. + +-- Sample counts for every callsite on the stack of any of the given samples. +-- +-- `self_count` is the number of samples whose leaf callsite is the row's +-- callsite. `total_count` additionally includes samples whose stack passes +-- through the callsite, i.e. samples taken in one of its callees. +CREATE PERFETTO MACRO _callsite_sample_counts( + -- Subquery with a `callsite_id` column and one row per sample. + samples TableOrSubquery +) +RETURNS TableOrSubquery +AS ( + WITH RECURSIVE + metrics AS MATERIALIZED ( + SELECT + callsite_id, + count() AS self_count + FROM $samples + WHERE + callsite_id IS NOT NULL + GROUP BY + callsite_id + ), + -- Every (ancestor or self, sampled callsite) pair. + ancestors(callsite_id, sample_callsite_id) AS ( + SELECT + callsite_id, + callsite_id + FROM metrics + UNION ALL + SELECT + c.parent_id, + a.sample_callsite_id + FROM ancestors AS a + JOIN stack_profile_callsite AS c + ON c.id = a.callsite_id + WHERE + c.parent_id IS NOT NULL + ) + SELECT + a.callsite_id, + sum(iif(a.callsite_id = m.callsite_id, m.self_count, 0)) AS self_count, + sum(m.self_count) AS total_count + FROM ancestors AS a + JOIN metrics AS m + ON m.callsite_id = a.sample_callsite_id + GROUP BY + a.callsite_id +); + +-- Sample counts per instruction address. +-- +-- For the leaf frame of a sample the address is the sampled program counter. +-- For every other frame it is the return address of the call the frame was +-- executing, so `total_count` on a call instruction includes the samples +-- taken inside the callee. +CREATE PERFETTO MACRO _sample_counts_by_address( + -- Subquery with a `callsite_id` column and one row per sample. + samples TableOrSubquery +) +RETURNS TableOrSubquery +AS ( + SELECT + f.mapping AS mapping_id, + m.name AS mapping_name, + m.build_id, + f.rel_pc, + sum(c.self_count) AS self_count, + sum(c.total_count) AS total_count + FROM _callsite_sample_counts!($samples) AS c + JOIN stack_profile_callsite AS sc + ON sc.id = c.callsite_id + JOIN stack_profile_frame AS f + ON f.id = sc.frame_id + JOIN stack_profile_mapping AS m + ON m.id = f.mapping + GROUP BY + f.mapping, + f.rel_pc +); + +-- Sample counts per source line, attributed through the innermost symbol of +-- each sampled frame. Frames without line information are not included. +CREATE PERFETTO MACRO _sample_counts_by_source_line( + -- Subquery with a `callsite_id` column and one row per sample. + samples TableOrSubquery +) +RETURNS TableOrSubquery +AS ( + SELECT + s.source_file, + s.line_number, + sum(c.self_count) AS self_count, + sum(c.total_count) AS total_count + FROM _callsite_sample_counts!($samples) AS c + JOIN stack_profile_callsite AS sc + ON sc.id = c.callsite_id + JOIN stack_profile_frame AS f + ON f.id = sc.frame_id + -- The innermost inlined symbol of a frame is the one whose id equals the + -- frame's symbol_set_id. + JOIN stack_profile_symbol AS s + ON s.id = f.symbol_set_id + WHERE + s.source_file IS NOT NULL AND s.line_number IS NOT NULL + GROUP BY + s.source_file, + s.line_number +); + +-- The bundled disassembly function containing an address in a mapping, or +-- NULL if no disassembly was bundled for it. +CREATE PERFETTO FUNCTION _disassembly_function_for_address( + -- Id of the stack_profile_mapping the address belongs to. + mapping_id LONG, + -- Address relative to the start of the mapping. + rel_pc LONG +) +-- Id in disassembly_function. +RETURNS LONG +AS +SELECT df.id +FROM disassembly_function AS df +JOIN stack_profile_mapping AS m + ON m.id = $mapping_id +WHERE + iif(df.build_id IS NOT NULL, df.build_id = m.build_id, df.path = m.name) + AND $rel_pc >= df.start_rel_pc + AND $rel_pc < df.start_rel_pc + df.size +LIMIT 1; + +-- The bundled disassembly of a function, in address order, with the sample +-- counts of each instruction. +CREATE PERFETTO MACRO _annotated_disassembly( + -- Subquery with a `callsite_id` column and one row per sample. + samples TableOrSubquery, + -- Id in disassembly_function. + function_id Expr +) +RETURNS TableOrSubquery +AS ( + WITH + fn AS ( + SELECT + * + FROM disassembly_function + WHERE + id = $function_id + ), + counts AS ( + SELECT + a.rel_pc, + sum(a.self_count) AS self_count, + sum(a.total_count) AS total_count + FROM _sample_counts_by_address!($samples) AS a + JOIN fn + ON iif(fn.build_id IS NOT NULL, a.build_id = fn.build_id, a.mapping_name = fn.path) + GROUP BY + a.rel_pc + ) + SELECT + i.id AS instruction_id, + i.rel_pc, + i.bytes, + i.text, + i.target_rel_pc, + i.target_symbol, + i.source_file, + i.line_number, + coalesce(c.self_count, 0) AS self_count, + coalesce(c.total_count, 0) AS total_count + FROM disassembly_instruction AS i + LEFT JOIN counts AS c + USING (rel_pc) + WHERE + i.function_id = $function_id + ORDER BY + i.rel_pc +);
diff --git a/src/trace_processor/perfetto_sql/stdlib/callstacks/stack_profile.sql b/src/trace_processor/perfetto_sql/stdlib/callstacks/stack_profile.sql index de6746c..3e4bde9 100644 --- a/src/trace_processor/perfetto_sql/stdlib/callstacks/stack_profile.sql +++ b/src/trace_processor/perfetto_sql/stdlib/callstacks/stack_profile.sql
@@ -85,6 +85,7 @@ coalesce(s.name, f.deobfuscated_name, f.name, 'unknown') ) AS name, f.mapping AS mapping_id, + f.rel_pc, s.source_file, coalesce(jsf.line, s.line_number) AS line_number, coalesce(jsf.col, 0) AS column_number, @@ -132,6 +133,8 @@ f.callsite_id, f.name, m.name AS mapping_name, + f.mapping_id, + f.rel_pc, f.source_file, f.line_number, f.inlined, @@ -168,6 +171,8 @@ c.parent_id, c.name, c.mapping_name, + c.mapping_id, + c.rel_pc, c.source_file, c.line_number, iif(c.is_leaf_function_in_callsite_frame, coalesce(m.self_count, 0), 0) AS self_count @@ -199,6 +204,8 @@ c.parent_id, c.name, c.mapping_name, + c.mapping_id, + c.rel_pc, c.source_file, c.line_number, iif(c.is_leaf_function_in_callsite_frame, coalesce(m.self_value, 0), 0) AS self_value
diff --git a/src/trace_processor/perfetto_sql/stdlib/linux/perf/samples.sql b/src/trace_processor/perfetto_sql/stdlib/linux/perf/samples.sql index e8e3d9e..2ba467e 100644 --- a/src/trace_processor/perfetto_sql/stdlib/linux/perf/samples.sql +++ b/src/trace_processor/perfetto_sql/stdlib/linux/perf/samples.sql
@@ -59,7 +59,15 @@ cumulative_count LONG ) AS -SELECT r.*, a.cumulative_count +SELECT + r.id, + r.parent_id, + r.name, + r.mapping_name, + r.source_file, + r.line_number, + r.self_count, + a.cumulative_count FROM _callstacks_self_to_cumulative!(( SELECT id, parent_id, self_count FROM _linux_perf_raw_callstacks
diff --git a/src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/views.sql b/src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/views.sql index 21fc526..f905ba1 100644 --- a/src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/views.sql +++ b/src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/views.sql
@@ -666,6 +666,69 @@ AS SELECT * FROM __intrinsic_stack_profile_symbol; +-- Contents of source files bundled with the trace, for the files referenced +-- by symbolized frames. +CREATE PERFETTO VIEW source_file( + -- The id of the row. + id ID, + -- Path of the file as it appears in the debug info. Joins to + -- stack_profile_symbol.source_file. + path STRING, + -- Raw contents of the file. + contents STRING +) +AS +SELECT * FROM __intrinsic_source_file; + +-- Functions for which disassembly is bundled with the trace. +CREATE PERFETTO VIEW disassembly_function( + -- The id of the row. + id ID, + -- Path of the module containing the function, matching + -- stack_profile_mapping.name. + path STRING, + -- Hex-encoded build id of the module, matching + -- stack_profile_mapping.build_id. + build_id STRING, + -- Symbol name of the function. + name STRING, + -- Address of the first instruction relative to the start of the module, in + -- the same space as stack_profile_frame.rel_pc. + start_rel_pc LONG, + -- Size of the function's code in bytes. + size LONG +) +AS +SELECT * FROM __intrinsic_disassembly_function; + +-- Instructions of functions in disassembly_function, in address order. +CREATE PERFETTO VIEW disassembly_instruction( + -- The id of the row. + id ID, + -- The function this instruction belongs to. + function_id JOINID(disassembly_function.id), + -- Address of the instruction relative to the start of the module, in the + -- same space as stack_profile_frame.rel_pc. + rel_pc LONG, + -- Hex-encoded raw bytes of the instruction. + bytes STRING, + -- Rendered mnemonic and operands. + text STRING, + -- For direct branches and calls, the module-relative address of the target + -- instruction. + target_rel_pc LONG, + -- For branches and calls out of the function, the name of the target symbol + -- if known. + target_symbol STRING, + -- Path of the source file the instruction was generated from. Joins to + -- source_file.path. + source_file STRING, + -- Line in source_file the instruction was generated from. + line_number LONG +) +AS +SELECT * FROM __intrinsic_disassembly_instruction; + -- Allocations that happened at a callsite. CREATE PERFETTO VIEW heap_profile_allocation( -- The id of the row.
diff --git a/src/trace_processor/plugins/storage_tables/storage_tables.cc b/src/trace_processor/plugins/storage_tables/storage_tables.cc index 4137548..541b71a 100644 --- a/src/trace_processor/plugins/storage_tables/storage_tables.cc +++ b/src/trace_processor/plugins/storage_tables/storage_tables.cc
@@ -159,6 +159,9 @@ AddDataframe(out, s->mutable_v8_wasm_code_table()); AddDataframe(out, s->mutable_v8_regexp_code_table()); AddDataframe(out, s->mutable_symbol_table()); + AddDataframe(out, s->mutable_source_file_table()); + AddDataframe(out, s->mutable_disassembly_function_table()); + AddDataframe(out, s->mutable_disassembly_instruction_table()); AddDataframe(out, s->mutable_jit_code_table()); AddDataframe(out, s->mutable_jit_frame_table()); AddDataframe(out, s->mutable_android_key_events_table());
diff --git a/src/trace_processor/storage/stats.h b/src/trace_processor/storage/stats.h index 097a9da..1d1f27d 100644 --- a/src/trace_processor/storage/stats.h +++ b/src/trace_processor/storage/stats.h
@@ -234,6 +234,9 @@ F(stackprofile_invalid_mapping_id, kSingle, kError, kTrace, Scope::kMachineAndTrace, ""), \ F(stackprofile_invalid_frame_id, kSingle, kError, kTrace, Scope::kMachineAndTrace, ""), \ F(stackprofile_invalid_callstack_id, kSingle, kError, kTrace, Scope::kMachineAndTrace, ""), \ + F(disassembly_invalid_mapping_id, kSingle, kError, kTrace, Scope::kMachineAndTrace, \ + "ModuleDisassembly packet referenced a module with no mapping in the " \ + "trace. Ignored"), \ F(stackprofile_parser_error, kSingle, kError, kTrace, Scope::kMachineAndTrace, ""), \ F(smaps_parser_errors, kSingle, kError, kTrace, Scope::kMachineAndTrace, \ "Count of malformed PackedSmaps packets. Data in smaps tables unreliable."), \
diff --git a/src/trace_processor/storage/trace_storage.h b/src/trace_processor/storage/trace_storage.h index a12d987..6d9bf38 100644 --- a/src/trace_processor/storage/trace_storage.h +++ b/src/trace_processor/storage/trace_storage.h
@@ -647,6 +647,28 @@ return mutable_table<tables::SymbolTable>(); } + const tables::SourceFileTable& source_file_table() const { + return table<tables::SourceFileTable>(); + } + tables::SourceFileTable* mutable_source_file_table() { + return mutable_table<tables::SourceFileTable>(); + } + + const tables::DisassemblyFunctionTable& disassembly_function_table() const { + return table<tables::DisassemblyFunctionTable>(); + } + tables::DisassemblyFunctionTable* mutable_disassembly_function_table() { + return mutable_table<tables::DisassemblyFunctionTable>(); + } + + const tables::DisassemblyInstructionTable& disassembly_instruction_table() + const { + return table<tables::DisassemblyInstructionTable>(); + } + tables::DisassemblyInstructionTable* mutable_disassembly_instruction_table() { + return mutable_table<tables::DisassemblyInstructionTable>(); + } + const tables::HeapGraphObjectTable& heap_graph_object_table() const { return table<tables::HeapGraphObjectTable>(); }
diff --git a/src/trace_processor/tables/profiler_tables.py b/src/trace_processor/tables/profiler_tables.py index cf8542a..6f9a090 100644 --- a/src/trace_processor/tables/profiler_tables.py +++ b/src/trace_processor/tables/profiler_tables.py
@@ -892,6 +892,126 @@ '''''' })) +SOURCE_FILE_TABLE = Table( + python_module=__file__, + class_name='SourceFileTable', + sql_name='__intrinsic_source_file', + wrapping_sql_view=WrappingSqlView('source_file'), + columns=[ + C('path', CppString()), + C('contents', CppString()), + ], + tabledoc=TableDoc( + doc=''' + Contents of source files bundled with the trace. Populated from + SourceFile packets, which `trace_processor bundle` emits for the + files referenced by symbolized frames. + ''', + group='Callstack profilers', + columns={ + 'path': + ''' + Path of the file as it appears in the debug info. Joins to + stack_profile_symbol.source_file. + ''', + 'contents': + '''Raw contents of the file.''', + })) + +DISASSEMBLY_FUNCTION_TABLE = Table( + python_module=__file__, + class_name='DisassemblyFunctionTable', + sql_name='__intrinsic_disassembly_function', + wrapping_sql_view=WrappingSqlView('disassembly_function'), + columns=[ + C('path', CppString()), + C('build_id', CppOptional(CppString())), + C('name', CppString()), + C('start_rel_pc', CppInt64()), + C('size', CppInt64()), + ], + tabledoc=TableDoc( + doc=''' + Functions for which disassembly is bundled with the trace. + Populated from ModuleDisassembly packets, which + `trace_processor bundle` emits for functions containing sampled + addresses. + ''', + group='Callstack profilers', + columns={ + 'path': + ''' + Path of the module containing the function, matching + stack_profile_mapping.name. + ''', + 'build_id': + ''' + Hex-encoded build id of the module, matching + stack_profile_mapping.build_id. + ''', + 'name': + '''Symbol name of the function.''', + 'start_rel_pc': + ''' + Address of the first instruction relative to the start of + the module, in the same space as stack_profile_frame.rel_pc. + ''', + 'size': + '''Size of the function's code in bytes.''', + })) + +DISASSEMBLY_INSTRUCTION_TABLE = Table( + python_module=__file__, + class_name='DisassemblyInstructionTable', + sql_name='__intrinsic_disassembly_instruction', + wrapping_sql_view=WrappingSqlView('disassembly_instruction'), + columns=[ + C('function_id', CppTableId(DISASSEMBLY_FUNCTION_TABLE)), + C('rel_pc', CppInt64()), + C('bytes', CppString()), + C('text', CppString()), + C('target_rel_pc', CppOptional(CppInt64())), + C('target_symbol', CppOptional(CppString())), + C('source_file', CppOptional(CppString())), + C('line_number', CppOptional(CppUint32())), + ], + tabledoc=TableDoc( + doc=''' + Instructions of functions in disassembly_function, in address + order. + ''', + group='Callstack profilers', + columns={ + 'function_id': + '''The function this instruction belongs to.''', + 'rel_pc': + ''' + Address of the instruction relative to the start of the + module, in the same space as stack_profile_frame.rel_pc. + ''', + 'bytes': + '''Hex-encoded raw bytes of the instruction.''', + 'text': + '''Rendered mnemonic and operands.''', + 'target_rel_pc': + ''' + For direct branches and calls, the module-relative address + of the target instruction. + ''', + 'target_symbol': + ''' + For branches and calls out of the function, the name of the + target symbol if known. + ''', + 'source_file': + ''' + Path of the source file the instruction was generated from. + Joins to source_file.path. + ''', + 'line_number': + '''Line in source_file the instruction was generated from.''', + })) + HEAP_PROFILE_TABLE = Table( python_module=__file__, class_name='HeapProfileTable', @@ -1655,6 +1775,8 @@ AGGREGATE_PROFILE_TABLE, AGGREGATE_SAMPLE_TABLE, CHROME_STACK_SAMPLE_EXTRAS_TABLE, + DISASSEMBLY_FUNCTION_TABLE, + DISASSEMBLY_INSTRUCTION_TABLE, EXPERIMENTAL_FLAMEGRAPH_TABLE, GPU_CONTEXT_TABLE, GPU_COUNTER_GROUP_TABLE, @@ -1676,6 +1798,7 @@ PROFILER_SESSION_TABLE, PROFILER_SMAPS_TABLE, PROFILER_TASK_CONTEXT_TABLE, + SOURCE_FILE_TABLE, STACK_PROFILE_CALLSITE_TABLE, STACK_PROFILE_FRAME_TABLE, STACK_PROFILE_MAPPING_TABLE,
diff --git a/test/trace_processor/diff_tests/include_index.py b/test/trace_processor/diff_tests/include_index.py index 3ffb076..b4f318d 100644 --- a/test/trace_processor/diff_tests/include_index.py +++ b/test/trace_processor/diff_tests/include_index.py
@@ -117,6 +117,7 @@ from diff_tests.parser.profiling.tests_heap_graph import ProfilingHeapGraph from diff_tests.parser.profiling.tests_heap_profiling import ProfilingHeapProfiling from diff_tests.parser.profiling.tests_llvm_symbolizer import ProfilingLlvmSymbolizer +from diff_tests.parser.profiling.tests_source_and_disassembly import ProfilingSourceAndDisassembly from diff_tests.parser.sched.tests import SchedParser from diff_tests.parser.simpleperf.tests import Simpleperf from diff_tests.parser.simpleperf_proto.tests import SimpleperfProtoParser @@ -179,6 +180,7 @@ from diff_tests.stdlib.span_join.tests_outer_join import SpanJoinOuterJoin from diff_tests.stdlib.span_join.tests_regression import SpanJoinRegression from diff_tests.stdlib.span_join.tests_smoke import SpanJoinSmoke +from diff_tests.stdlib.callstacks.tests import Callstacks from diff_tests.stdlib.stacks.tests import Stacks from diff_tests.stdlib.symbolize.tests import Symbolize from diff_tests.stdlib.tests import StdlibSmoke @@ -250,6 +252,7 @@ ProfilingHeapGraph, ProfilingHeapProfiling, ProfilingLlvmSymbolizer, + ProfilingSourceAndDisassembly, SchedParser, Simpleperf, SimpleperfProtoParser, @@ -363,6 +366,7 @@ SpanJoinOuterJoin, SpanJoinRegression, SpanJoinSmoke, + Callstacks, Stacks, CreateIntervals, IntervalsFillGaps,
diff --git a/test/trace_processor/diff_tests/parser/profiling/source_and_disassembly.textproto b/test/trace_processor/diff_tests/parser/profiling/source_and_disassembly.textproto new file mode 100644 index 0000000..3a5f245 --- /dev/null +++ b/test/trace_processor/diff_tests/parser/profiling/source_and_disassembly.textproto
@@ -0,0 +1,156 @@ +packet { + process_tree { + processes { + pid: 1 + ppid: 0 + cmdline: "init" + uid: 0 + } + processes { + pid: 2 + ppid: 1 + cmdline: "system_server" + uid: 1000 + } + } +} + +packet { + clock_snapshot { + clocks: { + clock_id: 6 # BOOTTIME + timestamp: 0 + } + clocks: { + clock_id: 4 # MONOTONIC_COARSE + timestamp: 10 + } + } +} + +packet { + trusted_packet_sequence_id: 999 + previous_packet_dropped: 1 + incremental_state_cleared: true + timestamp: 10 + profile_packet { + strings { + iid: 1 + str: "f1" + } + strings { + iid: 2 + str: "f2" + } + strings { + iid: 3 + str: "f3" + } + strings { + iid: 4 + str: "liblib.so" + } + strings { + iid: 5 + str: "build-id" + } + frames { + iid: 1 + function_name_id: 1 + mapping_id: 1 + rel_pc: 0x1000 + } + frames { + iid: 2 + function_name_id: 2 + mapping_id: 1 + rel_pc: 0x2001 + } + frames { + iid: 3 + function_name_id: 3 + mapping_id: 1 + rel_pc: 0x3000 + } + callstacks { + iid: 1 + frame_ids: 1 + frame_ids: 2 + frame_ids: 3 + } + mappings { + iid: 1 + path_string_ids: 4 + build_id: 5 + } + process_dumps { + pid: 2 + samples { + callstack_id: 1 + self_allocated: 2000 + self_freed: 1000 + alloc_count: 2 + free_count: 1 + } + } + } +} + +# Bundled source and disassembly packets. +packet { + source_file { + path: "f2.cc" + contents: "int f2() {\n return 2;\n}\n" + } +} +packet { + module_disassembly { + path: "/liblib.so" + build_id: "build-id" + source_files: "f2.cc" + functions { + name: "f2" + start_address: 0x2000 + size: 8 + instructions { + address: 0x2000 + bytes: "\x55" + text: "push rbp" + source_file_index: 0 + line_number: 1 + } + instructions { + address: 0x2001 + bytes: "\xeb\xfd" + text: "jmp 0x2000" + target_address: 0x2000 + source_file_index: 0 + line_number: 2 + } + instructions { + address: 0x2003 + bytes: "\xe8\x00\x00\x00\x00" + text: "call 0x3000" + target_address: 0x3000 + target_symbol: "f3" + } + } + } +} +# Disassembly for a module which does not appear in the trace is dropped. +packet { + module_disassembly { + path: "/libmissing.so" + build_id: "missing" + functions { + name: "missing" + start_address: 0x10 + size: 1 + instructions { + address: 0x10 + bytes: "\xc3" + text: "ret" + } + } + } +}
diff --git a/test/trace_processor/diff_tests/parser/profiling/tests_source_and_disassembly.py b/test/trace_processor/diff_tests/parser/profiling/tests_source_and_disassembly.py new file mode 100644 index 0000000..e900fcf --- /dev/null +++ b/test/trace_processor/diff_tests/parser/profiling/tests_source_and_disassembly.py
@@ -0,0 +1,83 @@ +#!/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. + +from python.generators.diff_tests.testing import Path +from python.generators.diff_tests.testing import Csv +from python.generators.diff_tests.testing import DiffTestBlueprint +from python.generators.diff_tests.testing import TestSuite + + +class ProfilingSourceAndDisassembly(TestSuite): + + def test_source_file(self): + return DiffTestBlueprint( + trace=Path('source_and_disassembly.textproto'), + query=""" + SELECT path, contents FROM source_file; + """, + out=Csv(""" + "path","contents" + "f2.cc","int f2() { + return 2; + } + " + """)) + + def test_disassembly_function(self): + return DiffTestBlueprint( + trace=Path('source_and_disassembly.textproto'), + query=""" + SELECT path, build_id, name, start_rel_pc, size + FROM disassembly_function; + """, + out=Csv(""" + "path","build_id","name","start_rel_pc","size" + "/liblib.so","6275696c642d6964","f2",8192,8 + """)) + + def test_disassembly_instruction(self): + return DiffTestBlueprint( + trace=Path('source_and_disassembly.textproto'), + query=""" + SELECT + f.name AS function_name, + i.rel_pc, + i.bytes, + i.text, + i.target_rel_pc, + i.target_symbol, + i.source_file, + i.line_number + FROM disassembly_instruction i + JOIN disassembly_function f ON i.function_id = f.id + ORDER BY i.rel_pc; + """, + out=Csv(""" + "function_name","rel_pc","bytes","text","target_rel_pc","target_symbol","source_file","line_number" + "f2",8192,"55","push rbp","[NULL]","[NULL]","f2.cc",1 + "f2",8193,"ebfd","jmp 0x2000",8192,"[NULL]","f2.cc",2 + "f2",8195,"e800000000","call 0x3000",12288,"f3","[NULL]","[NULL]" + """)) + + def test_disassembly_unknown_module_is_dropped(self): + return DiffTestBlueprint( + trace=Path('source_and_disassembly.textproto'), + query=""" + SELECT value FROM stats WHERE name = 'disassembly_invalid_mapping_id'; + """, + out=Csv(""" + "value" + 1 + """))
diff --git a/test/trace_processor/diff_tests/stdlib/callstacks/annotate.textproto b/test/trace_processor/diff_tests/stdlib/callstacks/annotate.textproto new file mode 100644 index 0000000..5de7718 --- /dev/null +++ b/test/trace_processor/diff_tests/stdlib/callstacks/annotate.textproto
@@ -0,0 +1,178 @@ +packet { + process_tree { + processes { + pid: 1 + ppid: 0 + cmdline: "init" + uid: 0 + } + processes { + pid: 2 + ppid: 1 + cmdline: "system_server" + uid: 1000 + } + } +} + +packet { + clock_snapshot { + clocks: { + clock_id: 6 # BOOTTIME + timestamp: 0 + } + clocks: { + clock_id: 4 # MONOTONIC_COARSE + timestamp: 10 + } + } +} + +# Two stacks sharing a prefix: [f1, f2, f3] and [f1, f2], one sample each. +packet { + trusted_packet_sequence_id: 999 + previous_packet_dropped: 1 + incremental_state_cleared: true + timestamp: 10 + profile_packet { + strings { + iid: 1 + str: "f1" + } + strings { + iid: 2 + str: "f2" + } + strings { + iid: 3 + str: "f3" + } + strings { + iid: 4 + str: "liblib.so" + } + strings { + iid: 5 + str: "build-id" + } + frames { + iid: 1 + function_name_id: 1 + mapping_id: 1 + rel_pc: 0x1000 + } + frames { + iid: 2 + function_name_id: 2 + mapping_id: 1 + rel_pc: 0x2001 + } + frames { + iid: 3 + function_name_id: 3 + mapping_id: 1 + rel_pc: 0x3000 + } + callstacks { + iid: 1 + frame_ids: 1 + frame_ids: 2 + frame_ids: 3 + } + callstacks { + iid: 2 + frame_ids: 1 + frame_ids: 2 + } + mappings { + iid: 1 + path_string_ids: 4 + build_id: 5 + } + process_dumps { + pid: 2 + samples { + callstack_id: 1 + self_allocated: 100 + self_freed: 0 + alloc_count: 1 + free_count: 0 + } + samples { + callstack_id: 2 + self_allocated: 100 + self_freed: 0 + alloc_count: 1 + free_count: 0 + } + } + } +} + +packet { + module_symbols { + path: "/liblib.so" + build_id: "build-id" + address_symbols { + address: 0x1000 + lines { + function_name: "f1" + source_file_name: "f1.cc" + line_number: 1 + } + } + address_symbols { + address: 0x2001 + lines { + function_name: "f2" + source_file_name: "f2.cc" + line_number: 2 + } + } + address_symbols { + address: 0x3000 + lines { + function_name: "f3" + source_file_name: "f3.cc" + line_number: 33 + } + } + } +} + +packet { + module_disassembly { + path: "/liblib.so" + build_id: "build-id" + source_files: "f2.cc" + functions { + name: "f2" + start_address: 0x2000 + size: 8 + instructions { + address: 0x2000 + bytes: "\x55" + text: "push rbp" + source_file_index: 0 + line_number: 1 + } + instructions { + address: 0x2001 + bytes: "\xe8\x00\x00\x00\x00" + text: "call 0x3000" + target_address: 0x3000 + target_symbol: "f3" + source_file_index: 0 + line_number: 2 + } + instructions { + address: 0x2006 + bytes: "\xeb\xf8" + text: "jmp 0x2000" + target_address: 0x2000 + source_file_index: 0 + line_number: 3 + } + } + } +}
diff --git a/test/trace_processor/diff_tests/stdlib/callstacks/tests.py b/test/trace_processor/diff_tests/stdlib/callstacks/tests.py new file mode 100644 index 0000000..8eaaa14 --- /dev/null +++ b/test/trace_processor/diff_tests/stdlib/callstacks/tests.py
@@ -0,0 +1,138 @@ +#!/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. + +from python.generators.diff_tests.testing import Path +from python.generators.diff_tests.testing import Csv +from python.generators.diff_tests.testing import DiffTestBlueprint +from python.generators.diff_tests.testing import TestSuite + +# One sample per heap profile allocation: stacks [f1, f2, f3] and [f1, f2]. +_SAMPLES = '(SELECT callsite_id FROM heap_profile_allocation WHERE count > 0)' + + +class Callstacks(TestSuite): + + def test_callsite_sample_counts(self): + return DiffTestBlueprint( + trace=Path('annotate.textproto'), + query=f""" + INCLUDE PERFETTO MODULE callstacks.annotate; + + SELECT f.name, c.self_count, c.total_count + FROM _callsite_sample_counts!({_SAMPLES}) c + JOIN stack_profile_callsite sc ON sc.id = c.callsite_id + JOIN stack_profile_frame f ON f.id = sc.frame_id + ORDER BY f.name; + """, + out=Csv(""" + "name","self_count","total_count" + "f1",0,2 + "f2",1,2 + "f3",1,1 + """)) + + def test_sample_counts_by_address(self): + return DiffTestBlueprint( + trace=Path('annotate.textproto'), + query=f""" + INCLUDE PERFETTO MODULE callstacks.annotate; + + SELECT mapping_name, build_id, rel_pc, self_count, total_count + FROM _sample_counts_by_address!({_SAMPLES}) + ORDER BY rel_pc; + """, + out=Csv(""" + "mapping_name","build_id","rel_pc","self_count","total_count" + "/liblib.so","6275696c642d6964",4096,0,2 + "/liblib.so","6275696c642d6964",8193,1,2 + "/liblib.so","6275696c642d6964",12288,1,1 + """)) + + def test_sample_counts_by_source_line(self): + return DiffTestBlueprint( + trace=Path('annotate.textproto'), + query=f""" + INCLUDE PERFETTO MODULE callstacks.annotate; + + SELECT source_file, line_number, self_count, total_count + FROM _sample_counts_by_source_line!({_SAMPLES}) + ORDER BY source_file; + """, + out=Csv(""" + "source_file","line_number","self_count","total_count" + "f1.cc",1,0,2 + "f2.cc",2,1,2 + "f3.cc",33,1,1 + """)) + + def test_disassembly_function_for_address(self): + return DiffTestBlueprint( + trace=Path('annotate.textproto'), + query=""" + INCLUDE PERFETTO MODULE callstacks.annotate; + + SELECT + f.rel_pc, + ( + SELECT name FROM disassembly_function + WHERE id = _disassembly_function_for_address(f.mapping, f.rel_pc) + ) AS function_name + FROM stack_profile_frame f + ORDER BY f.rel_pc; + """, + out=Csv(""" + "rel_pc","function_name" + 4096,"[NULL]" + 8193,"f2" + 12288,"[NULL]" + """)) + + def test_annotated_disassembly(self): + return DiffTestBlueprint( + trace=Path('annotate.textproto'), + query=f""" + INCLUDE PERFETTO MODULE callstacks.annotate; + + SELECT + rel_pc, text, target_rel_pc, target_symbol, source_file, + line_number, self_count, total_count + FROM _annotated_disassembly!( + {_SAMPLES}, + (SELECT id FROM disassembly_function WHERE name = 'f2') + ); + """, + out=Csv(""" + "rel_pc","text","target_rel_pc","target_symbol","source_file","line_number","self_count","total_count" + 8192,"push rbp","[NULL]","[NULL]","f2.cc",1,0,0 + 8193,"call 0x3000",12288,"f3","f2.cc",2,1,2 + 8198,"jmp 0x2000",8192,"[NULL]","f2.cc",3,0,0 + """)) + + def test_callstack_forest_rel_pc(self): + return DiffTestBlueprint( + trace=Path('annotate.textproto'), + query=f""" + INCLUDE PERFETTO MODULE callstacks.stack_profile; + + SELECT name, mapping_name, rel_pc, self_count + FROM _callstacks_for_callsites!({_SAMPLES}) + ORDER BY rel_pc; + """, + out=Csv(""" + "name","mapping_name","rel_pc","self_count" + "f1","/liblib.so",4096,0 + "f2","/liblib.so",8193,1 + "f3","/liblib.so",12288,1 + """))