blob: 12858f2cf9c42c8fa0e0b7ea1413ba598a7e328f [file]
// 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.
import m from 'mithril';
import type {Trace} from '../../../public/trace';
import type {time} from '../../../base/time';
import type {QueryFlamegraphMetric} from '../../../components/query_flamegraph';
import {FlamegraphPanel} from '../../../components/flamegraph_panel';
import {
Flamegraph,
type FlamegraphState,
type FlamegraphOptionalAction,
} from '../../../widgets/flamegraph';
// Referenced by session.openFlamegraphPivotedAt.
export const METRIC_OBJECT_SIZE = 'Object Size';
export const METRIC_DOMINATED_OBJECT_SIZE = 'Dominated Object Size';
interface FlamegraphViewAttrs {
readonly trace: Trace;
readonly upid: number;
readonly ts: time;
readonly state: FlamegraphState | undefined;
readonly onStateChange: (state: FlamegraphState) => void;
// Open the flamegraph-objects tab for `pathHashes` (CSV).
readonly onShowObjects: (pathHashes: string, isDominator: boolean) => void;
}
// path_hash_stable is exposed unaggregatable (and CAST to TEXT in SQL,
// since the stdlib emits it as INT64 and the flamegraph reads
// unaggregatable columns as STR_NULL) so it lands in `matchingColumns`
// — that's what lets a PIVOT filter target a specific node by its hash.
// Hidden from the tooltip via `isVisible: false`.
const UNAGG_PROPS = [
{name: 'root_type', displayName: 'Root Type'},
{name: 'heap_type', displayName: 'Heap Type'},
{
name: 'path_hash_stable',
displayName: 'Path Hash',
isVisible: () => false,
},
];
const SELF_COUNT_AGG_PROP = {
name: 'self_count',
displayName: 'Self Count',
mergeAggregation: 'SUM' as const,
};
// Build a JAVA_HEAP_GRAPH metric for the BFS or dominator class tree,
// projecting `valueColumn` as `value` and the other column for tooltips.
function buildMetric(
upid: number,
ts: time,
name: string,
unit: string,
valueColumn: 'self_size' | 'self_count',
isDominator: boolean,
showObjectsAction: FlamegraphOptionalAction,
): QueryFlamegraphMetric {
const tree = isDominator
? '_heap_graph_dominator_class_tree'
: '_heap_graph_class_tree';
const dependencyModule = isDominator
? 'android.memory.heap_graph.dominator_class_tree'
: 'android.memory.heap_graph.class_tree';
const otherCol = valueColumn === 'self_size' ? 'self_count' : 'self_size';
return {
name,
unit,
dependencySql: `include perfetto module ${dependencyModule};`,
statement: `
select
id,
parent_id as parentId,
ifnull(name, '[Unknown]') as name,
root_type,
heap_type,
${valueColumn} as value,
${otherCol},
CAST(path_hash_stable AS TEXT) AS path_hash_stable
from ${tree}
where graph_sample_ts = ${ts} and upid = ${upid}
`,
unaggregatableProperties: UNAGG_PROPS,
aggregatableProperties:
valueColumn === 'self_size' ? [SELF_COUNT_AGG_PROP] : [],
optionalNodeActions: [showObjectsAction],
};
}
interface MetricSpec {
readonly name: string;
readonly unit: string;
readonly valueColumn: 'self_size' | 'self_count';
readonly isDominator: boolean;
}
const METRIC_SPECS: ReadonlyArray<MetricSpec> = [
{
name: METRIC_OBJECT_SIZE,
unit: 'B',
valueColumn: 'self_size',
isDominator: false,
},
{
name: 'Object Count',
unit: '',
valueColumn: 'self_count',
isDominator: false,
},
{
name: METRIC_DOMINATED_OBJECT_SIZE,
unit: 'B',
valueColumn: 'self_size',
isDominator: true,
},
{
name: 'Dominated Object Count',
unit: '',
valueColumn: 'self_count',
isDominator: true,
},
];
function buildHeapGraphMetrics(
upid: number,
ts: time,
onShowObjects: (pathHashes: string, isDominator: boolean) => void,
): ReadonlyArray<QueryFlamegraphMetric> {
const showObjectsAction = (
isDominator: boolean,
): FlamegraphOptionalAction => ({
name: 'Show objects from this class',
execute: async ({properties}) => {
const pathHashes = properties.get('path_hash_stable');
if (pathHashes === undefined) return;
onShowObjects(pathHashes, isDominator);
},
});
return METRIC_SPECS.map((s) =>
buildMetric(
upid,
ts,
s.name,
s.unit,
s.valueColumn,
s.isDominator,
showObjectsAction(s.isDominator),
),
);
}
const FlamegraphView: m.ClosureComponent<FlamegraphViewAttrs> = () => {
let cachedMetrics: ReadonlyArray<QueryFlamegraphMetric> | undefined;
let cachedKey: string | undefined;
return {
view({attrs}) {
const key = `${attrs.upid}:${attrs.ts}`;
if (cachedMetrics === undefined || key !== cachedKey) {
cachedMetrics = buildHeapGraphMetrics(
attrs.upid,
attrs.ts,
attrs.onShowObjects,
);
cachedKey = key;
}
const metrics = cachedMetrics;
// First render or after a dump-change reset: create a default
// state so the panel renders meaningfully on the same frame.
let state = attrs.state;
if (state === undefined) {
state = Flamegraph.createDefaultState(metrics);
attrs.onStateChange(state);
}
return m(
'div',
{class: 'pf-hde-view-content pf-hde-flamegraph-view'},
m(FlamegraphPanel, {
trace: attrs.trace,
metrics,
state,
onStateChange: attrs.onStateChange,
}),
);
},
};
};
export default FlamegraphView;