Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ui/src/plugins/com.android.AndroidLockContention/OWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ivankc@google.com
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// 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,
getTrackUriForTrackId,
} from '../dev.perfetto.RelatedEvents';
import {time, duration} from '../../base/time';
import {STR, NUM_NULL, LONG_NULL} from '../../trace_processor/query_result';

export class AndroidLockContentionEventSource implements EventSource {
constructor(private trace: Trace) {}

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.blocked_thread_name,
amc.blocked_src,
amc.short_blocking_method,
amc.blocking_thread_name,
amc.blocking_src,
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,
blocked_thread_name: STR,
blocked_src: STR,
short_blocking_method: STR,
blocking_thread_name: STR,
blocking_src: STR,
blocking_slice_id: NUM_NULL,
blocking_track_id: NUM_NULL,
blocking_ts: LONG_NULL,
blocking_dur: LONG_NULL,
});

const events: RelatedEvent[] = [];

if (!it.valid()) {
return {events: [], relations: []};
}

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,
blocked_thread_name: it.blocked_thread_name,
blocked_src: it.blocked_src,
short_blocking_method: it.short_blocking_method,
blocking_thread_name: blockingThreadName,
blocking_src: it.blocking_src,
blockingTrackUri: blockingTrackUri,
blockingSliceId: it.blocking_slice_id,
},
};
events.push(tabEvent);
return {events, relations: []};
}
}
99 changes: 99 additions & 0 deletions ui/src/plugins/com.android.AndroidLockContention/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// 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 {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 description = `
This plugin shows the blocking thread which is causing monitor contention.

To use this, when selecting a track event beginning with 'monitor contention', you can call the command 'Android Lock
Contention: Toggle Blocked/Blocking Slice' which shows a tab with details of the blocking + blocked methods with
links to navigate. The default hotkey for this command is ']'.
`;
static readonly dependencies = [RelatedEventsPlugin];

async onTraceLoad(trace: Trace): Promise<void> {
trace.engine.query('INCLUDE PERFETTO MODULE android.monitor_contention');

const source = new AndroidLockContentionEventSource(trace);
const tab = new AndroidLockContentionTab({trace, source});

trace.tabs.registerTab({
uri: 'com.android.AndroidLockContentionTab',
isEphemeral: false,
content: tab,
});

trace.commands.registerCommand({
id: 'toggleContentionNavigation',
name: 'Android Lock Contention: Toggle Blocked/Blocking Slice',
defaultHotkey: ']',
callback: async () => {
const selection = trace.selection.selection;
const tabInstance = tab;

trace.tabs.showTab('com.android.AndroidLockContentionTab');

if (!tabInstance.hasEvent()) {
if (selection.kind === 'track_event') {
await tabInstance.loadData(selection.eventId);
}
return;
}

const currentEventArgs = tabInstance.getEventArgs();
if (!currentEventArgs) return;

const contentionId = tabInstance.getContentionId();
const {blockingTrackUri, blockingSliceId} = currentEventArgs;

if (selection.kind === 'track_event') {
if (selection.eventId === contentionId) {
// Currently on blocked, jump to blocking
if (blockingTrackUri && blockingSliceId !== undefined) {
trace.selection.selectTrackEvent(
blockingTrackUri,
blockingSliceId,
{
scrollToSelection: true,
switchToCurrentSelectionTab: false,
},
);
}
} else if (selection.eventId === blockingSliceId) {
// Currently on blocking, jump back to blocked
const blockedTrackUri = tabInstance.getEventTrackUri();
if (blockedTrackUri && contentionId !== undefined) {
trace.selection.selectTrackEvent(blockedTrackUri, contentionId, {
scrollToSelection: true,
switchToCurrentSelectionTab: false,
});
}
} else {
// New selection, load it
await tabInstance.loadData(selection.eventId);
}
} else {
// No selection, do nothing to the navigation
}
},
});
}
}
Loading
Loading