[ui] Port counter tracks to plugin tracks.
Summary:
- Each track instance has it's own state - accessed via mountState()
- Created 'CounterTrack' track impl for all counter tracks - port of
the original controller based track.
- Added suggestTrack() to TracePluginContext which replaces
findPotentialTracks().
- Added new "annotation" plugin for annotation counter tracks.
- Moved kind, cpu, trackIds, to PluginTrackInfo. kind is required,
but the others are optional. Use this new interface everywhere they
were previously extracted from config (e.g. aggregations, flows).
- Added new command "Find track by URI".
Change-Id: I6e75ee3dcf16ceb13b0cefd0a5743daa22d5e2c0
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index acb6a47..0999b31 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -16,6 +16,7 @@
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 @@
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 @@
traceEventEnd,
TraceEventScope,
} from './metatracing';
+import {pluginManager} from './plugins';
import {
AdbRecordingTarget,
Area,
@@ -61,7 +63,6 @@
Pagination,
PendingDeeplinkState,
PivotTableResult,
- PrimaryTrackSortKey,
ProfileType,
RecordingTarget,
SCROLLING_TRACK_GROUP,
@@ -225,16 +226,25 @@
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 @@
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 261dd2e..de857bc 100644
--- a/ui/src/common/actions_unittest.ts
+++ b/ui/src/common/actions_unittest.ts
@@ -16,6 +16,7 @@
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 {
@@ -27,7 +28,6 @@
import {createEmptyState} from './empty_state';
import {
InThreadTrackSortKey,
- PrimaryTrackSortKey,
ProfileType,
SCROLLING_TRACK_GROUP,
State,
diff --git a/ui/src/common/engine.ts b/ui/src/common/engine.ts
index 43c5f16..c442e73 100644
--- a/ui/src/common/engine.ts
+++ b/ui/src/common/engine.ts
@@ -499,6 +499,13 @@
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 9961921..7278e06 100644
--- a/ui/src/common/plugins.ts
+++ b/ui/src/common/plugins.ts
@@ -102,7 +102,8 @@
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 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
proxyStore,
engineProxy,
this.trackRegistry,
+ this.suggestedTracks,
this.commandRegistry);
pluginDetails.traceContext = traceCtx;
@@ -347,6 +348,7 @@
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 0d31e01..6ba8547 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -23,7 +23,7 @@
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 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 @@
trackIds?: number[];
};
uri?: string;
+ state?: unknown;
}
export interface TrackGroupState {
@@ -268,6 +239,7 @@
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 3a34afa..34646ba 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 9bb57a4..54b5055 100644
--- a/ui/src/common/track_adapter.ts
+++ b/ui/src/common/track_adapter.ts
@@ -181,6 +181,12 @@
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> {
@@ -210,6 +216,14 @@
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 3c3cf15..ae3e1d7 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 @@
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 @@
(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 46f0f47..db176d9 100644
--- a/ui/src/controller/aggregation/cpu_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/cpu_aggregation_controller.ts
@@ -31,9 +31,8 @@
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 aebaf94..dbdf07c 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 @@
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 06f9da9..a1c4170 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 @@
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 b8ee0d3..6055221 100644
--- a/ui/src/controller/search_controller.ts
+++ b/ui/src/controller/search_controller.ts
@@ -204,8 +204,8 @@
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 027398a..31000d6 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -470,8 +470,6 @@
}
}
- pluginManager.onTraceLoad(engine);
-
const emptyOmniboxState = {
omnibox: '',
mode: globals.state.omniboxState.mode || 'SEARCH',
@@ -501,6 +499,8 @@
// 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 e6ae81d..a806363 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -25,7 +25,6 @@
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 @@
} 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,7 +49,7 @@
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 {COUNTER_TRACK_KIND} from '../tracks/counter';
import {CPU_FREQ_TRACK_KIND} from '../tracks/cpu_freq';
import {CPU_PROFILE_TRACK_KIND} from '../tracks/cpu_profile';
import {
@@ -130,29 +130,6 @@
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 +148,6 @@
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(`
@@ -394,7 +311,7 @@
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 +329,7 @@
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 +379,24 @@
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 +441,12 @@
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 +767,6 @@
}
}
- 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 +907,30 @@
}
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 +977,7 @@
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 +992,7 @@
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 +1053,7 @@
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 +1066,7 @@
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 +1076,25 @@
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 +1149,8 @@
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 +1213,8 @@
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 +1278,8 @@
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 +1337,7 @@
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 +1383,7 @@
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 +1393,6 @@
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 +1401,17 @@
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}`,
});
}
}
@@ -1866,8 +1724,8 @@
labels: it.chromeProcessLabels.split(','),
});
- 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 +1792,20 @@
`);
}
- 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 +1834,7 @@
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 +1914,6 @@
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 23401a0..955df02 100644
--- a/ui/src/frontend/app.ts
+++ b/ui/src/frontend/app.ts
@@ -43,6 +43,7 @@
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 @@
},
},
{
- 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 @@
});
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/flow_events_renderer.ts b/ui/src/frontend/flow_events_renderer.ts
index 4d49c1a..c414454 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 @@
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 @@
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 bb1b2c7..80f690b 100644
--- a/ui/src/frontend/thread_state.ts
+++ b/ui/src/frontend/thread_state.ts
@@ -146,8 +146,8 @@
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_group_panel.ts b/ui/src/frontend/track_group_panel.ts
index e1f2c86..4459c98 100644
--- a/ui/src/frontend/track_group_panel.ts
+++ b/ui/src/frontend/track_group_panel.ts
@@ -18,17 +18,19 @@
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} from './track_panel';
import {trackRegistry} from './track_registry';
import {
drawVerticalLineAtTime,
@@ -43,21 +45,35 @@
private readonly trackGroupId: string;
private shellWidth = 0;
private backgroundColor = '#ffffff'; // Updated from CSS later.
- private summaryTrack: Track|undefined;
+ private summaryTrack: TrackLike|undefined;
- 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']);
+ },
+ };
+
+ this.summaryTrack =
+ uri ? pluginManager.createTrack(uri, ctx) : loadTrack(trackState, id);
+
+ this.summaryTrack?.onCreate();
}
get trackGroupState(): TrackGroupState {
@@ -69,6 +85,10 @@
}
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 +152,7 @@
'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) :
@@ -286,3 +306,26 @@
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 8dffff1..2c2d5e7 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -18,12 +18,13 @@
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,24 +69,37 @@
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;
-
- return [
- isMetric && m('span.chip', 'metric'),
- isDebuggable && m('span.chip', 'debuggable'),
- ];
+class TrackChip implements m.ClassComponent<TrackChipAttrs> {
+ view({attrs}: m.CVnode<TrackChipAttrs>) {
+ return m('span.chip', attrs.text);
}
}
+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;
trackState: TrackState;
@@ -134,7 +148,7 @@
},
},
attrs.trackState.name,
- m(TrackChips, {config: attrs.trackState.config}),
+ renderChips(attrs.trackState),
),
m('.track-buttons',
attrs.track.getTrackShellButtons(),
@@ -352,8 +366,21 @@
if (!trackState) return;
const {id, uri} = trackState;
- this.track =
- uri ? pluginManager.createTrack(uri, id) : loadTrack(trackState, id);
+
+ 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']);
+ },
+ };
+
+ this.track = uri ? pluginManager.createTrack(uri, trackCtx) :
+ loadTrack(trackState, id);
this.track?.onCreate();
this.trackState = trackState;
}
diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts
index b939428..8c90493 100644
--- a/ui/src/public/index.ts
+++ b/ui/src/public/index.ts
@@ -170,14 +170,21 @@
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
@@ -210,10 +217,55 @@
// 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 @@
// 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 @@
// 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 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 @@
// 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 0000000..d8db8f0
--- /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 948f82d..597d334 100644
--- a/ui/src/tracks/android_log/index.ts
+++ b/ui/src/tracks/android_log/index.ts
@@ -30,6 +30,8 @@
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 @@
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 0000000..e2baabb
--- /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 370c754..8632bf4 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 {
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,
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 a2f83f7..b4d47ff 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 @@
} 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,
diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_track.ts b/ui/src/tracks/chrome_scroll_jank/scroll_track.ts
index d6bd1d5..56a672b 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';
diff --git a/ui/src/tracks/counter/index.ts b/ui/src/tracks/counter/index.ts
index cb96f6f..478266d 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,25 +74,103 @@
minimumValue?: number;
startTs?: time;
endTs?: time;
- namespace: string;
+ namespace?: string;
trackId: number;
- scale?: CounterScaleOptions;
+ defaultScale?: CounterScaleOptions;
}
-class CounterTrackController extends TrackController<Config, Data> {
- static readonly kind = COUNTER_TRACK_KIND;
+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;
+ }
+}
+
+export class CounterTrack extends BasicAsyncTrack<Data> {
private setup = false;
private maximumValueSeen = 0;
private minimumValueSeen = 0;
private maximumDeltaSeen = 0;
private minimumDeltaSeen = 0;
private maxDurNs: duration = 0n;
+ 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 {
+ return {scale: this.config.defaultScale ?? 'ZERO_BASED'};
+ }
+ });
+ }
+
+ // Returns a valid SQL table name with the given prefix that should be unique
+ // for each track.
+ tableName(prefix: string) {
+ // Derive table name from, since that is unique for each track.
+ // Track ID can be UUID but '-' is not valid for sql table name.
+ const idSuffix = this.uuid.split('-').join('_');
+ return `${prefix}_${idSuffix}`;
+ }
+
+ private namespaceTable(tableName: string): string {
+ if (this.config.namespace) {
+ return this.config.namespace + '_' + tableName;
+ } else {
+ return tableName;
+ }
+ }
async onBoundsChange(start: time, end: time, resolution: duration):
Promise<Data> {
if (!this.setup) {
if (this.config.namespace === undefined) {
- await this.query(`
+ await this.engine.query(`
create view ${this.tableName('counter_view')} as
select
id,
@@ -96,7 +182,7 @@
where track_id = ${this.config.trackId};
`);
} else {
- await this.query(`
+ await this.engine.query(`
create view ${this.tableName('counter_view')} as
select
id,
@@ -109,7 +195,7 @@
`);
}
- const maxDurResult = await this.query(`
+ const maxDurResult = await this.engine.query(`
select
max(
iif(dur != -1, dur, (select end_ts from trace_bounds) - ts)
@@ -118,7 +204,7 @@
`);
this.maxDurNs = maxDurResult.firstRow({maxDur: LONG_NULL}).maxDur || 0n;
- const queryRes = await this.query(`
+ const queryRes = await this.engine.query(`
select
ifnull(max(value), 0) as maxValue,
ifnull(min(value), 0) as minValue,
@@ -135,7 +221,7 @@
this.setup = true;
}
- const queryRes = await this.query(`
+ const queryRes = await this.engine.query(`
select
(ts + ${resolution / 2n}) / ${resolution} * ${resolution} as tsq,
min(value) as minValue,
@@ -219,34 +305,18 @@
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 +328,8 @@
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 +350,7 @@
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 +363,7 @@
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 +553,17 @@
}
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 +581,7 @@
}
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 +595,58 @@
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;
}
}
+
+ onDestroy(): void {
+ 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,21 +667,234 @@
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: {
- name,
- trackId,
- },
+ 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);
+ },
+ });
+ }
+ }
}
export const plugin: PluginInfo = {
diff --git a/ui/src/tracks/cpu_slices/index.ts b/ui/src/tracks/cpu_slices/index.ts
index 40526a4..7e992ee 100644
--- a/ui/src/tracks/cpu_slices/index.ts
+++ b/ui/src/tracks/cpu_slices/index.ts
@@ -484,10 +484,8 @@
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 124696e..54cc44f 100644
--- a/ui/src/tracks/ftrace/index.ts
+++ b/ui/src/tracks/ftrace/index.ts
@@ -27,6 +27,7 @@
TracePluginContext,
} from '../../public';
+export const FTRACE_RAW_TRACK_KIND = 'FtraceRawTrack';
export interface Data extends TrackData {
timestamps: BigInt64Array;
@@ -146,6 +147,8 @@
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/screenshots/index.ts b/ui/src/tracks/screenshots/index.ts
index 63ff916..c15dec6 100644
--- a/ui/src/tracks/screenshots/index.ts
+++ b/ui/src/tracks/screenshots/index.ts
@@ -16,12 +16,16 @@
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,