blob: 4cbfed2e42a04dd91de6480f6c5dbee412bd51c3 [file]
// Copyright (C) 2025 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 './styles.scss';
import m from 'mithril';
import type {QueryFlamegraphMetric} from '../../components/query_flamegraph';
import type {PerfettoPlugin} from '../../public/plugin';
import type {Trace} from '../../public/trace';
import {NUM, STR} from '../../trace_processor/query_result';
import {AggregateProfilesPage} from './aggregate_profiles_page';
import {
type AggregateProfilesPageState,
AGGREGATE_PROFILES_PAGE_STATE_SCHEMA,
} from './types';
import type {Store} from '../../base/store';
import {assertExists} from '../../base/assert';
export default class implements PerfettoPlugin {
static readonly id = 'dev.perfetto.AggregateProfiles';
private store?: Store<AggregateProfilesPageState>;
private migratePageState(init: unknown): AggregateProfilesPageState {
const result = AGGREGATE_PROFILES_PAGE_STATE_SCHEMA.safeParse(init);
return result.data ?? {};
}
async onTraceLoad(trace: Trace): Promise<void> {
this.store = trace.mountStore('dev.perfetto.AggregateProfiles', (init) =>
this.migratePageState(init),
);
const profiles = await this.getProfiles(trace);
if (profiles.length === 0) {
return;
}
const store = assertExists(this.store);
trace.pages.registerPage({
route: '/aggregateprofiles',
render: () =>
m(AggregateProfilesPage, {
trace,
state: store.state,
onStateChange: (state: AggregateProfilesPageState) => {
store.edit((draft) => {
draft.selectedProfileId = state.selectedProfileId;
draft.flamegraphState = state.flamegraphState;
});
},
profiles,
}),
});
trace.sidebar.addMenuItem({
section: 'current_trace',
sortOrder: 11,
text: 'Aggregate Profiles',
href: '#!/aggregateprofiles',
icon: 'analytics',
});
trace.onTraceReady.addListener(async () => {
const hasAnyTracks = trace.workspaces.all[0].flatTracks.length > 0;
// TODO(lalitm): it's really bad that we're unconditionally navigating
// to the profiles page: really we should check if the user has not already
// set a page and then only navigate if no page is set. However:
// a) no API exists for checking the current page
// b) there is already some code in UI load time which navigates
// to the viewer page so we would always fail this check.
// So for now just leave this as-is.
if (!hasAnyTracks && profiles.length > 0) {
trace.navigate('#!/aggregateprofiles');
}
});
}
private async getProfiles(trace: Trace) {
const result = await trace.engine.query(
'SELECT DISTINCT scope FROM __intrinsic_aggregate_profile ORDER BY scope',
);
const profiles = [];
for (const it = result.iter({scope: STR}); it.valid(); it.next()) {
const metrics = await this.getProfileMetrics(trace, it.scope);
if (metrics.length > 0) {
profiles.push({
id: `profile_${it.scope}`,
displayName: it.scope,
metrics,
});
}
}
return profiles;
}
private async getProfileMetrics(
trace: Trace,
scope: string,
): Promise<QueryFlamegraphMetric[]> {
const result = await trace.engine.query(`
SELECT
id,
sample_type_type,
sample_type_unit,
sample_type_type || ' (' || sample_type_unit || ')' as display_name
FROM __intrinsic_aggregate_profile
WHERE scope = '${scope}'
ORDER BY sample_type_type
`);
const metrics: QueryFlamegraphMetric[] = [];
for (
const it = result.iter({
id: NUM,
sample_type_unit: STR,
display_name: STR,
});
it.valid();
it.next()
) {
metrics.push({
name: it.display_name,
unit: it.sample_type_unit,
nameColumnLabel: 'Symbol',
dependencySql: 'include perfetto module callstacks.stack_profile',
statement: `
WITH profile_samples AS MATERIALIZED (
SELECT callsite_id, sum(sample.value) AS sample_value
FROM __intrinsic_aggregate_sample sample
WHERE sample.aggregate_profile_id = ${it.id}
GROUP BY callsite_id
)
SELECT
c.id,
c.parent_id as parentId,
c.name,
c.mapping_name,
c.source_file || ':' || c.line_number as source_location,
cast_string!(c.inlined) AS inlined,
CASE WHEN c.is_leaf_function_in_callsite_frame
THEN coalesce(m.sample_value, 0)
ELSE 0
END AS value
FROM _callstacks_for_stack_profile_samples!(profile_samples) AS c
LEFT JOIN profile_samples AS m USING (callsite_id)
`,
unaggregatableProperties: [
{name: 'mapping_name', displayName: 'Mapping'},
{
name: 'inlined',
displayName: 'Inlined',
isVisible: () => false,
},
],
aggregatableProperties: [
{
name: 'source_location',
displayName: 'Source Location',
mergeAggregation: 'ONE_OR_SUMMARY',
},
],
optionalMarker: {
name: 'Inlined Function',
isVisible: (properties: ReadonlyMap<string, string>) =>
properties.get('inlined') === '1',
},
});
}
return metrics;
}
}