1 /*
2 * Copyright (C) 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 #include "src/trace_processor/util/profile_builder.h"
18 #include <algorithm>
19 #include <cstdint>
20 #include <deque>
21 #include <iostream>
22 #include <iterator>
23 #include <optional>
24 #include <vector>
25
26 #include "perfetto/base/logging.h"
27 #include "perfetto/ext/base/string_utils.h"
28 #include "perfetto/ext/base/string_view.h"
29 #include "perfetto/ext/trace_processor/demangle.h"
30 #include "src/trace_processor/containers/null_term_string_view.h"
31 #include "src/trace_processor/containers/string_pool.h"
32 #include "src/trace_processor/storage/trace_storage.h"
33 #include "src/trace_processor/types/trace_processor_context.h"
34 #include "src/trace_processor/util/annotated_callsites.h"
35
36 namespace perfetto {
37 namespace trace_processor {
38 namespace {
39
40 using protos::pbzero::Stack;
41
ToString(CallsiteAnnotation annotation)42 base::StringView ToString(CallsiteAnnotation annotation) {
43 switch (annotation) {
44 case CallsiteAnnotation::kNone:
45 return "";
46 case CallsiteAnnotation::kArtAot:
47 return "aot";
48 case CallsiteAnnotation::kArtInterpreted:
49 return "interp";
50 case CallsiteAnnotation::kArtJit:
51 return "jit";
52 case CallsiteAnnotation::kCommonFrame:
53 return "common-frame";
54 case CallsiteAnnotation::kCommonFrameInterp:
55 return "common-frame-interp";
56 }
57 PERFETTO_FATAL("For GCC");
58 }
59
60 } // namespace
61
StringTable(protozero::HeapBuffered<third_party::perftools::profiles::pbzero::Profile> * result,const StringPool * string_pool)62 GProfileBuilder::StringTable::StringTable(
63 protozero::HeapBuffered<third_party::perftools::profiles::pbzero::Profile>*
64 result,
65 const StringPool* string_pool)
66 : string_pool_(*string_pool), result_(*result) {
67 // String at index 0 of the string table must be the empty string (see
68 // profile.proto)
69 int64_t empty_index = WriteString("");
70 PERFETTO_CHECK(empty_index == kEmptyStringIndex);
71 }
72
InternString(base::StringView str)73 int64_t GProfileBuilder::StringTable::InternString(base::StringView str) {
74 if (str.empty()) {
75 return kEmptyStringIndex;
76 }
77 auto hash = str.Hash();
78 auto it = seen_strings_.find(hash);
79 if (it != seen_strings_.end()) {
80 return it->second;
81 }
82
83 auto pool_id = string_pool_.GetId(str);
84 int64_t index = pool_id ? InternString(*pool_id) : WriteString(str);
85
86 seen_strings_.insert({hash, index});
87 return index;
88 }
89
InternString(StringPool::Id string_pool_id)90 int64_t GProfileBuilder::StringTable::InternString(
91 StringPool::Id string_pool_id) {
92 auto it = seen_string_pool_ids_.find(string_pool_id);
93 if (it != seen_string_pool_ids_.end()) {
94 return it->second;
95 }
96
97 NullTermStringView str = string_pool_.Get(string_pool_id);
98
99 int64_t index = str.empty() ? kEmptyStringIndex : WriteString(str);
100 seen_string_pool_ids_.insert({string_pool_id, index});
101 return index;
102 }
103
GetAnnotatedString(StringPool::Id str,CallsiteAnnotation annotation)104 int64_t GProfileBuilder::StringTable::GetAnnotatedString(
105 StringPool::Id str,
106 CallsiteAnnotation annotation) {
107 if (str.is_null() || annotation == CallsiteAnnotation::kNone) {
108 return InternString(str);
109 }
110 return GetAnnotatedString(string_pool_.Get(str), annotation);
111 }
112
GetAnnotatedString(base::StringView str,CallsiteAnnotation annotation)113 int64_t GProfileBuilder::StringTable::GetAnnotatedString(
114 base::StringView str,
115 CallsiteAnnotation annotation) {
116 if (str.empty() || annotation == CallsiteAnnotation::kNone) {
117 return InternString(str);
118 }
119 return InternString(base::StringView(
120 str.ToStdString() + " [" + ToString(annotation).ToStdString() + "]"));
121 }
122
WriteString(base::StringView str)123 int64_t GProfileBuilder::StringTable::WriteString(base::StringView str) {
124 result_->add_string_table(str.data(), str.size());
125 return next_index_++;
126 }
127
MappingKey(const tables::StackProfileMappingTable::ConstRowReference & mapping,StringTable & string_table)128 GProfileBuilder::MappingKey::MappingKey(
129 const tables::StackProfileMappingTable::ConstRowReference& mapping,
130 StringTable& string_table) {
131 size = static_cast<uint64_t>(mapping.end() - mapping.start());
132 file_offset = static_cast<uint64_t>(mapping.exact_offset());
133 build_id_or_filename = string_table.InternString(mapping.build_id());
134 if (build_id_or_filename == kEmptyStringIndex) {
135 build_id_or_filename = string_table.InternString(mapping.name());
136 }
137 }
138
Mapping(const tables::StackProfileMappingTable::ConstRowReference & mapping,const StringPool & string_pool,StringTable & string_table)139 GProfileBuilder::Mapping::Mapping(
140 const tables::StackProfileMappingTable::ConstRowReference& mapping,
141 const StringPool& string_pool,
142 StringTable& string_table)
143 : memory_start(static_cast<uint64_t>(mapping.start())),
144 memory_limit(static_cast<uint64_t>(mapping.end())),
145 file_offset(static_cast<uint64_t>(mapping.exact_offset())),
146 filename(string_table.InternString(mapping.name())),
147 build_id(string_table.InternString(mapping.build_id())),
148 filename_str(string_pool.Get(mapping.name()).ToStdString()) {}
149
150 // Do some very basic scoring.
ComputeMainBinaryScore() const151 int64_t GProfileBuilder::Mapping::ComputeMainBinaryScore() const {
152 constexpr const char* kBadSuffixes[] = {".so"};
153 constexpr const char* kBadPrefixes[] = {"/apex", "/system", "/[", "["};
154
155 int64_t score = 0;
156 if (build_id != kEmptyStringIndex) {
157 score += 10;
158 }
159
160 if (filename != kEmptyStringIndex) {
161 score += 10;
162 }
163
164 if (debug_info.has_functions) {
165 score += 10;
166 }
167 if (debug_info.has_filenames) {
168 score += 10;
169 }
170 if (debug_info.has_line_numbers) {
171 score += 10;
172 }
173 if (debug_info.has_inline_frames) {
174 score += 10;
175 }
176
177 if (memory_limit == memory_start) {
178 score -= 1000;
179 }
180
181 for (const char* suffix : kBadSuffixes) {
182 if (base::EndsWith(filename_str, suffix)) {
183 score -= 1000;
184 break;
185 }
186 }
187
188 for (const char* prefix : kBadPrefixes) {
189 if (base::StartsWith(filename_str, prefix)) {
190 score -= 1000;
191 break;
192 }
193 }
194
195 return score;
196 }
197
GProfileBuilder(const TraceProcessorContext * context,const std::vector<ValueType> & sample_types)198 GProfileBuilder::GProfileBuilder(const TraceProcessorContext* context,
199 const std::vector<ValueType>& sample_types)
200 : context_(*context),
201 string_table_(&result_, &context->storage->string_pool()),
202 annotations_(context) {
203 // Make sure the empty function always gets id 0 which will be ignored when
204 // writing the proto file.
205 functions_.insert(
206 {Function{kEmptyStringIndex, kEmptyStringIndex, kEmptyStringIndex},
207 kNullFunctionId});
208 WriteSampleTypes(sample_types);
209 }
210
211 GProfileBuilder::~GProfileBuilder() = default;
212
WriteSampleTypes(const std::vector<ValueType> & sample_types)213 void GProfileBuilder::WriteSampleTypes(
214 const std::vector<ValueType>& sample_types) {
215 for (const auto& value_type : sample_types) {
216 // Write strings first
217 int64_t type =
218 string_table_.InternString(base::StringView(value_type.type));
219 int64_t unit =
220 string_table_.InternString(base::StringView(value_type.unit));
221 // Add message later, remember protozero does not allow you to interleave
222 // these write calls.
223 auto* sample_type = result_->add_sample_type();
224 sample_type->set_type(type);
225 sample_type->set_unit(unit);
226 }
227 }
228
AddSample(const protozero::PackedVarInt & location_ids,const protozero::PackedVarInt & values)229 bool GProfileBuilder::AddSample(const protozero::PackedVarInt& location_ids,
230 const protozero::PackedVarInt& values) {
231 PERFETTO_CHECK(!finalized_);
232 if (location_ids.size() == 0) {
233 return false;
234 }
235 auto* sample = result_->add_sample();
236 sample->set_value(values);
237 sample->set_location_id(location_ids);
238 return true;
239 }
240
AddSample(const Stack::Decoder & stack,const protozero::PackedVarInt & values)241 bool GProfileBuilder::AddSample(const Stack::Decoder& stack,
242 const protozero::PackedVarInt& values) {
243 PERFETTO_CHECK(!finalized_);
244
245 auto it = stack.entries();
246 if (!it) {
247 return true;
248 }
249
250 auto next = it;
251 ++next;
252 if (!next) {
253 Stack::Entry::Decoder entry(it->as_bytes());
254 if (entry.has_callsite_id() || entry.has_annotated_callsite_id()) {
255 bool annotated = entry.has_annotated_callsite_id();
256 uint32_t callsite_id = entry.has_callsite_id()
257 ? entry.callsite_id()
258 : entry.annotated_callsite_id();
259 return AddSample(
260 GetLocationIdsForCallsite(CallsiteId(callsite_id), annotated),
261 values);
262 }
263 }
264
265 // Note pprof orders the stacks leafs first. That is also the ordering
266 // StackBlob uses for entries
267 protozero::PackedVarInt location_ids;
268 for (; it; ++it) {
269 Stack::Entry::Decoder entry(it->as_bytes());
270 if (entry.has_name()) {
271 location_ids.Append(
272 WriteFakeLocationIfNeeded(entry.name().ToStdString()));
273 } else if (entry.has_callsite_id() || entry.has_annotated_callsite_id()) {
274 bool annotated = entry.has_annotated_callsite_id();
275 uint32_t callsite_id = entry.has_callsite_id()
276 ? entry.callsite_id()
277 : entry.annotated_callsite_id();
278 const protozero::PackedVarInt& ids =
279 GetLocationIdsForCallsite(CallsiteId(callsite_id), annotated);
280 for (auto* p = ids.data(); p < ids.data() + ids.size();) {
281 uint64_t location_id;
282 p = protozero::proto_utils::ParseVarInt(p, ids.data() + ids.size(),
283 &location_id);
284 location_ids.Append(location_id);
285 }
286 } else if (entry.has_frame_id()) {
287 location_ids.Append(WriteLocationIfNeeded(FrameId(entry.frame_id()),
288 CallsiteAnnotation::kNone));
289 }
290 }
291 return AddSample(location_ids, values);
292 }
293
Finalize()294 void GProfileBuilder::Finalize() {
295 if (finalized_) {
296 return;
297 }
298 WriteMappings();
299 WriteFunctions();
300 WriteLocations();
301 finalized_ = true;
302 }
303
Build()304 std::string GProfileBuilder::Build() {
305 Finalize();
306 return result_.SerializeAsString();
307 }
308
GetLocationIdsForCallsite(const CallsiteId & callsite_id,bool annotated)309 const protozero::PackedVarInt& GProfileBuilder::GetLocationIdsForCallsite(
310 const CallsiteId& callsite_id,
311 bool annotated) {
312 auto it = cached_location_ids_.find({callsite_id, annotated});
313 if (it != cached_location_ids_.end()) {
314 return it->second;
315 }
316
317 protozero::PackedVarInt& location_ids =
318 cached_location_ids_[{callsite_id, annotated}];
319
320 const auto& cs_table = context_.storage->stack_profile_callsite_table();
321
322 std::optional<tables::StackProfileCallsiteTable::ConstRowReference>
323 start_ref = cs_table.FindById(callsite_id);
324 if (!start_ref) {
325 return location_ids;
326 }
327
328 location_ids.Append(WriteLocationIfNeeded(
329 start_ref->frame_id(), annotated ? annotations_.GetAnnotation(*start_ref)
330 : CallsiteAnnotation::kNone));
331
332 std::optional<CallsiteId> parent_id = start_ref->parent_id();
333 while (parent_id) {
334 auto parent_ref = cs_table.FindById(*parent_id);
335 location_ids.Append(WriteLocationIfNeeded(
336 parent_ref->frame_id(), annotated
337 ? annotations_.GetAnnotation(*parent_ref)
338 : CallsiteAnnotation::kNone));
339 parent_id = parent_ref->parent_id();
340 }
341
342 return location_ids;
343 }
344
WriteLocationIfNeeded(FrameId frame_id,CallsiteAnnotation annotation)345 uint64_t GProfileBuilder::WriteLocationIfNeeded(FrameId frame_id,
346 CallsiteAnnotation annotation) {
347 AnnotatedFrameId key{frame_id, annotation};
348 auto it = seen_locations_.find(key);
349 if (it != seen_locations_.end()) {
350 return it->second;
351 }
352
353 auto& frames = context_.storage->stack_profile_frame_table();
354 auto frame = *frames.FindById(key.frame_id);
355
356 const auto& mappings = context_.storage->stack_profile_mapping_table();
357 auto mapping = *mappings.FindById(frame.mapping());
358 uint64_t mapping_id = WriteMappingIfNeeded(mapping);
359
360 uint64_t& id =
361 locations_[Location{mapping_id, static_cast<uint64_t>(frame.rel_pc()),
362 GetLines(frame, key.annotation, mapping_id)}];
363
364 if (id == 0) {
365 id = locations_.size();
366 }
367
368 seen_locations_.insert({key, id});
369
370 return id;
371 }
372
WriteFakeLocationIfNeeded(const std::string & name)373 uint64_t GProfileBuilder::WriteFakeLocationIfNeeded(const std::string& name) {
374 int64_t name_id = string_table_.InternString(base::StringView(name));
375 auto it = seen_fake_locations_.find(name_id);
376 if (it != seen_fake_locations_.end()) {
377 return it->second;
378 }
379
380 uint64_t& id =
381 locations_[Location{0, 0, {{WriteFakeFunctionIfNeeded(name_id), 0}}}];
382
383 if (id == 0) {
384 id = locations_.size();
385 }
386
387 seen_fake_locations_.insert({name_id, id});
388
389 return id;
390 }
391
WriteLocations()392 void GProfileBuilder::WriteLocations() {
393 for (const auto& entry : locations_) {
394 auto* location = result_->add_location();
395 location->set_id(entry.second);
396 location->set_mapping_id(entry.first.mapping_id);
397 if (entry.first.mapping_id != 0) {
398 location->set_address(entry.first.rel_pc +
399 GetMapping(entry.first.mapping_id).memory_start);
400 }
401 for (const Line& line : entry.first.lines) {
402 auto* l = location->add_line();
403 l->set_function_id(line.function_id);
404 if (line.line != 0) {
405 l->set_line(line.line);
406 }
407 }
408 }
409 }
410
GetLines(const tables::StackProfileFrameTable::ConstRowReference & frame,CallsiteAnnotation annotation,uint64_t mapping_id)411 std::vector<GProfileBuilder::Line> GProfileBuilder::GetLines(
412 const tables::StackProfileFrameTable::ConstRowReference& frame,
413 CallsiteAnnotation annotation,
414 uint64_t mapping_id) {
415 std::vector<Line> lines =
416 GetLinesForSymbolSetId(frame.symbol_set_id(), annotation, mapping_id);
417 if (!lines.empty()) {
418 return lines;
419 }
420
421 if (uint64_t function_id =
422 WriteFunctionIfNeeded(frame, annotation, mapping_id);
423 function_id != kNullFunctionId) {
424 lines.push_back({function_id, 0});
425 }
426
427 return lines;
428 }
429
GetLinesForSymbolSetId(std::optional<uint32_t> symbol_set_id,CallsiteAnnotation annotation,uint64_t mapping_id)430 std::vector<GProfileBuilder::Line> GProfileBuilder::GetLinesForSymbolSetId(
431 std::optional<uint32_t> symbol_set_id,
432 CallsiteAnnotation annotation,
433 uint64_t mapping_id) {
434 if (!symbol_set_id) {
435 return {};
436 }
437
438 auto& symbols = context_.storage->symbol_table();
439
440 using RowRef =
441 perfetto::trace_processor::tables::SymbolTable::ConstRowReference;
442 std::vector<RowRef> symbol_set;
443 for (auto it = symbols.FilterToIterator(
444 {symbols.symbol_set_id().eq(*symbol_set_id)});
445 it; ++it) {
446 symbol_set.push_back(it.row_reference());
447 }
448
449 std::sort(symbol_set.begin(), symbol_set.end(),
450 [](const RowRef& a, const RowRef& b) { return a.id() < b.id(); });
451
452 std::vector<GProfileBuilder::Line> lines;
453 for (const RowRef& symbol : symbol_set) {
454 if (uint64_t function_id =
455 WriteFunctionIfNeeded(symbol, annotation, mapping_id);
456 function_id != kNullFunctionId) {
457 lines.push_back({function_id, symbol.line_number()});
458 }
459 }
460
461 GetMapping(mapping_id).debug_info.has_inline_frames = true;
462 GetMapping(mapping_id).debug_info.has_line_numbers = true;
463
464 return lines;
465 }
466
WriteFakeFunctionIfNeeded(int64_t name_id)467 uint64_t GProfileBuilder::WriteFakeFunctionIfNeeded(int64_t name_id) {
468 auto ins = functions_.insert(
469 {Function{name_id, kEmptyStringIndex, kEmptyStringIndex},
470 functions_.size() + 1});
471 return ins.first->second;
472 }
473
WriteFunctionIfNeeded(const tables::SymbolTable::ConstRowReference & symbol,CallsiteAnnotation annotation,uint64_t mapping_id)474 uint64_t GProfileBuilder::WriteFunctionIfNeeded(
475 const tables::SymbolTable::ConstRowReference& symbol,
476 CallsiteAnnotation annotation,
477 uint64_t mapping_id) {
478 int64_t name = string_table_.GetAnnotatedString(symbol.name(), annotation);
479 int64_t filename = string_table_.InternString(symbol.source_file());
480
481 auto ins = functions_.insert(
482 {Function{name, kEmptyStringIndex, filename}, functions_.size() + 1});
483 uint64_t id = ins.first->second;
484
485 if (ins.second) {
486 if (name != kEmptyStringIndex) {
487 GetMapping(mapping_id).debug_info.has_functions = true;
488 }
489 if (filename != kEmptyStringIndex) {
490 GetMapping(mapping_id).debug_info.has_filenames = true;
491 }
492 }
493
494 return id;
495 }
496
GetNameForFrame(const tables::StackProfileFrameTable::ConstRowReference & frame,CallsiteAnnotation annotation)497 int64_t GProfileBuilder::GetNameForFrame(
498 const tables::StackProfileFrameTable::ConstRowReference& frame,
499 CallsiteAnnotation annotation) {
500 NullTermStringView system_name = context_.storage->GetString(frame.name());
501 int64_t name = kEmptyStringIndex;
502 if (frame.deobfuscated_name()) {
503 name = string_table_.GetAnnotatedString(*frame.deobfuscated_name(),
504 annotation);
505 } else if (!system_name.empty()) {
506 std::unique_ptr<char, base::FreeDeleter> demangled =
507 demangle::Demangle(system_name.c_str());
508 if (demangled) {
509 name = string_table_.GetAnnotatedString(demangled.get(), annotation);
510 } else {
511 // demangling failed, expected if the name wasn't mangled. In any case
512 // reuse the system_name as this is what UI will usually display.
513 name = string_table_.GetAnnotatedString(frame.name(), annotation);
514 }
515 }
516 return name;
517 }
518
GetSystemNameForFrame(const tables::StackProfileFrameTable::ConstRowReference & frame)519 int64_t GProfileBuilder::GetSystemNameForFrame(
520 const tables::StackProfileFrameTable::ConstRowReference& frame) {
521 return string_table_.InternString(frame.name());
522 }
523
WriteFunctionIfNeeded(const tables::StackProfileFrameTable::ConstRowReference & frame,CallsiteAnnotation annotation,uint64_t mapping_id)524 uint64_t GProfileBuilder::WriteFunctionIfNeeded(
525 const tables::StackProfileFrameTable::ConstRowReference& frame,
526 CallsiteAnnotation annotation,
527 uint64_t mapping_id) {
528 AnnotatedFrameId key{frame.id(), annotation};
529 auto it = seen_functions_.find(key);
530 if (it != seen_functions_.end()) {
531 return it->second;
532 }
533
534 auto ins = functions_.insert(
535 {Function{GetNameForFrame(frame, annotation),
536 GetSystemNameForFrame(frame), kEmptyStringIndex},
537 functions_.size() + 1});
538 uint64_t id = ins.first->second;
539 seen_functions_.insert({key, id});
540
541 if (ins.second && (ins.first->first.name != kEmptyStringIndex ||
542 ins.first->first.system_name != kEmptyStringIndex)) {
543 GetMapping(mapping_id).debug_info.has_functions = true;
544 }
545
546 return id;
547 }
548
WriteFunctions()549 void GProfileBuilder::WriteFunctions() {
550 for (const auto& entry : functions_) {
551 if (entry.second == kNullFunctionId) {
552 continue;
553 }
554 auto* func = result_->add_function();
555 func->set_id(entry.second);
556 if (entry.first.name != 0) {
557 func->set_name(entry.first.name);
558 }
559 if (entry.first.system_name != 0) {
560 func->set_system_name(entry.first.system_name);
561 }
562 if (entry.first.filename != 0) {
563 func->set_filename(entry.first.filename);
564 }
565 }
566 }
567
WriteMappingIfNeeded(const tables::StackProfileMappingTable::ConstRowReference & mapping_ref)568 uint64_t GProfileBuilder::WriteMappingIfNeeded(
569 const tables::StackProfileMappingTable::ConstRowReference& mapping_ref) {
570 auto it = seen_mappings_.find(mapping_ref.id());
571 if (it != seen_mappings_.end()) {
572 return it->second;
573 }
574
575 auto ins = mapping_keys_.insert(
576 {MappingKey(mapping_ref, string_table_), mapping_keys_.size() + 1});
577
578 if (ins.second) {
579 mappings_.push_back(
580 Mapping(mapping_ref, context_.storage->string_pool(), string_table_));
581 }
582
583 return ins.first->second;
584 }
585
WriteMapping(uint64_t mapping_id)586 void GProfileBuilder::WriteMapping(uint64_t mapping_id) {
587 const Mapping& mapping = GetMapping(mapping_id);
588 auto m = result_->add_mapping();
589 m->set_id(mapping_id);
590 m->set_memory_start(mapping.memory_start);
591 m->set_memory_limit(mapping.memory_limit);
592 m->set_file_offset(mapping.file_offset);
593 m->set_filename(mapping.filename);
594 m->set_build_id(mapping.build_id);
595 m->set_has_functions(mapping.debug_info.has_functions);
596 m->set_has_filenames(mapping.debug_info.has_filenames);
597 m->set_has_line_numbers(mapping.debug_info.has_line_numbers);
598 m->set_has_inline_frames(mapping.debug_info.has_inline_frames);
599 }
600
WriteMappings()601 void GProfileBuilder::WriteMappings() {
602 // The convention in pprof files is to write the mapping for the main binary
603 // first. So lets do just that.
604 std::optional<uint64_t> main_mapping_id = GuessMainBinary();
605 if (main_mapping_id) {
606 WriteMapping(*main_mapping_id);
607 }
608
609 for (size_t i = 0; i < mappings_.size(); ++i) {
610 uint64_t mapping_id = i + 1;
611 if (main_mapping_id && *main_mapping_id == mapping_id) {
612 continue;
613 }
614 WriteMapping(mapping_id);
615 }
616 }
617
GuessMainBinary() const618 std::optional<uint64_t> GProfileBuilder::GuessMainBinary() const {
619 std::vector<int64_t> mapping_scores;
620
621 for (const auto& mapping : mappings_) {
622 mapping_scores.push_back(mapping.ComputeMainBinaryScore());
623 }
624
625 auto it = std::max_element(mapping_scores.begin(), mapping_scores.end());
626
627 if (it == mapping_scores.end()) {
628 return std::nullopt;
629 }
630
631 return static_cast<uint64_t>(std::distance(mapping_scores.begin(), it) + 1);
632 }
633
634 } // namespace trace_processor
635 } // namespace perfetto
636