1 /*
2 * Copyright (C) 2021 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/perfetto_sql/intrinsics/table_functions/flamegraph_construction_algorithms.h"
18
19 #include <algorithm>
20 #include <cstdint>
21 #include <map>
22 #include <memory>
23 #include <optional>
24 #include <string>
25 #include <tuple>
26 #include <unordered_set>
27 #include <utility>
28 #include <vector>
29
30 #include "perfetto/base/logging.h"
31 #include "perfetto/ext/base/string_splitter.h"
32 #include "perfetto/ext/base/string_utils.h"
33 #include "perfetto/ext/base/string_view.h"
34 #include "perfetto/trace_processor/basic_types.h"
35 #include "src/trace_processor/containers/row_map.h"
36 #include "src/trace_processor/db/column/types.h"
37 #include "src/trace_processor/db/table.h"
38 #include "src/trace_processor/storage/trace_storage.h"
39 #include "src/trace_processor/tables/metadata_tables_py.h"
40 #include "src/trace_processor/tables/profiler_tables_py.h"
41
42 namespace perfetto::trace_processor {
43
44 namespace {
45 struct MergedCallsite {
46 StringId frame_name;
47 StringId mapping_name;
48 std::optional<StringId> source_file;
49 std::optional<uint32_t> line_number;
50 std::optional<uint32_t> parent_idx;
operator <perfetto::trace_processor::__anon3440342c0111::MergedCallsite51 bool operator<(const MergedCallsite& o) const {
52 return std::tie(frame_name, mapping_name, parent_idx) <
53 std::tie(o.frame_name, o.mapping_name, o.parent_idx);
54 }
55 };
56
57 struct FlamegraphTableAndMergedCallsites {
58 std::unique_ptr<tables::ExperimentalFlamegraphTable> tbl;
59 std::vector<uint32_t> callsite_to_merged_callsite;
60 };
61
GetMergedCallsites(TraceStorage * storage,uint32_t callstack_row)62 std::vector<MergedCallsite> GetMergedCallsites(TraceStorage* storage,
63 uint32_t callstack_row) {
64 const tables::StackProfileCallsiteTable& callsites_tbl =
65 storage->stack_profile_callsite_table();
66 const tables::StackProfileFrameTable& frames_tbl =
67 storage->stack_profile_frame_table();
68 const tables::SymbolTable& symbols_tbl = storage->symbol_table();
69 const tables::StackProfileMappingTable& mapping_tbl =
70 storage->stack_profile_mapping_table();
71
72 uint32_t frame_idx =
73 *frames_tbl.id().IndexOf(callsites_tbl.frame_id()[callstack_row]);
74
75 uint32_t mapping_idx =
76 *mapping_tbl.id().IndexOf(frames_tbl.mapping()[frame_idx]);
77 StringId mapping_name = mapping_tbl.name()[mapping_idx];
78
79 std::optional<uint32_t> symbol_set_id = frames_tbl.symbol_set_id()[frame_idx];
80
81 if (!symbol_set_id) {
82 StringId frame_name = frames_tbl.name()[frame_idx];
83 std::optional<StringId> deobfuscated_name =
84 frames_tbl.deobfuscated_name()[frame_idx];
85 return {{deobfuscated_name ? *deobfuscated_name : frame_name, mapping_name,
86 std::nullopt, std::nullopt, std::nullopt}};
87 }
88
89 std::vector<MergedCallsite> result;
90 // id == symbol_set_id for the bottommost frame.
91 // TODO(lalitm): Encode this optimization in the table and remove this
92 // custom optimization.
93 uint32_t symbol_set_idx = *symbols_tbl.id().IndexOf(SymbolId(*symbol_set_id));
94 for (uint32_t i = symbol_set_idx;
95 i < symbols_tbl.row_count() &&
96 symbols_tbl.symbol_set_id()[i] == *symbol_set_id;
97 ++i) {
98 result.emplace_back(MergedCallsite{
99 symbols_tbl.name()[i], mapping_name, symbols_tbl.source_file()[i],
100 symbols_tbl.line_number()[i], std::nullopt});
101 }
102 std::reverse(result.begin(), result.end());
103 return result;
104 }
105 } // namespace
106
BuildFlamegraphTableTreeStructure(TraceStorage * storage,std::optional<UniquePid> upid,std::optional<std::string> upid_group,int64_t default_timestamp,StringId profile_type)107 static FlamegraphTableAndMergedCallsites BuildFlamegraphTableTreeStructure(
108 TraceStorage* storage,
109 std::optional<UniquePid> upid,
110 std::optional<std::string> upid_group,
111 int64_t default_timestamp,
112 StringId profile_type) {
113 const tables::StackProfileCallsiteTable& callsites_tbl =
114 storage->stack_profile_callsite_table();
115
116 std::vector<uint32_t> callsite_to_merged_callsite(callsites_tbl.row_count(),
117 0);
118 std::map<MergedCallsite, uint32_t> merged_callsites_to_table_idx;
119
120 std::unique_ptr<tables::ExperimentalFlamegraphTable> tbl(
121 new tables::ExperimentalFlamegraphTable(storage->mutable_string_pool()));
122
123 // FORWARD PASS:
124 // Aggregate callstacks by frame name / mapping name. Use symbolization
125 // data.
126 for (uint32_t i = 0; i < callsites_tbl.row_count(); ++i) {
127 std::optional<uint32_t> parent_idx;
128
129 auto opt_parent_id = callsites_tbl.parent_id()[i];
130 if (opt_parent_id) {
131 parent_idx = callsites_tbl.id().IndexOf(*opt_parent_id);
132 // Make sure what we index into has been populated already.
133 PERFETTO_CHECK(*parent_idx < i);
134 parent_idx = callsite_to_merged_callsite[*parent_idx];
135 }
136
137 auto callsites = GetMergedCallsites(storage, i);
138 // Loop below needs to run at least once for parent_idx to get updated.
139 PERFETTO_CHECK(!callsites.empty());
140 std::map<MergedCallsite, uint32_t> callsites_to_rowid;
141 for (MergedCallsite& merged_callsite : callsites) {
142 merged_callsite.parent_idx = parent_idx;
143 auto it = merged_callsites_to_table_idx.find(merged_callsite);
144 if (it == merged_callsites_to_table_idx.end()) {
145 std::tie(it, std::ignore) = merged_callsites_to_table_idx.emplace(
146 merged_callsite, merged_callsites_to_table_idx.size());
147 tables::ExperimentalFlamegraphTable::Row row{};
148 if (parent_idx) {
149 row.depth = tbl->depth()[*parent_idx] + 1;
150 row.parent_id = tbl->id()[*parent_idx];
151 } else {
152 row.depth = 0;
153 row.parent_id = std::nullopt;
154 }
155
156 // The 'ts' column is given a default value, taken from the query.
157 // So if the query is:
158 // `select * from experimental_flamegraph(
159 // 'native',
160 // 605908369259172,
161 // NULL,
162 // 1,
163 // NULL,
164 // NULL
165 // )`
166 // then row.ts == 605908369259172, for all rows
167 // This is not accurate. However, at present there is no other
168 // straightforward way of assigning timestamps to non-leaf nodes in the
169 // flamegraph tree. Non-leaf nodes would have to be assigned >= 1
170 // timestamps, which would increase data size without an advantage.
171 row.ts = default_timestamp;
172 if (upid) {
173 row.upid = *upid;
174 }
175 if (upid_group) {
176 row.upid_group = storage->InternString(base::StringView(*upid_group));
177 }
178 row.profile_type = profile_type;
179 row.name = merged_callsite.frame_name;
180 row.map_name = merged_callsite.mapping_name;
181 tbl->Insert(row);
182 callsites_to_rowid[merged_callsite] =
183 static_cast<uint32_t>(merged_callsites_to_table_idx.size() - 1);
184
185 PERFETTO_CHECK(merged_callsites_to_table_idx.size() ==
186 tbl->row_count());
187 } else {
188 MergedCallsite saved_callsite = it->first;
189 callsites_to_rowid.erase(saved_callsite);
190 if (saved_callsite.source_file != merged_callsite.source_file) {
191 saved_callsite.source_file = std::nullopt;
192 }
193 if (saved_callsite.line_number != merged_callsite.line_number) {
194 saved_callsite.line_number = std::nullopt;
195 }
196 callsites_to_rowid[saved_callsite] = it->second;
197 }
198 parent_idx = it->second;
199 }
200
201 for (const auto& it : callsites_to_rowid) {
202 if (it.first.source_file) {
203 tbl->mutable_source_file()->Set(it.second, *it.first.source_file);
204 }
205 if (it.first.line_number) {
206 tbl->mutable_line_number()->Set(it.second, *it.first.line_number);
207 }
208 }
209
210 PERFETTO_CHECK(parent_idx);
211 callsite_to_merged_callsite[i] = *parent_idx;
212 }
213
214 return {std::move(tbl), callsite_to_merged_callsite};
215 }
216
217 static std::unique_ptr<tables::ExperimentalFlamegraphTable>
BuildFlamegraphTableHeapSizeAndCount(std::unique_ptr<tables::ExperimentalFlamegraphTable> tbl,const std::vector<uint32_t> & callsite_to_merged_callsite,tables::HeapProfileAllocationTable::ConstIterator it)218 BuildFlamegraphTableHeapSizeAndCount(
219 std::unique_ptr<tables::ExperimentalFlamegraphTable> tbl,
220 const std::vector<uint32_t>& callsite_to_merged_callsite,
221 tables::HeapProfileAllocationTable::ConstIterator it) {
222 for (; it; ++it) {
223 int64_t size = it.size();
224 int64_t count = it.count();
225 tables::StackProfileCallsiteTable::Id callsite_id = it.callsite_id();
226
227 PERFETTO_CHECK((size <= 0 && count <= 0) || (size >= 0 && count >= 0));
228 uint32_t merged_idx = callsite_to_merged_callsite[callsite_id.value];
229 // On old heapprofd producers, the count field is incorrectly set and we
230 // zero it in proto_trace_parser.cc.
231 // As such, we cannot depend on count == 0 to imply size == 0, so we check
232 // for both of them separately.
233 if (size > 0) {
234 tbl->mutable_alloc_size()->Set(merged_idx,
235 tbl->alloc_size()[merged_idx] + size);
236 }
237 if (count > 0) {
238 tbl->mutable_alloc_count()->Set(merged_idx,
239 tbl->alloc_count()[merged_idx] + count);
240 }
241
242 tbl->mutable_size()->Set(merged_idx, tbl->size()[merged_idx] + size);
243 tbl->mutable_count()->Set(merged_idx, tbl->count()[merged_idx] + count);
244 }
245
246 // BACKWARD PASS:
247 // Propagate sizes to parents.
248 for (int64_t i = tbl->row_count() - 1; i >= 0; --i) {
249 auto idx = static_cast<uint32_t>(i);
250
251 tbl->mutable_cumulative_size()->Set(
252 idx, tbl->cumulative_size()[idx] + tbl->size()[idx]);
253 tbl->mutable_cumulative_count()->Set(
254 idx, tbl->cumulative_count()[idx] + tbl->count()[idx]);
255
256 tbl->mutable_cumulative_alloc_size()->Set(
257 idx, tbl->cumulative_alloc_size()[idx] + tbl->alloc_size()[idx]);
258 tbl->mutable_cumulative_alloc_count()->Set(
259 idx, tbl->cumulative_alloc_count()[idx] + tbl->alloc_count()[idx]);
260
261 auto parent = tbl->parent_id()[idx];
262 if (parent) {
263 uint32_t parent_idx =
264 *tbl->id().IndexOf(tables::ExperimentalFlamegraphTable::Id(*parent));
265 tbl->mutable_cumulative_size()->Set(
266 parent_idx,
267 tbl->cumulative_size()[parent_idx] + tbl->cumulative_size()[idx]);
268 tbl->mutable_cumulative_count()->Set(
269 parent_idx,
270 tbl->cumulative_count()[parent_idx] + tbl->cumulative_count()[idx]);
271
272 tbl->mutable_cumulative_alloc_size()->Set(
273 parent_idx, tbl->cumulative_alloc_size()[parent_idx] +
274 tbl->cumulative_alloc_size()[idx]);
275 tbl->mutable_cumulative_alloc_count()->Set(
276 parent_idx, tbl->cumulative_alloc_count()[parent_idx] +
277 tbl->cumulative_alloc_count()[idx]);
278 }
279 }
280
281 return tbl;
282 }
283
284 static std::unique_ptr<tables::ExperimentalFlamegraphTable>
BuildFlamegraphTableCallstackSizeAndCount(const tables::PerfSampleTable & table,std::unique_ptr<tables::ExperimentalFlamegraphTable> tbl,const std::vector<uint32_t> & callsite_to_merged_callsite,std::vector<Constraint> constraints,const std::unordered_set<uint32_t> & utids)285 BuildFlamegraphTableCallstackSizeAndCount(
286 const tables::PerfSampleTable& table,
287 std::unique_ptr<tables::ExperimentalFlamegraphTable> tbl,
288 const std::vector<uint32_t>& callsite_to_merged_callsite,
289 std::vector<Constraint> constraints,
290 const std::unordered_set<uint32_t>& utids) {
291 Query q;
292 q.constraints = std::move(constraints);
293 for (auto it = table.FilterToIterator(q); it; ++it) {
294 if (utids.find(it.utid()) == utids.end()) {
295 continue;
296 }
297
298 uint32_t callsite_id = it.callsite_id().value_or(CallsiteId(0u)).value;
299 int64_t ts = it.ts();
300 uint32_t merged_idx = callsite_to_merged_callsite[callsite_id];
301 tbl->mutable_size()->Set(merged_idx, tbl->size()[merged_idx] + 1);
302 tbl->mutable_count()->Set(merged_idx, tbl->count()[merged_idx] + 1);
303 tbl->mutable_ts()->Set(merged_idx, ts);
304 }
305
306 // BACKWARD PASS:
307 // Propagate sizes to parents.
308 for (int64_t i = tbl->row_count() - 1; i >= 0; --i) {
309 auto idx = static_cast<uint32_t>(i);
310
311 tbl->mutable_cumulative_size()->Set(
312 idx, tbl->cumulative_size()[idx] + tbl->size()[idx]);
313 tbl->mutable_cumulative_count()->Set(
314 idx, tbl->cumulative_count()[idx] + tbl->count()[idx]);
315
316 auto parent = tbl->parent_id()[idx];
317 if (parent) {
318 uint32_t parent_idx =
319 *tbl->id().IndexOf(tables::ExperimentalFlamegraphTable::Id(*parent));
320 tbl->mutable_cumulative_size()->Set(
321 parent_idx,
322 tbl->cumulative_size()[parent_idx] + tbl->cumulative_size()[idx]);
323 tbl->mutable_cumulative_count()->Set(
324 parent_idx,
325 tbl->cumulative_count()[parent_idx] + tbl->cumulative_count()[idx]);
326 }
327 }
328 return tbl;
329 }
330
BuildHeapProfileFlamegraph(TraceStorage * storage,UniquePid upid,int64_t timestamp)331 std::unique_ptr<tables::ExperimentalFlamegraphTable> BuildHeapProfileFlamegraph(
332 TraceStorage* storage,
333 UniquePid upid,
334 int64_t timestamp) {
335 const tables::HeapProfileAllocationTable& allocation_tbl =
336 storage->heap_profile_allocation_table();
337 // PASS OVER ALLOCATIONS:
338 // Aggregate allocations into the newly built tree.
339 Query q;
340 q.constraints = {allocation_tbl.ts().le(timestamp),
341 allocation_tbl.upid().eq(upid)};
342 auto it = allocation_tbl.FilterToIterator(q);
343 if (!it) {
344 return nullptr;
345 }
346 StringId profile_type = storage->InternString("native");
347 FlamegraphTableAndMergedCallsites table_and_callsites =
348 BuildFlamegraphTableTreeStructure(storage, upid, std::nullopt, timestamp,
349 profile_type);
350 return BuildFlamegraphTableHeapSizeAndCount(
351 std::move(table_and_callsites.tbl),
352 table_and_callsites.callsite_to_merged_callsite, std::move(it));
353 }
354
355 std::unique_ptr<tables::ExperimentalFlamegraphTable>
BuildNativeCallStackSamplingFlamegraph(TraceStorage * storage,std::optional<UniquePid> upid,std::optional<std::string> upid_group,const std::vector<TimeConstraints> & time_constraints)356 BuildNativeCallStackSamplingFlamegraph(
357 TraceStorage* storage,
358 std::optional<UniquePid> upid,
359 std::optional<std::string> upid_group,
360 const std::vector<TimeConstraints>& time_constraints) {
361 // 1. Extract required upids from input.
362 std::unordered_set<UniquePid> upids;
363 if (upid) {
364 upids.insert(*upid);
365 } else {
366 for (base::StringSplitter sp(*upid_group, ','); sp.Next();) {
367 std::optional<uint32_t> maybe = base::CStringToUInt32(sp.cur_token());
368 if (maybe) {
369 upids.insert(*maybe);
370 }
371 }
372 }
373
374 // 2. Create set of all utids mapped to the given vector of upids
375 std::unordered_set<UniqueTid> utids;
376 {
377 Query q;
378 q.constraints = {storage->thread_table().upid().is_not_null()};
379 auto it = storage->thread_table().FilterToIterator(q);
380 for (; it; ++it) {
381 if (upids.count(*it.upid())) {
382 utids.emplace(it.id().value);
383 }
384 }
385 }
386
387 // 3. Get all row indices in perf_sample that have callstacks (some samples
388 // can have only counter values), are in timestamp bounds and correspond to
389 // the requested utids.
390 std::vector<Constraint> cs{
391 storage->perf_sample_table().callsite_id().is_not_null()};
392 for (const auto& tc : time_constraints) {
393 if (tc.op != FilterOp::kGt && tc.op != FilterOp::kLt &&
394 tc.op != FilterOp::kGe && tc.op != FilterOp::kLe) {
395 PERFETTO_FATAL("Filter operation %d not permitted for perf.",
396 static_cast<int>(tc.op));
397 }
398 cs.emplace_back(Constraint{tables::PerfSampleTable::ColumnIndex::ts, tc.op,
399 SqlValue::Long(tc.value)});
400 }
401
402 // The logic underneath is selecting a default timestamp to be used by all
403 // frames which do not have a timestamp. The timestamp is taken from the
404 // query value and it's not meaningful for the row. It prevents however the
405 // rows with no timestamp from being filtered out by Sqlite, after we create
406 // the table ExperimentalFlamegraphTable in this class.
407 int64_t default_timestamp = 0;
408 if (!time_constraints.empty()) {
409 const auto& tc = time_constraints[0];
410 if (tc.op == FilterOp::kGt) {
411 default_timestamp = tc.value + 1;
412 } else if (tc.op == FilterOp::kLt) {
413 default_timestamp = tc.value - 1;
414 } else {
415 default_timestamp = tc.value;
416 }
417 }
418
419 // 4. Build the flamegraph structure.
420 FlamegraphTableAndMergedCallsites table_and_callsites =
421 BuildFlamegraphTableTreeStructure(storage, upid, upid_group,
422 default_timestamp,
423 storage->InternString("perf"));
424 return BuildFlamegraphTableCallstackSizeAndCount(
425 storage->perf_sample_table(), std::move(table_and_callsites.tbl),
426 table_and_callsites.callsite_to_merged_callsite, std::move(cs), utids);
427 }
428
429 } // namespace perfetto::trace_processor
430