/* * Copyright (C) 2022 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/profile_builder.h" #include #include #include #include #include #include #include #include #include #include #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/ext/trace_processor/demangle.h" #include "perfetto/protozero/packed_repeated_fields.h" #include "perfetto/protozero/proto_utils.h" #include "perfetto/protozero/scattered_heap_buffer.h" #include "src/trace_processor/containers/null_term_string_view.h" #include "src/trace_processor/containers/string_pool.h" #include "src/trace_processor/db/column/types.h" #include "src/trace_processor/storage/trace_storage.h" #include "src/trace_processor/tables/profiler_tables_py.h" #include "src/trace_processor/types/trace_processor_context.h" #include "src/trace_processor/util/annotated_callsites.h" #include "protos/third_party/pprof/profile.pbzero.h" namespace perfetto::trace_processor { namespace { using protos::pbzero::Stack; using third_party::perftools::profiles::pbzero::Profile; using third_party::perftools::profiles::pbzero::Sample; base::StringView ToString(CallsiteAnnotation annotation) { switch (annotation) { case CallsiteAnnotation::kNone: return ""; case CallsiteAnnotation::kArtAot: return "aot"; case CallsiteAnnotation::kArtInterpreted: return "interp"; case CallsiteAnnotation::kArtJit: return "jit"; case CallsiteAnnotation::kCommonFrame: return "common-frame"; case CallsiteAnnotation::kCommonFrameInterp: return "common-frame-interp"; } PERFETTO_FATAL("For GCC"); } } // namespace GProfileBuilder::StringTable::StringTable( protozero::HeapBuffered* result, const StringPool* string_pool) : string_pool_(*string_pool), result_(*result) { // String at index 0 of the string table must be the empty string (see // profile.proto) int64_t empty_index = WriteString(""); PERFETTO_CHECK(empty_index == kEmptyStringIndex); } int64_t GProfileBuilder::StringTable::InternString(base::StringView str) { if (str.empty()) { return kEmptyStringIndex; } auto hash = str.Hash(); auto it = seen_strings_.find(hash); if (it != seen_strings_.end()) { return it->second; } auto pool_id = string_pool_.GetId(str); int64_t index = pool_id ? InternString(*pool_id) : WriteString(str); seen_strings_.insert({hash, index}); return index; } int64_t GProfileBuilder::StringTable::InternString( StringPool::Id string_pool_id) { auto it = seen_string_pool_ids_.find(string_pool_id); if (it != seen_string_pool_ids_.end()) { return it->second; } NullTermStringView str = string_pool_.Get(string_pool_id); int64_t index = str.empty() ? kEmptyStringIndex : WriteString(str); seen_string_pool_ids_.insert({string_pool_id, index}); return index; } int64_t GProfileBuilder::StringTable::GetAnnotatedString( StringPool::Id str, CallsiteAnnotation annotation) { if (str.is_null() || annotation == CallsiteAnnotation::kNone) { return InternString(str); } return GetAnnotatedString(string_pool_.Get(str), annotation); } int64_t GProfileBuilder::StringTable::GetAnnotatedString( base::StringView str, CallsiteAnnotation annotation) { if (str.empty() || annotation == CallsiteAnnotation::kNone) { return InternString(str); } return InternString(base::StringView( str.ToStdString() + " [" + ToString(annotation).ToStdString() + "]")); } int64_t GProfileBuilder::StringTable::WriteString(base::StringView str) { result_->add_string_table(str.data(), str.size()); return next_index_++; } GProfileBuilder::MappingKey::MappingKey( const tables::StackProfileMappingTable::ConstRowReference& mapping, StringTable& string_table) { size = static_cast(mapping.end() - mapping.start()); file_offset = static_cast(mapping.exact_offset()); build_id_or_filename = string_table.InternString(mapping.build_id()); if (build_id_or_filename == kEmptyStringIndex) { build_id_or_filename = string_table.InternString(mapping.name()); } } GProfileBuilder::Mapping::Mapping( const tables::StackProfileMappingTable::ConstRowReference& mapping, const StringPool& string_pool, StringTable& string_table) : memory_start(static_cast(mapping.start())), memory_limit(static_cast(mapping.end())), file_offset(static_cast(mapping.exact_offset())), filename(string_table.InternString(mapping.name())), build_id(string_table.InternString(mapping.build_id())), filename_str(string_pool.Get(mapping.name()).ToStdString()) {} // Do some very basic scoring. int64_t GProfileBuilder::Mapping::ComputeMainBinaryScore() const { constexpr const char* kBadSuffixes[] = {".so"}; constexpr const char* kBadPrefixes[] = {"/apex", "/system", "/[", "["}; int64_t score = 0; if (build_id != kEmptyStringIndex) { score += 10; } if (filename != kEmptyStringIndex) { score += 10; } if (debug_info.has_functions) { score += 10; } if (debug_info.has_filenames) { score += 10; } if (debug_info.has_line_numbers) { score += 10; } if (debug_info.has_inline_frames) { score += 10; } if (memory_limit == memory_start) { score -= 1000; } for (const char* suffix : kBadSuffixes) { if (base::EndsWith(filename_str, suffix)) { score -= 1000; break; } } for (const char* prefix : kBadPrefixes) { if (base::StartsWith(filename_str, prefix)) { score -= 1000; break; } } return score; } bool GProfileBuilder::SampleAggregator::AddSample( const protozero::PackedVarInt& location_ids, const std::vector& values) { SerializedLocationId key(location_ids.data(), location_ids.data() + location_ids.size()); std::vector* agg_values = samples_.Find(key); if (!agg_values) { samples_.Insert(std::move(key), values); return true; } // All samples must have the same number of values. if (values.size() != agg_values->size()) { return false; } std::transform(values.begin(), values.end(), agg_values->begin(), agg_values->begin(), std::plus()); return true; } void GProfileBuilder::SampleAggregator::WriteTo(Profile& profile) { protozero::PackedVarInt values; for (auto it = samples_.GetIterator(); it; ++it) { values.Reset(); for (int64_t value : it.value()) { values.Append(value); } Sample* sample = profile.add_sample(); sample->set_value(values); // Map key is the serialized varint. Just append the bytes. sample->AppendBytes(Sample::kLocationIdFieldNumber, it.key().data(), it.key().size()); } } GProfileBuilder::GProfileBuilder(const TraceProcessorContext* context, const std::vector& sample_types) : context_(*context), string_table_(&result_, &context->storage->string_pool()), annotations_(context) { // Make sure the empty function always gets id 0 which will be ignored // when writing the proto file. functions_.insert( {Function{kEmptyStringIndex, kEmptyStringIndex, kEmptyStringIndex}, kNullFunctionId}); WriteSampleTypes(sample_types); } GProfileBuilder::~GProfileBuilder() = default; void GProfileBuilder::WriteSampleTypes( const std::vector& sample_types) { for (const auto& value_type : sample_types) { // Write strings first int64_t type = string_table_.InternString(base::StringView(value_type.type)); int64_t unit = string_table_.InternString(base::StringView(value_type.unit)); // Add message later, remember protozero does not allow you to // interleave these write calls. auto* sample_type = result_->add_sample_type(); sample_type->set_type(type); sample_type->set_unit(unit); } } bool GProfileBuilder::AddSample(const Stack::Decoder& stack, const std::vector& values) { PERFETTO_CHECK(!finalized_); auto it = stack.entries(); if (!it) { return true; } auto next = it; ++next; if (!next) { Stack::Entry::Decoder entry(it->as_bytes()); if (entry.has_callsite_id() || entry.has_annotated_callsite_id()) { bool annotated = entry.has_annotated_callsite_id(); uint32_t callsite_id = entry.has_callsite_id() ? entry.callsite_id() : entry.annotated_callsite_id(); return samples_.AddSample( GetLocationIdsForCallsite(CallsiteId(callsite_id), annotated), values); } } // Note pprof orders the stacks leafs first. That is also the ordering // StackBlob uses for entries protozero::PackedVarInt location_ids; for (; it; ++it) { Stack::Entry::Decoder entry(it->as_bytes()); if (entry.has_name()) { location_ids.Append( WriteFakeLocationIfNeeded(entry.name().ToStdString())); } else if (entry.has_callsite_id() || entry.has_annotated_callsite_id()) { bool annotated = entry.has_annotated_callsite_id(); uint32_t callsite_id = entry.has_callsite_id() ? entry.callsite_id() : entry.annotated_callsite_id(); const protozero::PackedVarInt& ids = GetLocationIdsForCallsite(CallsiteId(callsite_id), annotated); for (const uint8_t* p = ids.data(); p < ids.data() + ids.size();) { uint64_t location_id; p = protozero::proto_utils::ParseVarInt(p, ids.data() + ids.size(), &location_id); location_ids.Append(location_id); } } else if (entry.has_frame_id()) { location_ids.Append(WriteLocationIfNeeded(FrameId(entry.frame_id()), CallsiteAnnotation::kNone)); } } return samples_.AddSample(location_ids, values); } void GProfileBuilder::Finalize() { if (finalized_) { return; } WriteMappings(); WriteFunctions(); WriteLocations(); samples_.WriteTo(*result_.get()); finalized_ = true; } std::string GProfileBuilder::Build() { Finalize(); return result_.SerializeAsString(); } const protozero::PackedVarInt& GProfileBuilder::GetLocationIdsForCallsite( const CallsiteId& callsite_id, bool annotated) { auto it = cached_location_ids_.find({callsite_id, annotated}); if (it != cached_location_ids_.end()) { return it->second; } protozero::PackedVarInt& location_ids = cached_location_ids_[{callsite_id, annotated}]; const auto& cs_table = context_.storage->stack_profile_callsite_table(); std::optional start_ref = cs_table.FindById(callsite_id); if (!start_ref) { return location_ids; } location_ids.Append(WriteLocationIfNeeded( start_ref->frame_id(), annotated ? annotations_.GetAnnotation(*start_ref) : CallsiteAnnotation::kNone)); std::optional parent_id = start_ref->parent_id(); while (parent_id) { auto parent_ref = cs_table.FindById(*parent_id); location_ids.Append(WriteLocationIfNeeded( parent_ref->frame_id(), annotated ? annotations_.GetAnnotation(*parent_ref) : CallsiteAnnotation::kNone)); parent_id = parent_ref->parent_id(); } return location_ids; } uint64_t GProfileBuilder::WriteLocationIfNeeded(FrameId frame_id, CallsiteAnnotation annotation) { AnnotatedFrameId key{frame_id, annotation}; auto it = seen_locations_.find(key); if (it != seen_locations_.end()) { return it->second; } const auto& frames = context_.storage->stack_profile_frame_table(); auto frame = *frames.FindById(key.frame_id); const auto& mappings = context_.storage->stack_profile_mapping_table(); auto mapping = *mappings.FindById(frame.mapping()); uint64_t mapping_id = WriteMappingIfNeeded(mapping); uint64_t& id = locations_[Location{mapping_id, static_cast(frame.rel_pc()), GetLines(frame, key.annotation, mapping_id)}]; if (id == 0) { id = locations_.size(); } seen_locations_.insert({key, id}); return id; } uint64_t GProfileBuilder::WriteFakeLocationIfNeeded(const std::string& name) { int64_t name_id = string_table_.InternString(base::StringView(name)); auto it = seen_fake_locations_.find(name_id); if (it != seen_fake_locations_.end()) { return it->second; } uint64_t& id = locations_[Location{0, 0, {{WriteFakeFunctionIfNeeded(name_id), 0}}}]; if (id == 0) { id = locations_.size(); } seen_fake_locations_.insert({name_id, id}); return id; } void GProfileBuilder::WriteLocations() { for (const auto& entry : locations_) { auto* location = result_->add_location(); location->set_id(entry.second); location->set_mapping_id(entry.first.mapping_id); if (entry.first.mapping_id != 0) { location->set_address(entry.first.rel_pc + GetMapping(entry.first.mapping_id).memory_start); } for (const Line& line : entry.first.lines) { auto* l = location->add_line(); l->set_function_id(line.function_id); if (line.line != 0) { l->set_line(line.line); } } } } std::vector GProfileBuilder::GetLines( const tables::StackProfileFrameTable::ConstRowReference& frame, CallsiteAnnotation annotation, uint64_t mapping_id) { std::vector lines; lines = GetLinesForJitFrame(frame, annotation, mapping_id); if (!lines.empty()) { return lines; } lines = GetLinesForSymbolSetId(frame.symbol_set_id(), annotation, mapping_id); if (!lines.empty()) { return lines; } if (uint64_t function_id = WriteFunctionIfNeeded(frame, annotation, mapping_id); function_id != kNullFunctionId) { lines.push_back({function_id, 0}); } return lines; } namespace { template std::optional GetByConstraint( const Table& table, Constraint&& c) { Query q; q.constraints = {c}; q.limit = 1; auto row_map = table.QueryToRowMap(q); if (row_map.empty()) return {}; return table[row_map.Get(0)]; } } // namespace std::vector GProfileBuilder::GetLinesForJitFrame( const tables::StackProfileFrameTable::ConstRowReference& frame, CallsiteAnnotation annotation, uint64_t mapping_id) { // Execute the equivalent of the SQL logic in callstacks/stack_profile.sql, // namely // // ``` // COALESCE( // 'JS: ' || IIF(jsf.name = "", "(anonymous)", jsf.name) || ':' || // jsf.line || ':' || jsf.col || ' [' || LOWER(jsc.tier) || ']', 'WASM: ' // || wc.function_name || ' [' || LOWER(wc.tier) || ']', 'REGEXP: ' || // rc.pattern, 'V8: ' || v8c.function_name, 'JIT: ' || jc.function_name // ) AS name, // FROM _callstack_spc_raw_forest c // JOIN stack_profile_frame f ON c.frame_id = f.id // LEFT JOIN _v8_js_code jsc USING(jit_code_id) // LEFT JOIN v8_js_function jsf USING(v8_js_function_id) // LEFT JOIN _v8_internal_code v8c USING(jit_code_id) // LEFT JOIN _v8_wasm_code wc USING(jit_code_id) // LEFT JOIN _v8_regexp_code rc USING(jit_code_id) // LEFT JOIN __intrinsic_jit_code jc ON c.jit_code_id = jc.id // ``` // // TODO(leszeks): Unify these manual table lookups with the SQL // implementation, e.g. by calling _callstacks_for_callsites in // GProfileBuilder. const auto& jit_frames = context_.storage->jit_frame_table(); auto maybe_jf = GetByConstraint(jit_frames, jit_frames.frame_id().eq(frame.id().value)); if (!maybe_jf) { return {}; } auto jf = *maybe_jf; const auto& v8_js_code = context_.storage->v8_js_code_table(); auto maybe_jsc = GetByConstraint( v8_js_code, v8_js_code.jit_code_id().eq(jf.jit_code_id().value)); if (maybe_jsc) { auto jsc = *maybe_jsc; const auto& v8_js_funcs = context_.storage->v8_js_function_table(); auto maybe_jsf = v8_js_funcs.FindById(jsc.v8_js_function_id()); if (maybe_jsf) { auto jsf = *maybe_jsf; auto jsf_name = context_.storage->string_pool().Get(jsf.name()); auto jsf_tier = context_.storage->string_pool().Get(jsc.tier()); base::StackString<64> name( "JS: %s:%u:%u [%s]", jsf_name.empty() ? "(anonymous)" : jsf_name.c_str(), jsf.line().value_or(0), jsf.col().value_or(0), jsf_tier.c_str()); const auto& v8_js_scripts = context_.storage->v8_js_script_table(); auto maybe_jss = v8_js_scripts.FindById(jsf.v8_js_script_id()); return { Line{WriteFunctionIfNeeded( name.string_view(), maybe_jss.has_value() ? maybe_jss->name() : kNullStringId, annotation, mapping_id), jsf.line().value_or(0)}}; } } const auto& v8_wasm_code = context_.storage->v8_wasm_code_table(); auto maybe_wc = GetByConstraint( v8_wasm_code, v8_wasm_code.jit_code_id().eq(jf.jit_code_id().value)); if (maybe_wc) { auto wc = *maybe_wc; std::string name = "WASM: " + context_.storage->GetString(wc.function_name()).ToStdString(); return {Line{WriteFunctionIfNeeded(base::StringView(name), kNullStringId, annotation, mapping_id), 0}}; } const auto& v8_regexp_code = context_.storage->v8_regexp_code_table(); auto maybe_rc = GetByConstraint( v8_regexp_code, v8_regexp_code.jit_code_id().eq(jf.jit_code_id().value)); if (maybe_rc) { auto rc = *maybe_rc; std::string name = "REGEXP: " + context_.storage->GetString(rc.pattern()).ToStdString(); return {Line{WriteFunctionIfNeeded(base::StringView(name), kNullStringId, annotation, mapping_id), 0}}; } const auto& v8_internal_code = context_.storage->v8_internal_code_table(); auto maybe_v8c = GetByConstraint( v8_internal_code, v8_internal_code.jit_code_id().eq(jf.jit_code_id().value)); if (maybe_v8c) { auto v8c = *maybe_v8c; std::string name = "V8: " + context_.storage->GetString(v8c.function_name()).ToStdString(); return {Line{WriteFunctionIfNeeded(base::StringView(name), kNullStringId, annotation, mapping_id), 0}}; } const auto& jit_code = context_.storage->jit_code_table(); auto maybe_jc = jit_code.FindById(jf.jit_code_id()); if (maybe_jc) { auto jc = *maybe_jc; std::string name = "JIT: " + context_.storage->GetString(jc.function_name()).ToStdString(); return {Line{WriteFunctionIfNeeded(base::StringView(name), kNullStringId, annotation, mapping_id), 0}}; } return {}; } std::vector GProfileBuilder::GetLinesForSymbolSetId( std::optional symbol_set_id, CallsiteAnnotation annotation, uint64_t mapping_id) { if (!symbol_set_id) { return {}; } const auto& symbols = context_.storage->symbol_table(); using RowRef = perfetto::trace_processor::tables::SymbolTable::ConstRowReference; std::vector symbol_set; Query q; q.constraints = {symbols.symbol_set_id().eq(*symbol_set_id)}; for (auto it = symbols.FilterToIterator(q); it; ++it) { symbol_set.push_back(it.row_reference()); } std::sort(symbol_set.begin(), symbol_set.end(), [](const RowRef& a, const RowRef& b) { return a.id() < b.id(); }); std::vector lines; for (const RowRef& symbol : symbol_set) { if (uint64_t function_id = WriteFunctionIfNeeded(symbol, annotation, mapping_id); function_id != kNullFunctionId) { lines.push_back({function_id, symbol.line_number().value_or(0)}); } } GetMapping(mapping_id).debug_info.has_inline_frames = true; GetMapping(mapping_id).debug_info.has_line_numbers = true; return lines; } uint64_t GProfileBuilder::WriteFakeFunctionIfNeeded(int64_t name_id) { auto ins = functions_.insert( {Function{name_id, kEmptyStringIndex, kEmptyStringIndex}, functions_.size() + 1}); return ins.first->second; } uint64_t GProfileBuilder::WriteFunctionIfNeeded(base::StringView name, StringPool::Id filename, CallsiteAnnotation annotation, uint64_t mapping_id) { int64_t name_id = string_table_.GetAnnotatedString(name, annotation); int64_t filename_id = !filename.is_null() ? string_table_.InternString(filename) : kEmptyStringIndex; auto ins = functions_.insert({Function{name_id, kEmptyStringIndex, filename_id}, functions_.size() + 1}); uint64_t id = ins.first->second; if (ins.second) { if (name_id != kEmptyStringIndex) { GetMapping(mapping_id).debug_info.has_functions = true; } if (filename_id != kEmptyStringIndex) { GetMapping(mapping_id).debug_info.has_filenames = true; } } return id; } uint64_t GProfileBuilder::WriteFunctionIfNeeded( const tables::SymbolTable::ConstRowReference& symbol, CallsiteAnnotation annotation, uint64_t mapping_id) { int64_t name = string_table_.GetAnnotatedString(symbol.name(), annotation); int64_t filename = symbol.source_file().has_value() ? string_table_.InternString(*symbol.source_file()) : kEmptyStringIndex; auto ins = functions_.insert( {Function{name, kEmptyStringIndex, filename}, functions_.size() + 1}); uint64_t id = ins.first->second; if (ins.second) { if (name != kEmptyStringIndex) { GetMapping(mapping_id).debug_info.has_functions = true; } if (filename != kEmptyStringIndex) { GetMapping(mapping_id).debug_info.has_filenames = true; } } return id; } int64_t GProfileBuilder::GetNameForFrame( const tables::StackProfileFrameTable::ConstRowReference& frame, CallsiteAnnotation annotation) { NullTermStringView system_name = context_.storage->GetString(frame.name()); int64_t name = kEmptyStringIndex; if (frame.deobfuscated_name()) { name = string_table_.GetAnnotatedString(*frame.deobfuscated_name(), annotation); } else if (!system_name.empty()) { std::unique_ptr demangled = demangle::Demangle(system_name.c_str()); if (demangled) { name = string_table_.GetAnnotatedString(demangled.get(), annotation); } else { // demangling failed, expected if the name wasn't mangled. In any case // reuse the system_name as this is what UI will usually display. name = string_table_.GetAnnotatedString(frame.name(), annotation); } } return name; } int64_t GProfileBuilder::GetSystemNameForFrame( const tables::StackProfileFrameTable::ConstRowReference& frame) { return string_table_.InternString(frame.name()); } uint64_t GProfileBuilder::WriteFunctionIfNeeded( const tables::StackProfileFrameTable::ConstRowReference& frame, CallsiteAnnotation annotation, uint64_t mapping_id) { AnnotatedFrameId key{frame.id(), annotation}; auto it = seen_functions_.find(key); if (it != seen_functions_.end()) { return it->second; } auto ins = functions_.insert( {Function{GetNameForFrame(frame, annotation), GetSystemNameForFrame(frame), kEmptyStringIndex}, functions_.size() + 1}); uint64_t id = ins.first->second; seen_functions_.insert({key, id}); if (ins.second && (ins.first->first.name != kEmptyStringIndex || ins.first->first.system_name != kEmptyStringIndex)) { GetMapping(mapping_id).debug_info.has_functions = true; } return id; } void GProfileBuilder::WriteFunctions() { for (const auto& entry : functions_) { if (entry.second == kNullFunctionId) { continue; } auto* func = result_->add_function(); func->set_id(entry.second); if (entry.first.name != 0) { func->set_name(entry.first.name); } if (entry.first.system_name != 0) { func->set_system_name(entry.first.system_name); } if (entry.first.filename != 0) { func->set_filename(entry.first.filename); } } } uint64_t GProfileBuilder::WriteMappingIfNeeded( const tables::StackProfileMappingTable::ConstRowReference& mapping_ref) { auto it = seen_mappings_.find(mapping_ref.id()); if (it != seen_mappings_.end()) { return it->second; } auto ins = mapping_keys_.insert( {MappingKey(mapping_ref, string_table_), mapping_keys_.size() + 1}); if (ins.second) { mappings_.emplace_back(mapping_ref, context_.storage->string_pool(), string_table_); } return ins.first->second; } void GProfileBuilder::WriteMapping(uint64_t mapping_id) { const Mapping& mapping = GetMapping(mapping_id); auto* m = result_->add_mapping(); m->set_id(mapping_id); m->set_memory_start(mapping.memory_start); m->set_memory_limit(mapping.memory_limit); m->set_file_offset(mapping.file_offset); m->set_filename(mapping.filename); m->set_build_id(mapping.build_id); m->set_has_functions(mapping.debug_info.has_functions); m->set_has_filenames(mapping.debug_info.has_filenames); m->set_has_line_numbers(mapping.debug_info.has_line_numbers); m->set_has_inline_frames(mapping.debug_info.has_inline_frames); } void GProfileBuilder::WriteMappings() { // The convention in pprof files is to write the mapping for the main // binary first. So lets do just that. std::optional main_mapping_id = GuessMainBinary(); if (main_mapping_id) { WriteMapping(*main_mapping_id); } for (size_t i = 0; i < mappings_.size(); ++i) { uint64_t mapping_id = i + 1; if (main_mapping_id && *main_mapping_id == mapping_id) { continue; } WriteMapping(mapping_id); } } std::optional GProfileBuilder::GuessMainBinary() const { std::vector mapping_scores; mapping_scores.reserve(mappings_.size()); for (const auto& mapping : mappings_) { mapping_scores.push_back(mapping.ComputeMainBinaryScore()); } auto it = std::max_element(mapping_scores.begin(), mapping_scores.end()); if (it == mapping_scores.end()) { return std::nullopt; } return static_cast(std::distance(mapping_scores.begin(), it) + 1); } } // namespace perfetto::trace_processor