| /* |
| * Copyright (C) 2026 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #include "src/trace_processor/plugins/flamegraph/flamegraph.h" |
| |
| #include <algorithm> |
| #include <cmath> |
| #include <cstdint> |
| #include <cstring> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| #include <vector> |
| |
| #include "perfetto/base/logging.h" |
| #include "perfetto/base/status.h" |
| #include "perfetto/ext/base/flat_hash_map.h" |
| #include "perfetto/ext/base/regex.h" |
| #include "perfetto/ext/base/status_macros.h" |
| #include "perfetto/ext/base/status_or.h" |
| #include "src/trace_processor/core/dataframe/adhoc_dataframe_builder.h" |
| #include "src/trace_processor/core/dataframe/dataframe.h" |
| #include "src/trace_processor/core/util/flex_vector.h" |
| |
| namespace perfetto::trace_processor::flamegraph { |
| namespace { |
| |
| using core::FlexVector; |
| |
| // Show-stack and show-from filters are tracked as per-frame bitmasks, one |
| // bit per filter, ORed along each path: a stack satisfies them when every |
| // bit is present. |
| constexpr size_t kMaxBitFilters = 63; |
| |
| // Row helpers for the flattened metric columns; explicit loops so they |
| // inline instead of dispatching into libc for a handful of bytes. |
| void AddRow(double* dst, const double* src, uint32_t k) { |
| for (uint32_t m = 0; m < k; ++m) { |
| dst[m] += src[m]; |
| } |
| } |
| |
| void CopyRow(double* dst, const double* src, uint32_t k) { |
| for (uint32_t m = 0; m < k; ++m) { |
| dst[m] = src[m]; |
| } |
| } |
| |
| bool AnyNonZero(const double* row, uint32_t k) { |
| for (uint32_t m = 0; m < k; ++m) { |
| if (row[m] < 0 || row[m] > 0) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| // The compiled filter regexes. The pivot pattern is an extra show-stack |
| // filter (only pivot-containing stacks are shown), remembered by index so |
| // pivot frames can be identified from the match bitmask. |
| struct Filters { |
| std::vector<base::Regex> show_stack; |
| std::vector<base::Regex> hide_stack; |
| std::vector<base::Regex> show_from; |
| std::vector<base::Regex> hide_frame; |
| uint32_t pivot_idx = 0; |
| uint64_t show_stack_mask = 0; |
| uint64_t show_from_mask = 0; |
| // A bit outside the mask: assigning it to a frame's bits makes its |
| // stacks unable to ever satisfy the mask, implementing hide-stack. |
| uint64_t impossible_bits = 0; |
| |
| bool any() const { |
| return !show_stack.empty() || !hide_stack.empty() || !show_from.empty() || |
| !hide_frame.empty(); |
| } |
| }; |
| |
| base::StatusOr<Filters> CompileFilters(const Config& config) { |
| Filters f; |
| for (const auto& spec : config.filters) { |
| ASSIGN_OR_RETURN(base::Regex re, base::Regex::Create(spec.pattern)); |
| switch (spec.kind) { |
| case Config::FilterKind::kShowStack: |
| f.show_stack.push_back(std::move(re)); |
| break; |
| case Config::FilterKind::kHideStack: |
| f.hide_stack.push_back(std::move(re)); |
| break; |
| case Config::FilterKind::kShowFromFrame: |
| f.show_from.push_back(std::move(re)); |
| break; |
| case Config::FilterKind::kHideFrame: |
| f.hide_frame.push_back(std::move(re)); |
| break; |
| } |
| } |
| if (config.view == Config::View::kPivot) { |
| if (!config.pivot_pattern) { |
| return base::ErrStatus( |
| "flamegraph: pivot pattern is required for the pivot view"); |
| } |
| ASSIGN_OR_RETURN(base::Regex re, |
| base::Regex::Create(*config.pivot_pattern)); |
| f.pivot_idx = static_cast<uint32_t>(f.show_stack.size()); |
| f.show_stack.push_back(std::move(re)); |
| } else if (config.pivot_pattern) { |
| return base::ErrStatus( |
| "flamegraph: pivot pattern is only valid for the pivot view"); |
| } |
| if (f.show_stack.size() > kMaxBitFilters || |
| f.show_from.size() > kMaxBitFilters) { |
| return base::ErrStatus("flamegraph: too many filters (max %zu per kind)", |
| kMaxBitFilters); |
| } |
| f.show_stack_mask = (uint64_t(1) << f.show_stack.size()) - 1; |
| f.show_from_mask = (uint64_t(1) << f.show_from.size()) - 1; |
| f.impossible_bits = uint64_t(1) << f.show_stack.size(); |
| return base::StatusOr<Filters>(std::move(f)); |
| } |
| |
| // Returns the frames in parents-before-children order, excluding frames |
| // unreachable from a root (dangling parent indices, cycles). Input that is |
| // already ordered, the common case, is detected and returned as the |
| // identity order with no index built. |
| FlexVector<uint32_t> TopoOrder(const Forest& forest) { |
| auto n = static_cast<uint32_t>(forest.size()); |
| bool ordered = true; |
| for (uint32_t i = 0; i < n && ordered; ++i) { |
| uint32_t p = forest.parent[i]; |
| ordered = p == kNoParent || p < i; |
| } |
| auto order = FlexVector<uint32_t>::CreateWithSize(n); |
| if (ordered) { |
| for (uint32_t i = 0; i < n; ++i) { |
| order[i] = i; |
| } |
| return order; |
| } |
| // Reverse the parent index into child lists with a count + scatter pass, |
| // then DFS from the roots. |
| auto child_offset = FlexVector<uint32_t>::CreateFilled(n + 2, 0); |
| for (uint32_t i = 0; i < n; ++i) { |
| if (forest.parent[i] < n) { |
| child_offset[forest.parent[i] + 2]++; |
| } |
| } |
| for (uint32_t i = 2; i < n + 2; ++i) { |
| child_offset[i] += child_offset[i - 1]; |
| } |
| auto child_list = FlexVector<uint32_t>::CreateWithSize(child_offset[n + 1]); |
| for (uint32_t i = 0; i < n; ++i) { |
| if (forest.parent[i] < n) { |
| child_list[child_offset[forest.parent[i] + 1]++] = i; |
| } |
| } |
| order.clear(); |
| FlexVector<uint32_t> stack; |
| for (uint32_t i = n; i-- > 0;) { |
| if (forest.parent[i] == kNoParent) { |
| stack.push_back(i); |
| } |
| } |
| while (!stack.empty()) { |
| uint32_t i = stack.back(); |
| stack.pop_back(); |
| order.push_back(i); |
| for (uint32_t c = child_offset[i + 1]; c-- > child_offset[i];) { |
| stack.push_back(child_list[c]); |
| } |
| } |
| return order; |
| } |
| |
| // A merged tree under construction. Node identity is (parent node, key), |
| // resolved through one exact hash map; nodes are created in |
| // parents-before-children order by construction. |expected_nodes| sizes |
| // the map and node columns so steady-state growth never reallocates; pass |
| // 0 when the output is expected to be small (pivot views). |
| struct Trie { |
| Trie(uint32_t metric_count, uint32_t expected_nodes) |
| : k(metric_count), |
| // 1.5x keeps the expected node count under the map's load limit. |
| map(NextPow2(uint64_t(expected_nodes) + expected_nodes / 2)) { |
| parent.reserve(expected_nodes); |
| rep_frame.reserve(expected_nodes); |
| self.reserve(uint64_t(expected_nodes) * k); |
| cumulative.reserve(uint64_t(expected_nodes) * k); |
| } |
| |
| static size_t NextPow2(uint64_t v) { |
| uint64_t p = 16; |
| while (p < v) { |
| p *= 2; |
| } |
| PERFETTO_CHECK(p <= std::numeric_limits<size_t>::max()); |
| return static_cast<size_t>(p); |
| } |
| |
| uint32_t ChildOf(uint32_t node, uint32_t key, uint32_t frame) { |
| auto next = static_cast<uint32_t>(parent.size()); |
| auto [id, inserted] = map.Insert((uint64_t(node) << 32) | key, next); |
| if (inserted) { |
| parent.push_back(node); |
| rep_frame.push_back(frame); |
| self.push_back_multiple(0, k); |
| cumulative.push_back_multiple(0, k); |
| } |
| return *id; |
| } |
| |
| size_t size() const { return parent.size(); } |
| |
| uint32_t k; |
| base::FlatHashMapV2<uint64_t, uint32_t> map; |
| FlexVector<uint32_t> parent; |
| FlexVector<uint32_t> rep_frame; |
| FlexVector<double> self; |
| FlexVector<double> cumulative; |
| }; |
| |
| // Drops every node whose subtree is zero on all metrics (uncounted or |
| // valueless stacks) and packs the survivors into the output Tree, along |
| // with the constituent index built from the (node, frame) pairs by a |
| // count + scatter pass. |
| Tree PackTree(Trie trie, const FlexVector<uint64_t>& node_frame_pairs) { |
| auto nodes = static_cast<uint32_t>(trie.size()); |
| uint32_t k = trie.k; |
| if (nodes == 0) { |
| Tree tree; |
| tree.metric_count = k; |
| tree.constituents_offset = FlexVector<uint32_t>::CreateFilled(1, 0); |
| return tree; |
| } |
| // A node stays if its own subtree has value; children come after their |
| // parent, so one reverse scan propagates "has a kept descendant". |
| auto keep = FlexVector<uint8_t>::CreateFilled(nodes, 0); |
| for (uint32_t i = nodes; i-- > 0;) { |
| keep[i] = keep[i] || AnyNonZero(&trie.cumulative[i * k], k); |
| if (keep[i] && trie.parent[i] != kNoParent) { |
| keep[trie.parent[i]] = 1; |
| } |
| } |
| auto remap = FlexVector<uint32_t>::CreateWithSize(nodes); |
| uint32_t packed = 0; |
| for (uint32_t i = 0; i < nodes; ++i) { |
| remap[i] = keep[i] ? packed++ : kNoParent; |
| } |
| |
| Tree tree; |
| tree.metric_count = k; |
| if (packed == nodes) { |
| // Nothing dropped: adopt the trie's columns as-is (remap is the |
| // identity). |
| tree.parent = std::move(trie.parent); |
| tree.rep_frame = std::move(trie.rep_frame); |
| tree.self = std::move(trie.self); |
| tree.cumulative = std::move(trie.cumulative); |
| } else { |
| tree.parent = FlexVector<uint32_t>::CreateWithSize(packed); |
| tree.rep_frame = FlexVector<uint32_t>::CreateWithSize(packed); |
| tree.self = FlexVector<double>::CreateWithSize(uint64_t(packed) * k); |
| tree.cumulative = FlexVector<double>::CreateWithSize(uint64_t(packed) * k); |
| for (uint32_t i = 0; i < nodes; ++i) { |
| if (!keep[i]) { |
| continue; |
| } |
| uint32_t out = remap[i]; |
| tree.parent[out] = |
| trie.parent[i] == kNoParent ? kNoParent : remap[trie.parent[i]]; |
| tree.rep_frame[out] = trie.rep_frame[i]; |
| CopyRow(&tree.self[out * k], &trie.self[i * k], k); |
| CopyRow(&tree.cumulative[out * k], &trie.cumulative[i * k], k); |
| } |
| } |
| |
| tree.constituents_offset = FlexVector<uint32_t>::CreateFilled(packed + 1, 0); |
| for (uint64_t pair : node_frame_pairs) { |
| uint32_t node = remap[pair >> 32]; |
| if (node != kNoParent) { |
| tree.constituents_offset[node + 1]++; |
| } |
| } |
| for (uint32_t i = 1; i <= packed; ++i) { |
| tree.constituents_offset[i] += tree.constituents_offset[i - 1]; |
| } |
| tree.constituents = |
| FlexVector<uint32_t>::CreateWithSize(tree.constituents_offset[packed]); |
| if (packed != 0) { |
| auto cursor = FlexVector<uint32_t>::CreateWithSize(packed); |
| memcpy(cursor.data(), tree.constituents_offset.data(), |
| packed * sizeof(uint32_t)); |
| for (uint64_t pair : node_frame_pairs) { |
| uint32_t node = remap[pair >> 32]; |
| if (node != kNoParent) { |
| tree.constituents[cursor[node]++] = static_cast<uint32_t>(pair); |
| } |
| } |
| } |
| return tree; |
| } |
| |
| // The state shared by the phases of one Build() call. See Build() at the |
| // bottom for the phase sequence. |
| class Builder { |
| public: |
| Builder(const Forest& forest, |
| const Config& config, |
| const StringPool& pool, |
| Filters filters) |
| : forest_(forest), |
| config_(config), |
| pool_(pool), |
| filters_(std::move(filters)), |
| frame_count_(static_cast<uint32_t>(forest.size())), |
| metric_count_(forest.metric_count), |
| // Full top-down / bottom-up output has up to one node per frame; |
| // pivot output is typically far smaller, so let that grow on |
| // demand. |
| expected_nodes_(config.view == Config::View::kPivot ? 0 |
| : frame_count_) {} |
| |
| Flamegraph Run() { |
| EvaluateNameMasks(); |
| order_ = TopoOrder(forest_); |
| EvaluatePaths(); |
| SelectAnchors(); |
| Flamegraph result; |
| result.down.metric_count = metric_count_; |
| result.down.constituents_offset = FlexVector<uint32_t>::CreateFilled(1, 0); |
| result.up.metric_count = metric_count_; |
| result.up.constituents_offset = FlexVector<uint32_t>::CreateFilled(1, 0); |
| if (config_.view != Config::View::kBottomUp) { |
| result.down = BuildDown(); |
| } |
| if (config_.view != Config::View::kTopDown) { |
| result.up = BuildUp(); |
| } |
| return result; |
| } |
| |
| private: |
| // Per distinct name, its regex results across all filters. |
| struct NameMasks { |
| uint64_t show_stack_bits; |
| uint64_t show_from_bits; |
| bool shown; |
| }; |
| |
| const NameMasks& MasksOf(uint32_t frame) const { |
| return masks_[forest_.name[frame]]; |
| } |
| |
| bool IsPivotFrame(uint32_t frame) const { |
| return ((MasksOf(frame).show_stack_bits >> filters_.pivot_idx) & 1) != 0; |
| } |
| |
| // Runs every filter regex against every distinct name once. |
| void EvaluateNameMasks() { |
| auto names = static_cast<uint32_t>(forest_.name_table.size()); |
| masks_ = FlexVector<NameMasks>::CreateFilled(names, {0, 0, true}); |
| if (!filters_.any()) { |
| return; |
| } |
| for (uint32_t t = 0; t < names; ++t) { |
| NullTermStringView name = pool_.Get(forest_.name_table[t]); |
| std::string_view view(name.data(), name.size()); |
| auto matches = [&](const base::Regex& re) { |
| if (re.PartialMatch(view)) { |
| return true; |
| } |
| if (forest_.match_offset.empty()) { |
| return false; |
| } |
| for (uint32_t s = forest_.match_offset[t]; |
| s < forest_.match_offset[t + 1]; ++s) { |
| NullTermStringView extra = pool_.Get(forest_.match_strings[s]); |
| if (re.PartialMatch(std::string_view(extra.data(), extra.size()))) { |
| return true; |
| } |
| } |
| return false; |
| }; |
| uint64_t sb = 0; |
| for (uint32_t j = 0; j < filters_.show_stack.size(); ++j) { |
| sb |= uint64_t(matches(filters_.show_stack[j])) << j; |
| } |
| bool hidden = std::any_of(filters_.hide_stack.begin(), |
| filters_.hide_stack.end(), matches); |
| masks_[t].show_stack_bits = hidden ? filters_.impossible_bits : sb; |
| for (uint32_t j = 0; j < filters_.show_from.size(); ++j) { |
| masks_[t].show_from_bits |= uint64_t(matches(filters_.show_from[j])) |
| << j; |
| } |
| masks_[t].shown = !std::any_of(filters_.hide_frame.begin(), |
| filters_.hide_frame.end(), matches); |
| } |
| } |
| |
| // One pass along every path, in topological order. Per frame: whether it |
| // is kept (hide-frame, show-from), its nearest kept ancestor, whether |
| // its stack satisfies every show/hide-stack filter (so its values |
| // count), and its metrics with hidden frames' values folded into the |
| // kept ancestor. |
| void EvaluatePaths() { |
| kept_ = FlexVector<uint8_t>::CreateFilled(frame_count_, 0); |
| counted_ = FlexVector<uint8_t>::CreateFilled(frame_count_, 0); |
| ancestor_ = FlexVector<uint32_t>::CreateFilled(frame_count_, kNoParent); |
| auto sb_path = FlexVector<uint64_t>::CreateWithSize(frame_count_); |
| auto sf_path = FlexVector<uint64_t>::CreateWithSize(frame_count_); |
| folded_ = FlexVector<double>::CreateWithSize(uint64_t(frame_count_) * |
| metric_count_); |
| memcpy(folded_.data(), forest_.metrics.data(), |
| uint64_t(frame_count_) * metric_count_ * sizeof(double)); |
| for (uint32_t i : order_) { |
| uint32_t p = forest_.parent[i]; |
| bool has_parent = p != kNoParent; |
| const NameMasks& m = MasksOf(i); |
| sf_path[i] = |
| (has_parent ? sf_path[p] : 0) | (m.shown ? m.show_from_bits : 0); |
| kept_[i] = m.shown && sf_path[i] == filters_.show_from_mask; |
| ancestor_[i] = !has_parent ? kNoParent : (kept_[p] ? p : ancestor_[p]); |
| sb_path[i] = |
| (has_parent ? sb_path[p] : 0) | (kept_[i] ? m.show_stack_bits : 0); |
| counted_[i] = kept_[i] && sb_path[i] == filters_.show_stack_mask; |
| if (!kept_[i] && ancestor_[i] != kNoParent) { |
| AddRow(&folded_[ancestor_[i] * metric_count_], |
| &forest_.metrics[i * metric_count_], metric_count_); |
| } |
| } |
| } |
| |
| // Selects the frames the merged trees grow from and re-root at. This is |
| // the only place the views differ; every phase after this treats |
| // anchors uniformly. |
| void SelectAnchors() { |
| anchor_ = FlexVector<uint8_t>::CreateFilled(frame_count_, 0); |
| for (uint32_t i : order_) { |
| switch (config_.view) { |
| case Config::View::kTopDown: |
| // The roots of the kept forest. |
| anchor_[i] = kept_[i] && ancestor_[i] == kNoParent; |
| break; |
| case Config::View::kBottomUp: |
| // Every frame whose own values count: each one is a stack |
| // sample whose caller chain the up tree attributes. |
| anchor_[i] = counted_[i]; |
| break; |
| case Config::View::kPivot: |
| // Frames matching the pivot pattern; nested matches re-root. |
| anchor_[i] = kept_[i] && IsPivotFrame(i); |
| break; |
| } |
| } |
| } |
| |
| // The downward half: one forward scan with a trie cursor. Anchors start |
| // merged roots; every other kept frame merges under its ancestor's |
| // node. |
| Tree BuildDown() { |
| Trie trie(metric_count_, expected_nodes_); |
| auto trie_of = FlexVector<uint32_t>::CreateFilled(frame_count_, kNoParent); |
| for (uint32_t i : order_) { |
| if (!kept_[i]) { |
| continue; |
| } |
| uint32_t node; |
| if (anchor_[i]) { |
| node = trie.ChildOf(kNoParent, forest_.key[i], i); |
| } else if (ancestor_[i] != kNoParent && |
| trie_of[ancestor_[i]] != kNoParent) { |
| node = trie.ChildOf(trie_of[ancestor_[i]], forest_.key[i], i); |
| } else { |
| continue; // Not under any anchor. |
| } |
| trie_of[i] = node; |
| // Self shows the merged frames' own values regardless of stack |
| // filters; only cumulative is limited to counted stacks. |
| AddRow(&trie.self[node * metric_count_], &folded_[i * metric_count_], |
| metric_count_); |
| if (counted_[i]) { |
| AddRow(&trie.cumulative[node * metric_count_], |
| &folded_[i * metric_count_], metric_count_); |
| } |
| } |
| // Cumulative: children were created after their parent, so one |
| // reverse scan sums subtrees onto the counted bases. |
| for (uint32_t i = static_cast<uint32_t>(trie.size()); i-- > 0;) { |
| if (trie.parent[i] != kNoParent) { |
| AddRow(&trie.cumulative[trie.parent[i] * metric_count_], |
| &trie.cumulative[i * metric_count_], metric_count_); |
| } |
| } |
| FlexVector<uint64_t> pairs; |
| for (uint32_t i = 0; i < frame_count_; ++i) { |
| if (trie_of[i] != kNoParent) { |
| pairs.push_back((uint64_t(trie_of[i]) << 32) | i); |
| } |
| } |
| return PackTree(std::move(trie), pairs); |
| } |
| |
| // The upward half: walk each anchor's kept ancestor chain, descending |
| // the trie by key and attributing the anchor's weight to every node on |
| // the chain. |
| Tree BuildUp() { |
| FlexVector<double> weights = AnchorWeights(); |
| // Unlike the downward half, the up tree is usually much smaller than |
| // the input (distinct caller suffixes); growing on demand keeps the |
| // map compact and cache-resident. |
| Trie trie(metric_count_, 0); |
| FlexVector<uint64_t> pairs; |
| for (uint32_t i : order_) { |
| if (!anchor_[i] || |
| !AnyNonZero(&weights[i * metric_count_], metric_count_)) { |
| continue; |
| } |
| uint32_t node = kNoParent; |
| for (uint32_t f = i; f != kNoParent; f = ancestor_[f]) { |
| node = trie.ChildOf(node, forest_.key[f], f); |
| AddRow(&trie.cumulative[node * metric_count_], |
| &weights[i * metric_count_], metric_count_); |
| AddRow(&trie.self[node * metric_count_], &folded_[f * metric_count_], |
| metric_count_); |
| pairs.push_back((uint64_t(node) << 32) | f); |
| } |
| } |
| return PackTree(std::move(trie), pairs); |
| } |
| |
| // The weight each anchor carries up its caller chain: its counted |
| // subtree total, accumulated bottom-up along the kept tree and stopping |
| // at other anchors (the same re-rooting the downward half applies). |
| // With bottom-up anchors, where every counted frame is an anchor, this |
| // reduces to each frame's own values. |
| FlexVector<double> AnchorWeights() { |
| auto weights = FlexVector<double>::CreateFilled( |
| uint64_t(frame_count_) * metric_count_, 0); |
| for (uint32_t idx = static_cast<uint32_t>(order_.size()); idx-- > 0;) { |
| uint32_t i = order_[idx]; |
| if (!kept_[i]) { |
| continue; |
| } |
| if (counted_[i]) { |
| AddRow(&weights[i * metric_count_], &folded_[i * metric_count_], |
| metric_count_); |
| } |
| if (!anchor_[i] && ancestor_[i] != kNoParent) { |
| AddRow(&weights[ancestor_[i] * metric_count_], |
| &weights[i * metric_count_], metric_count_); |
| } |
| } |
| return weights; |
| } |
| |
| const Forest& forest_; |
| const Config& config_; |
| const StringPool& pool_; |
| Filters filters_; |
| uint32_t frame_count_; |
| uint32_t metric_count_; |
| uint32_t expected_nodes_; |
| |
| FlexVector<NameMasks> masks_; |
| FlexVector<uint32_t> order_; // Topological, unreachable frames absent. |
| FlexVector<uint8_t> kept_; // Survives hide-frame / show-from. |
| FlexVector<uint8_t> counted_; // Kept and stack satisfies every filter. |
| FlexVector<uint8_t> anchor_; // Roots the merged trees. SelectAnchors(). |
| FlexVector<uint32_t> ancestor_; // Nearest kept ancestor, or kNoParent. |
| FlexVector<double> folded_; // Metrics, hidden frames folded in. |
| }; |
| |
| } // namespace |
| |
| base::StatusOr<Flamegraph> Build(const Forest& forest, |
| const Config& config, |
| const StringPool& pool) { |
| auto n = static_cast<uint32_t>(forest.size()); |
| uint32_t k = forest.metric_count; |
| if (k == 0) { |
| return base::ErrStatus("flamegraph: at least one metric is required"); |
| } |
| if (forest.key.size() != n || forest.name.size() != n || |
| forest.metrics.size() != uint64_t(n) * k) { |
| return base::ErrStatus("flamegraph: forest column sizes do not match"); |
| } |
| for (uint32_t i = 0; i < n; ++i) { |
| if (forest.name[i] >= forest.name_table.size()) { |
| return base::ErrStatus("flamegraph: frame name index out of range"); |
| } |
| } |
| if (!forest.match_offset.empty() && |
| forest.match_offset.size() != forest.name_table.size() + 1) { |
| return base::ErrStatus( |
| "flamegraph: match_offset must have one entry per name plus one"); |
| } |
| ASSIGN_OR_RETURN(Filters filters, CompileFilters(config)); |
| if (n == 0) { |
| Flamegraph empty; |
| empty.down.metric_count = k; |
| empty.down.constituents_offset = FlexVector<uint32_t>::CreateFilled(1, 0); |
| empty.up.metric_count = k; |
| empty.up.constituents_offset = FlexVector<uint32_t>::CreateFilled(1, 0); |
| return base::StatusOr<Flamegraph>(std::move(empty)); |
| } |
| return Builder(forest, config, pool, std::move(filters)).Run(); |
| } |
| |
| base::StatusOr<core::dataframe::Dataframe> ToDataframe( |
| const Flamegraph& flamegraph, |
| const Forest& forest, |
| StringPool* pool) { |
| uint32_t k = forest.metric_count; |
| std::vector<std::string> columns = {"id", "parentId", "depth", "name"}; |
| for (uint32_t m = 0; m < k; ++m) { |
| std::string suffix = k == 1 ? "" : std::to_string(m); |
| columns.push_back("selfValue" + suffix); |
| columns.push_back("cumulativeValue" + suffix); |
| } |
| // Sums of integral inputs are integral, so inspecting the output values |
| // decides each metric's column type. |
| std::vector<bool> integral(k, true); |
| for (const Tree* tree : {&flamegraph.down, &flamegraph.up}) { |
| for (const FlexVector<double>* col : {&tree->self, &tree->cumulative}) { |
| for (uint64_t i = 0; i < col->size(); ++i) { |
| double v = (*col)[i]; |
| double t = std::trunc(v); |
| if (t < v || t > v) { |
| integral[i % k] = false; |
| } |
| } |
| } |
| } |
| |
| core::dataframe::AdhocDataframeBuilder builder(columns, pool); |
| bool ok = true; |
| int64_t base = 0; |
| for (const Tree* tree : {&flamegraph.down, &flamegraph.up}) { |
| bool up = tree == &flamegraph.up; |
| std::vector<int64_t> depth(tree->size()); |
| for (uint32_t i = 0; ok && i < tree->size(); ++i) { |
| bool root = tree->parent[i] == kNoParent; |
| depth[i] = root ? 1 : depth[tree->parent[i]] + 1; |
| ok = ok && builder.PushNonNull(0, base + i); |
| ok = ok && |
| builder.PushNonNull(1, root ? int64_t(-1) : base + tree->parent[i]); |
| ok = ok && builder.PushNonNull(2, up ? -depth[i] : depth[i]); |
| ok = ok && builder.PushNonNull( |
| 3, forest.name_table[forest.name[tree->rep_frame[i]]]); |
| for (uint32_t m = 0; m < k; ++m) { |
| double self = tree->self[i * k + m]; |
| double cumulative = tree->cumulative[i * k + m]; |
| if (integral[m]) { |
| ok = ok && builder.PushNonNull(4 + 2 * m, static_cast<int64_t>(self)); |
| ok = ok && |
| builder.PushNonNull(5 + 2 * m, static_cast<int64_t>(cumulative)); |
| } else { |
| ok = ok && builder.PushNonNull(4 + 2 * m, self); |
| ok = ok && builder.PushNonNull(5 + 2 * m, cumulative); |
| } |
| } |
| } |
| base += static_cast<int64_t>(tree->size()); |
| } |
| if (!ok) { |
| return builder.status(); |
| } |
| ASSIGN_OR_RETURN(core::dataframe::Dataframe df, std::move(builder).Build()); |
| df.Finalize(); |
| return base::StatusOr<core::dataframe::Dataframe>(std::move(df)); |
| } |
| |
| } // namespace perfetto::trace_processor::flamegraph |