aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSteve Golton <stevegolton@google.com>2023-10-04 20:43:27 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2023-10-04 20:43:27 +0000
commitabce5e9eba588f5000acfe33462d4b42f9a052ce (patch)
treeed46d4316201906275c25998260cba8689dad879
parentdce95412adeebf1e37aff8d33ef5e9ac2800cf8d (diff)
parentb5ea6711875eb07a4adf72aa5c5e62195a951c3a (diff)
downloadperfetto-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>
-rw-r--r--ui/src/common/actions.ts37
-rw-r--r--ui/src/common/actions_unittest.ts4
-rw-r--r--ui/src/common/basic_async_track.ts4
-rw-r--r--ui/src/common/engine.ts7
-rw-r--r--ui/src/common/plugins.ts40
-rw-r--r--ui/src/common/state.ts34
-rw-r--r--ui/src/common/state_unittest.ts4
-rw-r--r--ui/src/common/track_adapter.ts34
-rw-r--r--ui/src/controller/aggregation/counter_aggregation_controller.ts19
-rw-r--r--ui/src/controller/aggregation/cpu_aggregation_controller.ts5
-rw-r--r--ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts5
-rw-r--r--ui/src/controller/flow_events_controller.ts12
-rw-r--r--ui/src/controller/search_controller.ts4
-rw-r--r--ui/src/controller/trace_controller.ts4
-rw-r--r--ui/src/controller/track_decider.ts346
-rw-r--r--ui/src/frontend/app.ts31
-rw-r--r--ui/src/frontend/base_slice_track.ts6
-rw-r--r--ui/src/frontend/flow_events_renderer.ts31
-rw-r--r--ui/src/frontend/thread_state.ts4
-rw-r--r--ui/src/frontend/track.ts4
-rw-r--r--ui/src/frontend/track_group_panel.ts81
-rw-r--r--ui/src/frontend/track_panel.ts194
-rw-r--r--ui/src/public/index.ts102
-rw-r--r--ui/src/public/utils.ts73
-rw-r--r--ui/src/tracks/android_log/index.ts3
-rw-r--r--ui/src/tracks/annotation/index.ts90
-rw-r--r--ui/src/tracks/chrome_scroll_jank/event_latency_track.ts7
-rw-r--r--ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts6
-rw-r--r--ui/src/tracks/chrome_scroll_jank/scroll_track.ts16
-rw-r--r--ui/src/tracks/counter/index.ts512
-rw-r--r--ui/src/tracks/cpu_freq/index.ts89
-rw-r--r--ui/src/tracks/cpu_slices/index.ts6
-rw-r--r--ui/src/tracks/ftrace/index.ts3
-rw-r--r--ui/src/tracks/process_summary/index.ts503
-rw-r--r--ui/src/tracks/process_summary/process_scheduling_track.ts (renamed from ui/src/tracks/process_scheduling/index.ts)44
-rw-r--r--ui/src/tracks/process_summary/process_summary_track.ts207
-rw-r--r--ui/src/tracks/screenshots/index.ts8
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,