diff options
author | Steve Golton <stevegolton@google.com> | 2023-10-04 20:43:27 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2023-10-04 20:43:27 +0000 |
commit | abce5e9eba588f5000acfe33462d4b42f9a052ce (patch) | |
tree | ed46d4316201906275c25998260cba8689dad879 | |
parent | dce95412adeebf1e37aff8d33ef5e9ac2800cf8d (diff) | |
parent | b5ea6711875eb07a4adf72aa5c5e62195a951c3a (diff) | |
download | perfetto-abce5e9eba588f5000acfe33462d4b42f9a052ce.tar.gz |
Merge changes I5998fea7,I2cbfc5fe,I6e75ee3d into main am: b5ea671187
Original change: https://android-review.googlesource.com/c/platform/external/perfetto/+/2760427
Change-Id: Iece8c811ebc9097a8538da75bbc297b7a7b86fff
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
37 files changed, 1784 insertions, 795 deletions
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts index acb6a47f0..0999b3115 100644 --- a/ui/src/common/actions.ts +++ b/ui/src/common/actions.ts @@ -16,6 +16,7 @@ import {Draft} from 'immer'; import {assertExists, assertTrue, assertUnreachable} from '../base/logging'; import {duration, time} from '../base/time'; +import {exists} from '../base/utils'; import {RecordConfig} from '../controller/record_config_types'; import { GenericSliceDetailsTabConfig, @@ -29,7 +30,7 @@ import { tableColumnEquals, toggleEnabled, } from '../frontend/pivot_table_types'; -import {TrackTags} from '../public/index'; +import {PrimaryTrackSortKey, TrackTags} from '../public/index'; import {DebugTrackV2Config} from '../tracks/debug/slice_track'; import {randomColor} from './colorizer'; @@ -47,6 +48,7 @@ import { traceEventEnd, TraceEventScope, } from './metatracing'; +import {pluginManager} from './plugins'; import { AdbRecordingTarget, Area, @@ -61,7 +63,6 @@ import { Pagination, PendingDeeplinkState, PivotTableResult, - PrimaryTrackSortKey, ProfileType, RecordingTarget, SCROLLING_TRACK_GROUP, @@ -225,16 +226,25 @@ export const StateActions = { state.uiTrackIdByTraceTrackId[trackId] = uiTrackId; }; - const config = trackState.config as {trackId: number}; - if (config.trackId !== undefined) { - setUiTrackId(config.trackId, uiTrackId); - return; - } - - const multiple = trackState.config as {trackIds: number[]}; - if (multiple.trackIds !== undefined) { - for (const trackId of multiple.trackIds) { + const {uri, config} = trackState; + if (exists(uri)) { + // If track is a new "plugin" type track (i.e. it has a uri), resolve the + // track ids from through the pluginManager. + const trackInfo = pluginManager.resolveTrackInfo(uri); + if (trackInfo?.trackIds) { + for (const trackId of trackInfo.trackIds) { + setUiTrackId(trackId, uiTrackId); + } + } + } else { + // Traditional track - resolve track ids through the config. + const {trackId, trackIds} = config; + if (exists(trackId)) { setUiTrackId(trackId, uiTrackId); + } else if (exists(trackIds)) { + for (const trackId of trackIds) { + setUiTrackId(trackId, uiTrackId); + } } } }, @@ -413,11 +423,6 @@ export const StateActions = { state.visibleTracks = args.tracks; }, - updateTrackConfig(state: StateDraft, args: {id: string, config: {}}) { - if (state.tracks[args.id] === undefined) return; - state.tracks[args.id].config = args.config; - }, - moveTrack( state: StateDraft, args: {srcId: string; op: 'before' | 'after', dstId: string}): void { diff --git a/ui/src/common/actions_unittest.ts b/ui/src/common/actions_unittest.ts index 261dd2efc..8713c1753 100644 --- a/ui/src/common/actions_unittest.ts +++ b/ui/src/common/actions_unittest.ts @@ -16,18 +16,18 @@ import {produce} from 'immer'; import {assertExists} from '../base/logging'; import {Time} from '../base/time'; +import {PrimaryTrackSortKey} from '../public'; import {SLICE_TRACK_KIND} from '../tracks/chrome_slices'; import {HEAP_PROFILE_TRACK_KIND} from '../tracks/heap_profile'; import { PROCESS_SCHEDULING_TRACK_KIND, -} from '../tracks/process_scheduling'; +} from '../tracks/process_summary/process_scheduling_track'; import {THREAD_STATE_TRACK_KIND} from '../tracks/thread_state'; import {StateActions} from './actions'; import {createEmptyState} from './empty_state'; import { InThreadTrackSortKey, - PrimaryTrackSortKey, ProfileType, SCROLLING_TRACK_GROUP, State, diff --git a/ui/src/common/basic_async_track.ts b/ui/src/common/basic_async_track.ts index 24e3f28e5..4e65e492d 100644 --- a/ui/src/common/basic_async_track.ts +++ b/ui/src/common/basic_async_track.ts @@ -46,9 +46,9 @@ export abstract class BasicAsyncTrack<Data> implements TrackLike { private currentState?: TrackData; protected data?: Data; - onCreate(): void {} + async onCreate(): Promise<void> {} - onDestroy(): void { + async onDestroy(): Promise<void> { this.queuedRequest = false; this.currentState = undefined; this.data = undefined; diff --git a/ui/src/common/engine.ts b/ui/src/common/engine.ts index 43c5f1652..c442e7339 100644 --- a/ui/src/common/engine.ts +++ b/ui/src/common/engine.ts @@ -499,6 +499,13 @@ export class EngineProxy implements Disposable { return this.engine.getCpus(); } + async getNumberOfGpus(): Promise<number> { + if (!this.isAlive) { + return Promise.reject(new Error(`EngineProxy ${this.tag} was disposed.`)); + } + return this.engine.getNumberOfGpus(); + } + get engineId(): string { return this.engine.id; } diff --git a/ui/src/common/plugins.ts b/ui/src/common/plugins.ts index 9961921a4..7278e066b 100644 --- a/ui/src/common/plugins.ts +++ b/ui/src/common/plugins.ts @@ -102,7 +102,8 @@ class TracePluginContextImpl<T> implements TracePluginContext<T>, Disposable { constructor( private ctx: PluginContext, readonly store: Store<T>, readonly engine: EngineProxy, - private trackRegistry: Map<string, PluginTrackInfo>, + readonly trackRegistry: Map<string, PluginTrackInfo>, + private suggestedTracks: Set<TrackInfo>, private commandRegistry: Map<string, Command>) { this.trash.add(engine); this.trash.add(store); @@ -147,11 +148,17 @@ class TracePluginContextImpl<T> implements TracePluginContext<T>, Disposable { if (!this.alive) return; const {uri} = trackDetails; this.trackRegistry.set(uri, trackDetails); - this.trash.add({ - dispose: () => { - this.trackRegistry.delete(uri); - }, - }); + this.trash.addCallback(() => this.trackRegistry.delete(uri)); + } + + // Ask Perfetto to add a track to the track list when a fresh trace is loaded. + // Ignored when a trace is loaded from a permalink. + // This is a direct replacement for findPotentialTracks(). + // Note: This interface is likely to be deprecated soon, but is required while + // both plugin and original type tracks coexist. + suggestTrack(trackInfo: TrackInfo): void { + this.suggestedTracks.add(trackInfo); + this.trash.addCallback(() => this.suggestedTracks.delete(trackInfo)); } dispose(): void { @@ -170,7 +177,7 @@ export class PluginRegistry extends Registry<PluginInfo<unknown>> { interface PluginDetails<T> { plugin: Plugin<T>; context: PluginContext&Disposable; - traceContext?: TracePluginContext<T>&Disposable; + traceContext?: TracePluginContextImpl<unknown>; } function isPluginClass<T>(v: unknown): v is PluginClass<T> { @@ -200,6 +207,7 @@ export class PluginManager { private engine?: Engine; readonly trackRegistry = new Map<string, PluginTrackInfo>(); readonly commandRegistry = new Map<string, Command>(); + readonly suggestedTracks = new Set<TrackInfo>(); constructor(registry: PluginRegistry) { this.registry = registry; @@ -257,15 +265,8 @@ export class PluginManager { return this.plugins.get(pluginId); } - findPotentialTracks(): Promise<TrackInfo[]>[] { - const promises: Promise<TrackInfo[]>[] = []; - for (const {plugin, traceContext} of this.plugins.values()) { - if (plugin.findPotentialTracks && traceContext) { - const promise = plugin.findPotentialTracks(traceContext); - promises.push(promise); - } - } - return promises; + findPotentialTracks(): TrackInfo[] { + return Array.from(this.suggestedTracks); } onTraceLoad(engine: Engine): void { @@ -305,10 +306,9 @@ export class PluginManager { // Create a new plugin track object from its URI. // Returns undefined if no such track is registered. - createTrack(uri: string, trackInstanceId: string): TrackLike|undefined { + createTrack(uri: string, trackCtx: TrackContext): TrackLike|undefined { const trackInfo = pluginManager.trackRegistry.get(uri); - const trackContext: TrackContext = {trackInstanceId}; - return trackInfo && trackInfo.trackFactory(trackContext); + return trackInfo && trackInfo.trackFactory(trackCtx); } private doPluginTraceLoad<T>( @@ -331,6 +331,7 @@ export class PluginManager { proxyStore, engineProxy, this.trackRegistry, + this.suggestedTracks, this.commandRegistry); pluginDetails.traceContext = traceCtx; @@ -347,6 +348,7 @@ export class PluginManager { proxyStore, engineProxy, this.trackRegistry, + this.suggestedTracks, this.commandRegistry); pluginDetails.traceContext = traceCtx; diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts index 0d31e01c9..6ba85475a 100644 --- a/ui/src/common/state.ts +++ b/ui/src/common/state.ts @@ -23,7 +23,7 @@ import { PivotTree, TableColumn, } from '../frontend/pivot_table_types'; -import {TrackTags} from '../public/index'; +import {PrimaryTrackSortKey, TrackTags} from '../public/index'; import {Direction} from './event_set'; @@ -130,36 +130,6 @@ export type EngineMode = 'WASM'|'HTTP_RPC'; export type NewEngineMode = 'USE_HTTP_RPC_IF_AVAILABLE'|'FORCE_BUILTIN_WASM'; -// Tracks within track groups (usually corresponding to processes) are sorted. -// As we want to group all tracks related to a given thread together, we use -// two keys: -// - Primary key corresponds to a priority of a track block (all tracks related -// to a given thread or a single track if it's not thread-associated). -// - Secondary key corresponds to a priority of a given thread-associated track -// within its thread track block. -// Each track will have a sort key, which either a primary sort key -// (for non-thread tracks) or a tid and secondary sort key (mapping of tid to -// primary sort key is done independently). -export enum PrimaryTrackSortKey { - DEBUG_SLICE_TRACK, - NULL_TRACK, - PROCESS_SCHEDULING_TRACK, - PROCESS_SUMMARY_TRACK, - EXPECTED_FRAMES_SLICE_TRACK, - ACTUAL_FRAMES_SLICE_TRACK, - PERF_SAMPLES_PROFILE_TRACK, - HEAP_PROFILE_TRACK, - MAIN_THREAD, - RENDER_THREAD, - GPU_COMPLETION_THREAD, - CHROME_IO_THREAD, - CHROME_COMPOSITOR_THREAD, - ORDINARY_THREAD, - COUNTER_TRACK, - ASYNC_SLICE_TRACK, - ORDINARY_TRACK, -} - // Key that is used to sort tracks within a block of tracks associated with a // given thread. export enum InThreadTrackSortKey { @@ -260,6 +230,7 @@ export interface TrackState { trackIds?: number[]; }; uri?: string; + state?: unknown; } export interface TrackGroupState { @@ -268,6 +239,7 @@ export interface TrackGroupState { name: string; collapsed: boolean; tracks: string[]; // Child track ids. + state?: unknown; } export interface EngineConfig { diff --git a/ui/src/common/state_unittest.ts b/ui/src/common/state_unittest.ts index 3a34afac9..34646bad3 100644 --- a/ui/src/common/state_unittest.ts +++ b/ui/src/common/state_unittest.ts @@ -12,8 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +import {PrimaryTrackSortKey} from '../public'; + import {createEmptyState} from './empty_state'; -import {getContainingTrackId, PrimaryTrackSortKey, State} from './state'; +import {getContainingTrackId, State} from './state'; import {deserializeStateObject, serializeStateObject} from './upload_utils'; test('createEmptyState', () => { diff --git a/ui/src/common/track_adapter.ts b/ui/src/common/track_adapter.ts index 9bb57a489..87d9a560a 100644 --- a/ui/src/common/track_adapter.ts +++ b/ui/src/common/track_adapter.ts @@ -57,15 +57,15 @@ export class TrackWithControllerAdapter<Config, Data> extends this.controller = new Controller(config, engine); } - onCreate(): void { - this.controller.onSetup(); - super.onCreate(); + async onCreate(): Promise<void> { + await this.controller.onSetup(); + await super.onCreate(); } - onDestroy(): void { - this.track.onDestroy(); - this.controller.onDestroy(); - super.onDestroy(); + async onDestroy(): Promise<void> { + await this.track.onDestroy(); + await this.controller.onDestroy(); + await super.onDestroy(); } getSliceRect( @@ -181,6 +181,12 @@ type TrackAdapterClass<Config, Data> = { new (args: NewTrackArgs): TrackAdapter<Config, Data> } +function hasNamespace(config: unknown): config is { + namespace: string +} { + return !!config && typeof config === 'object' && 'namespace' in config; +} + // Extend from this class instead of `TrackController` to use existing track // controller implementations with `TrackWithControllerAdapter`. export abstract class TrackControllerAdapter<Config, Data> { @@ -189,7 +195,7 @@ export abstract class TrackControllerAdapter<Config, Data> { // don't have access to it. private uuid = uuidv4(); - constructor(protected config: Config, private engine: EngineProxy) {} + constructor(protected config: Config, protected engine: EngineProxy) {} protected async query(query: string) { const result = await this.engine.query(query); @@ -199,8 +205,8 @@ export abstract class TrackControllerAdapter<Config, Data> { abstract onBoundsChange(start: time, end: time, resolution: duration): Promise<Data>; - onSetup(): void {} - onDestroy(): void {} + async onSetup(): Promise<void> {} + async onDestroy(): Promise<void> {} // Returns a valid SQL table name with the given prefix that should be unique // for each track. @@ -210,6 +216,14 @@ export abstract class TrackControllerAdapter<Config, Data> { const idSuffix = this.uuid.split('-').join('_'); return `${prefix}_${idSuffix}`; } + + namespaceTable(tableName: string): string { + if (hasNamespace(this.config)) { + return this.config.namespace + '_' + tableName; + } else { + return tableName; + } + } } type TrackControllerAdapterClass<Config, Data> = { diff --git a/ui/src/controller/aggregation/counter_aggregation_controller.ts b/ui/src/controller/aggregation/counter_aggregation_controller.ts index 3c3cf155e..ae3e1d715 100644 --- a/ui/src/controller/aggregation/counter_aggregation_controller.ts +++ b/ui/src/controller/aggregation/counter_aggregation_controller.ts @@ -15,9 +15,10 @@ import {Duration} from '../../base/time'; import {ColumnDef} from '../../common/aggregation_data'; import {Engine} from '../../common/engine'; +import {pluginManager} from '../../common/plugins'; import {Area, Sorting} from '../../common/state'; import {globals} from '../../frontend/globals'; -import {Config, COUNTER_TRACK_KIND} from '../../tracks/counter'; +import {COUNTER_TRACK_KIND} from '../../tracks/counter'; import {AggregationController} from './aggregation_controller'; @@ -25,19 +26,17 @@ export class CounterAggregationController extends AggregationController { async createAggregateView(engine: Engine, area: Area) { await engine.query(`drop view if exists ${this.kind};`); - const ids = []; + const trackIds: (string|number)[] = []; for (const trackId of area.tracks) { const track = globals.state.tracks[trackId]; - // Track will be undefined for track groups. - if (track !== undefined && track.kind === COUNTER_TRACK_KIND) { - const config = track.config as Config; - // TODO(hjd): Also aggregate annotation (with namespace) counters. - if (config.namespace === undefined) { - ids.push(config.trackId); + if (track?.uri) { + const trackInfo = pluginManager.resolveTrackInfo(track.uri); + if (trackInfo?.kind === COUNTER_TRACK_KIND) { + trackInfo.trackIds && trackIds.push(...trackInfo.trackIds); } } } - if (ids.length === 0) return false; + if (trackIds.length === 0) return false; const duration = area.end - area.start; const durationSec = Duration.toSeconds(duration); @@ -61,7 +60,7 @@ export class CounterAggregationController extends AggregationController { (partition by track_id order by ts range between unbounded preceding and unbounded following) as last from experimental_counter_dur - where track_id in (${ids}) + where track_id in (${trackIds}) and ts + dur >= ${area.start} and ts <= ${area.end}) join counter_track diff --git a/ui/src/controller/aggregation/cpu_aggregation_controller.ts b/ui/src/controller/aggregation/cpu_aggregation_controller.ts index 46f0f4756..db176d9e3 100644 --- a/ui/src/controller/aggregation/cpu_aggregation_controller.ts +++ b/ui/src/controller/aggregation/cpu_aggregation_controller.ts @@ -31,9 +31,8 @@ export class CpuAggregationController extends AggregationController { const track = globals.state.tracks[trackId]; if (track?.uri) { const trackInfo = pluginManager.resolveTrackInfo(track.uri); - if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) { - const cpu = trackInfo?.tags?.cpu; - cpu && selectedCpus.push(cpu); + if (trackInfo?.kind === CPU_SLICE_TRACK_KIND) { + trackInfo.cpu && selectedCpus.push(trackInfo.cpu); } } } diff --git a/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts b/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts index aebaf942f..dbdf07cfc 100644 --- a/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts +++ b/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts @@ -30,9 +30,8 @@ export class CpuByProcessAggregationController extends AggregationController { const track = globals.state.tracks[trackId]; if (track?.uri) { const trackInfo = pluginManager.resolveTrackInfo(track.uri); - if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) { - const cpu = trackInfo?.tags?.cpu; - cpu && selectedCpus.push(cpu); + if (trackInfo?.kind === CPU_SLICE_TRACK_KIND) { + trackInfo.cpu && selectedCpus.push(trackInfo.cpu); } } } diff --git a/ui/src/controller/flow_events_controller.ts b/ui/src/controller/flow_events_controller.ts index 06f9da97a..a1c4170fa 100644 --- a/ui/src/controller/flow_events_controller.ts +++ b/ui/src/controller/flow_events_controller.ts @@ -15,6 +15,7 @@ import {Time} from '../base/time'; import {Engine} from '../common/engine'; import {featureFlags} from '../common/feature_flags'; +import {pluginManager} from '../common/plugins'; import {LONG, NUM, STR_NULL} from '../common/query_result'; import {Area} from '../common/state'; import {Flow, globals} from '../frontend/globals'; @@ -243,6 +244,17 @@ export class FlowEventsController extends Controller<'main'> { return null; } + // Perform the same check for "plugin" style tracks. + if (track.uri) { + const trackInfo = pluginManager.resolveTrackInfo(track.uri); + const trackIds = trackInfo?.trackIds; + if (trackIds === undefined || trackIds.length <= 1) { + uiTrackIdToInfo.set(uiTrackId, null); + trackIdToInfo.set(trackId, null); + return null; + } + } + const newInfo = { uiTrackId, siblingTrackIds: trackIds, diff --git a/ui/src/controller/search_controller.ts b/ui/src/controller/search_controller.ts index b8ee0d3dc..6055221d9 100644 --- a/ui/src/controller/search_controller.ts +++ b/ui/src/controller/search_controller.ts @@ -204,8 +204,8 @@ export class SearchController extends Controller<'main'> { for (const track of Object.values(globals.state.tracks)) { if (exists(track?.uri)) { const trackInfo = pluginManager.resolveTrackInfo(track.uri); - if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) { - const cpu = trackInfo?.tags?.cpu; + if (trackInfo?.kind === CPU_SLICE_TRACK_KIND) { + const cpu = trackInfo?.cpu; cpu && cpuToTrackId.set(cpu, track.id); } } diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts index 027398abb..31000d60a 100644 --- a/ui/src/controller/trace_controller.ts +++ b/ui/src/controller/trace_controller.ts @@ -470,8 +470,6 @@ export class TraceController extends Controller<States> { } } - pluginManager.onTraceLoad(engine); - const emptyOmniboxState = { omnibox: '', mode: globals.state.omniboxState.mode || 'SEARCH', @@ -501,6 +499,8 @@ export class TraceController extends Controller<States> { // Make sure the helper views are available before we start adding tracks. await this.initialiseHelperViews(); + pluginManager.onTraceLoad(engine); + { // When we reload from a permalink don't create extra tracks: const {pinnedTracks, tracks} = globals.state; diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts index 04bac66f3..4be0a2457 100644 --- a/ui/src/controller/track_decider.ts +++ b/ui/src/controller/track_decider.ts @@ -25,7 +25,6 @@ import {Engine, EngineProxy} from '../common/engine'; import {featureFlags, PERF_SAMPLE_FLAG} from '../common/feature_flags'; import {pluginManager} from '../common/plugins'; import { - LONG_NULL, NUM, NUM_NULL, STR, @@ -33,11 +32,12 @@ import { } from '../common/query_result'; import { InThreadTrackSortKey, - PrimaryTrackSortKey, SCROLLING_TRACK_GROUP, TrackSortKey, UtidToTrackSortKey, } from '../common/state'; +import {PrimaryTrackSortKey} from '../public'; +import {getTrackName} from '../public/utils'; import {ACTUAL_FRAMES_SLICE_TRACK_KIND} from '../tracks/actual_frames'; import {ASYNC_SLICE_TRACK_KIND} from '../tracks/async_slices'; import { @@ -49,8 +49,7 @@ import { decideTracks as scrollJankDecideTracks, } from '../tracks/chrome_scroll_jank/chrome_tasks_scroll_jank_track'; import {SLICE_TRACK_KIND} from '../tracks/chrome_slices'; -import {COUNTER_TRACK_KIND, CounterScaleOptions} from '../tracks/counter'; -import {CPU_FREQ_TRACK_KIND} from '../tracks/cpu_freq'; +import {COUNTER_TRACK_KIND} from '../tracks/counter'; import {CPU_PROFILE_TRACK_KIND} from '../tracks/cpu_profile'; import { EXPECTED_FRAMES_SLICE_TRACK_KIND, @@ -61,10 +60,6 @@ import { PERF_SAMPLES_PROFILE_TRACK_KIND, } from '../tracks/perf_samples_profile'; import { - PROCESS_SCHEDULING_TRACK_KIND, -} from '../tracks/process_scheduling'; -import {PROCESS_SUMMARY_TRACK} from '../tracks/process_summary'; -import { decideTracks as screenshotDecideTracks, } from '../tracks/screenshots'; import {THREAD_STATE_TRACK_KIND} from '../tracks/thread_state'; @@ -130,29 +125,6 @@ const CHROME_TRACK_REGEX = new RegExp('^Chrome.*|^InputLatency::.*'); const CHROME_TRACK_GROUP = 'Chrome Global Tracks'; const MISC_GROUP = 'Misc Global Tracks'; -// 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 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; -} - export async function decideTracks( engineId: string, engine: Engine): Promise<DeferredAction[]> { return (new TrackDecider(engineId, engine)).decideTracks(); @@ -171,66 +143,6 @@ class TrackDecider { this.engine = engine; } - static getTrackName(args: Partial<{ - name: string | null, - utid: number, - processName: string|null, - pid: number|null, - threadName: string|null, - tid: number|null, - upid: number|null, - kind: string, - threadTrack: boolean - }>) { - const { - name, - upid, - utid, - processName, - threadName, - pid, - tid, - kind, - threadTrack, - } = args; - - const hasName = name !== undefined && name !== null && name !== '[NULL]'; - const hasUpid = upid !== undefined && upid !== null; - const hasUtid = utid !== undefined && utid !== null; - const hasProcessName = processName !== undefined && processName !== null; - const hasThreadName = threadName !== undefined && threadName !== null; - const hasTid = tid !== undefined && tid !== null; - const hasPid = pid !== undefined && pid !== null; - const hasKind = kind !== undefined; - const isThreadTrack = threadTrack !== undefined && threadTrack; - - // If we don't have any useful information (better than - // upid/utid) we show the track kind to help with tracking - // down where this is coming from. - const kindSuffix = hasKind ? ` (${kind})` : ''; - - if (isThreadTrack && hasName && hasTid) { - return `${name} (${tid})`; - } else if (hasName) { - return `${name}`; - } else if (hasUpid && hasPid && hasProcessName) { - return `${processName} ${pid}`; - } else if (hasUpid && hasPid) { - return `Process ${pid}`; - } else if (hasThreadName && hasTid) { - return `${threadName} ${tid}`; - } else if (hasTid) { - return `Thread ${tid}`; - } else if (hasUpid) { - return `upid: ${upid}${kindSuffix}`; - } else if (hasUtid) { - return `utid: ${utid}${kindSuffix}`; - } else if (hasKind) { - return `Unnamed ${kind}`; - } - return 'Unknown'; - } - async guessCpuSizes(): Promise<Map<number, string>> { const cpuToSize = new Map<number, string>(); await this.engine.query(` @@ -289,54 +201,35 @@ class TrackDecider { async addCpuFreqTracks(engine: EngineProxy): Promise<void> { const cpus = await this.engine.getCpus(); - const maxCpuFreqResult = await engine.query(` - select ifnull(max(value), 0) as freq - from counter c - inner join cpu_counter_track t on c.track_id = t.id - where name = 'cpufreq'; - `); - const maxCpuFreq = maxCpuFreqResult.firstRow({freq: NUM}).freq; - for (const cpu of cpus) { // Only add a cpu freq track if we have // cpu freq data. // TODO(hjd): Find a way to display cpu idle // events even if there are no cpu freq events. const cpuFreqIdleResult = await engine.query(` - select - id as cpuFreqId, - ( - select id - from cpu_counter_track - where name = 'cpuidle' - and cpu = ${cpu} - limit 1 - ) as cpuIdleId - from cpu_counter_track - where name = 'cpufreq' and cpu = ${cpu} - limit 1; - `); + select + id as cpuFreqId, + ( + select id + from cpu_counter_track + where name = 'cpuidle' + and cpu = ${cpu} + limit 1 + ) as cpuIdleId + from cpu_counter_track + where name = 'cpufreq' and cpu = ${cpu} + limit 1; + `); if (cpuFreqIdleResult.numRows() > 0) { - const row = cpuFreqIdleResult.firstRow({ - cpuFreqId: NUM, - cpuIdleId: NUM_NULL, - }); - const freqTrackId = row.cpuFreqId; - const idleTrackId = row.cpuIdleId === null ? undefined : row.cpuIdleId; - this.tracksToAdd.push({ engineId: this.engineId, - kind: CPU_FREQ_TRACK_KIND, + kind: PLUGIN_TRACK_KIND, trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK, name: `Cpu ${cpu} Frequency`, trackGroup: SCROLLING_TRACK_GROUP, - config: { - cpu, - maximumValue: maxCpuFreq, - freqTrackId, - idleTrackId, - }, + config: {}, + uri: `perfetto.CpuFreq#${cpu}`, }); } } @@ -394,7 +287,7 @@ class TrackDecider { const kind = ASYNC_SLICE_TRACK_KIND; const rawName = it.name === null ? undefined : it.name; const rawParentName = it.parentName === null ? undefined : it.parentName; - const name = TrackDecider.getTrackName({name: rawName, kind}); + const name = getTrackName({name: rawName, kind}); const rawTrackIds = it.trackIds; const trackIds = rawTrackIds.split(',').map((v) => Number(v)); const parentTrackId = it.parentId; @@ -412,8 +305,7 @@ class TrackDecider { trackGroup = uuidv4(); parentIdToGroupId.set(parentTrackId, trackGroup); - const parentName = - TrackDecider.getTrackName({name: rawParentName, kind}); + const parentName = getTrackName({name: rawParentName, kind}); const summaryTrackId = uuidv4(); this.tracksToAdd.push({ @@ -463,36 +355,24 @@ class TrackDecider { async addGpuFreqTracks(engine: EngineProxy): Promise<void> { const numGpus = await this.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 + select * from gpu_counter_track where name = 'gpufreq' and gpu_id = ${gpu} limit 1; `); if (freqExistsResult.numRows() > 0) { - const trackId = freqExistsResult.firstRow({id: NUM}).id; this.tracksToAdd.push({ engineId: this.engineId, - kind: COUNTER_TRACK_KIND, + kind: PLUGIN_TRACK_KIND, name: `Gpu ${gpu} Frequency`, trackSortKey: PrimaryTrackSortKey.COUNTER_TRACK, trackGroup: SCROLLING_TRACK_GROUP, - config: { - trackId, - maximumValue, - }, + config: {}, + uri: `perfetto.Counter#gpu_freq${gpu}`, }); } } @@ -537,15 +417,12 @@ class TrackDecider { const trackId = it.id; this.tracksToAdd.push({ engineId: this.engineId, - kind: COUNTER_TRACK_KIND, + kind: PLUGIN_TRACK_KIND, name, trackSortKey: PrimaryTrackSortKey.COUNTER_TRACK, trackGroup: SCROLLING_TRACK_GROUP, - config: { - name, - trackId, - scale: getCounterScale(name), - }, + config: {}, + uri: `perfetto.Counter#cpu${trackId}`, }); } } @@ -866,17 +743,6 @@ class TrackDecider { } } - applyDefaultCounterScale(): void { - for (const track of this.tracksToAdd) { - if (track.kind === COUNTER_TRACK_KIND) { - const scaleConfig = { - scale: getCounterScale(track.name), - }; - track.config = Object.assign({}, track.config, scaleConfig); - } - } - } - async addLogsTrack(engine: EngineProxy): Promise<void> { const result = await engine.query(`select count(1) as cnt from android_logs`); @@ -1017,44 +883,30 @@ class TrackDecider { } const counterResult = await engine.query(` - SELECT - id, - name, - upid, - min_value as minValue, - max_value as maxValue - FROM annotation_counter_track`); + SELECT id, name, upid FROM annotation_counter_track + `); const counterIt = counterResult.iter({ id: NUM, name: STR, upid: NUM, - minValue: NUM_NULL, - maxValue: NUM_NULL, }); for (; counterIt.valid(); counterIt.next()) { const id = counterIt.id; const name = counterIt.name; const upid = counterIt.upid; - const minimumValue = - counterIt.minValue === null ? undefined : counterIt.minValue; - const maximumValue = - counterIt.maxValue === null ? undefined : counterIt.maxValue; this.tracksToAdd.push({ engineId: this.engineId, - kind: 'CounterTrack', + kind: PLUGIN_TRACK_KIND, name, trackSortKey: PrimaryTrackSortKey.COUNTER_TRACK, trackGroup: upid === 0 ? SCROLLING_TRACK_GROUP : this.upidToUuid.get(upid), config: { - name, namespace: 'annotation', - trackId: id, - minimumValue, - maximumValue, }, + uri: `perfetto.Annotation#counter${id}`, }); } } @@ -1101,7 +953,7 @@ class TrackDecider { this.tracksToAdd.push({ engineId: this.engineId, kind: THREAD_STATE_TRACK_KIND, - name: TrackDecider.getTrackName({utid, tid, threadName, kind}), + name: getTrackName({utid, tid, threadName, kind}), trackGroup: uuid, trackSortKey: { utid, @@ -1116,7 +968,7 @@ class TrackDecider { this.tracksToAdd.push({ engineId: this.engineId, kind, - name: TrackDecider.getTrackName({utid, tid, threadName, kind}), + name: getTrackName({utid, tid, threadName, kind}), trackGroup: uuid, trackSortKey: { utid, @@ -1177,9 +1029,7 @@ class TrackDecider { upid, tid, thread.name as threadName, - thread_counter_track.id as trackId, - thread.start_ts as startTs, - thread.end_ts as endTs + thread_counter_track.id as trackId from thread_counter_track join thread using(utid) left join process using(upid) @@ -1192,9 +1042,7 @@ class TrackDecider { upid: NUM_NULL, tid: NUM_NULL, threadName: STR_NULL, - startTs: LONG_NULL, trackId: NUM, - endTs: LONG_NULL, }); for (; it.valid(); it.next()) { const utid = it.utid; @@ -1204,27 +1052,25 @@ class TrackDecider { const trackName = it.trackName; const threadName = it.threadName; const uuid = this.getUuid(utid, upid); - const startTs = it.startTs === null ? undefined : it.startTs; - const endTs = it.endTs === null ? undefined : it.endTs; - const kind = COUNTER_TRACK_KIND; - const name = TrackDecider.getTrackName( - {name: trackName, utid, tid, kind, threadName, threadTrack: true}); + const name = getTrackName({ + name: trackName, + utid, + tid, + kind: COUNTER_TRACK_KIND, + threadName, + threadTrack: true, + }); this.tracksToAdd.push({ engineId: this.engineId, - kind, + kind: PLUGIN_TRACK_KIND, name, trackSortKey: { utid, priority: InThreadTrackSortKey.ORDINARY, }, trackGroup: uuid, - config: { - name, - trackId, - startTs, - endTs, - tid, - }, + config: {}, + uri: `perfetto.Counter#thread${trackId}`, }); } } @@ -1279,8 +1125,8 @@ class TrackDecider { const uuid = this.getUuid(0, upid); const kind = ASYNC_SLICE_TRACK_KIND; - const name = TrackDecider.getTrackName( - {name: trackName, upid, pid, processName, kind}); + const name = + getTrackName({name: trackName, upid, pid, processName, kind}); this.tracksToAdd.push({ engineId: this.engineId, kind, @@ -1343,8 +1189,8 @@ class TrackDecider { const uuid = this.getUuid(0, upid); const kind = ACTUAL_FRAMES_SLICE_TRACK_KIND; - const name = TrackDecider.getTrackName( - {name: trackName, upid, pid, processName, kind}); + const name = + getTrackName({name: trackName, upid, pid, processName, kind}); this.tracksToAdd.push({ engineId: this.engineId, kind, @@ -1408,8 +1254,8 @@ class TrackDecider { const uuid = this.getUuid(0, upid); const kind = EXPECTED_FRAMES_SLICE_TRACK_KIND; - const name = TrackDecider.getTrackName( - {name: trackName, upid, pid, processName, kind}); + const name = + getTrackName({name: trackName, upid, pid, processName, kind}); this.tracksToAdd.push({ engineId: this.engineId, kind, @@ -1467,8 +1313,7 @@ class TrackDecider { const uuid = this.getUuid(utid, upid); const kind = SLICE_TRACK_KIND; - const name = TrackDecider.getTrackName( - {name: trackName, utid, tid, threadName, kind}); + const name = getTrackName({name: trackName, utid, tid, threadName, kind}); if (showV1()) { this.tracksToAdd.push({ engineId: this.engineId, @@ -1514,9 +1359,7 @@ class TrackDecider { process_counter_track.name as trackName, upid, process.pid, - process.name as processName, - process.start_ts as startTs, - process.end_ts as endTs + process.name as processName from process_counter_track join process using(upid); `); @@ -1526,8 +1369,6 @@ class TrackDecider { upid: NUM, pid: NUM_NULL, processName: STR_NULL, - startTs: LONG_NULL, - endTs: LONG_NULL, }); for (let i = 0; it.valid(); ++i, it.next()) { const pid = it.pid; @@ -1536,24 +1377,17 @@ class TrackDecider { const trackName = it.trackName; const processName = it.processName; const uuid = this.getUuid(0, upid); - const startTs = it.startTs === null ? undefined : it.startTs; - const endTs = it.endTs === null ? undefined : it.endTs; - const kind = COUNTER_TRACK_KIND; - const name = TrackDecider.getTrackName( - {name: trackName, upid, pid, kind, processName}); + const name = getTrackName( + {name: trackName, upid, pid, kind: COUNTER_TRACK_KIND, processName}); this.tracksToAdd.push({ engineId: this.engineId, - kind, + kind: PLUGIN_TRACK_KIND, name, trackSortKey: await this.resolveTrackSortKeyForProcessCounterTrack( upid, trackName || undefined), trackGroup: uuid, - config: { - name, - trackId, - startTs, - endTs, - }, + config: {}, + uri: `perfetto.Counter#process${trackId}`, }); } } @@ -1671,10 +1505,11 @@ class TrackDecider { this.tracksToAdd.push({ id: summaryTrackId, engineId: this.engineId, - kind: PROCESS_SUMMARY_TRACK, + kind: PLUGIN_TRACK_KIND, trackSortKey: PrimaryTrackSortKey.PROCESS_SUMMARY_TRACK, name: `Kernel thread summary`, - config: {pidForColor: 2, upid: it.upid, utid: it.utid}, + config: {}, + uri: 'perfetto.ProcessSummary#kernel', }); const addTrackGroup = Actions.addTrackGroup({ engineId: this.engineId, @@ -1822,7 +1657,6 @@ class TrackDecider { processName: STR_NULL, hasSched: NUM_NULL, hasHeapProfiles: NUM_NULL, - isDebuggable: NUM_NULL, chromeProcessLabels: STR, }); for (; it.valid(); it.next()) { @@ -1834,7 +1668,6 @@ class TrackDecider { const processName = it.processName; const hasSched = !!it.hasSched; const hasHeapProfiles = !!it.hasHeapProfiles; - const isDebuggable = !!it.isDebuggable; // Group by upid if present else by utid. let pUuid = @@ -1843,31 +1676,24 @@ class TrackDecider { if (pUuid === undefined) { pUuid = this.getOrCreateUuid(utid, upid); const summaryTrackId = uuidv4(); - - const pidForColor = pid || tid || upid || utid || 0; - const kind = - hasSched ? PROCESS_SCHEDULING_TRACK_KIND : PROCESS_SUMMARY_TRACK; + const type = hasSched ? 'schedule' : 'summary'; + const uri = `perfetto.ProcessScheduling#${utid}.${type}`; this.tracksToAdd.push({ id: summaryTrackId, engineId: this.engineId, - kind, + kind: PLUGIN_TRACK_KIND, trackSortKey: hasSched ? PrimaryTrackSortKey.PROCESS_SCHEDULING_TRACK : PrimaryTrackSortKey.PROCESS_SUMMARY_TRACK, name: `${upid === null ? tid : pid} summary`, - config: { - pidForColor, - upid, - utid, - tid, - isDebuggable: isDebuggable ?? undefined, - }, + config: {}, labels: it.chromeProcessLabels.split(','), + uri, }); - const name = TrackDecider.getTrackName( - {utid, processName, pid, threadName, tid, upid}); + const name = + getTrackName({utid, processName, pid, threadName, tid, upid}); const addTrackGroup = Actions.addTrackGroup({ engineId: this.engineId, summaryTrackId, @@ -1934,22 +1760,20 @@ class TrackDecider { `); } - async addPluginTracks(): Promise<void> { - const promises = pluginManager.findPotentialTracks(); - const groups = await Promise.all(promises); - for (const infos of groups) { - for (const info of infos) { - this.tracksToAdd.push({ - engineId: this.engineId, - kind: info.trackKind, - name: info.name, - // TODO(hjd): Fix how sorting works. Plugins should expose - // 'sort keys' which the user can use to choose a sort order. - trackSortKey: PrimaryTrackSortKey.COUNTER_TRACK, - trackGroup: SCROLLING_TRACK_GROUP, - config: info.config, - }); - } + addPluginTracks(): void { + const tracks = pluginManager.findPotentialTracks(); + for (const info of tracks) { + this.tracksToAdd.push({ + engineId: this.engineId, + kind: PLUGIN_TRACK_KIND, + name: info.name, + uri: info.uri, + // TODO(hjd): Fix how sorting works. Plugins should expose + // 'sort keys' which the user can use to choose a sort order. + trackSortKey: info.sortKey, + trackGroup: SCROLLING_TRACK_GROUP, + config: {}, + }); } } @@ -1978,7 +1802,7 @@ class TrackDecider { this.engine.getProxy('TrackDecider::addCpuFreqLimitCounterTracks')); await this.addCpuPerfCounterTracks( this.engine.getProxy('TrackDecider::addCpuPerfCounterTracks')); - await this.addPluginTracks(); + this.addPluginTracks(); await this.addAnnotationTracks( this.engine.getProxy('TrackDecider::addAnnotationTracks')); await this.groupGlobalIonTracks(); @@ -2058,8 +1882,6 @@ class TrackDecider { this.addTrackGroupActions.push( Actions.setUtidToTrackSortKey({threadOrderingMetadata})); - this.applyDefaultCounterScale(); - return this.addTrackGroupActions; } diff --git a/ui/src/frontend/app.ts b/ui/src/frontend/app.ts index 23401a011..955df023d 100644 --- a/ui/src/frontend/app.ts +++ b/ui/src/frontend/app.ts @@ -43,6 +43,7 @@ import {toggleHelp} from './help_modal'; import {fullscreenModalContainer} from './modal'; import {Omnibox, OmniboxOption} from './omnibox'; import {runQueryInNewTab} from './query_result_tab'; +import {verticalScrollToTrack} from './scroll_helper'; import {executeSearch} from './search_handler'; import {Sidebar} from './sidebar'; import {SqlTableTab} from './sql_table/tab'; @@ -307,8 +308,9 @@ export class App implements m.ClassComponent { }, }, { - id: 'perfetto.PrintTrackInfoToConsole', - name: 'Print track info to console', + // Selects & reveals the first track on the timeline with a given URI. + id: 'perfetto.FindTrack', + name: 'Find track by URI', callback: async () => { const tracks = Array.from(pluginManager.trackRegistry.values()); @@ -326,9 +328,28 @@ export class App implements m.ClassComponent { }); try { - const uri = await this.prompt('Choose a track...', sortedOptions); - const trackDetails = pluginManager.resolveTrackInfo(uri); - console.log(trackDetails); + const selectedUri = + await this.prompt('Choose a track...', sortedOptions); + + // Find the first track with this URI + const firstTrack = Object.values(globals.state.tracks) + .find(({uri}) => uri === selectedUri); + if (firstTrack) { + console.log(firstTrack); + verticalScrollToTrack(firstTrack.id, true); + const traceTime = globals.stateTraceTimeTP(); + globals.makeSelection( + Actions.selectArea({ + area: { + start: traceTime.start, + end: traceTime.end, + tracks: [firstTrack.id], + }, + }), + ); + } else { + alert(`No tracks with uri ${selectedUri} on the timeline`); + } } catch { // Prompt was probably cancelled - do nothing. } diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts index f16057b93..130987bce 100644 --- a/ui/src/frontend/base_slice_track.ts +++ b/ui/src/frontend/base_slice_track.ts @@ -505,10 +505,10 @@ export abstract class BaseSliceTrack<T extends BaseSliceTrackTypes = } // if (hoveredSlice) } - onDestroy() { - super.onDestroy(); + async onDestroy() { + await super.onDestroy(); this.isDestroyed = true; - this.engine.query(`DROP VIEW IF EXISTS ${this.tableName}`); + await this.engine.query(`DROP VIEW IF EXISTS ${this.tableName}`); } // This method figures out if the visible window is outside the bounds of diff --git a/ui/src/frontend/flow_events_renderer.ts b/ui/src/frontend/flow_events_renderer.ts index 4d49c1a6f..c414454ba 100644 --- a/ui/src/frontend/flow_events_renderer.ts +++ b/ui/src/frontend/flow_events_renderer.ts @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +import {TrackState} from 'src/common/state'; + import {time} from '../base/time'; +import {pluginManager} from '../common/plugins'; import {TRACK_SHELL_WIDTH} from './css_constants'; import {ALL_CATEGORIES, getFlowCategories} from './flow_events_panel'; @@ -67,6 +70,22 @@ function hasTrackGroupId(obj: {}): obj is {trackGroupId: string} { return (obj as {trackGroupId?: string}).trackGroupId !== undefined; } +function getTrackIds(track: TrackState): number[] { + if (track.uri) { + const trackInfo = pluginManager.resolveTrackInfo(track.uri); + if (trackInfo?.trackIds) return trackInfo?.trackIds; + } else { + const config = track.config; + if (hasTrackId(config)) { + return [config.trackId]; + } + if (hasManyTrackIds(config)) { + return config.trackIds; + } + } + return []; +} + export class FlowEventsRendererArgs { trackIdToTrackPanel: Map<number, TrackPanelInfo>; groupIdToTrackGroupPanel: Map<string, TrackGroupPanelInfo>; @@ -78,15 +97,9 @@ export class FlowEventsRendererArgs { registerPanel(panel: PanelVNode, yStart: number, height: number) { if (panel.state instanceof TrackPanel && hasId(panel.attrs)) { - const config = globals.state.tracks[panel.attrs.id].config; - if (hasTrackId(config)) { - this.trackIdToTrackPanel.set( - config.trackId, {panel: panel.state, yStart}); - } - if (hasManyTrackIds(config)) { - for (const trackId of config.trackIds) { - this.trackIdToTrackPanel.set(trackId, {panel: panel.state, yStart}); - } + const track = globals.state.tracks[panel.attrs.id]; + for (const trackId of getTrackIds(track)) { + this.trackIdToTrackPanel.set(trackId, {panel: panel.state, yStart}); } } else if ( panel.state instanceof TrackGroupPanel && diff --git a/ui/src/frontend/thread_state.ts b/ui/src/frontend/thread_state.ts index bb1b2c706..80f690b9b 100644 --- a/ui/src/frontend/thread_state.ts +++ b/ui/src/frontend/thread_state.ts @@ -146,8 +146,8 @@ export function goToSchedSlice(cpu: number, id: SchedSqlId, ts: time) { for (const track of Object.values(globals.state.tracks)) { if (exists(track?.uri)) { const trackInfo = pluginManager.resolveTrackInfo(track.uri); - if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) { - if (trackInfo?.tags?.cpu === cpu) { + if (trackInfo?.kind === CPU_SLICE_TRACK_KIND) { + if (trackInfo?.cpu === cpu) { trackId = track.id; break; } diff --git a/ui/src/frontend/track.ts b/ui/src/frontend/track.ts index 31e9c7980..46d16c677 100644 --- a/ui/src/frontend/track.ts +++ b/ui/src/frontend/track.ts @@ -75,11 +75,11 @@ export abstract class Track<Config = {}, Data extends TrackData = TrackData> this.lastTrackState = assertExists(globals.state.tracks[this.trackId]); } - onCreate() {} + async onCreate(): Promise<void> {} // Last call the track will receive. Called just before the last reference to // this object is removed. - onDestroy() {} + async onDestroy(): Promise<void> {} protected abstract renderCanvas(ctx: CanvasRenderingContext2D): void; diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts index e1f2c8621..a79911205 100644 --- a/ui/src/frontend/track_group_panel.ts +++ b/ui/src/frontend/track_group_panel.ts @@ -18,17 +18,23 @@ import m from 'mithril'; import {assertExists} from '../base/logging'; import {Icons} from '../base/semantic_icons'; import {Actions} from '../common/actions'; +import {pluginManager} from '../common/plugins'; +import {RegistryError} from '../common/registry'; import { getContainingTrackId, TrackGroupState, TrackState, } from '../common/state'; +import {Migrate, TrackContext, TrackLike} from '../public'; import {globals} from './globals'; import {drawGridLines} from './gridline_helper'; import {Panel, PanelSize} from './panel'; -import {Track} from './track'; -import {TrackChips, TrackContent} from './track_panel'; +import { + renderChips, + TrackContent, + TrackLifecycleContainer, +} from './track_panel'; import {trackRegistry} from './track_registry'; import { drawVerticalLineAtTime, @@ -43,20 +49,36 @@ export class TrackGroupPanel extends Panel<Attrs> { private readonly trackGroupId: string; private shellWidth = 0; private backgroundColor = '#ffffff'; // Updated from CSS later. - private summaryTrack: Track|undefined; + private summaryTrack?: TrackLifecycleContainer; - constructor({attrs}: m.CVnode<Attrs>) { + constructor(vnode: m.CVnode<Attrs>) { super(); - this.trackGroupId = attrs.trackGroupId; - const trackCreator = trackRegistry.get(this.summaryTrackState.kind); - const engineId = this.summaryTrackState.engineId; - const engine = globals.engines.get(engineId); - if (engine !== undefined) { - this.summaryTrack = trackCreator.create({ - trackId: this.summaryTrackState.id, - engine: engine.getProxy(`Track; kind: ${ - this.summaryTrackState.kind}; id: ${this.summaryTrackState.id}`), - }); + this.trackGroupId = vnode.attrs.trackGroupId; + } + + private tryLoadTrack() { + const trackId = this.trackGroupId; + const trackState = this.summaryTrackState; + + const {id, uri} = trackState; + + const ctx: TrackContext = { + trackInstanceId: id, + mountStore: <T>(migrate: Migrate<T>) => { + const {store, state} = globals; + const migratedState = migrate(state.trackGroups[trackId].state); + store.edit((draft) => { + draft.trackGroups[trackId].state = migratedState; + }); + return store.createProxy<T>(['trackGroups', trackId, 'state']); + }, + }; + + const track = + uri ? pluginManager.createTrack(uri, ctx) : loadTrack(trackState, id); + + if (track) { + this.summaryTrack = new TrackLifecycleContainer(track); } } @@ -69,6 +91,10 @@ export class TrackGroupPanel extends Panel<Attrs> { } view({attrs}: m.CVnode<Attrs>) { + if (!this.summaryTrack) { + this.tryLoadTrack(); + } + const collapsed = this.trackGroupState.collapsed; let name = this.trackGroupState.name; if (name[0] === '/') { @@ -132,7 +158,7 @@ export class TrackGroupPanel extends Panel<Attrs> { 'h1.track-title', {title: name}, name, - m(TrackChips, {config: this.summaryTrackState.config}), + renderChips(this.summaryTrackState), ), (this.trackGroupState.collapsed && child !== null) ? m('h2.track-subtitle', child) : @@ -180,7 +206,7 @@ export class TrackGroupPanel extends Panel<Attrs> { onremove() { if (this.summaryTrack !== undefined) { - this.summaryTrack.onDestroy(); + this.summaryTrack.dispose(); this.summaryTrack = undefined; } } @@ -286,3 +312,26 @@ export class TrackGroupPanel extends Panel<Attrs> { function StripPathFromExecutable(path: string) { return path.split('/').slice(-1)[0]; } + +function loadTrack(trackState: TrackState, trackId: string): TrackLike| + undefined { + const engine = globals.engines.get(trackState.engineId); + if (engine === undefined) { + return undefined; + } + + try { + const trackCreator = trackRegistry.get(trackState.kind); + return trackCreator.create({ + trackId, + engine: + engine.getProxy(`Track; kind: ${trackState.kind}; id: ${trackId}`), + }); + } catch (e) { + if (e instanceof RegistryError) { + return undefined; + } else { + throw e; + } + } +} diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts index 8dffff16d..64cfd63be 100644 --- a/ui/src/frontend/track_panel.ts +++ b/ui/src/frontend/track_panel.ts @@ -15,15 +15,17 @@ import {hex} from 'color-convert'; import m from 'mithril'; +import {Disposable} from '../base/disposable'; import {currentTargetOffset} from '../base/dom_utils'; import {Icons} from '../base/semantic_icons'; import {duration, Span, time} from '../base/time'; +import {exists} from '../base/utils'; import {Actions} from '../common/actions'; import {pluginManager} from '../common/plugins'; import {RegistryError} from '../common/registry'; import {TrackState} from '../common/state'; import {raf} from '../core/raf_scheduler'; -import {TrackLike} from '../public'; +import {Migrate, TrackContext, TrackLike} from '../public'; import {SELECTION_FILL_COLOR, TRACK_SHELL_WIDTH} from './css_constants'; import {globals} from './globals'; @@ -68,26 +70,39 @@ function isSelected(id: string) { return selectedArea.tracks.includes(id); } -interface TrackChipsAttrs { - config: {[k: string]: any}; +interface TrackChipAttrs { + text: string; } -export class TrackChips implements m.ClassComponent<TrackChipsAttrs> { - view({attrs}: m.CVnode<TrackChipsAttrs>) { - const {config} = attrs; - - const isMetric = 'namespace' in config; - const isDebuggable = ('isDebuggable' in config) && config.isDebuggable; +class TrackChip implements m.ClassComponent<TrackChipAttrs> { + view({attrs}: m.CVnode<TrackChipAttrs>) { + return m('span.chip', attrs.text); + } +} - return [ - isMetric && m('span.chip', 'metric'), - isDebuggable && m('span.chip', 'debuggable'), - ]; +export function renderChips({uri, config}: TrackState) { + const tagElements: m.Children = []; + if (exists(uri)) { + const trackInfo = pluginManager.resolveTrackInfo(uri); + const tags = trackInfo?.tags; + tags?.metric && tagElements.push(m(TrackChip, {text: 'metric'})); + tags?.debuggable && tagElements.push(m(TrackChip, {text: 'debuggable'})); + } else { + if (config && typeof config === 'object') { + if ('namespace' in config) { + tagElements.push(m(TrackChip, {text: 'metric'})); + } + if ('isDebuggable' in config && config.isDebuggable) { + tagElements.push(m(TrackChip, {text: 'debuggable'})); + } + } } + + return tagElements; } interface TrackShellAttrs { - track: TrackLike; + track: TrackLifecycleContainer; trackState: TrackState; } @@ -134,7 +149,7 @@ class TrackShell implements m.ClassComponent<TrackShellAttrs> { }, }, attrs.trackState.name, - m(TrackChips, {config: attrs.trackState.config}), + renderChips(attrs.trackState), ), m('.track-buttons', attrs.track.getTrackShellButtons(), @@ -219,7 +234,7 @@ class TrackShell implements m.ClassComponent<TrackShellAttrs> { } export interface TrackContentAttrs { - track: TrackLike; + track: TrackLifecycleContainer; } export class TrackContent implements m.ClassComponent<TrackContentAttrs> { private mouseDownX?: number; @@ -278,7 +293,7 @@ export class TrackContent implements m.ClassComponent<TrackContentAttrs> { interface TrackComponentAttrs { trackState: TrackState; - track: TrackLike; + track: TrackLifecycleContainer; } class TrackComponent implements m.ClassComponent<TrackComponentAttrs> { view({attrs}: m.CVnode<TrackComponentAttrs>) { @@ -338,12 +353,121 @@ interface TrackPanelAttrs { selectable: boolean; } +enum TrackLifecycleState { + Initializing, + Initialized, + DestroyPending, + Destroying, + Destroyed, +} + +export class TrackLifecycleContainer implements Disposable { + private state = TrackLifecycleState.Initializing; + + constructor(private track: TrackLike) { + track.onCreate().then(() => { + if (this.state === TrackLifecycleState.DestroyPending) { + track.onDestroy(); + this.state = TrackLifecycleState.Destroying; + } else { + this.state = TrackLifecycleState.Initialized; + } + }); + } + + onFullRedraw(): void { + if (this.state === TrackLifecycleState.Initialized) { + this.track.onFullRedraw(); + } + } + + getSliceRect( + visibleTimeScale: TimeScale, visibleWindow: Span<time, bigint>, + windowSpan: PxSpan, tStart: time, tEnd: time, depth: number): SliceRect + |undefined { + if (this.state === TrackLifecycleState.Initialized) { + return this.track.getSliceRect( + visibleTimeScale, visibleWindow, windowSpan, tStart, tEnd, depth); + } else { + return undefined; + } + } + + getHeight(): number { + if (this.state === TrackLifecycleState.Initialized) { + return this.track.getHeight(); + } else { + return 18; + } + } + + getTrackShellButtons(): m.Vnode<TrackButtonAttrs, {}>[] { + if (this.state === TrackLifecycleState.Initialized) { + return this.track.getTrackShellButtons(); + } else { + return []; + } + } + + getContextMenu(): m.Vnode<any, {}>|null { + if (this.state === TrackLifecycleState.Initialized) { + return this.track.getContextMenu(); + } else { + return null; + } + } + + onMouseMove(position: {x: number; y: number;}): void { + if (this.state === TrackLifecycleState.Initialized) { + this.track.onMouseMove(position); + } + } + + onMouseClick(position: {x: number; y: number;}): boolean { + if (this.state === TrackLifecycleState.Initialized) { + return this.track.onMouseClick(position); + } else { + return false; + } + } + + onMouseOut(): void { + if (this.state === TrackLifecycleState.Initialized) { + this.track.onMouseOut(); + } + } + + render(ctx: CanvasRenderingContext2D) { + if (this.state === TrackLifecycleState.Initialized) { + this.track.render(ctx); + } + } + + dispose() { + switch (this.state) { + case TrackLifecycleState.Initializing: + this.state = TrackLifecycleState.DestroyPending; + break; + case TrackLifecycleState.Initialized: + this.state = TrackLifecycleState.Destroying; + this.track.onDestroy().then(() => { + this.state = TrackLifecycleState.Destroyed; + }); + break; + case TrackLifecycleState.DestroyPending: + case TrackLifecycleState.Destroying: + case TrackLifecycleState.Destroyed: + break; + default: + const x: never = this.state; + throw new Error(`Invalid state "${x}"`); + } + } +} + export class TrackPanel extends Panel<TrackPanelAttrs> { - // TODO(hjd): It would be nicer if these could not be undefined here. - // We should implement a NullTrack which can be used if the trackState - // has disappeared. - private track: TrackLike|undefined; - private trackState: TrackState|undefined; + private track?: TrackLifecycleContainer; + private trackState?: TrackState; private tryLoadTrack(vnode: m.CVnode<TrackPanelAttrs>) { const trackId = vnode.attrs.id; @@ -352,10 +476,26 @@ export class TrackPanel extends Panel<TrackPanelAttrs> { if (!trackState) return; const {id, uri} = trackState; - this.track = - uri ? pluginManager.createTrack(uri, id) : loadTrack(trackState, id); - this.track?.onCreate(); - this.trackState = trackState; + + const trackCtx: TrackContext = { + trackInstanceId: id, + mountStore: <T>(migrate: Migrate<T>) => { + const {store, state} = globals; + const migratedState = migrate(state.tracks[trackId].state); + globals.store.edit((draft) => { + draft.tracks[trackId].state = migratedState; + }); + return store.createProxy<T>(['tracks', trackId, 'state']); + }, + }; + + const track = uri ? pluginManager.createTrack(uri, trackCtx) : + loadTrack(trackState, id); + + if (track) { + this.track = new TrackLifecycleContainer(track); + this.trackState = trackState; + } } view(vnode: m.CVnode<TrackPanelAttrs>) { @@ -383,7 +523,7 @@ export class TrackPanel extends Panel<TrackPanelAttrs> { onremove() { if (this.track !== undefined) { - this.track.onDestroy(); + this.track.dispose(); this.track = undefined; } } diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts index b93942870..1ae68b0b9 100644 --- a/ui/src/public/index.ts +++ b/ui/src/public/index.ts @@ -170,20 +170,27 @@ export interface PluginContext { addCommand(command: Command): void; } -export interface TrackContext { - // A unique ID for the instance of this track. - trackInstanceId: string; -} +export type Migrate<State> = (init: unknown) => State; export interface TrackContext { - // A unique ID for the instance of this track. + // The ID of this track instance. trackInstanceId: string; + + // Creates a new store overlaying the track instance's state object. + // A migrate function must be passed to convert any existing state to a + // compatible format. + // When opening a fresh trace, the value of |init| will be undefined, and + // state should be updated to an appropriate default value. + // When loading a permalink, the value of |init| will be whatever was saved + // when the permalink was shared, which might be from an old version of this + // track. + mountStore<State>(migrate: Migrate<State>): Store<State>; } // TODO(stevegolton): Rename `Track` to `BaseTrack` (or similar) and rename this // interface to `Track`. export interface TrackLike { - onCreate(): void; + onCreate(): Promise<void>; render(ctx: CanvasRenderingContext2D): void; onFullRedraw(): void; getSliceRect( @@ -196,7 +203,7 @@ export interface TrackLike { onMouseMove(position: {x: number, y: number}): void; onMouseClick(position: {x: number, y: number}): boolean; onMouseOut(): void; - onDestroy(): void; + onDestroy(): Promise<void>; } export interface PluginTrackInfo { @@ -210,10 +217,55 @@ export interface PluginTrackInfo { // A factory function returning the track object. trackFactory: (ctx: TrackContext) => TrackLike; - // A list of tags used for sorting and grouping. + // The track "kind" Uued by various subsystems e.g. aggregation controllers. + // This is where "XXX_TRACK_KIND" values should be placed. + // TODO(stevegolton): This will be deprecated once we handle group selections + // in a more generic way - i.e. EventSet. + kind: string; + + // An optional list of track IDs represented by this trace. + // This list is used for participation in track indexing by track ID. + // This index is used by various subsystems to find links between tracks based + // on the track IDs used by trace processor. + trackIds?: number[]; + + // Optional: The CPU number associated with this track. + cpu?: number; + + // Optional: A list of tags used for sorting, grouping and "chips". tags?: TrackTags; } +// Tracks within track groups (usually corresponding to processes) are sorted. +// As we want to group all tracks related to a given thread together, we use +// two keys: +// - Primary key corresponds to a priority of a track block (all tracks related +// to a given thread or a single track if it's not thread-associated). +// - Secondary key corresponds to a priority of a given thread-associated track +// within its thread track block. +// Each track will have a sort key, which either a primary sort key +// (for non-thread tracks) or a tid and secondary sort key (mapping of tid to +// primary sort key is done independently). +export enum PrimaryTrackSortKey { + DEBUG_SLICE_TRACK, + NULL_TRACK, + PROCESS_SCHEDULING_TRACK, + PROCESS_SUMMARY_TRACK, + EXPECTED_FRAMES_SLICE_TRACK, + ACTUAL_FRAMES_SLICE_TRACK, + PERF_SAMPLES_PROFILE_TRACK, + HEAP_PROFILE_TRACK, + MAIN_THREAD, + RENDER_THREAD, + GPU_COMPLETION_THREAD, + CHROME_IO_THREAD, + CHROME_COMPOSITOR_THREAD, + ORDINARY_THREAD, + COUNTER_TRACK, + ASYNC_SLICE_TRACK, + ORDINARY_TRACK, +} + // Similar to PluginContext but with additional properties to operate on the // currently loaded trace. Passed to trace-relevant hooks instead of // PluginContext. @@ -224,6 +276,11 @@ export interface TracePluginContext<T = undefined> extends PluginContext { // Add a new track from this plugin. The track is just made available here, // it's not automatically shown until it's added to a workspace. addTrack(trackDetails: PluginTrackInfo): void; + + // Suggest a track be added to the workspace on a fresh trace load. + // Supersedes `findPotentialTracks()` which has been removed. + // Note: this API will be deprecated soon. + suggestTrack(trackInfo: TrackInfo): void; } export interface BasePlugin<State> { @@ -235,7 +292,6 @@ export interface BasePlugin<State> { // Extension points. metricVisualisations?(ctx: PluginContext): MetricVisualisation[]; - findPotentialTracks?(ctx: TracePluginContext<State>): Promise<TrackInfo[]>; } export interface StatefulPlugin<State> extends BasePlugin<State> { @@ -265,16 +321,16 @@ export interface PluginClass<T> { } export interface TrackInfo { - // The id of this 'type' of track. This id is used to select the - // correct |TrackCreator| to construct the track. - trackKind: string; - // A human readable name for this specific track. It will normally be // displayed on the left-hand-side of the track. name: string; - // An opaque config for the track. - config: {}; + // Used to define default sort order for new traces. + // Note: sortKey will be deprecated soon in favour of tags. + sortKey: PrimaryTrackSortKey; + + // URI of the suggested track. + uri: string; } // A predicate for selecting a groups of tracks. @@ -284,23 +340,21 @@ interface WellKnownTrackTags { // A human readable name for this specific track. name: string; - // This is where "XXX_TRACK_KIND" values should be placed. - kind: string; + // Controls whether to show the "metric" chip. + metric: boolean; - // The CPU number associated with this track. - cpu: number; + // Controls whether to show the "debuggable" chip. + debuggable: boolean; } // An set of key/value pairs describing a given track. These are used for -// selecting tracks to pin/unpin and (in future) the sorting and grouping of -// tracks. -// These are also (ab)used for communicating information about tracks for the -// purposes of locating tracks by their properties e.g. aggregation & search. +// selecting tracks to pin/unpin, diplsaying "chips" in the track shell, and +// (in future) the sorting and grouping of tracks. // We define a handful of well known fields, and the rest are arbitrary key- // value pairs. export type TrackTags = Partial<WellKnownTrackTags>&{ // There may be arbitrary other key/value pairs. - [key: string]: string|number|undefined; + [key: string]: string|number|boolean|undefined; } // Plugins can be passed as class refs, factory functions, or concrete plugin diff --git a/ui/src/public/utils.ts b/ui/src/public/utils.ts new file mode 100644 index 000000000..d8db8f02e --- /dev/null +++ b/ui/src/public/utils.ts @@ -0,0 +1,73 @@ +// 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. + +export function getTrackName(args: Partial<{ + name: string | null, + utid: number, + processName: string | null, + pid: number | null, + threadName: string | null, + tid: number | null, + upid: number | null, + kind: string, + threadTrack: boolean +}>) { + const { + name, + upid, + utid, + processName, + threadName, + pid, + tid, + kind, + threadTrack, + } = args; + + const hasName = name !== undefined && name !== null && name !== '[NULL]'; + const hasUpid = upid !== undefined && upid !== null; + const hasUtid = utid !== undefined && utid !== null; + const hasProcessName = processName !== undefined && processName !== null; + const hasThreadName = threadName !== undefined && threadName !== null; + const hasTid = tid !== undefined && tid !== null; + const hasPid = pid !== undefined && pid !== null; + const hasKind = kind !== undefined; + const isThreadTrack = threadTrack !== undefined && threadTrack; + + // If we don't have any useful information (better than + // upid/utid) we show the track kind to help with tracking + // down where this is coming from. + const kindSuffix = hasKind ? ` (${kind})` : ''; + + if (isThreadTrack && hasName && hasTid) { + return `${name} (${tid})`; + } else if (hasName) { + return `${name}`; + } else if (hasUpid && hasPid && hasProcessName) { + return `${processName} ${pid}`; + } else if (hasUpid && hasPid) { + return `Process ${pid}`; + } else if (hasThreadName && hasTid) { + return `${threadName} ${tid}`; + } else if (hasTid) { + return `Thread ${tid}`; + } else if (hasUpid) { + return `upid: ${upid}${kindSuffix}`; + } else if (hasUtid) { + return `utid: ${utid}${kindSuffix}`; + } else if (hasKind) { + return `Unnamed ${kind}`; + } + return 'Unknown'; +} diff --git a/ui/src/tracks/android_log/index.ts b/ui/src/tracks/android_log/index.ts index 948f82d60..597d334d9 100644 --- a/ui/src/tracks/android_log/index.ts +++ b/ui/src/tracks/android_log/index.ts @@ -30,6 +30,8 @@ import { TracePluginContext, } from '../../public'; +export const ANDROID_LOGS_TRACK_KIND = 'AndroidLogTrack'; + export interface Data extends TrackData { // Total number of log events within [start, end], before any quantization. numEvents: number; @@ -155,6 +157,7 @@ class AndroidLog implements Plugin { ctx.addTrack({ uri: 'perfetto.AndroidLog', displayName: 'Android logs', + kind: ANDROID_LOGS_TRACK_KIND, trackFactory: ({trackInstanceId}) => { return new TrackWithControllerAdapter<Config, Data>( ctx.engine, diff --git a/ui/src/tracks/annotation/index.ts b/ui/src/tracks/annotation/index.ts new file mode 100644 index 000000000..e2baabb42 --- /dev/null +++ b/ui/src/tracks/annotation/index.ts @@ -0,0 +1,90 @@ +// 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 { + NUM, + NUM_NULL, + STR, +} from '../../common/query_result'; +import { + Plugin, + PluginContext, + PluginInfo, + TracePluginContext, +} from '../../public'; +import { + Config as CounterTrackConfig, + COUNTER_TRACK_KIND, + CounterTrack, +} from '../counter'; + +class AnnotationPlugin implements Plugin { + onActivate(_ctx: PluginContext): void {} + + async onTraceLoad(ctx: TracePluginContext): Promise<void> { + await this.addAnnotationCounterTracks(ctx); + } + + private async addAnnotationCounterTracks(ctx: TracePluginContext) { + const {engine} = ctx; + const counterResult = await engine.query(` + SELECT + id, + name, + min_value as minValue, + max_value as maxValue + FROM annotation_counter_track`); + + const counterIt = counterResult.iter({ + id: NUM, + name: STR, + minValue: NUM_NULL, + maxValue: NUM_NULL, + }); + + for (; counterIt.valid(); counterIt.next()) { + const id = counterIt.id; + const name = counterIt.name; + const minimumValue = + counterIt.minValue === null ? undefined : counterIt.minValue; + const maximumValue = + counterIt.maxValue === null ? undefined : counterIt.maxValue; + + const config: CounterTrackConfig = { + name, + trackId: id, + namespace: 'annotation', + minimumValue, + maximumValue, + }; + + ctx.addTrack({ + uri: `perfetto.Annotation#counter${id}`, + displayName: name, + kind: COUNTER_TRACK_KIND, + tags: { + metric: true, + }, + trackFactory: (trackCtx) => { + return new CounterTrack(trackCtx, config, ctx.engine); + }, + }); + } + } +} + +export const plugin: PluginInfo = { + pluginId: 'perfetto.Annotation', + plugin: AnnotationPlugin, +}; diff --git a/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts b/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts index c69e9a209..c8688985f 100644 --- a/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts +++ b/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts @@ -21,12 +21,13 @@ import {Engine} from '../../common/engine'; import { generateSqlWithInternalLayout, } from '../../common/internal_layout_utils'; -import {PrimaryTrackSortKey, SCROLLING_TRACK_GROUP} from '../../common/state'; +import {SCROLLING_TRACK_GROUP} from '../../common/state'; import {globals} from '../../frontend/globals'; import { NamedSliceTrackTypes, } from '../../frontend/named_slice_track'; import {NewTrackArgs, Track} from '../../frontend/track'; +import {PrimaryTrackSortKey} from '../../public'; import { CustomSqlDetailsPanelConfig, CustomSqlTableDefConfig, @@ -64,8 +65,8 @@ export class EventLatencyTrack extends }); } - onDestroy() { - super.onDestroy(); + async onDestroy() { + await super.onDestroy(); ScrollJankPluginState.getInstance().unregisterTrack(EventLatencyTrack.kind); } diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts b/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts index 6e554d452..46247cc79 100644 --- a/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts +++ b/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts @@ -19,12 +19,12 @@ import { } from '../../common/colorizer'; import {Engine} from '../../common/engine'; import { - PrimaryTrackSortKey, SCROLLING_TRACK_GROUP, } from '../../common/state'; import {globals} from '../../frontend/globals'; import {NamedSliceTrackTypes} from '../../frontend/named_slice_track'; import {NewTrackArgs, Track} from '../../frontend/track'; +import {PrimaryTrackSortKey} from '../../public'; import { CustomSqlDetailsPanelConfig, CustomSqlTableDefConfig, @@ -89,8 +89,8 @@ export class ScrollJankV3Track extends }; } - onDestroy() { - super.onDestroy(); + async onDestroy() { + await super.onDestroy(); ScrollJankPluginState.getInstance().unregisterTrack(ScrollJankV3Track.kind); } diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_track.ts b/ui/src/tracks/chrome_scroll_jank/scroll_track.ts index d82944b93..a05baa35e 100644 --- a/ui/src/tracks/chrome_scroll_jank/scroll_track.ts +++ b/ui/src/tracks/chrome_scroll_jank/scroll_track.ts @@ -15,20 +15,20 @@ import {v4 as uuidv4} from 'uuid'; import {Engine} from '../../common/engine'; -import { - PrimaryTrackSortKey, - SCROLLING_TRACK_GROUP, -} from '../../common/state'; +import {SCROLLING_TRACK_GROUP} from '../../common/state'; import {NamedSliceTrackTypes} from '../../frontend/named_slice_track'; import {NewTrackArgs, Track} from '../../frontend/track'; +import {PrimaryTrackSortKey} from '../../public'; import { CustomSqlDetailsPanelConfig, CustomSqlTableDefConfig, CustomSqlTableSliceTrack, } from '../custom_sql_table_slices'; -import {ScrollJankPluginState} from './index'; -import {ScrollJankTracks as DecideTracksResult} from './index'; +import { + ScrollJankPluginState, + ScrollJankTracks as DecideTracksResult, +} from './index'; import {ScrollDetailsPanel} from './scroll_details_panel'; export {Data} from '../chrome_slices'; @@ -69,8 +69,8 @@ export class TopLevelScrollTrack extends }); } - onDestroy() { - super.onDestroy(); + async onDestroy() { + await super.onDestroy(); ScrollJankPluginState.getInstance().unregisterTrack( TopLevelScrollTrack.kind); } diff --git a/ui/src/tracks/counter/index.ts b/ui/src/tracks/counter/index.ts index cb96f6f1c..00a89cacc 100644 --- a/ui/src/tracks/counter/index.ts +++ b/ui/src/tracks/counter/index.ts @@ -13,28 +13,36 @@ // 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 {duration, time, Time} from '../../base/time'; import {Actions} from '../../common/actions'; +import { + BasicAsyncTrack, + NUM_NULL, + STR_NULL, +} from '../../common/basic_async_track'; import {drawTrackHoverTooltip} from '../../common/canvas_utils'; import {TrackData} from '../../common/track_data'; -import {TrackController} from '../../controller/track_controller'; import {checkerboardExcept} from '../../frontend/checkerboard'; import {globals} from '../../frontend/globals'; -import {NewTrackArgs, Track} from '../../frontend/track'; import { + EngineProxy, LONG, LONG_NULL, NUM, Plugin, PluginContext, PluginInfo, + PrimaryTrackSortKey, + Store, STR, TracePluginContext, - TrackInfo, + TrackContext, } from '../../public'; +import {getTrackName} from '../../public/utils'; import {Button} from '../../widgets/button'; import {MenuItem, PopupMenu2} from '../../widgets/menu'; @@ -66,76 +74,151 @@ export interface Config { minimumValue?: number; startTs?: time; endTs?: time; - namespace: string; + namespace?: string; trackId: number; - scale?: CounterScaleOptions; + 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 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 (typeof x.scale === 'string') { + return true; + } else { + return false; + } + } else { + return false; + } } -class CounterTrackController extends TrackController<Config, Data> { - static readonly kind = COUNTER_TRACK_KIND; - private setup = false; +export class CounterTrack extends BasicAsyncTrack<Data> { private maximumValueSeen = 0; private minimumValueSeen = 0; private maximumDeltaSeen = 0; private minimumDeltaSeen = 0; private maxDurNs: duration = 0n; - - async onBoundsChange(start: time, end: time, resolution: duration): - Promise<Data> { - if (!this.setup) { - if (this.config.namespace === undefined) { - await this.query(` - create view ${this.tableName('counter_view')} as - select - id, - ts, - dur, - value, - delta - from experimental_counter_dur - where track_id = ${this.config.trackId}; - `); + private store: Store<CounterTrackState>; + private id: string; + private uuid = uuidv4(); + + constructor( + ctx: TrackContext, private config: Config, private engine: EngineProxy) { + super(); + this.id = ctx.trackInstanceId; + this.store = ctx.mountStore<CounterTrackState>((init: unknown) => { + if (isCounterState(init)) { + return init; } else { - await this.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}; - `); + return {scale: this.config.defaultScale ?? 'ZERO_BASED'}; } + }); + } - const maxDurResult = await this.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; + // 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}`; + } - const queryRes = await this.query(` + 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 - 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; - - this.setup = true; + 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 queryRes = await this.query(` + 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, @@ -219,34 +302,18 @@ class CounterTrackController extends TrackController<Config, Data> { return this.config.minimumValue; } } -} - - -// 0.5 Makes the horizontal lines sharp. -const MARGIN_TOP = 3.5; -const RECT_HEIGHT = 24.5; - -class CounterTrack extends Track<Config, Data> { - static readonly kind = COUNTER_TRACK_KIND; - static create(args: NewTrackArgs): CounterTrack { - return new CounterTrack(args); - } private mousePos = {x: 0, y: 0}; private hoveredValue: number|undefined = undefined; private hoveredTs: time|undefined = undefined; private hoveredTsEnd: time|undefined = undefined; - constructor(args: NewTrackArgs) { - super(args); - } - getHeight() { return MARGIN_TOP + RECT_HEIGHT; } getContextMenu(): m.Vnode<any> { - const currentScale = this.config.scale; + const currentScale = this.store.state.scale; const scales: {name: CounterScaleOptions, humanName: string}[] = [ {name: 'ZERO_BASED', humanName: 'Zero based'}, {name: 'MIN_MAX', humanName: 'Min/Max'}, @@ -258,10 +325,8 @@ class CounterTrack extends Track<Config, Data> { label: scale.humanName, active: currentScale === scale.name, onclick: () => { - this.config.scale = scale.name; - Actions.updateTrackConfig({ - id: this.trackState.id, - config: this.config, + this.store.edit((draft) => { + draft.scale = scale.name; }); }, }); @@ -282,7 +347,7 @@ class CounterTrack extends Track<Config, Data> { visibleTimeScale: timeScale, windowSpan, } = globals.frontendLocalState; - const data = this.data(); + const data = this.data; // Can't possibly draw anything. if (data === undefined || data.timestamps.length === 0) { @@ -295,7 +360,7 @@ class CounterTrack extends Track<Config, Data> { assertTrue(data.timestamps.length === data.totalDeltas.length); assertTrue(data.timestamps.length === data.rate.length); - const scale: CounterScaleOptions = this.config.scale || 'ZERO_BASED'; + const scale: CounterScaleOptions = this.store.state.scale; let minValues = data.minValues; let maxValues = data.maxValues; @@ -485,17 +550,17 @@ class CounterTrack extends Track<Config, Data> { } onMouseMove(pos: {x: number, y: number}) { - const data = this.data(); + const data = this.data; if (data === undefined) return; this.mousePos = pos; const {visibleTimeScale} = globals.frontendLocalState; const time = visibleTimeScale.pxToHpTime(pos.x); let values = data.lastValues; - if (this.config.scale === 'DELTA_FROM_PREVIOUS') { + if (this.store.state.scale === 'DELTA_FROM_PREVIOUS') { values = data.totalDeltas; } - if (this.config.scale === 'RATE') { + if (this.store.state.scale === 'RATE') { values = data.rate; } @@ -513,7 +578,7 @@ class CounterTrack extends Track<Config, Data> { } onMouseClick({x}: {x: number}): boolean { - const data = this.data(); + const data = this.data; if (data === undefined) return false; const {visibleTimeScale} = globals.frontendLocalState; const time = visibleTimeScale.pxToHpTime(x); @@ -527,21 +592,59 @@ class CounterTrack extends Track<Config, Data> { leftTs: Time.fromRaw(data.timestamps[left]), rightTs: Time.fromRaw(right !== -1 ? data.timestamps[right] : -1n), id: counterId, - trackId: this.trackState.id, + trackId: this.id, })); return true; } } + + async onDestroy(): Promise<void> { + await this.engine.query( + `DROP VIEW IF EXISTS ${this.tableName('counter_view')}`); + } +} + +interface CounterInfo { + name: string; + trackId: number; } class CounterPlugin implements Plugin { - onActivate(ctx: PluginContext): void { - ctx.registerTrackController(CounterTrackController); - ctx.registerTrack(CounterTrack); + onActivate(_ctx: PluginContext): void {} + + async onTraceLoad(ctx: TracePluginContext): 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); } - async findPotentialTracks({engine}: TracePluginContext): - Promise<TrackInfo[]> { + private async addCounterTracks(ctx: TracePluginContext) { + const counters = await this.getCounterNames(ctx.engine); + for (const {trackId, name} of counters) { + const config: + Config = {name, trackId, defaultScale: getCounterScale(name)}; + const uri = `perfetto.Counter#${trackId}`; + ctx.addTrack({ + uri, + displayName: name, + kind: COUNTER_TRACK_KIND, + trackIds: [trackId], + trackFactory: (trackCtx) => { + return new CounterTrack(trackCtx, config, ctx.engine); + }, + }); + ctx.suggestTrack({ + uri, + name, + sortKey: PrimaryTrackSortKey.COUNTER_TRACK, + }); + } + } + + private async getCounterNames(engine: EngineProxy): Promise<CounterInfo[]> { const result = await engine.query(` select name, id from ( @@ -562,20 +665,233 @@ class CounterPlugin implements Plugin { id: NUM, }); - const tracks: TrackInfo[] = []; + const tracks: CounterInfo[] = []; for (; it.valid(); it.next()) { - const name = it.name; - const trackId = it.id; tracks.push({ - trackKind: COUNTER_TRACK_KIND, - name, - config: { + trackId: it.id, + name: it.name, + }); + } + return tracks; + } + + private async addGpuFrequencyTracks(ctx: TracePluginContext) { + 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.addTrack({ + uri, + displayName: name, + kind: COUNTER_TRACK_KIND, + trackIds: [trackId], + trackFactory: (trackCtx) => { + return new CounterTrack(trackCtx, config, ctx.engine); + }, + }); + } + } + } + + async addCpuFreqLimitCounterTracks(ctx: TracePluginContext): 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: TracePluginContext): 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: TracePluginContext, 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.addTrack({ + uri: `perfetto.Counter#cpu${trackId}`, + displayName: name, + kind: COUNTER_TRACK_KIND, + trackIds: [trackId], + trackFactory: (trackCtx) => { + return new CounterTrack(trackCtx, config, ctx.engine); + }, + }); + } + } + + async addThreadCounterTracks(ctx: TracePluginContext): 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.addTrack({ + uri: `perfetto.Counter#thread${trackId}`, + displayName: name, + kind, + trackIds: [trackId], + trackFactory: (trackCtx) => { + return new CounterTrack(trackCtx, config, ctx.engine); + }, + }); + } + } + + async addProcessCounterTracks(ctx: TracePluginContext): 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.addTrack({ + uri: `perfetto.Counter#process${trackId}`, + displayName: name, + kind: COUNTER_TRACK_KIND, + trackIds: [trackId], + trackFactory: (trackCtx) => { + return new CounterTrack(trackCtx, config, ctx.engine); }, }); } - return tracks; } } diff --git a/ui/src/tracks/cpu_freq/index.ts b/ui/src/tracks/cpu_freq/index.ts index baa24f906..5b182beca 100644 --- a/ui/src/tracks/cpu_freq/index.ts +++ b/ui/src/tracks/cpu_freq/index.ts @@ -26,12 +26,21 @@ import { NUM_NULL, QueryResult, } from '../../common/query_result'; +import { + TrackAdapter, + TrackControllerAdapter, + TrackWithControllerAdapter, +} from '../../common/track_adapter'; import {TrackData} from '../../common/track_data'; -import {TrackController} from '../../controller/track_controller'; import {checkerboardExcept} from '../../frontend/checkerboard'; import {globals} from '../../frontend/globals'; -import {NewTrackArgs, Track} from '../../frontend/track'; -import {Plugin, PluginContext, PluginInfo} from '../../public'; +import {NewTrackArgs} from '../../frontend/track'; +import { + Plugin, + PluginContext, + PluginInfo, + TracePluginContext, +} from '../../public'; export const CPU_FREQ_TRACK_KIND = 'CpuFreqTrack'; @@ -55,9 +64,7 @@ export interface Config { minimumValue?: number; } -class CpuFreqTrackController extends TrackController<Config, Data> { - static readonly kind = CPU_FREQ_TRACK_KIND; - +class CpuFreqTrackController extends TrackControllerAdapter<Config, Data> { private maxDur: duration = 0n; private maxTsEnd: time = Time.ZERO; private maximumValueSeen = 0; @@ -266,8 +273,7 @@ class CpuFreqTrackController extends TrackController<Config, Data> { const MARGIN_TOP = 4.5; const RECT_HEIGHT = 20; -class CpuFreqTrack extends Track<Config, Data> { - static readonly kind = CPU_FREQ_TRACK_KIND; +class CpuFreqTrack extends TrackAdapter<Config, Data> { static create(args: NewTrackArgs): CpuFreqTrack { return new CpuFreqTrack(args); } @@ -484,9 +490,70 @@ class CpuFreqTrack extends Track<Config, Data> { } class CpuFreq implements Plugin { - onActivate(ctx: PluginContext): void { - ctx.registerTrackController(CpuFreqTrackController); - ctx.registerTrack(CpuFreqTrack); + onActivate(_ctx: PluginContext): void {} + + async onTraceLoad(ctx: TracePluginContext): Promise<void> { + const {engine} = ctx; + + const cpus = await engine.getCpus(); + + const maxCpuFreqResult = await engine.query(` + select ifnull(max(value), 0) as freq + from counter c + inner join cpu_counter_track t on c.track_id = t.id + where name = 'cpufreq'; + `); + const maxCpuFreq = maxCpuFreqResult.firstRow({freq: NUM}).freq; + + for (const cpu of cpus) { + // Only add a cpu freq track if we have + // cpu freq data. + // TODO(hjd): Find a way to display cpu idle + // events even if there are no cpu freq events. + const cpuFreqIdleResult = await engine.query(` + select + id as cpuFreqId, + ( + select id + from cpu_counter_track + where name = 'cpuidle' + and cpu = ${cpu} + limit 1 + ) as cpuIdleId + from cpu_counter_track + where name = 'cpufreq' and cpu = ${cpu} + limit 1; + `); + + if (cpuFreqIdleResult.numRows() > 0) { + const row = cpuFreqIdleResult.firstRow({ + cpuFreqId: NUM, + cpuIdleId: NUM_NULL, + }); + const freqTrackId = row.cpuFreqId; + const idleTrackId = row.cpuIdleId === null ? undefined : row.cpuIdleId; + + ctx.addTrack({ + uri: `perfetto.CpuFreq#${cpu}`, + displayName: `Cpu ${cpu} Frequency`, + kind: CPU_FREQ_TRACK_KIND, + cpu, + trackFactory: ({trackInstanceId}) => { + return new TrackWithControllerAdapter<Config, Data>( + engine, + trackInstanceId, + { + cpu, + maximumValue: maxCpuFreq, + freqTrackId, + idleTrackId, + }, + CpuFreqTrack, + CpuFreqTrackController); + }, + }); + } + } } } diff --git a/ui/src/tracks/cpu_slices/index.ts b/ui/src/tracks/cpu_slices/index.ts index 6694ccdaa..1d8693c9d 100644 --- a/ui/src/tracks/cpu_slices/index.ts +++ b/ui/src/tracks/cpu_slices/index.ts @@ -484,10 +484,8 @@ class CpuSlices implements Plugin { ctx.addTrack({ uri, displayName: name, - tags: { - cpu, - kind: CPU_SLICE_TRACK_KIND, - }, + kind: CPU_SLICE_TRACK_KIND, + cpu, trackFactory: ({trackInstanceId}) => { return new TrackWithControllerAdapter<Config, Data>( ctx.engine, diff --git a/ui/src/tracks/ftrace/index.ts b/ui/src/tracks/ftrace/index.ts index 124696e5c..54cc44fe8 100644 --- a/ui/src/tracks/ftrace/index.ts +++ b/ui/src/tracks/ftrace/index.ts @@ -27,6 +27,7 @@ import { TracePluginContext, } from '../../public'; +export const FTRACE_RAW_TRACK_KIND = 'FtraceRawTrack'; export interface Data extends TrackData { timestamps: BigInt64Array; @@ -146,6 +147,8 @@ class FtraceRawPlugin implements Plugin { ctx.addTrack({ uri, displayName: `Ftrace Track for CPU ${cpuNum}`, + kind: FTRACE_RAW_TRACK_KIND, + cpu: cpuNum, trackFactory: () => { return new FtraceRawTrack(ctx.engine, cpuNum); }, diff --git a/ui/src/tracks/process_summary/index.ts b/ui/src/tracks/process_summary/index.ts index ea795a4da..4e46456c9 100644 --- a/ui/src/tracks/process_summary/index.ts +++ b/ui/src/tracks/process_summary/index.ts @@ -12,212 +12,337 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {BigintMath} from '../../base/bigint_math'; -import {assertFalse} from '../../base/logging'; -import {duration, Time, time} from '../../base/time'; -import {colorForTid} from '../../common/colorizer'; -import {NUM} from '../../common/query_result'; -import {LIMIT, TrackData} from '../../common/track_data'; -import {TrackController} from '../../controller/track_controller'; -import {checkerboardExcept} from '../../frontend/checkerboard'; -import {globals} from '../../frontend/globals'; -import {NewTrackArgs, Track} from '../../frontend/track'; -import {Plugin, PluginContext, PluginInfo} from '../../public'; - -export const PROCESS_SUMMARY_TRACK = 'ProcessSummaryTrack'; - -// TODO(dproy): Consider deduping with CPU summary data. -export interface Data extends TrackData { - bucketSize: duration; - utilizations: Float64Array; -} - -export interface Config { - pidForColor: number; - upid: number|null; - utid: number; -} - -// This is the summary displayed when a process only contains chrome slices -// and no cpu scheduling. -class ProcessSummaryTrackController extends TrackController<Config, Data> { - static readonly kind = PROCESS_SUMMARY_TRACK; - private setup = false; - - async onBoundsChange(start: time, end: time, resolution: duration): - Promise<Data> { - assertFalse(resolution === 0n, 'Resolution cannot be 0'); - - if (this.setup === false) { - await this.query( - `create virtual table ${this.tableName('window')} using window;`); - - let utids = [this.config.utid]; - if (this.config.upid) { - const threadQuery = await this.query( - `select utid from thread where upid=${this.config.upid}`); - utids = []; - for (const it = threadQuery.iter({utid: NUM}); it.valid(); it.next()) { - utids.push(it.utid); - } - } - - const trackQuery = await this.query( - `select id from thread_track where utid in (${utids.join(',')})`); - const tracks = []; - for (const it = trackQuery.iter({id: NUM}); it.valid(); it.next()) { - tracks.push(it.id); - } - - const processSliceView = this.tableName('process_slice_view'); - await this.query( - `create view ${processSliceView} as ` + - // 0 as cpu is a dummy column to perform span join on. - `select ts, dur/${utids.length} as dur ` + - `from slice s ` + - `where depth = 0 and track_id in ` + - `(${tracks.join(',')})`); - await this.query(`create virtual table ${this.tableName('span')} - using span_join(${processSliceView}, - ${this.tableName('window')});`); - this.setup = true; - } - - // |resolution| is in ns/px we want # ns for 10px window: - // Max value with 1 so we don't end up with resolution 0. - const bucketSize = resolution * 10n; - const windowStart = Time.quant(start, bucketSize); - const windowDur = BigintMath.max(1n, end - windowStart); +import {v4 as uuidv4} from 'uuid'; + +import { + NUM, + NUM_NULL, + STR, + STR_NULL, +} from '../../common/query_result'; +import {TrackWithControllerAdapter} from '../../common/track_adapter'; +import { + Plugin, + PluginContext, + PluginInfo, + TracePluginContext, +} from '../../public'; + +import { + Config as ProcessSchedulingTrackConfig, + Data as ProcessSchedulingTrackData, + PROCESS_SCHEDULING_TRACK_KIND, + ProcessSchedulingTrack, + ProcessSchedulingTrackController, +} from './process_scheduling_track'; +import { + Config as ProcessSummaryTrackConfig, + Data as ProcessSummaryTrackData, + PROCESS_SUMMARY_TRACK, + ProcessSummaryTrack, + ProcessSummaryTrackController, +} from './process_summary_track'; + +// This plugin now manages both process "scheduling" and "summary" tracks. +class ProcessSummaryPlugin implements Plugin { + private upidToUuid = new Map<number, string>(); + private utidToUuid = new Map<number, string>(); - await this.query(`update ${this.tableName('window')} set - window_start=${windowStart}, - window_dur=${windowDur}, - quantum=${bucketSize} - where rowid = 0;`); + onActivate(_ctx: PluginContext): void {} - return this.computeSummary(windowStart, end, resolution, bucketSize); + async onTraceLoad(ctx: TracePluginContext): Promise<void> { + await this.addProcessTrackGroups(ctx); + await this.addKernelThreadSummary(ctx); } - private async computeSummary( - start: time, end: time, resolution: duration, - bucketSize: duration): Promise<Data> { - const duration = end - start; - const numBuckets = Math.min(Number(duration / bucketSize), LIMIT); - - const query = `select - quantum_ts as bucket, - sum(dur)/cast(${bucketSize} as float) as utilization - from ${this.tableName('span')} - group by quantum_ts - limit ${LIMIT}`; - - const summary: Data = { - start, - end, - resolution, - length: numBuckets, - bucketSize, - utilizations: new Float64Array(numBuckets), - }; - - const queryRes = await this.query(query); - const it = queryRes.iter({bucket: NUM, utilization: NUM}); + private async addProcessTrackGroups(ctx: TracePluginContext): Promise<void> { + this.upidToUuid.clear(); + this.utidToUuid.clear(); + + // We want to create groups of tracks in a specific order. + // The tracks should be grouped: + // by upid + // or (if upid is null) by utid + // the groups should be sorted by: + // Chrome-based process rank based on process names (e.g. Browser) + // has a heap profile or not + // total cpu time *for the whole parent process* + // process name + // upid + // thread name + // utid + const result = await ctx.engine.query(` + select + the_tracks.upid, + the_tracks.utid, + total_dur as hasSched, + hasHeapProfiles, + process.pid as pid, + thread.tid as tid, + process.name as processName, + thread.name as threadName, + package_list.debuggable as isDebuggable, + ifnull(( + select group_concat(string_value) + from args + where + process.arg_set_id is not null and + arg_set_id = process.arg_set_id and + flat_key = 'chrome.process_label' + ), '') AS chromeProcessLabels, + (case process.name + when 'Browser' then 3 + when 'Gpu' then 2 + when 'Renderer' then 1 + else 0 + end) as chromeProcessRank + from ( + select upid, 0 as utid from process_track + union + select upid, 0 as utid from process_counter_track + union + select upid, utid from thread_counter_track join thread using(utid) + union + select upid, utid from thread_track join thread using(utid) + union + select upid, utid from sched join thread using(utid) group by utid + union + select upid, 0 as utid from ( + select distinct upid + from perf_sample join thread using (utid) join process using (upid) + where callsite_id is not null) + union + select upid, utid from ( + select distinct(utid) from cpu_profile_stack_sample + ) join thread using(utid) + union + select distinct(upid) as upid, 0 as utid from heap_profile_allocation + union + select distinct(upid) as upid, 0 as utid from heap_graph_object + ) the_tracks + left join ( + select upid, sum(thread_total_dur) as total_dur + from ( + select utid, sum(dur) as thread_total_dur + from sched where dur != -1 and utid != 0 + group by utid + ) + join thread using (utid) + group by upid + ) using(upid) + left join ( + select + distinct(upid) as upid, + true as hasHeapProfiles + from heap_profile_allocation + union + select + distinct(upid) as upid, + true as hasHeapProfiles + from heap_graph_object + ) using (upid) + left join ( + select + thread.upid as upid, + sum(cnt) as perfSampleCount + from ( + select utid, count(*) as cnt + from perf_sample where callsite_id is not null + group by utid + ) join thread using (utid) + group by thread.upid + ) using (upid) + left join ( + select + process.upid as upid, + sum(cnt) as sliceCount + from (select track_id, count(*) as cnt from slice group by track_id) + left join thread_track on track_id = thread_track.id + left join thread on thread_track.utid = thread.utid + left join process_track on track_id = process_track.id + join process on process.upid = thread.upid + or process_track.upid = process.upid + where process.upid is not null + group by process.upid + ) using (upid) + left join thread using(utid) + left join process using(upid) + left join package_list using(uid) + order by + chromeProcessRank desc, + hasHeapProfiles desc, + perfSampleCount desc, + total_dur desc, + sliceCount desc, + processName asc nulls last, + the_tracks.upid asc nulls last, + threadName asc nulls last, + the_tracks.utid asc nulls last; + `); + + const it = result.iter({ + utid: NUM, + upid: NUM_NULL, + tid: NUM_NULL, + pid: NUM_NULL, + threadName: STR_NULL, + processName: STR_NULL, + hasSched: NUM_NULL, + hasHeapProfiles: NUM_NULL, + isDebuggable: NUM_NULL, + chromeProcessLabels: STR, + }); for (; it.valid(); it.next()) { - const bucket = it.bucket; - if (bucket > numBuckets) { - continue; + const utid = it.utid; + const tid = it.tid; + const upid = it.upid; + const pid = it.pid; + const hasSched = !!it.hasSched; + const isDebuggable = !!it.isDebuggable; + + // Group by upid if present else by utid. + let pUuid = + upid === null ? this.utidToUuid.get(utid) : this.upidToUuid.get(upid); + // These should only happen once for each track group. + if (pUuid === undefined) { + pUuid = this.getOrCreateUuid(utid, upid); + const pidForColor = pid || tid || upid || utid || 0; + const type = hasSched ? 'schedule' : 'summary'; + const uri = `perfetto.ProcessScheduling#${utid}.${type}`; + + if (hasSched) { + const config: ProcessSchedulingTrackConfig = { + pidForColor, + upid, + utid, + }; + + ctx.addTrack({ + uri, + displayName: `${upid === null ? tid : pid} schedule`, + kind: PROCESS_SCHEDULING_TRACK_KIND, + tags: { + isDebuggable, + }, + trackFactory: ({trackInstanceId}) => { + return new TrackWithControllerAdapter< + ProcessSchedulingTrackConfig, + ProcessSchedulingTrackData>( + ctx.engine, + trackInstanceId, + config, + ProcessSchedulingTrack, + ProcessSchedulingTrackController); + }, + }); + } else { + const config: ProcessSummaryTrackConfig = { + pidForColor, + upid, + utid, + }; + + ctx.addTrack({ + uri, + displayName: `${upid === null ? tid : pid} summary`, + kind: PROCESS_SUMMARY_TRACK, + tags: { + isDebuggable, + }, + trackFactory: ({trackInstanceId}) => { + return new TrackWithControllerAdapter< + ProcessSummaryTrackConfig, + ProcessSummaryTrackData>( + ctx.engine, + trackInstanceId, + config, + ProcessSummaryTrack, + ProcessSummaryTrackController); + }, + }); + } } - summary.utilizations[bucket] = it.utilization; } - - return summary; } - onDestroy(): void { - if (this.setup) { - this.query(`drop table ${this.tableName('window')}`); - this.query(`drop table ${this.tableName('span')}`); - this.setup = false; + private async addKernelThreadSummary(ctx: TracePluginContext): Promise<void> { + const {engine} = ctx; + + // Identify kernel threads if this is a linux system trace, and sufficient + // process information is available. Kernel threads are identified by being + // children of kthreadd (always pid 2). + // The query will return the kthreadd process row first, which must exist + // for any other kthreads to be returned by the query. + // TODO(rsavitski): figure out how to handle the idle process (swapper), + // which has pid 0 but appears as a distinct process (with its own comm) on + // each cpu. It'd make sense to exclude its thread state track, but still + // put process-scoped tracks in this group. + const result = await engine.query(` + select + t.utid, p.upid, (case p.pid when 2 then 1 else 0 end) isKthreadd + from + thread t + join process p using (upid) + left join process parent on (p.parent_upid = parent.upid) + join + (select true from metadata m + where (m.name = 'system_name' and m.str_value = 'Linux') + union + select 1 from (select true from sched limit 1)) + where + p.pid = 2 or parent.pid = 2 + order by isKthreadd desc + `); + + const it = result.iter({ + utid: NUM, + upid: NUM, + }); + + // Not applying kernel thread grouping. + if (!it.valid()) { + return; } - } -} - -const MARGIN_TOP = 5; -const RECT_HEIGHT = 30; -const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT; -const SUMMARY_HEIGHT = TRACK_HEIGHT - MARGIN_TOP; - -class ProcessSummaryTrack extends Track<Config, Data> { - static readonly kind = PROCESS_SUMMARY_TRACK; - static create(args: NewTrackArgs): ProcessSummaryTrack { - return new ProcessSummaryTrack(args); - } - constructor(args: NewTrackArgs) { - super(args); - } - - getHeight(): number { - return TRACK_HEIGHT; - } + const config: ProcessSummaryTrackConfig = { + pidForColor: 2, + upid: it.upid, + utid: it.utid, + }; - renderCanvas(ctx: CanvasRenderingContext2D): void { - const { - visibleTimeScale, - windowSpan, - } = globals.frontendLocalState; - const data = this.data(); - if (data === undefined) return; // Can't possibly draw anything. - - checkerboardExcept( - ctx, - this.getHeight(), - windowSpan.start, - windowSpan.end, - visibleTimeScale.timeToPx(data.start), - visibleTimeScale.timeToPx(data.end)); - - this.renderSummary(ctx, data); + ctx.addTrack({ + uri: 'perfetto.ProcessSummary#kernel', + displayName: `Kernel thread summary`, + kind: PROCESS_SUMMARY_TRACK, + trackFactory: ({trackInstanceId}) => { + return new TrackWithControllerAdapter< + ProcessSummaryTrackConfig, + ProcessSummaryTrackData>( + ctx.engine, + trackInstanceId, + config, + ProcessSummaryTrack, + ProcessSummaryTrackController); + }, + }); } - // TODO(dproy): Dedup with CPU slices. - renderSummary(ctx: CanvasRenderingContext2D, data: Data): void { - const {visibleTimeScale, windowSpan} = globals.frontendLocalState; - const startPx = windowSpan.start; - const bottomY = TRACK_HEIGHT; - - let lastX = startPx; - let lastY = bottomY; - - // TODO(hjd): Dedupe this math. - const color = colorForTid(this.config.pidForColor); - color.l = Math.min(color.l + 10, 60); - color.s -= 20; - - ctx.fillStyle = `hsl(${color.h}, ${color.s}%, ${color.l}%)`; - ctx.beginPath(); - ctx.moveTo(lastX, lastY); - for (let i = 0; i < data.utilizations.length; i++) { - // TODO(dproy): Investigate why utilization is > 1 sometimes. - const utilization = Math.min(data.utilizations[i], 1); - const startTime = Time.fromRaw(BigInt(i) * data.bucketSize + data.start); - - lastX = Math.floor(visibleTimeScale.timeToPx(startTime)); - - ctx.lineTo(lastX, lastY); - lastY = MARGIN_TOP + Math.round(SUMMARY_HEIGHT * (1 - utilization)); - ctx.lineTo(lastX, lastY); + private getOrCreateUuid(utid: number, upid: number|null) { + let uuid = this.getUuidUnchecked(utid, upid); + if (uuid === undefined) { + uuid = uuidv4(); + if (upid === null) { + this.utidToUuid.set(utid, uuid); + } else { + this.upidToUuid.set(upid, uuid); + } } - ctx.lineTo(lastX, bottomY); - ctx.closePath(); - ctx.fill(); + return uuid; } -} -class ProcessSummaryPlugin implements Plugin { - onActivate(ctx: PluginContext): void { - ctx.registerTrack(ProcessSummaryTrack); - ctx.registerTrackController(ProcessSummaryTrackController); + getUuidUnchecked(utid: number, upid: number|null) { + return upid === null ? this.utidToUuid.get(utid) : + this.upidToUuid.get(upid); } } diff --git a/ui/src/tracks/process_scheduling/index.ts b/ui/src/tracks/process_summary/process_scheduling_track.ts index 631998cc0..8759260bc 100644 --- a/ui/src/tracks/process_scheduling/index.ts +++ b/ui/src/tracks/process_summary/process_scheduling_track.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2021 The Android Open Source Project +// 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. @@ -20,16 +20,26 @@ import {Actions} from '../../common/actions'; import {calcCachedBucketSize} from '../../common/cache_utils'; import {drawTrackHoverTooltip} from '../../common/canvas_utils'; import {colorForThread} from '../../common/colorizer'; -import {LONG, NUM, QueryResult} from '../../common/query_result'; +import { + LONG, + NUM, + QueryResult, +} from '../../common/query_result'; +import { + TrackAdapter, + TrackControllerAdapter, +} from '../../common/track_adapter'; import {TrackData} from '../../common/track_data'; -import {TrackController} from '../../controller/track_controller'; import {checkerboardExcept} from '../../frontend/checkerboard'; import {globals} from '../../frontend/globals'; -import {NewTrackArgs, Track} from '../../frontend/track'; -import {Plugin, PluginContext, PluginInfo} from '../../public'; +import {NewTrackArgs} from '../../frontend/track'; export const PROCESS_SCHEDULING_TRACK_KIND = 'ProcessSchedulingTrack'; +const MARGIN_TOP = 5; +const RECT_HEIGHT = 30; +const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT; + export interface Data extends TrackData { kind: 'slice'; maxCpu: number; @@ -49,9 +59,8 @@ export interface Config { // This summary is displayed for any processes that have CPU scheduling activity // associated with them. -class ProcessSchedulingTrackController extends TrackController<Config, Data> { - static readonly kind = PROCESS_SCHEDULING_TRACK_KIND; - +export class ProcessSchedulingTrackController extends + TrackControllerAdapter<Config, Data> { private maxCpu = 0; private maxDur = 0n; private cachedBucketSize = BIMath.INT64_MAX; @@ -178,12 +187,7 @@ class ProcessSchedulingTrackController extends TrackController<Config, Data> { } } -const MARGIN_TOP = 5; -const RECT_HEIGHT = 30; -const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT; - -class ProcessSchedulingTrack extends Track<Config, Data> { - static readonly kind = PROCESS_SCHEDULING_TRACK_KIND; +export class ProcessSchedulingTrack extends TrackAdapter<Config, Data> { static create(args: NewTrackArgs): ProcessSchedulingTrack { return new ProcessSchedulingTrack(args); } @@ -312,15 +316,3 @@ class ProcessSchedulingTrack extends Track<Config, Data> { this.mousePos = undefined; } } - -class ProcessSchedulingPlugin implements Plugin { - onActivate(ctx: PluginContext): void { - ctx.registerTrackController(ProcessSchedulingTrackController); - ctx.registerTrack(ProcessSchedulingTrack); - } -} - -export const plugin: PluginInfo = { - pluginId: 'perfetto.ProcessScheduling', - plugin: ProcessSchedulingPlugin, -}; diff --git a/ui/src/tracks/process_summary/process_summary_track.ts b/ui/src/tracks/process_summary/process_summary_track.ts new file mode 100644 index 000000000..7085cb863 --- /dev/null +++ b/ui/src/tracks/process_summary/process_summary_track.ts @@ -0,0 +1,207 @@ +// 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 {assertFalse} from '../../base/logging'; +import {duration, Time, time} from '../../base/time'; +import {colorForTid} from '../../common/colorizer'; +import {NUM} from '../../common/query_result'; +import {TrackAdapter, TrackControllerAdapter} from '../../common/track_adapter'; +import {LIMIT, TrackData} from '../../common/track_data'; +import {checkerboardExcept} from '../../frontend/checkerboard'; +import {globals} from '../../frontend/globals'; +import {NewTrackArgs} from '../../frontend/track'; + +export const PROCESS_SUMMARY_TRACK = 'ProcessSummaryTrack'; + +// TODO(dproy): Consider deduping with CPU summary data. +export interface Data extends TrackData { + bucketSize: duration; + utilizations: Float64Array; +} + +export interface Config { + pidForColor: number; + upid: number|null; + utid: number; +} + +// This is the summary displayed when a process only contains chrome slices +// and no cpu scheduling. +export class ProcessSummaryTrackController extends + TrackControllerAdapter<Config, Data> { + async onSetup(): Promise<void> { + await this.query( + `create virtual table ${this.tableName('window')} using window;`); + + let utids = [this.config.utid]; + if (this.config.upid) { + const threadQuery = await this.query( + `select utid from thread where upid=${this.config.upid}`); + utids = []; + for (const it = threadQuery.iter({utid: NUM}); it.valid(); it.next()) { + utids.push(it.utid); + } + } + + const trackQuery = await this.query( + `select id from thread_track where utid in (${utids.join(',')})`); + const tracks = []; + for (const it = trackQuery.iter({id: NUM}); it.valid(); it.next()) { + tracks.push(it.id); + } + + const processSliceView = this.tableName('process_slice_view'); + await this.query( + `create view ${processSliceView} as ` + + // 0 as cpu is a dummy column to perform span join on. + `select ts, dur/${utids.length} as dur ` + + `from slice s ` + + `where depth = 0 and track_id in ` + + `(${tracks.join(',')})`); + await this.query(`create virtual table ${this.tableName('span')} + using span_join(${processSliceView}, + ${this.tableName('window')});`); + } + + async onBoundsChange(start: time, end: time, resolution: duration): + Promise<Data> { + assertFalse(resolution === 0n, 'Resolution cannot be 0'); + + // |resolution| is in ns/px we want # ns for 10px window: + // Max value with 1 so we don't end up with resolution 0. + const bucketSize = resolution * 10n; + const windowStart = Time.quant(start, bucketSize); + const windowDur = BigintMath.max(1n, end - windowStart); + + await this.query(`update ${this.tableName('window')} set + window_start=${windowStart}, + window_dur=${windowDur}, + quantum=${bucketSize} + where rowid = 0;`); + + return this.computeSummary(windowStart, end, resolution, bucketSize); + } + + private async computeSummary( + start: time, end: time, resolution: duration, + bucketSize: duration): Promise<Data> { + const duration = end - start; + const numBuckets = Math.min(Number(duration / bucketSize), LIMIT); + + const query = `select + quantum_ts as bucket, + sum(dur)/cast(${bucketSize} as float) as utilization + from ${this.tableName('span')} + group by quantum_ts + limit ${LIMIT}`; + + const summary: Data = { + start, + end, + resolution, + length: numBuckets, + bucketSize, + utilizations: new Float64Array(numBuckets), + }; + + const queryRes = await this.query(query); + const it = queryRes.iter({bucket: NUM, utilization: NUM}); + for (; it.valid(); it.next()) { + const bucket = it.bucket; + if (bucket > numBuckets) { + continue; + } + summary.utilizations[bucket] = it.utilization; + } + + return summary; + } + + async onDestroy(): Promise<void> { + await this.query(`drop table ${this.tableName('window')}; drop table ${ + this.tableName('span')}`); + } +} + +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 extends TrackAdapter<Config, Data> { + static create(args: NewTrackArgs): ProcessSummaryTrack { + return new ProcessSummaryTrack(args); + } + + constructor(args: NewTrackArgs) { + super(args); + } + + getHeight(): number { + return TRACK_HEIGHT; + } + + renderCanvas(ctx: CanvasRenderingContext2D): void { + const { + visibleTimeScale, + windowSpan, + } = globals.frontendLocalState; + const data = this.data(); + if (data === undefined) return; // Can't possibly draw anything. + + checkerboardExcept( + ctx, + this.getHeight(), + windowSpan.start, + windowSpan.end, + visibleTimeScale.timeToPx(data.start), + visibleTimeScale.timeToPx(data.end)); + + this.renderSummary(ctx, data); + } + + // TODO(dproy): Dedup with CPU slices. + renderSummary(ctx: CanvasRenderingContext2D, data: Data): void { + const {visibleTimeScale, windowSpan} = globals.frontendLocalState; + const startPx = windowSpan.start; + const bottomY = TRACK_HEIGHT; + + let lastX = startPx; + let lastY = bottomY; + + // TODO(hjd): Dedupe this math. + const color = colorForTid(this.config.pidForColor); + color.l = Math.min(color.l + 10, 60); + color.s -= 20; + + ctx.fillStyle = `hsl(${color.h}, ${color.s}%, ${color.l}%)`; + ctx.beginPath(); + ctx.moveTo(lastX, lastY); + for (let i = 0; i < data.utilizations.length; i++) { + // TODO(dproy): Investigate why utilization is > 1 sometimes. + const utilization = Math.min(data.utilizations[i], 1); + const startTime = Time.fromRaw(BigInt(i) * data.bucketSize + data.start); + + lastX = Math.floor(visibleTimeScale.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/tracks/screenshots/index.ts b/ui/src/tracks/screenshots/index.ts index c1eb5ed6a..eaf29981a 100644 --- a/ui/src/tracks/screenshots/index.ts +++ b/ui/src/tracks/screenshots/index.ts @@ -16,12 +16,16 @@ import {v4 as uuidv4} from 'uuid'; import {AddTrackArgs} from '../../common/actions'; import {Engine} from '../../common/engine'; -import {PrimaryTrackSortKey} from '../../common/state'; import { NamedSliceTrackTypes, } from '../../frontend/named_slice_track'; import {NewTrackArgs, Track} from '../../frontend/track'; -import {Plugin, PluginContext, PluginInfo} from '../../public'; +import { + Plugin, + PluginContext, + PluginInfo, + PrimaryTrackSortKey, +} from '../../public'; import { CustomSqlDetailsPanelConfig, CustomSqlTableDefConfig, |