| // 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 {CounterTrack} from '../../components/tracks/counter_track'; |
| import type {Trace} from '../../public/trace'; |
| import {TrackNode} from '../../public/workspace'; |
| import {THREAD_STATE_TRACK_KIND} from '../../public/track_kinds'; |
| import {WATTSON_THREAD_TRACK_KIND} from './track_kinds'; |
| |
| export async function addWattsonThreadTrack( |
| trace: Trace, |
| utid: number, |
| options?: { |
| threadTrackUri?: string; |
| pin?: boolean; |
| scrollTo?: boolean; |
| }, |
| ): Promise<void> { |
| const uri = `dev.perfetto.Sched#WattsonThreadCounter_${utid}`; |
| const existingTrack = trace.currentWorkspace.getTrackByUri(uri); |
| if (existingTrack) { |
| if (options?.pin && !existingTrack.isPinned) { |
| existingTrack.pin(); |
| } |
| if (options?.scrollTo ?? !options?.pin) { |
| trace.scrollTo({track: {uri, expandGroup: true}}); |
| } |
| return; |
| } |
| |
| await trace.engine.query(` |
| INCLUDE PERFETTO MODULE wattson.tasks.attribution; |
| INCLUDE PERFETTO MODULE wattson.tasks.task_slices; |
| `); |
| |
| const sqlSource = ` |
| WITH _per_thread_estimate AS ( |
| SELECT ts, dur, estimated_mw |
| FROM _estimates_w_tasks_attribution |
| WHERE utid = ${utid} |
| ), |
| -- Need to fill in gaps where thread isn't running with 0mW entry |
| gapless AS ( |
| SELECT ts, dur, estimated_mw FROM _per_thread_estimate |
| UNION ALL |
| SELECT |
| ts + dur AS ts, |
| COALESCE(LEAD(ts) OVER (ORDER BY ts), trace_end()) - ts - dur AS dur, |
| 0 |
| FROM _per_thread_estimate |
| ) |
| SELECT |
| ts, |
| dur, |
| estimated_mw AS value |
| FROM gapless |
| WHERE dur > 0 |
| `; |
| |
| const renderer = await CounterTrack.createMaterialized({ |
| trace, |
| uri, |
| sqlSource, |
| }); |
| |
| trace.tracks.registerTrack({ |
| uri, |
| renderer, |
| tags: { |
| kinds: [WATTSON_THREAD_TRACK_KIND], |
| utid, |
| }, |
| }); |
| |
| // Find the thread track and add the new track as a sibling |
| let threadNode: TrackNode | undefined; |
| if (options?.threadTrackUri) { |
| threadNode = trace.currentWorkspace.getTrackByUri(options.threadTrackUri); |
| } else { |
| const threadTrack = trace.tracks |
| .getAllTracks() |
| .find( |
| (t) => |
| t.tags?.kinds?.includes(THREAD_STATE_TRACK_KIND) && |
| t.tags?.utid === utid, |
| ); |
| if (threadTrack) { |
| threadNode = trace.currentWorkspace.getTrackByUri(threadTrack.uri); |
| } |
| } |
| |
| const name = threadNode?.name ?? (utid === 0 ? 'swapper' : `utid ${utid}`); |
| const newNode = new TrackNode({ |
| uri, |
| name: `${name} Wattson power estimates`, |
| }); |
| |
| if (threadNode?.parent) { |
| threadNode.parent.addChildBefore(newNode, threadNode); |
| } else { |
| trace.currentWorkspace.addChildLast(newNode); |
| } |
| |
| if (options?.pin) { |
| newNode.pin(); |
| } |
| |
| if (options?.scrollTo ?? !options?.pin) { |
| trace.scrollTo({track: {uri, expandGroup: true}}); |
| } |
| } |