diff --git a/ui/src/plugins/com.android.InputEvents/index.ts b/ui/src/plugins/com.android.InputEvents/index.ts index ffb037ddd80..e9fbc5dec9f 100644 --- a/ui/src/plugins/com.android.InputEvents/index.ts +++ b/ui/src/plugins/com.android.InputEvents/index.ts @@ -12,6 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +import {createQueryCounterTrack} from '../../components/tracks/query_counter_track'; +import {getTimeSpanOfSelectionOrVisibleWindow} from '../../public/utils'; +import {uuidv4} from '../../base/uuid'; import {LONG, LONG_NULL, STR} from '../../trace_processor/query_result'; import {PerfettoPlugin} from '../../public/plugin'; import {Trace} from '../../public/trace'; @@ -19,12 +22,24 @@ import {SliceTrack} from '../../components/tracks/slice_track'; import {SourceDataset} from '../../trace_processor/dataset'; import {TrackNode} from '../../public/workspace'; import StandardGroupsPlugin from '../dev.perfetto.StandardGroups'; +import {TimeSpan} from '../../base/time'; -export default class implements PerfettoPlugin { +export default class AndroidInputEvents implements PerfettoPlugin { static readonly id = 'com.android.InputEvents'; static readonly dependencies = [StandardGroupsPlugin]; async onTraceLoad(ctx: Trace): Promise { + await ctx.engine.query(` + INCLUDE PERFETTO MODULE android.input; + INCLUDE PERFETTO MODULE intervals.overlap; + `); + + ctx.commands.registerCommand({ + id: 'com.android.InputEvents.visualizeOverlaps', + name: 'Input Events: Visualize event overlaps (over selection)', + callback: () => this.visualizeOverlaps(ctx), + }); + const cnt = await ctx.engine.query(` SELECT COUNT(*) AS cnt @@ -35,7 +50,6 @@ export default class implements PerfettoPlugin { return; } - await ctx.engine.query('INCLUDE PERFETTO MODULE android.input;'); const uri = 'com.android.InputEvents#InputEventsTrack'; const track = await SliceTrack.createMaterialized({ trace: ctx, @@ -66,4 +80,206 @@ export default class implements PerfettoPlugin { .getOrCreateStandardGroup(ctx.defaultWorkspace, 'USER_INTERACTION'); group.addChildInOrder(node); } + + async visualizeOverlaps(ctx: Trace): Promise { + const window = await getTimeSpanOfSelectionOrVisibleWindow(ctx); + const rootNode = await this.createRootTrack(ctx, window); + ctx.defaultWorkspace.pinnedTracksNode.addChildLast(rootNode); + + const processes = await this.getProcesses(ctx, window); + const processTrackPromises: Promise[] = []; + + for ( + const it = processes.iter({upid: LONG, process_name: STR}); + it.valid(); + it.next() + ) { + const upid = Number(it.upid); + const processName = it.process_name; + processTrackPromises.push( + this.createProcessTrack(ctx, window, upid, processName), + ); + } + + const processTracks = await Promise.all(processTrackPromises); + for (const processTrack of processTracks) { + rootNode.addChildLast(processTrack); + } + } + + private async createRootTrack( + ctx: Trace, + window: TimeSpan, + ): Promise { + const uri = `com.android.InputEvents.event_overlaps_parent.${uuidv4()}`; + const sqlSource = this.getOverlapSqlSource(window); + return this.createTrack( + ctx, + uri, + sqlSource, + 'Input Events', + 'Number of concurrent input events (from input dispatch to input ACK received).', + ); + } + + private async getProcesses(ctx: Trace, window: TimeSpan) { + return ctx.engine.query(` + WITH + process_peaks AS ( + SELECT + group_name AS upid, + MAX(value) AS peak + FROM intervals_overlap_count_by_group!((${this.getEventsSubquery(window)}), dispatch_ts, total_latency_dur, upid) + GROUP BY upid + HAVING MAX(value) > 0 + ) + SELECT + pp.upid, + p.name AS process_name + FROM process_peaks pp + JOIN process p USING (upid) + ORDER BY pp.peak DESC + `); + } + + private async createProcessTrack( + ctx: Trace, + window: TimeSpan, + upid: number, + processName: string, + ): Promise { + const uri = `com.android.InputEvents.event_overlaps.proc_${upid}.${uuidv4()}`; + + const channels = await this.getChannels(ctx, window, upid); + const numberOfChannels = channels.numRows(); + const plural = numberOfChannels === 1 ? '' : 's'; + const name = `${processName} ${upid} (${numberOfChannels} channel${plural})`; + + const sqlSource = this.getOverlapSqlSource(window, [`upid = ${upid}`]); + const processNode = await this.createTrack( + ctx, + uri, + sqlSource, + name, + `Number of concurrent input events received by process ${processName} ${upid} (from input dispatch to input ACK received).`, + ); + + const channelTrackPromises: Promise[] = []; + for ( + const it = channels.iter({event_channel: STR}); + it.valid(); + it.next() + ) { + const channel = it.event_channel; + channelTrackPromises.push( + this.createChannelTrack(ctx, window, channel, upid), + ); + } + + const channelTracks = await Promise.all(channelTrackPromises); + for (const channelTrack of channelTracks) { + processNode.addChildLast(channelTrack); + } + return processNode; + } + + private async getChannels(ctx: Trace, window: TimeSpan, upid: number) { + return ctx.engine.query(` + SELECT + group_name AS event_channel + FROM intervals_overlap_count_by_group!((${this.getEventsSubquery(window, [`upid = ${upid}`])}), dispatch_ts, total_latency_dur, event_channel) + GROUP BY event_channel + HAVING MAX(value) > 0 + ORDER BY MAX(value) DESC + `); + } + + private async createChannelTrack( + ctx: Trace, + window: TimeSpan, + channel: string, + upid: number, + ): Promise { + const uri = `com.android.InputEvents.event_overlaps.proc_${upid}.${channel}.${uuidv4()}`; + const sqlSource = this.getOverlapSqlSource(window, [ + `upid = ${upid}`, + `event_channel = '${channel}'`, + ]); + return this.createTrack( + ctx, + uri, + sqlSource, + `Channel: ${channel}`, + `Number of concurrent input events on the ${channel} channel (from input dispatch to input ACK received).`, + ); + } + + private async createTrack( + ctx: Trace, + uri: string, + sqlSource: string, + name: string, + description: string, + removable = true, + ): Promise { + const track = await createQueryCounterTrack({ + trace: ctx, + uri, + materialize: false, + data: { + sqlSource, + }, + columns: { + ts: 'ts', + value: 'value', + }, + }); + ctx.tracks.registerTrack({ + uri, + renderer: track, + description, + }); + + return new TrackNode({ + uri, + name, + removable, + }); + } + + private getOverlapSqlSource( + window: TimeSpan, + whereClauses: string[] = [], + ): string { + const subquery = this.getEventsSubquery(window, whereClauses); + return ` + SELECT * + FROM intervals_overlap_count!( + (${subquery}), + dispatch_ts, + total_latency_dur + ) + `; + } + + private getEventsSubquery( + window: TimeSpan, + whereClauses: string[] = [], + ): string { + const whereClause = + whereClauses.length > 0 ? `AND ${whereClauses.join(' AND ')}` : ''; + return ` + SELECT + upid, + process_name, + event_channel, + MAX(dispatch_ts, ${window.start}) AS dispatch_ts, + MIN(dispatch_ts + total_latency_dur, ${window.end}) - MAX(dispatch_ts, ${window.start}) AS total_latency_dur + FROM android_input_events + WHERE + total_latency_dur IS NOT NULL AND + dispatch_ts < ${window.end} AND dispatch_ts + total_latency_dur > ${window.start} + ${whereClause} + `; + } }