diff --git a/ui/src/plugins/dev.perfetto.ProcessSummary/index.ts b/ui/src/plugins/dev.perfetto.ProcessSummary/index.ts index 5ed3b68e0c9..6c136f77367 100644 --- a/ui/src/plugins/dev.perfetto.ProcessSummary/index.ts +++ b/ui/src/plugins/dev.perfetto.ProcessSummary/index.ts @@ -25,15 +25,10 @@ import ThreadPlugin from '../dev.perfetto.Thread'; import {createPerfettoIndex} from '../../trace_processor/sql_utils'; import {uuidv4Sql} from '../../base/uuid'; import { - Config as ProcessSchedulingTrackConfig, - PROCESS_SCHEDULING_TRACK_KIND, - ProcessSchedulingTrack, -} from './process_scheduling_track'; -import { - Config as ProcessSummaryTrackConfig, - PROCESS_SUMMARY_TRACK_KIND, - ProcessSummaryTrack, -} from './process_summary_track'; + Config as SliceTrackSummaryConfig, + SLICE_TRACK_SUMMARY_KIND, + SliceTrackSummary, +} from './slice_track_summary'; // This plugin is responsible for adding summary tracks for process and thread // groups. @@ -47,7 +42,8 @@ export default class implements PerfettoPlugin { } private async addProcessTrackGroups(ctx: Trace): Promise { - // Makes the queries in `ProcessSchedulingTrack` significantly faster. + // Makes the queries in `SliceTrackSummary` significantly faster when using + // scheduling data. // TODO(lalitm): figure out a better way to do this without hardcoding this // here. await createPerfettoIndex({ @@ -55,12 +51,13 @@ export default class implements PerfettoPlugin { name: `__process_scheduling_${uuidv4Sql()}`, on: `__intrinsic_sched_slice(utid)`, }); - // Makes the queries in `ProcessSummaryTrack` significantly faster. + // Makes the queries in `SliceTrackSummary` significantly faster when using + // slice data. // TODO(lalitm): figure out a better way to do this without hardcoding this // here. await createPerfettoIndex({ engine: ctx.engine, - name: `__process_summary_${uuidv4Sql()}`, + name: `__slice_track_summary_${uuidv4Sql()}`, on: `__intrinsic_slice(track_id)`, }); @@ -171,39 +168,28 @@ export default class implements PerfettoPlugin { // for additional details. isBootImageProfiling && chips.push('boot image profiling'); - if (hasSched) { - const config: ProcessSchedulingTrackConfig = { - pidForColor, - upid, - utid, - }; - - ctx.tracks.registerTrack({ - uri, - tags: { - kinds: [PROCESS_SCHEDULING_TRACK_KIND], - }, - chips, - renderer: new ProcessSchedulingTrack(ctx, config, cpuCount, threads), - subtitle, - }); - } else { - const config: ProcessSummaryTrackConfig = { - pidForColor, - upid, - utid, - }; + const config: SliceTrackSummaryConfig = { + pidForColor, + upid, + utid, + }; - ctx.tracks.registerTrack({ + ctx.tracks.registerTrack({ + uri, + tags: { + kinds: [SLICE_TRACK_SUMMARY_KIND], + }, + chips, + renderer: new SliceTrackSummary( + ctx, uri, - tags: { - kinds: [PROCESS_SUMMARY_TRACK_KIND], - }, - chips, - renderer: new ProcessSummaryTrack(ctx.engine, config), - subtitle, - }); - } + config, + cpuCount, + threads, + hasSched, + ), + subtitle, + }); } } @@ -246,7 +232,14 @@ export default class implements PerfettoPlugin { return; } - const config: ProcessSummaryTrackConfig = { + // Get CPU count for kernel thread summary + const cpuCountResult = await engine.query(` + SELECT COUNT(*) as cpu_count FROM cpu WHERE IFNULL(machine_id, 0) = 0 + `); + const cpuCount = cpuCountResult.firstRow({cpu_count: NUM}).cpu_count; + + const threads = ctx.plugins.getPlugin(ThreadPlugin).getThreadMap(); + const config: SliceTrackSummaryConfig = { pidForColor: 2, upid: it.upid, utid: it.utid, @@ -255,9 +248,16 @@ export default class implements PerfettoPlugin { ctx.tracks.registerTrack({ uri: '/kernel', tags: { - kinds: [PROCESS_SUMMARY_TRACK_KIND], + kinds: [SLICE_TRACK_SUMMARY_KIND], }, - renderer: new ProcessSummaryTrack(ctx.engine, config), + renderer: new SliceTrackSummary( + ctx, + '/kernel', + config, + cpuCount, + threads, + false, + ), }); } } diff --git a/ui/src/plugins/dev.perfetto.ProcessSummary/process_summary_track.ts b/ui/src/plugins/dev.perfetto.ProcessSummary/process_summary_track.ts deleted file mode 100644 index 9d7bd1dd8c3..00000000000 --- a/ui/src/plugins/dev.perfetto.ProcessSummary/process_summary_track.ts +++ /dev/null @@ -1,225 +0,0 @@ -// Copyright (C) 2023 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 {BigintMath} from '../../base/bigint_math'; -import {assertExists, assertTrue} from '../../base/logging'; -import {duration, Time, time} from '../../base/time'; -import {colorForTid} from '../../components/colorizer'; -import {TrackData} from '../../components/tracks/track_data'; -import {TimelineFetcher} from '../../components/tracks/track_helper'; -import {checkerboardExcept} from '../../components/checkerboard'; -import {Engine} from '../../trace_processor/engine'; -import {TrackRenderer} from '../../public/track'; -import {LONG, NUM} from '../../trace_processor/query_result'; -import {uuidv4Sql} from '../../base/uuid'; -import {TrackRenderContext} from '../../public/track'; -import {AsyncDisposableStack} from '../../base/disposable_stack'; -import { - createPerfettoTable, - createVirtualTable, -} from '../../trace_processor/sql_utils'; - -export const PROCESS_SUMMARY_TRACK_KIND = 'ProcessSummaryTrack'; - -interface Data extends TrackData { - starts: BigInt64Array; - utilizations: Float64Array; -} - -export interface Config { - pidForColor: number | bigint; - upid: number | null; - utid: number | null; -} - -const MARGIN_TOP = 5; -const RECT_HEIGHT = 30; -const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT; -const SUMMARY_HEIGHT = TRACK_HEIGHT - MARGIN_TOP; - -export class ProcessSummaryTrack implements TrackRenderer { - private fetcher = new TimelineFetcher(this.onBoundsChange.bind(this)); - private engine: Engine; - private config: Config; - private uuid = uuidv4Sql(); - - constructor(engine: Engine, config: Config) { - this.engine = engine; - this.config = config; - } - - async onCreate(): Promise { - const getQuery = () => { - if (this.config.upid !== null) { - return ` - select tt.id as track_id - from thread_track as tt - join _thread_available_info_summary using (utid) - join thread using (utid) - where thread.upid = ${this.config.upid} - order by slice_count desc - `; - } - return ` - select tt.id as track_id - from thread_track as tt - join _thread_available_info_summary using (utid) - where tt.utid = ${assertExists(this.config.utid)} - order by slice_count desc - `; - }; - - const trash = new AsyncDisposableStack(); - trash.use( - await createPerfettoTable({ - engine: this.engine, - name: `tmp_${this.uuid}`, - as: getQuery(), - }), - ); - trash.use( - await createPerfettoTable({ - engine: this.engine, - name: `changes_${this.uuid}`, - as: ` - select ts, 1.0 as value - from tmp_${this.uuid} - cross join slice using (track_id) - where slice.depth = 0 - union all - select ts + dur as ts, -1.0 as value - from tmp_${this.uuid} - cross join slice using (track_id) - where slice.depth = 0 - `, - }), - ); - await createVirtualTable({ - engine: this.engine, - name: `process_summary_${this.uuid}`, - using: `__intrinsic_counter_mipmap(( - select - ts, - sum(value) over (order by ts) / ( - select count() from tmp_${this.uuid} - ) as value - from changes_${this.uuid} - order by ts - ))`, - }); - await trash.asyncDispose(); - } - - async onUpdate({ - visibleWindow, - resolution, - }: TrackRenderContext): Promise { - await this.fetcher.requestData(visibleWindow.toTimeSpan(), resolution); - } - - async onBoundsChange( - start: time, - end: time, - resolution: duration, - ): Promise { - // Resolution must always be a power of 2 for this logic to work - assertTrue( - BigintMath.popcount(resolution) === 1, - `${resolution} not pow of 2`, - ); - - const queryRes = await this.engine.query(` - select last_ts as ts, last_value as utilization - from process_summary_${this.uuid}(${start}, ${end}, ${resolution}); - `); - const numRows = queryRes.numRows(); - const slices: Data = { - start, - end, - resolution, - length: numRows, - starts: new BigInt64Array(numRows), - utilizations: new Float64Array(numRows), - }; - const it = queryRes.iter({ - ts: LONG, - utilization: NUM, - }); - for (let row = 0; it.valid(); it.next(), row++) { - slices.starts[row] = it.ts; - slices.utilizations[row] = it.utilization; - } - return slices; - } - - async onDestroy(): Promise { - await this.engine.tryQuery( - `drop table if exists process_summary_${this.uuid};`, - ); - this.fetcher[Symbol.dispose](); - } - - getHeight(): number { - return TRACK_HEIGHT; - } - - render(trackCtx: TrackRenderContext): void { - const {ctx, size, timescale} = trackCtx; - - const data = this.fetcher.data; - if (data === undefined) { - return; - } - - // If the cached trace slices don't fully cover the visible time range, - // show a gray rectangle with a "Loading..." label. - checkerboardExcept( - ctx, - this.getHeight(), - 0, - size.width, - timescale.timeToPx(data.start), - timescale.timeToPx(data.end), - ); - - this.renderSummary(trackCtx, data); - } - - private renderSummary( - {ctx, timescale}: TrackRenderContext, - data: Data, - ): void { - const startPx = 0; - const bottomY = TRACK_HEIGHT; - - let lastX = startPx; - let lastY = bottomY; - - const color = colorForTid(this.config.pidForColor); - ctx.fillStyle = color.base.cssString; - ctx.beginPath(); - ctx.moveTo(lastX, lastY); - for (let i = 0; i < data.utilizations.length; i++) { - const startTime = Time.fromRaw(data.starts[i]); - const utilization = data.utilizations[i]; - lastX = Math.floor(timescale.timeToPx(startTime)); - ctx.lineTo(lastX, lastY); - lastY = MARGIN_TOP + Math.round(SUMMARY_HEIGHT * (1 - utilization)); - ctx.lineTo(lastX, lastY); - } - ctx.lineTo(lastX, bottomY); - ctx.closePath(); - ctx.fill(); - } -} diff --git a/ui/src/plugins/dev.perfetto.ProcessSummary/slice_track_summary.ts b/ui/src/plugins/dev.perfetto.ProcessSummary/slice_track_summary.ts new file mode 100644 index 00000000000..f00b46cb9e1 --- /dev/null +++ b/ui/src/plugins/dev.perfetto.ProcessSummary/slice_track_summary.ts @@ -0,0 +1,508 @@ +// 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 {BigintMath as BIMath} from '../../base/bigint_math'; +import {searchEq, searchRange} from '../../base/binary_search'; +import {assertExists, assertTrue} from '../../base/logging'; +import {duration, time, Time} from '../../base/time'; +import m from 'mithril'; +import {colorForThread, colorForTid} from '../../components/colorizer'; +import {TrackData} from '../../components/tracks/track_data'; +import {TimelineFetcher} from '../../components/tracks/track_helper'; +import {checkerboardExcept} from '../../components/checkerboard'; +import {TrackRenderer} from '../../public/track'; +import {LONG, NUM, QueryResult} from '../../trace_processor/query_result'; +import {uuidv4Sql} from '../../base/uuid'; +import {TrackMouseEvent, TrackRenderContext} from '../../public/track'; +import {Point2D} from '../../base/geom'; +import {Trace} from '../../public/trace'; +import {ThreadMap} from '../dev.perfetto.Thread/threads'; +import {AsyncDisposableStack} from '../../base/disposable_stack'; +import { + createPerfettoTable, + createVirtualTable, +} from '../../trace_processor/sql_utils'; +import {Dataset} from '../../trace_processor/dataset'; +import {TrackNode} from '../../public/workspace'; + +export const SLICE_TRACK_SUMMARY_KIND = 'SliceTrackSummary'; + +const MARGIN_TOP = 5; +const RECT_HEIGHT = 30; +const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT; + +interface Data extends TrackData { + kind: 'slice'; + maxLanes: number; + + // Slices are stored in a columnar fashion. All fields have the same length. + counts: Uint32Array; + starts: BigInt64Array; + ends: BigInt64Array; + utids: Int32Array; + lanes: Uint32Array; +} + +export interface Config { + pidForColor: bigint | number; + upid: number | null; + utid: number | null; +} + +type Mode = 'sched' | 'slices'; + +export class SliceTrackSummary implements TrackRenderer { + private mousePos?: Point2D; + private utidHoveredInThisTrack = -1; + private laneHoveredInThisTrack = -1; + private countHoveredInThisTrack = -1; + private fetcher = new TimelineFetcher(this.onBoundsChange.bind(this)); + private trackUuid = uuidv4Sql(); + private mode: Mode = 'slices'; + private maxLanes: number = 1; + private sliceTracks: Array<{uri: string; dataset: Dataset}> = []; + + constructor( + private readonly trace: Trace, + private readonly uri: string, + private readonly config: Config, + private readonly cpuCount: number, + private readonly threads: ThreadMap, + hasSched: boolean, + ) { + this.mode = hasSched ? 'sched' : 'slices'; + } + + async onCreate(): Promise { + if (this.mode === 'sched') { + await this.createSchedMipmap(); + } else { + await this.createSlicesMipmap(); + } + } + + private async createSchedMipmap(): Promise { + const getQuery = () => { + if (this.config.upid !== null) { + return ` + select + s.ts, + s.dur, + s.cpu, + s.utid + from thread t + cross join sched s using (utid) + where + not t.is_idle and + t.upid = ${this.config.upid} + order by ts + `; + } + assertExists(this.config.utid); + return ` + select + s.ts, + s.dur, + s.cpu, + s.utid + from sched s + where + s.utid = ${this.config.utid} + `; + }; + + const trash = new AsyncDisposableStack(); + trash.use( + await createPerfettoTable({ + engine: this.trace.engine, + name: `tmp_${this.trackUuid}`, + as: getQuery(), + }), + ); + await createVirtualTable({ + engine: this.trace.engine, + name: `process_summary_${this.trackUuid}`, + using: `__intrinsic_slice_mipmap(( + select + s.ts, + iif( + s.dur = -1, + ifnull( + ( + select n.ts + from tmp_${this.trackUuid} n + where n.ts > s.ts and n.cpu = s.cpu + order by ts + limit 1 + ), + trace_end() + ) - s.ts, + s.dur + ) as dur, + s.cpu as depth + from tmp_${this.trackUuid} s + ))`, + }); + await trash.asyncDispose(); + + this.maxLanes = this.cpuCount; + } + + private collectSliceTracks(): Array<{uri: string; dataset: Dataset}> { + const trackNode = this.trace.workspaces.currentWorkspace.getTrackByUri( + this.uri, + ); + if (!trackNode) return []; + + const validSliceTracks: Array<{uri: string; dataset: Dataset}> = []; + const stack: TrackNode[] = [trackNode]; + + while (stack.length > 0 && validSliceTracks.length < 8) { + const node = stack.pop()!; + + // Try to get track and dataset + const track = + node.uri !== undefined + ? this.trace.tracks.getTrack(node.uri) + : undefined; + const dataset = track?.renderer.getDataset?.(); + + // Check if it's a valid slice track WITH depth column + const sliceSchema = {ts: LONG, dur: LONG, depth: NUM}; + const isValidSliceTrack = dataset?.implements(sliceSchema) ?? false; + + if (isValidSliceTrack && dataset !== undefined) { + // Add track - we'll filter to depth = 0 in SQL + validSliceTracks.push({ + uri: node.uri!, + dataset: dataset, + }); + } else { + // Not valid - traverse children + for (let i = node.children.length - 1; i >= 0; i--) { + stack.push(node.children[i]); + } + } + } + + return validSliceTracks; + } + + private async createSlicesMipmap(): Promise { + this.sliceTracks = this.collectSliceTracks(); + + if (this.sliceTracks.length === 0) { + // No valid slice tracks found - create empty table + await createVirtualTable({ + engine: this.trace.engine, + name: `process_summary_${this.trackUuid}`, + using: `__intrinsic_slice_mipmap(( + select + cast(0 as int) as id, + cast(0 as bigint) as ts, + cast(0 as bigint) as dur, + cast(0 as int) as depth + where 0 + ))`, + }); + this.maxLanes = 1; + return; + } + + // Create union of all slice tracks with track index as depth + const unions = this.sliceTracks + .map(({dataset}, idx) => { + return ` + select + id, + ts, + iif(dur = -1, trace_end() - ts, dur) as dur, + ${idx} as depth + from (${dataset.query()}) + where depth = 0 + `; + }) + .join(' union all '); + + await createVirtualTable({ + engine: this.trace.engine, + name: `process_summary_${this.trackUuid}`, + using: `__intrinsic_slice_mipmap(( + ${unions} + ))`, + }); + this.maxLanes = 8; + } + + async onUpdate({ + visibleWindow, + resolution, + }: TrackRenderContext): Promise { + await this.fetcher.requestData(visibleWindow.toTimeSpan(), resolution); + } + + async onDestroy(): Promise { + this.fetcher[Symbol.dispose](); + await this.trace.engine.tryQuery(` + drop table process_summary_${this.trackUuid} + `); + } + + async onBoundsChange( + start: time, + end: time, + resolution: duration, + ): Promise { + // Resolution must always be a power of 2 for this logic to work + assertTrue(BIMath.popcount(resolution) === 1, `${resolution} not pow of 2`); + + const queryRes = await this.queryData(start, end, resolution); + const numRows = queryRes.numRows(); + const slices: Data = { + kind: 'slice', + start, + end, + resolution, + length: numRows, + maxLanes: this.maxLanes, + counts: new Uint32Array(numRows), + starts: new BigInt64Array(numRows), + ends: new BigInt64Array(numRows), + lanes: new Uint32Array(numRows), + utids: new Int32Array(numRows), + }; + + const it = queryRes.iter({ + count: NUM, + ts: LONG, + dur: LONG, + lane: NUM, + utid: NUM, + }); + + for (let row = 0; it.valid(); it.next(), row++) { + const start = Time.fromRaw(it.ts); + const dur = it.dur; + const end = Time.add(start, dur); + + slices.counts[row] = it.count; + slices.starts[row] = start; + slices.ends[row] = end; + slices.lanes[row] = it.lane; + slices.utids[row] = it.utid; + slices.end = Time.max(end, slices.end); + } + return slices; + } + + private async queryData( + start: time, + end: time, + bucketSize: duration, + ): Promise { + if (this.mode === 'sched') { + return this.trace.engine.query(` + select + (z.ts / ${bucketSize}) * ${bucketSize} as ts, + iif(s.dur = -1, s.dur, max(z.dur, ${bucketSize})) as dur, + z.count, + z.depth as lane, + s.utid + from process_summary_${this.trackUuid}( + ${start}, ${end}, ${bucketSize} + ) z + cross join sched s using (id) + `); + } else { + return this.trace.engine.query(` + select + (z.ts / ${bucketSize}) * ${bucketSize} as ts, + max(z.dur, ${bucketSize}) as dur, + z.count, + z.depth as lane, + -1 as utid + from process_summary_${this.trackUuid}( + ${start}, ${end}, ${bucketSize} + ) z + `); + } + } + + getHeight(): number { + return TRACK_HEIGHT; + } + + renderTooltip(): m.Children { + if (this.mousePos === undefined) { + return undefined; + } + + if (this.mode === 'sched') { + // Show thread/process info for scheduling mode + if (this.utidHoveredInThisTrack === -1) { + return undefined; + } + + const hoveredThread = this.threads.get(this.utidHoveredInThisTrack); + if (!hoveredThread) { + return undefined; + } + + const tidText = `T: ${hoveredThread.threadName} [${hoveredThread.tid}]`; + + const count = this.countHoveredInThisTrack; + const countDiv = count > 1 && m('div', `and ${count - 1} other events`); + if (hoveredThread.pid !== undefined) { + const pidText = `P: ${hoveredThread.procName} [${hoveredThread.pid}]`; + return m('.tooltip', [m('div', pidText), m('div', tidText), countDiv]); + } else { + return m('.tooltip', tidText, countDiv); + } + } else { + // Show track name/info for slice mode + const laneIndex = this.laneHoveredInThisTrack; + if (laneIndex < 0 || laneIndex >= this.sliceTracks.length) { + return undefined; + } + + const trackUri = this.sliceTracks[laneIndex].uri; + const track = this.trace.tracks.getTrack(trackUri); + const trackTitle = (track as {title?: string})?.title ?? trackUri; + + const count = this.countHoveredInThisTrack; + const countDiv = count > 1 && m('div', `${count} slices`); + + return m('.tooltip', [m('div', `Track: ${trackTitle}`), countDiv]); + } + } + + render({ctx, size, timescale, visibleWindow}: TrackRenderContext): void { + const data = this.fetcher.data; + + if (data === undefined) return; // Can't possibly draw anything. + + // If the cached trace slices don't fully cover the visible time range, + // show a gray rectangle with a "Loading..." label. + checkerboardExcept( + ctx, + this.getHeight(), + 0, + size.width, + timescale.timeToPx(data.start), + timescale.timeToPx(data.end), + ); + + assertTrue(data.starts.length === data.ends.length); + assertTrue(data.starts.length === data.utids.length); + + const laneHeight = Math.floor(RECT_HEIGHT / data.maxLanes); + + for (let i = 0; i < data.ends.length; i++) { + const tStart = Time.fromRaw(data.starts[i]); + const tEnd = Time.fromRaw(data.ends[i]); + + // Cull slices that lie completely outside the visible window + if (!visibleWindow.overlaps(tStart, tEnd)) continue; + + const utid = data.utids[i]; + const lane = data.lanes[i]; + + const rectStart = Math.floor(timescale.timeToPx(tStart)); + const rectEnd = Math.floor(timescale.timeToPx(tEnd)); + const rectWidth = Math.max(1, rectEnd - rectStart); + + let colorScheme; + + if (this.mode === 'sched') { + // Scheduling mode: color by thread (utid) + const threadInfo = this.threads.get(utid); + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + const pid = (threadInfo ? threadInfo.pid : -1) || -1; + + const isHovering = this.trace.timeline.hoveredUtid !== undefined; + const isThreadHovered = this.trace.timeline.hoveredUtid === utid; + const isProcessHovered = this.trace.timeline.hoveredPid === pid; + colorScheme = colorForThread(threadInfo); + + if (isHovering && !isThreadHovered) { + if (!isProcessHovered) { + ctx.fillStyle = colorScheme.disabled.cssString; + } else { + ctx.fillStyle = colorScheme.variant.cssString; + } + } else { + ctx.fillStyle = colorScheme.base.cssString; + } + } else { + // Slice mode: consistent color based on pidForColor + colorScheme = colorForTid(this.config.pidForColor); + ctx.fillStyle = colorScheme.base.cssString; + } + + const y = MARGIN_TOP + laneHeight * lane + lane; + ctx.fillRect(rectStart, y, rectWidth, laneHeight); + } + } + + onMouseMove({x, y, timescale}: TrackMouseEvent) { + const data = this.fetcher.data; + this.mousePos = {x, y}; + if (data === undefined) return; + if (y < MARGIN_TOP || y > MARGIN_TOP + RECT_HEIGHT) { + this.utidHoveredInThisTrack = -1; + this.laneHoveredInThisTrack = -1; + this.countHoveredInThisTrack = -1; + this.trace.timeline.hoveredUtid = undefined; + this.trace.timeline.hoveredPid = undefined; + return; + } + + const laneHeight = Math.floor(RECT_HEIGHT / data.maxLanes); + const lane = Math.floor((y - MARGIN_TOP) / (laneHeight + 1)); + const t = timescale.pxToHpTime(x).toTime('floor'); + + const [i, j] = searchRange(data.starts, t, searchEq(data.lanes, lane)); + if (i === j || i >= data.starts.length || t > data.ends[i]) { + this.utidHoveredInThisTrack = -1; + this.laneHoveredInThisTrack = -1; + this.countHoveredInThisTrack = -1; + this.trace.timeline.hoveredUtid = undefined; + this.trace.timeline.hoveredPid = undefined; + return; + } + + const utid = data.utids[i]; + const count = data.counts[i]; + this.utidHoveredInThisTrack = utid; + this.laneHoveredInThisTrack = lane; + this.countHoveredInThisTrack = count; + + if (this.mode === 'sched') { + const threadInfo = this.threads.get(utid); + this.trace.timeline.hoveredUtid = utid; + this.trace.timeline.hoveredPid = threadInfo?.pid; + } + + // Trigger redraw to update tooltip + m.redraw(); + } + + onMouseOut() { + this.utidHoveredInThisTrack = -1; + this.laneHoveredInThisTrack = -1; + this.trace.timeline.hoveredUtid = undefined; + this.trace.timeline.hoveredPid = undefined; + this.mousePos = undefined; + } +} diff --git a/ui/src/plugins/dev.perfetto.TrackEvent/index.ts b/ui/src/plugins/dev.perfetto.TrackEvent/index.ts index 3ac9235e0ef..d67b54f816b 100644 --- a/ui/src/plugins/dev.perfetto.TrackEvent/index.ts +++ b/ui/src/plugins/dev.perfetto.TrackEvent/index.ts @@ -41,6 +41,13 @@ import {CallstackDetailsSection} from '../dev.perfetto.TraceProcessorTrack/calls import {Store} from '../../base/store'; import {z} from 'zod'; import {createPerfettoTable} from '../../trace_processor/sql_utils'; +import ThreadPlugin from '../dev.perfetto.Thread'; +import ProcessSummaryPlugin from '../dev.perfetto.ProcessSummary'; +import { + Config as SliceTrackSummaryConfig, + SLICE_TRACK_SUMMARY_KIND, + SliceTrackSummary, +} from '../dev.perfetto.ProcessSummary/slice_track_summary'; function createTrackEventDetailsPanel(trace: Trace) { return () => @@ -60,6 +67,8 @@ export default class TrackEventPlugin implements PerfettoPlugin { static readonly dependencies = [ ProcessThreadGroupsPlugin, TraceProcessorTrackPlugin, + ThreadPlugin, + ProcessSummaryPlugin, ]; private parentTrackNodes = new Map(); @@ -150,6 +159,14 @@ export default class TrackEventPlugin implements PerfettoPlugin { ProcessThreadGroupsPlugin, ); const trackIdToTrackNode = new Map(); + + // Get CPU count and threads for summary tracks + const cpuCountResult = await ctx.engine.query(` + SELECT COUNT(*) as cpu_count FROM cpu WHERE IFNULL(machine_id, 0) = 0 + `); + const cpuCount = cpuCountResult.firstRow({cpu_count: NUM}).cpu_count; + const threads = ctx.plugins.getPlugin(ThreadPlugin).getThreadMap(); + for (; it.valid(); it.next()) { const { upid, @@ -248,6 +265,33 @@ export default class TrackEventPlugin implements PerfettoPlugin { : undefined, }), }); + } else { + // Summary track with no data but has children - use SliceTrackSummary + const config: SliceTrackSummaryConfig = { + pidForColor: pid ?? 0, + upid: upid ?? null, + utid: utid ?? null, + }; + + ctx.tracks.registerTrack({ + uri, + description: description ?? undefined, + tags: { + kinds: [SLICE_TRACK_SUMMARY_KIND], + trackIds: trackIds, + upid: upid ?? undefined, + utid: utid ?? undefined, + trackEvent: true, + }, + renderer: new SliceTrackSummary( + ctx, + uri, + config, + cpuCount, + threads, + false, + ), + }); } const parent = this.findParentTrackNode( ctx,