| // Copyright (C) 2021 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 {v4 as uuidv4} from 'uuid'; |
| |
| import {searchSegment} from '../../base/binary_search'; |
| import {assertTrue} from '../../base/logging'; |
| import {isString} from '../../base/object_utils'; |
| import {duration, time, Time} from '../../base/time'; |
| import {Actions} from '../../common/actions'; |
| import {drawTrackHoverTooltip} from '../../common/canvas_utils'; |
| import {TrackData} from '../../common/track_data'; |
| import { |
| NUM_NULL, |
| STR_NULL, |
| TimelineFetcher, |
| } from '../../common/track_helper'; |
| import {checkerboardExcept} from '../../frontend/checkerboard'; |
| import {CounterDetailsPanel} from '../../frontend/counter_panel'; |
| import {globals} from '../../frontend/globals'; |
| import {PanelSize} from '../../frontend/panel'; |
| import { |
| EngineProxy, |
| LONG, |
| LONG_NULL, |
| NUM, |
| Plugin, |
| PluginContext, |
| PluginContextTrace, |
| PluginDescriptor, |
| PrimaryTrackSortKey, |
| Store, |
| STR, |
| Track, |
| TrackContext, |
| } from '../../public'; |
| import {getTrackName} from '../../public/utils'; |
| import {Button} from '../../widgets/button'; |
| import {MenuItem, PopupMenu2} from '../../widgets/menu'; |
| |
| export const COUNTER_TRACK_KIND = 'CounterTrack'; |
| |
| // TODO(hjd): Convert to enum. |
| export type CounterScaleOptions = |
| 'ZERO_BASED'|'MIN_MAX'|'DELTA_FROM_PREVIOUS'|'RATE'; |
| |
| export interface Data extends TrackData { |
| maximumValue: number; |
| minimumValue: number; |
| maximumDelta: number; |
| minimumDelta: number; |
| maximumRate: number; |
| minimumRate: number; |
| timestamps: BigInt64Array; |
| lastIds: Float64Array; |
| minValues: Float64Array; |
| maxValues: Float64Array; |
| lastValues: Float64Array; |
| totalDeltas: Float64Array; |
| rate: Float64Array; |
| } |
| |
| export interface Config { |
| name: string; |
| maximumValue?: number; |
| minimumValue?: number; |
| startTs?: time; |
| endTs?: time; |
| namespace?: string; |
| trackId: number; |
| defaultScale?: CounterScaleOptions; |
| } |
| |
| const NETWORK_TRACK_REGEX = new RegExp('^.* (Received|Transmitted)( KB)?$'); |
| const ENTITY_RESIDENCY_REGEX = new RegExp('^Entity residency:'); |
| |
| // Sets the default 'scale' for counter tracks. If the regex matches |
| // then the paired mode is used. Entries are in priority order so the |
| // first match wins. |
| const COUNTER_REGEX: [RegExp, CounterScaleOptions][] = [ |
| // Power counters make more sense in rate mode since you're typically |
| // interested in the slope of the graph rather than the absolute |
| // value. |
| [new RegExp('^power\..*$'), 'RATE'], |
| // Same for cumulative PSI stall time counters, e.g., psi.cpu.some. |
| [new RegExp('^psi\..*$'), 'RATE'], |
| // Same for network counters. |
| [NETWORK_TRACK_REGEX, 'RATE'], |
| // Entity residency |
| [ENTITY_RESIDENCY_REGEX, 'RATE'], |
| ]; |
| |
| function getCounterScale(name: string): CounterScaleOptions|undefined { |
| for (const [re, scale] of COUNTER_REGEX) { |
| if (name.match(re)) { |
| return scale; |
| } |
| } |
| return undefined; |
| } |
| |
| // 0.5 Makes the horizontal lines sharp. |
| const MARGIN_TOP = 3.5; |
| const RECT_HEIGHT = 24.5; |
| |
| interface CounterTrackState { |
| scale: CounterScaleOptions; |
| } |
| |
| function isCounterState(x: unknown): x is CounterTrackState { |
| if (x && typeof x === 'object' && 'scale' in x) { |
| if (isString(x.scale)) { |
| return true; |
| } else { |
| return false; |
| } |
| } else { |
| return false; |
| } |
| } |
| |
| export class CounterTrack implements Track { |
| private maximumValueSeen = 0; |
| private minimumValueSeen = 0; |
| private maximumDeltaSeen = 0; |
| private minimumDeltaSeen = 0; |
| private maxDurNs: duration = 0n; |
| private store: Store<CounterTrackState>; |
| private trackKey: string; |
| private uuid = uuidv4(); |
| private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this)); |
| |
| constructor( |
| ctx: TrackContext, private config: Config, private engine: EngineProxy) { |
| this.trackKey = ctx.trackKey; |
| this.store = ctx.mountStore<CounterTrackState>((init: unknown) => { |
| if (isCounterState(init)) { |
| return init; |
| } else { |
| return {scale: this.config.defaultScale ?? 'ZERO_BASED'}; |
| } |
| }); |
| } |
| |
| async onUpdate(): Promise<void> { |
| await this.fetcher.requestDataForCurrentTime(); |
| } |
| |
| // Returns a valid SQL table name with the given prefix that should be unique |
| // for each track. |
| tableName(prefix: string) { |
| // Derive table name from, since that is unique for each track. |
| // Track ID can be UUID but '-' is not valid for sql table name. |
| const idSuffix = this.uuid.split('-').join('_'); |
| return `${prefix}_${idSuffix}`; |
| } |
| |
| private namespaceTable(tableName: string): string { |
| if (this.config.namespace) { |
| return this.config.namespace + '_' + tableName; |
| } else { |
| return tableName; |
| } |
| } |
| |
| async onCreate() { |
| if (this.config.namespace === undefined) { |
| await this.engine.query(` |
| create view ${this.tableName('counter_view')} as |
| select |
| id, |
| ts, |
| dur, |
| value, |
| delta |
| from experimental_counter_dur |
| where track_id = ${this.config.trackId}; |
| `); |
| } else { |
| await this.engine.query(` |
| create view ${this.tableName('counter_view')} as |
| select |
| id, |
| ts, |
| lead(ts, 1, ts) over (order by ts) - ts as dur, |
| lead(value, 1, value) over (order by ts) - value as delta, |
| value |
| from ${this.namespaceTable('counter')} |
| where track_id = ${this.config.trackId}; |
| `); |
| } |
| |
| const maxDurResult = await this.engine.query(` |
| select |
| max( |
| iif(dur != -1, dur, (select end_ts from trace_bounds) - ts) |
| ) as maxDur |
| from ${this.tableName('counter_view')} |
| `); |
| this.maxDurNs = maxDurResult.firstRow({maxDur: LONG_NULL}).maxDur ?? 0n; |
| |
| const queryRes = await this.engine.query(` |
| select |
| ifnull(max(value), 0) as maxValue, |
| ifnull(min(value), 0) as minValue, |
| ifnull(max(delta), 0) as maxDelta, |
| ifnull(min(delta), 0) as minDelta |
| from ${this.tableName('counter_view')}`); |
| const row = queryRes.firstRow( |
| {maxValue: NUM, minValue: NUM, maxDelta: NUM, minDelta: NUM}); |
| this.maximumValueSeen = row.maxValue; |
| this.minimumValueSeen = row.minValue; |
| this.maximumDeltaSeen = row.maxDelta; |
| this.minimumDeltaSeen = row.minDelta; |
| } |
| |
| async onBoundsChange(start: time, end: time, resolution: duration): |
| Promise<Data> { |
| const queryRes = await this.engine.query(` |
| select |
| (ts + ${resolution / 2n}) / ${resolution} * ${resolution} as tsq, |
| min(value) as minValue, |
| max(value) as maxValue, |
| sum(delta) as totalDelta, |
| value_at_max_ts(ts, id) as lastId, |
| value_at_max_ts(ts, value) as lastValue |
| from ${this.tableName('counter_view')} |
| where ts >= ${start - this.maxDurNs} and ts <= ${end} |
| group by tsq |
| order by tsq |
| `); |
| |
| const numRows = queryRes.numRows(); |
| |
| const data: Data = { |
| start, |
| end, |
| length: numRows, |
| maximumValue: this.maximumValue(), |
| minimumValue: this.minimumValue(), |
| maximumDelta: this.maximumDeltaSeen, |
| minimumDelta: this.minimumDeltaSeen, |
| maximumRate: 0, |
| minimumRate: 0, |
| resolution, |
| timestamps: new BigInt64Array(numRows), |
| lastIds: new Float64Array(numRows), |
| minValues: new Float64Array(numRows), |
| maxValues: new Float64Array(numRows), |
| lastValues: new Float64Array(numRows), |
| totalDeltas: new Float64Array(numRows), |
| rate: new Float64Array(numRows), |
| }; |
| |
| const it = queryRes.iter({ |
| 'tsq': LONG, |
| 'lastId': NUM, |
| 'minValue': NUM, |
| 'maxValue': NUM, |
| 'lastValue': NUM, |
| 'totalDelta': NUM, |
| }); |
| let lastValue = 0; |
| let lastTs = 0n; |
| for (let row = 0; it.valid(); it.next(), row++) { |
| const ts = Time.fromRaw(it.tsq); |
| const value = it.lastValue; |
| const rate = (value - lastValue) / (Time.toSeconds(Time.sub(ts, lastTs))); |
| lastTs = ts; |
| lastValue = value; |
| |
| data.timestamps[row] = ts; |
| data.lastIds[row] = it.lastId; |
| data.minValues[row] = it.minValue; |
| data.maxValues[row] = it.maxValue; |
| data.lastValues[row] = value; |
| data.totalDeltas[row] = it.totalDelta; |
| data.rate[row] = rate; |
| if (row > 0) { |
| data.rate[row - 1] = rate; |
| data.maximumRate = Math.max(data.maximumRate, rate); |
| data.minimumRate = Math.min(data.minimumRate, rate); |
| } |
| } |
| return data; |
| } |
| |
| private maximumValue() { |
| if (this.config.maximumValue === undefined) { |
| return this.maximumValueSeen; |
| } else { |
| return this.config.maximumValue; |
| } |
| } |
| |
| private minimumValue() { |
| if (this.config.minimumValue === undefined) { |
| return this.minimumValueSeen; |
| } else { |
| return this.config.minimumValue; |
| } |
| } |
| |
| private mousePos = {x: 0, y: 0}; |
| private hoveredValue: number|undefined = undefined; |
| private hoveredTs: time|undefined = undefined; |
| private hoveredTsEnd: time|undefined = undefined; |
| |
| getHeight() { |
| return MARGIN_TOP + RECT_HEIGHT; |
| } |
| |
| getTrackShellButtons(): m.Children { |
| const currentScale = this.store.state.scale; |
| const scales: {name: CounterScaleOptions, humanName: string}[] = [ |
| {name: 'ZERO_BASED', humanName: 'Zero based'}, |
| {name: 'MIN_MAX', humanName: 'Min/Max'}, |
| {name: 'DELTA_FROM_PREVIOUS', humanName: 'Delta'}, |
| {name: 'RATE', humanName: 'Rate'}, |
| ]; |
| const menuItems = scales.map((scale) => { |
| return m(MenuItem, { |
| label: scale.humanName, |
| active: currentScale === scale.name, |
| onclick: () => { |
| this.store.edit((draft) => { |
| draft.scale = scale.name; |
| }); |
| }, |
| }); |
| }); |
| |
| return m( |
| PopupMenu2, |
| { |
| trigger: m(Button, {icon: 'show_chart', minimal: true}), |
| }, |
| menuItems, |
| ); |
| } |
| |
| render(ctx: CanvasRenderingContext2D, size: PanelSize): void { |
| // TODO: fonts and colors should come from the CSS and not hardcoded here. |
| const { |
| visibleTimeScale: timeScale, |
| } = globals.timeline; |
| const data = this.fetcher.data; |
| |
| // Can't possibly draw anything. |
| if (data === undefined || data.timestamps.length === 0) { |
| return; |
| } |
| |
| assertTrue(data.timestamps.length === data.minValues.length); |
| assertTrue(data.timestamps.length === data.maxValues.length); |
| assertTrue(data.timestamps.length === data.lastValues.length); |
| assertTrue(data.timestamps.length === data.totalDeltas.length); |
| assertTrue(data.timestamps.length === data.rate.length); |
| |
| const scale: CounterScaleOptions = this.store.state.scale; |
| |
| let minValues = data.minValues; |
| let maxValues = data.maxValues; |
| let lastValues = data.lastValues; |
| let maximumValue = data.maximumValue; |
| let minimumValue = data.minimumValue; |
| if (scale === 'DELTA_FROM_PREVIOUS') { |
| lastValues = data.totalDeltas; |
| minValues = data.totalDeltas; |
| maxValues = data.totalDeltas; |
| maximumValue = data.maximumDelta; |
| minimumValue = data.minimumDelta; |
| } |
| if (scale === 'RATE') { |
| lastValues = data.rate; |
| minValues = data.rate; |
| maxValues = data.rate; |
| maximumValue = data.maximumRate; |
| minimumValue = data.minimumRate; |
| } |
| |
| const endPx = size.width; |
| const zeroY = MARGIN_TOP + RECT_HEIGHT / (minimumValue < 0 ? 2 : 1); |
| |
| // Quantize the Y axis to quarters of powers of tens (7.5K, 10K, 12.5K). |
| const maxValue = Math.max(maximumValue, 0); |
| |
| let yMax = Math.max(Math.abs(minimumValue), maxValue); |
| const kUnits = ['', 'K', 'M', 'G', 'T', 'E']; |
| const exp = Math.ceil(Math.log10(Math.max(yMax, 1))); |
| const pow10 = Math.pow(10, exp); |
| yMax = Math.ceil(yMax / (pow10 / 4)) * (pow10 / 4); |
| let yRange = 0; |
| const unitGroup = Math.floor(exp / 3); |
| let yMin = 0; |
| let yLabel = ''; |
| if (scale === 'MIN_MAX') { |
| yRange = maximumValue - minimumValue; |
| yMin = minimumValue; |
| yLabel = 'min - max'; |
| } else { |
| yRange = minimumValue < 0 ? yMax * 2 : yMax; |
| yMin = minimumValue < 0 ? -yMax : 0; |
| yLabel = `${yMax / Math.pow(10, unitGroup * 3)} ${kUnits[unitGroup]}`; |
| if (scale === 'DELTA_FROM_PREVIOUS') { |
| yLabel += '\u0394'; |
| } else if (scale === 'RATE') { |
| yLabel += '\u0394/t'; |
| } |
| } |
| |
| // There are 360deg of hue. We want a scale that starts at green with |
| // exp <= 3 (<= 1KB), goes orange around exp = 6 (~1MB) and red/violet |
| // around exp >= 9 (1GB). |
| // The hue scale looks like this: |
| // 0 180 360 |
| // Red orange green | blue purple magenta |
| // So we want to start @ 180deg with pow=0, go down to 0deg and then wrap |
| // back from 360deg back to 180deg. |
| const expCapped = Math.min(Math.max(exp - 3), 9); |
| const hue = (180 - Math.floor(expCapped * (180 / 6)) + 360) % 360; |
| |
| ctx.fillStyle = `hsl(${hue}, 45%, 75%)`; |
| ctx.strokeStyle = `hsl(${hue}, 45%, 45%)`; |
| |
| const calculateX = (ts: time) => { |
| return Math.floor(timeScale.timeToPx(ts)); |
| }; |
| const calculateY = (value: number) => { |
| return MARGIN_TOP + RECT_HEIGHT - |
| Math.round(((value - yMin) / yRange) * RECT_HEIGHT); |
| }; |
| |
| ctx.beginPath(); |
| const timestamp = Time.fromRaw(data.timestamps[0]); |
| ctx.moveTo(calculateX(timestamp), zeroY); |
| let lastDrawnY = zeroY; |
| for (let i = 0; i < data.timestamps.length; i++) { |
| const timestamp = Time.fromRaw(data.timestamps[i]); |
| const x = calculateX(timestamp); |
| const minY = calculateY(minValues[i]); |
| const maxY = calculateY(maxValues[i]); |
| const lastY = calculateY(lastValues[i]); |
| |
| ctx.lineTo(x, lastDrawnY); |
| if (minY === maxY) { |
| assertTrue(lastY === minY); |
| ctx.lineTo(x, lastY); |
| } else { |
| ctx.lineTo(x, minY); |
| ctx.lineTo(x, maxY); |
| ctx.lineTo(x, lastY); |
| } |
| lastDrawnY = lastY; |
| } |
| ctx.lineTo(endPx, lastDrawnY); |
| ctx.lineTo(endPx, zeroY); |
| ctx.closePath(); |
| ctx.fill(); |
| ctx.stroke(); |
| |
| // Draw the Y=0 dashed line. |
| ctx.strokeStyle = `hsl(${hue}, 10%, 71%)`; |
| ctx.beginPath(); |
| ctx.setLineDash([2, 4]); |
| ctx.moveTo(0, zeroY); |
| ctx.lineTo(endPx, zeroY); |
| ctx.closePath(); |
| ctx.stroke(); |
| ctx.setLineDash([]); |
| |
| ctx.font = '10px Roboto Condensed'; |
| |
| if (this.hoveredValue !== undefined && this.hoveredTs !== undefined) { |
| // TODO(hjd): Add units. |
| let text: string; |
| if (scale === 'DELTA_FROM_PREVIOUS') { |
| text = 'delta: '; |
| } else if (scale === 'RATE') { |
| text = 'delta/t: '; |
| } else { |
| text = 'value: '; |
| } |
| |
| text += `${this.hoveredValue.toLocaleString()}`; |
| |
| ctx.fillStyle = `hsl(${hue}, 45%, 75%)`; |
| ctx.strokeStyle = `hsl(${hue}, 45%, 45%)`; |
| |
| const xStart = Math.floor(timeScale.timeToPx(this.hoveredTs)); |
| const xEnd = this.hoveredTsEnd === undefined ? |
| endPx : |
| Math.floor(timeScale.timeToPx(this.hoveredTsEnd)); |
| const y = MARGIN_TOP + RECT_HEIGHT - |
| Math.round(((this.hoveredValue - yMin) / yRange) * RECT_HEIGHT); |
| |
| // Highlight line. |
| ctx.beginPath(); |
| ctx.moveTo(xStart, y); |
| ctx.lineTo(xEnd, y); |
| ctx.lineWidth = 3; |
| ctx.stroke(); |
| ctx.lineWidth = 1; |
| |
| // Draw change marker. |
| ctx.beginPath(); |
| ctx.arc( |
| xStart, y, 3 /* r*/, 0 /* start angle*/, 2 * Math.PI /* end angle*/); |
| ctx.fill(); |
| ctx.stroke(); |
| |
| // Draw the tooltip. |
| drawTrackHoverTooltip(ctx, this.mousePos, this.getHeight(), text); |
| } |
| |
| // Write the Y scale on the top left corner. |
| ctx.fillStyle = 'rgba(255, 255, 255, 0.6)'; |
| ctx.fillRect(0, 0, 42, 16); |
| ctx.fillStyle = '#666'; |
| ctx.textAlign = 'left'; |
| ctx.textBaseline = 'alphabetic'; |
| ctx.fillText(`${yLabel}`, 5, 14); |
| |
| // TODO(hjd): Refactor this into checkerboardExcept |
| { |
| let counterEndPx = Infinity; |
| if (this.config.endTs) { |
| counterEndPx = Math.min(timeScale.timeToPx(this.config.endTs), endPx); |
| } |
| |
| // Grey out RHS. |
| if (counterEndPx < endPx) { |
| ctx.fillStyle = '#0000001f'; |
| ctx.fillRect(counterEndPx, 0, endPx - counterEndPx, this.getHeight()); |
| } |
| } |
| |
| // 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)); |
| } |
| |
| onMouseMove(pos: {x: number, y: number}) { |
| const data = this.fetcher.data; |
| if (data === undefined) return; |
| this.mousePos = pos; |
| const {visibleTimeScale} = globals.timeline; |
| const time = visibleTimeScale.pxToHpTime(pos.x); |
| |
| let values = data.lastValues; |
| if (this.store.state.scale === 'DELTA_FROM_PREVIOUS') { |
| values = data.totalDeltas; |
| } |
| if (this.store.state.scale === 'RATE') { |
| values = data.rate; |
| } |
| |
| const [left, right] = searchSegment(data.timestamps, time.toTime()); |
| this.hoveredTs = |
| left === -1 ? undefined : Time.fromRaw(data.timestamps[left]); |
| this.hoveredTsEnd = |
| right === -1 ? undefined : Time.fromRaw(data.timestamps[right]); |
| this.hoveredValue = left === -1 ? undefined : values[left]; |
| } |
| |
| onMouseOut() { |
| this.hoveredValue = undefined; |
| this.hoveredTs = undefined; |
| } |
| |
| onMouseClick({x}: {x: number}): boolean { |
| const data = this.fetcher.data; |
| if (data === undefined) return false; |
| const {visibleTimeScale} = globals.timeline; |
| const time = visibleTimeScale.pxToHpTime(x); |
| const [left, right] = searchSegment(data.timestamps, time.toTime()); |
| if (left === -1) { |
| return false; |
| } else { |
| const counterId = data.lastIds[left]; |
| if (counterId === -1) return true; |
| globals.makeSelection(Actions.selectCounter({ |
| leftTs: Time.fromRaw(data.timestamps[left]), |
| rightTs: Time.fromRaw(right !== -1 ? data.timestamps[right] : -1n), |
| id: counterId, |
| trackKey: this.trackKey, |
| })); |
| return true; |
| } |
| } |
| |
| async onDestroy(): Promise<void> { |
| if (this.engine.isAlive) { |
| await this.engine.query( |
| `DROP VIEW IF EXISTS ${this.tableName('counter_view')}`); |
| } |
| this.store.dispose(); |
| } |
| } |
| |
| interface CounterInfo { |
| name: string; |
| trackId: number; |
| } |
| |
| class CounterPlugin implements Plugin { |
| onActivate(_ctx: PluginContext): void {} |
| |
| async onTraceLoad(ctx: PluginContextTrace): Promise<void> { |
| await this.addCounterTracks(ctx); |
| await this.addGpuFrequencyTracks(ctx); |
| await this.addCpuFreqLimitCounterTracks(ctx); |
| await this.addCpuPerfCounterTracks(ctx); |
| await this.addThreadCounterTracks(ctx); |
| await this.addProcessCounterTracks(ctx); |
| |
| ctx.registerDetailsPanel({ |
| render: (sel) => { |
| if (sel.kind === 'COUNTER') { |
| return m(CounterDetailsPanel); |
| } else { |
| return undefined; |
| } |
| }, |
| }); |
| } |
| |
| private async addCounterTracks(ctx: PluginContextTrace) { |
| const counters = await this.getCounterNames(ctx.engine); |
| for (const {trackId, name} of counters) { |
| const config: |
| Config = {name, trackId, defaultScale: getCounterScale(name)}; |
| ctx.registerStaticTrack({ |
| uri: `perfetto.Counter#${trackId}`, |
| displayName: name, |
| kind: COUNTER_TRACK_KIND, |
| trackIds: [trackId], |
| track: (trackCtx) => { |
| return new CounterTrack(trackCtx, config, ctx.engine); |
| }, |
| sortKey: PrimaryTrackSortKey.COUNTER_TRACK, |
| }); |
| } |
| } |
| |
| private async getCounterNames(engine: EngineProxy): Promise<CounterInfo[]> { |
| const result = await engine.query(` |
| select name, id |
| from ( |
| select name, id |
| from counter_track |
| where type = 'counter_track' |
| union |
| select name, id |
| from gpu_counter_track |
| where name != 'gpufreq' |
| ) |
| order by name |
| `); |
| |
| // Add global or GPU counter tracks that are not bound to any pid/tid. |
| const it = result.iter({ |
| name: STR, |
| id: NUM, |
| }); |
| |
| const tracks: CounterInfo[] = []; |
| for (; it.valid(); it.next()) { |
| tracks.push({ |
| trackId: it.id, |
| name: it.name, |
| }); |
| } |
| return tracks; |
| } |
| |
| private async addGpuFrequencyTracks(ctx: PluginContextTrace) { |
| const engine = ctx.engine; |
| const numGpus = await engine.getNumberOfGpus(); |
| const maxGpuFreqResult = await engine.query(` |
| select ifnull(max(value), 0) as maximumValue |
| from counter c |
| inner join gpu_counter_track t on c.track_id = t.id |
| where name = 'gpufreq'; |
| `); |
| const maximumValue = |
| maxGpuFreqResult.firstRow({maximumValue: NUM}).maximumValue; |
| |
| for (let gpu = 0; gpu < numGpus; gpu++) { |
| // Only add a gpu freq track if we have |
| // gpu freq data. |
| const freqExistsResult = await engine.query(` |
| select id |
| from gpu_counter_track |
| where name = 'gpufreq' and gpu_id = ${gpu} |
| limit 1; |
| `); |
| if (freqExistsResult.numRows() > 0) { |
| const trackId = freqExistsResult.firstRow({id: NUM}).id; |
| const uri = `perfetto.Counter#gpu_freq${gpu}`; |
| const name = `Gpu ${gpu} Frequency`; |
| const config: Config = { |
| name, |
| trackId, |
| maximumValue, |
| defaultScale: getCounterScale(name), |
| }; |
| ctx.registerTrack({ |
| uri, |
| displayName: name, |
| kind: COUNTER_TRACK_KIND, |
| trackIds: [trackId], |
| track: (trackCtx) => { |
| return new CounterTrack(trackCtx, config, ctx.engine); |
| }, |
| }); |
| } |
| } |
| } |
| |
| async addCpuFreqLimitCounterTracks(ctx: PluginContextTrace): Promise<void> { |
| const cpuFreqLimitCounterTracksSql = ` |
| select name, id |
| from cpu_counter_track |
| where name glob "Cpu * Freq Limit" |
| order by name asc |
| `; |
| |
| this.addCpuCounterTracks(ctx, cpuFreqLimitCounterTracksSql); |
| } |
| |
| async addCpuPerfCounterTracks(ctx: PluginContextTrace): Promise<void> { |
| // Perf counter tracks are bound to CPUs, follow the scheduling and |
| // frequency track naming convention ("Cpu N ..."). |
| // Note: we might not have a track for a given cpu if no data was seen from |
| // it. This might look surprising in the UI, but placeholder tracks are |
| // wasteful as there's no way of collapsing global counter tracks at the |
| // moment. |
| const addCpuPerfCounterTracksSql = ` |
| select printf("Cpu %u %s", cpu, name) as name, id |
| from perf_counter_track as pct |
| order by perf_session_id asc, pct.name asc, cpu asc |
| `; |
| this.addCpuCounterTracks(ctx, addCpuPerfCounterTracksSql); |
| } |
| |
| async addCpuCounterTracks(ctx: PluginContextTrace, sql: string): |
| Promise<void> { |
| const result = await ctx.engine.query(sql); |
| |
| const it = result.iter({ |
| name: STR, |
| id: NUM, |
| }); |
| |
| for (; it.valid(); it.next()) { |
| const name = it.name; |
| const trackId = it.id; |
| const config: Config = { |
| name, |
| trackId, |
| defaultScale: getCounterScale(name), |
| }; |
| ctx.registerTrack({ |
| uri: `perfetto.Counter#cpu${trackId}`, |
| displayName: name, |
| kind: COUNTER_TRACK_KIND, |
| trackIds: [trackId], |
| track: (trackCtx) => { |
| return new CounterTrack(trackCtx, config, ctx.engine); |
| }, |
| }); |
| } |
| } |
| |
| async addThreadCounterTracks(ctx: PluginContextTrace): Promise<void> { |
| const result = await ctx.engine.query(` |
| select |
| thread_counter_track.name as trackName, |
| utid, |
| upid, |
| tid, |
| thread.name as threadName, |
| thread_counter_track.id as trackId, |
| thread.start_ts as startTs, |
| thread.end_ts as endTs |
| from thread_counter_track |
| join thread using(utid) |
| left join process using(upid) |
| where thread_counter_track.name != 'thread_time' |
| `); |
| |
| const it = result.iter({ |
| startTs: LONG_NULL, |
| trackId: NUM, |
| endTs: LONG_NULL, |
| trackName: STR_NULL, |
| utid: NUM, |
| upid: NUM_NULL, |
| tid: NUM_NULL, |
| threadName: STR_NULL, |
| }); |
| for (; it.valid(); it.next()) { |
| const utid = it.utid; |
| const tid = it.tid; |
| const startTs = it.startTs === null ? undefined : it.startTs; |
| const endTs = it.endTs === null ? undefined : it.endTs; |
| const trackId = it.trackId; |
| const trackName = it.trackName; |
| const threadName = it.threadName; |
| const kind = COUNTER_TRACK_KIND; |
| const name = getTrackName({ |
| name: trackName, |
| utid, |
| tid, |
| kind, |
| threadName, |
| threadTrack: true, |
| }); |
| const config: Config = { |
| name, |
| trackId, |
| startTs: Time.fromRaw(startTs), |
| endTs: Time.fromRaw(endTs), |
| defaultScale: getCounterScale(name), |
| }; |
| ctx.registerTrack({ |
| uri: `perfetto.Counter#thread${trackId}`, |
| displayName: name, |
| kind, |
| trackIds: [trackId], |
| track: (trackCtx) => { |
| return new CounterTrack(trackCtx, config, ctx.engine); |
| }, |
| }); |
| } |
| } |
| |
| async addProcessCounterTracks(ctx: PluginContextTrace): Promise<void> { |
| const result = await ctx.engine.query(` |
| select |
| process_counter_track.id as trackId, |
| process_counter_track.name as trackName, |
| upid, |
| process.pid, |
| process.name as processName, |
| process.start_ts as startTs, |
| process.end_ts as endTs |
| from process_counter_track |
| join process using(upid); |
| `); |
| const it = result.iter({ |
| trackId: NUM, |
| trackName: STR_NULL, |
| upid: NUM, |
| startTs: LONG_NULL, |
| endTs: LONG_NULL, |
| pid: NUM_NULL, |
| processName: STR_NULL, |
| }); |
| for (let i = 0; it.valid(); ++i, it.next()) { |
| const trackId = it.trackId; |
| const startTs = it.startTs === null ? undefined : it.startTs; |
| const endTs = it.endTs === null ? undefined : it.endTs; |
| const pid = it.pid; |
| const trackName = it.trackName; |
| const upid = it.upid; |
| const processName = it.processName; |
| const kind = COUNTER_TRACK_KIND; |
| const name = getTrackName({ |
| name: trackName, |
| upid, |
| pid, |
| kind, |
| processName, |
| }); |
| const config: Config = { |
| name, |
| trackId, |
| startTs: Time.fromRaw(startTs), |
| endTs: Time.fromRaw(endTs), |
| defaultScale: getCounterScale(name), |
| }; |
| ctx.registerTrack({ |
| uri: `perfetto.Counter#process${trackId}`, |
| displayName: name, |
| kind: COUNTER_TRACK_KIND, |
| trackIds: [trackId], |
| track: (trackCtx) => { |
| return new CounterTrack(trackCtx, config, ctx.engine); |
| }, |
| }); |
| } |
| } |
| } |
| |
| export const plugin: PluginDescriptor = { |
| pluginId: 'perfetto.Counter', |
| plugin: CounterPlugin, |
| }; |