Add lock contention plugin
diff --git a/ui/src/plugins/com.android.AndroidLockContention/OWNERS b/ui/src/plugins/com.android.AndroidLockContention/OWNERS new file mode 100644 index 0000000..d523cb6 --- /dev/null +++ b/ui/src/plugins/com.android.AndroidLockContention/OWNERS
@@ -0,0 +1 @@ +file://depot/google3/third_party/perfetto/OWNERS
diff --git a/ui/src/plugins/com.android.AndroidLockContention/android_lock_contention_event_source.ts b/ui/src/plugins/com.android.AndroidLockContention/android_lock_contention_event_source.ts new file mode 100644 index 0000000..b895818 --- /dev/null +++ b/ui/src/plugins/com.android.AndroidLockContention/android_lock_contention_event_source.ts
@@ -0,0 +1,162 @@ +// 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 {Trace} from '../../public/trace'; +import { + EventSource, + RelatedEvent, + RelatedEventData, + Relation, + getTrackUriForTrackId, +} from '../dev.perfetto.RelatedEvents'; +import {time, duration} from '../../base/time'; +import {STR, NUM_NULL, LONG_NULL} from '../../trace_processor/query_result'; +import {enrichDepths} from '../dev.perfetto.RelatedEvents/utils'; + +export type OnDataLoadedCallback = (data: RelatedEventData) => void; + +export class AndroidLockContentionEventSource implements EventSource { + private onDataLoadedCallback?: OnDataLoadedCallback; + + constructor(private trace: Trace) {} + + setOnDataLoadedCallback(callback: OnDataLoadedCallback) { + this.onDataLoadedCallback = callback; + } + + async getRelatedEventData(eventId: number): Promise<RelatedEventData> { + const query = ` + SELECT + amc.id AS contention_id, + amc.ts AS contention_ts, + amc.dur AS contention_dur, + amc.track_id AS blocked_track_id, + amc.blocked_utid, + amc.blocking_utid, + amc.short_blocked_method, + amc.short_blocking_method, + amc.blocking_thread_name, + s.id AS blocking_slice_id, + s.track_id AS blocking_track_id, + s.ts AS blocking_ts, + s.dur AS blocking_dur + FROM android_monitor_contention amc + LEFT JOIN thread_or_process_slice s ON s.utid = amc.blocking_utid + AND amc.ts >= s.ts + AND amc.ts < s.ts + s.dur + WHERE amc.id = ${eventId} + ORDER BY s.depth DESC, s.id DESC + LIMIT 1 + `; + + const result = await this.trace.engine.query(query); + const it = result.iter({ + contention_id: NUM_NULL, + contention_ts: LONG_NULL, + contention_dur: LONG_NULL, + blocked_track_id: NUM_NULL, + blocked_utid: NUM_NULL, + blocking_utid: NUM_NULL, + short_blocked_method: STR, + short_blocking_method: STR, + blocking_thread_name: STR, + blocking_slice_id: NUM_NULL, + blocking_track_id: NUM_NULL, + blocking_ts: LONG_NULL, + blocking_dur: LONG_NULL, + }); + + const events: RelatedEvent[] = []; + const overlayEvents: RelatedEvent[] = []; + const overlayRelations: Relation[] = []; + + if (!it.valid()) { + const data = {events: [], relations: [], overlayEvents, overlayRelations}; + this.onDataLoadedCallback?.(data); + return data; + } + + const blockedEventId = it.contention_id!; + const blockedTs = BigInt(it.contention_ts!) as time; + const blockedDur = BigInt(it.contention_dur!) as duration; + const blockedTrackId = it.blocked_track_id!; + const blockingThreadName = it.blocking_thread_name; + + const blockedTrackUri = getTrackUriForTrackId(this.trace, blockedTrackId); + const blockingTrackId = it.blocking_track_id; + const blockingTrackUri = + typeof blockingTrackId === 'number' + ? getTrackUriForTrackId(this.trace, blockingTrackId) + : undefined; + + const tabEvent: RelatedEvent = { + id: blockedEventId, + ts: blockedTs, + dur: blockedDur, + trackUri: blockedTrackUri, + type: 'Lock Contention', + customArgs: { + short_blocked_method: it.short_blocked_method, + short_blocking_method: it.short_blocking_method, + blocking_thread_name: blockingThreadName, + blockingTrackUri: blockingTrackUri, + blockingSliceId: it.blocking_slice_id, + }, + }; + events.push(tabEvent); + + // Event for the selected contention slice itself + const blockedOverlayEvent: RelatedEvent = { + id: blockedEventId, + ts: blockedTs, + dur: blockedDur, + trackUri: blockedTrackUri, + type: 'Lock Contention', + customArgs: { + Blocked: it.short_blocked_method, + Blocking: it.short_blocking_method, + }, + }; + overlayEvents.push(blockedOverlayEvent); + + const blockingSliceId = it.blocking_slice_id; + if (blockingSliceId !== null && blockingTrackUri) { + const blockingEvent: RelatedEvent = { + id: blockingSliceId, + ts: BigInt(it.blocking_ts!) as time, + dur: BigInt(it.blocking_dur!) as duration, + trackUri: blockingTrackUri, + type: 'Blocking Slice', + customArgs: { + name: it.short_blocking_method, + }, + }; + overlayEvents.push(blockingEvent); + + const relation: Relation = { + sourceId: blockedEventId, + targetId: blockingSliceId, + type: 'blocked_on', + customArgs: {color: 'red'}, + }; + overlayRelations.push(relation); + } + + await enrichDepths(this.trace, overlayEvents); + + const data = {events, relations: [], overlayEvents, overlayRelations}; + this.onDataLoadedCallback?.(data); + return data; + } +}
diff --git a/ui/src/plugins/com.android.AndroidLockContention/index.ts b/ui/src/plugins/com.android.AndroidLockContention/index.ts new file mode 100644 index 0000000..92fcc8e --- /dev/null +++ b/ui/src/plugins/com.android.AndroidLockContention/index.ts
@@ -0,0 +1,60 @@ +// 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 {PerfettoPlugin} from '../../public/plugin'; +import {Trace} from '../../public/trace'; +import RelatedEventsPlugin from '../dev.perfetto.RelatedEvents'; +import {GenericRelatedEventsOverlay} from '../dev.perfetto.RelatedEvents/generic_overlay'; +import {AndroidLockContentionEventSource} from './android_lock_contention_event_source'; +import {AndroidLockContentionTab} from './tab'; + +export default class AndroidLockContentionPlugin implements PerfettoPlugin { + static readonly id = 'com.android.AndroidLockContention'; + static readonly dependencies = [RelatedEventsPlugin]; + + async onTraceLoad(trace: Trace): Promise<void> { + trace.engine.query('INCLUDE PERFETTO MODULE android.monitor_contention'); + + const overlay = new GenericRelatedEventsOverlay(trace); + trace.tracks.registerOverlay(overlay); + + const source = new AndroidLockContentionEventSource(trace); + + const tab = new AndroidLockContentionTab({trace, source}); + source.setOnDataLoadedCallback((data) => { + overlay.update(data); + }); + + trace.tabs.registerTab({ + uri: 'com.android.AndroidLockContentionTab', + isEphemeral: false, + content: tab, + onHide() { + overlay.update({ + events: [], + relations: [], + }); + }, + }); + + trace.commands.registerCommand({ + id: 'openAndroidLockContentionTab', + name: 'Show Android Lock Contention', + callback: () => { + trace.tabs.showTab('com.android.AndroidLockContentionTab'); + tab.syncSelection(); + }, + }); + } +}
diff --git a/ui/src/plugins/com.android.AndroidLockContention/tab.ts b/ui/src/plugins/com.android.AndroidLockContention/tab.ts new file mode 100644 index 0000000..f447759 --- /dev/null +++ b/ui/src/plugins/com.android.AndroidLockContention/tab.ts
@@ -0,0 +1,170 @@ +// 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 {Tab} from '../../public/tab'; +import {Trace} from '../../public/trace'; +import {DetailsShell} from '../../widgets/details_shell'; +import {Section} from '../../widgets/section'; +import {AndroidLockContentionEventSource} from './android_lock_contention_event_source'; +import {Anchor} from '../../widgets/anchor'; +import {Icons} from '../../base/semantic_icons'; +import {Spinner} from '../../widgets/spinner'; +import {time} from '../../base/time'; +import {RelatedEvent} from '../dev.perfetto.RelatedEvents/interface'; + +interface LockContentionArgs { + short_blocked_method: string; + short_blocking_method: string; + blocking_thread_name: string; + blockingTrackUri: string; + blockingSliceId?: number; + allTrackUris?: string[]; +} + +function isLockContentionArgs(args: unknown): args is LockContentionArgs { + if (typeof args !== 'object' || args === null) return false; + const obj = args as Record<string, unknown>; + return ( + typeof obj.short_blocked_method === 'string' && + typeof obj.short_blocking_method === 'string' + ); +} + +interface LockContentionTabConfig { + trace: Trace; + source: AndroidLockContentionEventSource; +} + +export class AndroidLockContentionTab implements Tab { + private event: RelatedEvent | null = null; + private isLoading = false; + + constructor(private config: LockContentionTabConfig) {} + + syncSelection() { + const selection = this.config.trace.selection.selection; + if (selection.kind === 'track_event') { + this.loadData(selection.eventId); + } else { + this.event = null; + } + } + + async loadData(eventId: number) { + if (this.isLoading) return; + this.isLoading = true; + this.event = null; + try { + const data = await this.config.source.getRelatedEventData(eventId); + this.event = data.events.length > 0 ? data.events[0] : null; + } finally { + this.isLoading = false; + } + } + + getTitle() { + return 'Lock Contention'; + } + + private goTo(trackUri: string, eventId: number) { + this.config.trace.selection.selectTrackEvent(trackUri, eventId, { + scrollToSelection: true, + switchToCurrentSelectionTab: false, + }); + } + + private scrollToTime(trackUri: string, ts: time) { + this.config.trace.scrollTo({ + time: { + start: ts, + behavior: 'pan', + }, + track: { + uri: trackUri, + expandGroup: true, + }, + }); + } + + render(): m.Children { + if (this.isLoading) { + return m(DetailsShell, {title: this.getTitle()}, m(Spinner, {})); + } + + if (!this.event || !this.event.customArgs) { + return m( + DetailsShell, + {title: this.getTitle()}, + m('.note', 'Select a lock contention event.'), + ); + } + + const args = this.event.customArgs; + if (!isLockContentionArgs(args)) { + console.error('Invalid customArgs for LockContention event', args); + return m( + DetailsShell, + {title: this.getTitle()}, + m('.note', 'Error: Invalid event data.'), + ); + } + + return m( + DetailsShell, + {title: this.getTitle()}, + m( + Section, + { + title: 'Contention Details', + }, + m('div', `Blocked Method: ${args.short_blocked_method}`), + m('div', `Blocking Method: ${args.short_blocking_method}`), + m( + Anchor, + { + icon: Icons.GoTo, + onclick: () => this.goTo(this.event!.trackUri, this.event!.id), + title: 'Go to Blocked Event', + }, + 'Go to Blocked', + ), + args.blockingTrackUri && + args.blockingSliceId !== undefined && + m( + Anchor, + { + icon: Icons.GoTo, + onclick: () => + this.goTo(args.blockingTrackUri!, args.blockingSliceId!), + title: 'Go to Blocking Slice', + }, + 'Go to Blocking Slice', + ), + args.blockingTrackUri && + args.blockingSliceId === undefined && + m( + Anchor, + { + icon: Icons.GoTo, + onclick: () => + this.scrollToTime(args.blockingTrackUri!, this.event!.ts), + title: 'Scroll to Blocking Thread at Contention Time', + }, + 'Scroll to Blocking Thread', + ), + ), + ); + } +}