ui: Actual & Expected frame tracks
Change-Id: I7a69276e108868326c394a342d34e603fd3c6646
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 7ddd662..8d49590 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -17,9 +17,13 @@
import {assertExists} from '../base/logging';
import {randomColor} from '../common/colorizer';
import {ConvertTrace, ConvertTraceToPprof} from '../controller/trace_converter';
+import {ACTUAL_FRAMES_SLICE_TRACK_KIND} from '../tracks/actual_frames/common';
import {ASYNC_SLICE_TRACK_KIND} from '../tracks/async_slices/common';
import {COUNTER_TRACK_KIND} from '../tracks/counter/common';
import {DEBUG_SLICE_TRACK_KIND} from '../tracks/debug_slices/common';
+import {
+ EXPECTED_FRAMES_SLICE_TRACK_KIND
+} from '../tracks/expected_frames/common';
import {HEAP_PROFILE_TRACK_KIND} from '../tracks/heap_profile/common';
import {
PROCESS_SCHEDULING_TRACK_KIND
@@ -240,6 +244,8 @@
const threadTrackOrder = [
PROCESS_SCHEDULING_TRACK_KIND,
PROCESS_SUMMARY_TRACK,
+ EXPECTED_FRAMES_SLICE_TRACK_KIND,
+ ACTUAL_FRAMES_SLICE_TRACK_KIND,
HEAP_PROFILE_TRACK_KIND,
COUNTER_TRACK_KIND,
ASYNC_SLICE_TRACK_KIND
@@ -271,6 +277,7 @@
}
},
+
updateAggregateSorting(
state: StateDraft, args: {id: string, column: string}) {
let prefs = state.aggregatePreferences[args.id];
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index d9b88ee..8d823b9 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -29,6 +29,7 @@
STR_NULL,
} from '../common/query_iterator';
import {SCROLLING_TRACK_GROUP} from '../common/state';
+import {ACTUAL_FRAMES_SLICE_TRACK_KIND} from '../tracks/actual_frames/common';
import {ANDROID_LOGS_TRACK_KIND} from '../tracks/android_log/common';
import {ASYNC_SLICE_TRACK_KIND} from '../tracks/async_slices/common';
import {SLICE_TRACK_KIND} from '../tracks/chrome_slices/common';
@@ -36,6 +37,9 @@
import {CPU_FREQ_TRACK_KIND} from '../tracks/cpu_freq/common';
import {CPU_PROFILE_TRACK_KIND} from '../tracks/cpu_profile/common';
import {CPU_SLICE_TRACK_KIND} from '../tracks/cpu_slices/common';
+import {
+ EXPECTED_FRAMES_SLICE_TRACK_KIND
+} from '../tracks/expected_frames/common';
import {HEAP_PROFILE_TRACK_KIND} from '../tracks/heap_profile/common';
import {
PROCESS_SCHEDULING_TRACK_KIND
@@ -515,6 +519,7 @@
process.pid as pid
from process_track
left join process using(upid)
+ where process_track.name not like "% Timeline"
group by
process_track.upid,
process_track.name
@@ -563,6 +568,138 @@
return tracks;
}
+async function getActualFramesTracks(
+ engineId: string, engine: Engine, getUuid: GetUuid) {
+ const tracks: AddTrackArgs[] = [];
+ const query = await engine.query(`
+ select
+ upid,
+ trackName,
+ trackIds,
+ process.name as processName,
+ process.pid as pid
+ from (
+ select
+ process_track.upid as upid,
+ process_track.name as trackName,
+ group_concat(process_track.id) as trackIds
+ from process_track
+ where process_track.name like "Actual Timeline"
+ group by
+ process_track.upid,
+ process_track.name
+ ) left join process using(upid)
+ `);
+
+ const it = iter(
+ {
+ upid: NUM,
+ trackName: STR_NULL,
+ trackIds: STR,
+ processName: STR_NULL,
+ pid: NUM_NULL,
+ },
+ query);
+ for (let i = 0; it.valid(); ++i, it.next()) {
+ const row = it.row;
+ const upid = row.upid;
+ const trackName = row.trackName;
+ const rawTrackIds = row.trackIds;
+ const trackIds = rawTrackIds.split(',').map(v => Number(v));
+ const processName = row.processName;
+ const pid = row.pid;
+
+ const uuid = getUuid(0, upid);
+
+ // TODO(hjd): 1+N queries are bad in the track_decider
+ const depthResult = await engine.query(`
+ SELECT MAX(layout_depth) as max_depth
+ FROM experimental_slice_layout('${rawTrackIds}');
+ `);
+ const maxDepth = +depthResult.columns[0].longValues![0];
+
+ const kind = ACTUAL_FRAMES_SLICE_TRACK_KIND;
+ const name = getTrackName({name: trackName, upid, pid, processName, kind});
+ tracks.push({
+ engineId,
+ kind,
+ name,
+ trackGroup: uuid,
+ config: {
+ trackIds,
+ maxDepth,
+ }
+ });
+ }
+ return tracks;
+}
+
+async function getExpectedFramesTracks(
+ engineId: string, engine: Engine, getUuid: GetUuid) {
+ const tracks: AddTrackArgs[] = [];
+ const query = await engine.query(`
+ select
+ upid,
+ trackName,
+ trackIds,
+ process.name as processName,
+ process.pid as pid
+ from (
+ select
+ process_track.upid as upid,
+ process_track.name as trackName,
+ group_concat(process_track.id) as trackIds
+ from process_track
+ where process_track.name like "Expected Timeline"
+ group by
+ process_track.upid,
+ process_track.name
+ ) left join process using(upid)
+ `);
+
+ const it = iter(
+ {
+ upid: NUM,
+ trackName: STR_NULL,
+ trackIds: STR,
+ processName: STR_NULL,
+ pid: NUM_NULL,
+ },
+ query);
+ for (let i = 0; it.valid(); ++i, it.next()) {
+ const row = it.row;
+ const upid = row.upid;
+ const trackName = row.trackName;
+ const rawTrackIds = row.trackIds;
+ const trackIds = rawTrackIds.split(',').map(v => Number(v));
+ const processName = row.processName;
+ const pid = row.pid;
+
+ const uuid = getUuid(0, upid);
+
+ // TODO(hjd): 1+N queries are bad in the track_decider
+ const depthResult = await engine.query(`
+ SELECT MAX(layout_depth) as max_depth
+ FROM experimental_slice_layout('${rawTrackIds}');
+ `);
+ const maxDepth = +depthResult.columns[0].longValues![0];
+
+ const kind = EXPECTED_FRAMES_SLICE_TRACK_KIND;
+ const name = getTrackName({name: trackName, upid, pid, processName, kind});
+ tracks.push({
+ engineId,
+ kind,
+ name,
+ trackGroup: uuid,
+ config: {
+ trackIds,
+ maxDepth,
+ }
+ });
+ }
+ return tracks;
+}
+
async function getThreadSliceTracks(
engineId: string, engine: Engine, getUuid: GetUuid) {
const tracks: AddTrackArgs[] = [];
@@ -868,6 +1005,8 @@
extend(tracksToAdd, await getProcessCounterTracks(engineId, engine, getUuid));
extend(
tracksToAdd, await getProcessAsyncSliceTracks(engineId, engine, getUuid));
+ extend(tracksToAdd, await getActualFramesTracks(engineId, engine, getUuid));
+ extend(tracksToAdd, await getExpectedFramesTracks(engineId, engine, getUuid));
extend(tracksToAdd, await getThreadCounterTracks(engineId, engine, getUuid));
extend(tracksToAdd, await getThreadStateTracks(engineId, engine, getUuid));
extend(tracksToAdd, await getThreadSliceTracks(engineId, engine, getUuid));
diff --git a/ui/src/tracks/actual_frames/common.ts b/ui/src/tracks/actual_frames/common.ts
new file mode 100644
index 0000000..a5b5a78
--- /dev/null
+++ b/ui/src/tracks/actual_frames/common.ts
@@ -0,0 +1,34 @@
+// 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 {TrackData} from '../../common/track_data';
+
+export const ACTUAL_FRAMES_SLICE_TRACK_KIND = 'ActualFramesSliceTrack';
+
+export interface Config {
+ maxDepth: number;
+ trackIds: number[];
+}
+
+export interface Data extends TrackData {
+ // Slices are stored in a columnar fashion. All fields have the same length.
+ strings: string[];
+ sliceIds: Float64Array;
+ starts: Float64Array;
+ ends: Float64Array;
+ depths: Uint16Array;
+ titles: Uint16Array; // Index in |strings|.
+ colors?: Uint16Array; // Index in |strings|.
+ isInstant: Uint16Array;
+ isIncomplete: Uint16Array;
+}
diff --git a/ui/src/tracks/actual_frames/controller.ts b/ui/src/tracks/actual_frames/controller.ts
new file mode 100644
index 0000000..ca5778e
--- /dev/null
+++ b/ui/src/tracks/actual_frames/controller.ts
@@ -0,0 +1,135 @@
+// 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 {assertTrue} from '../../base/logging';
+import {slowlyCountRows} from '../../common/query_iterator';
+import {fromNs, toNs} from '../../common/time';
+import {
+ TrackController,
+ trackControllerRegistry,
+} from '../../controller/track_controller';
+
+import {ACTUAL_FRAMES_SLICE_TRACK_KIND, Config, Data} from './common';
+
+const ERROR_COLOR = '#03A9F4'; // Blue 500
+const GOOD_COLOR = '#4CAF50'; // Green 500
+const WARNING_COLOR = '#FFEB3B'; // Yellow 500
+const BAD_COLOR = '#FF5722'; // Red 500
+
+class ActualFramesSliceTrackController extends TrackController<Config, Data> {
+ static readonly kind = ACTUAL_FRAMES_SLICE_TRACK_KIND;
+ private maxDurNs = 0;
+
+ async onBoundsChange(start: number, end: number, resolution: number):
+ Promise<Data> {
+ const startNs = toNs(start);
+ const endNs = toNs(end);
+
+ const pxSize = this.pxSize();
+
+ // ns per quantization bucket (i.e. ns per pixel). /2 * 2 is to force it to
+ // be an even number, so we can snap in the middle.
+ const bucketNs = Math.max(Math.round(resolution * 1e9 * pxSize / 2) * 2, 1);
+
+ if (this.maxDurNs === 0) {
+ const maxDurResult = await this.query(`
+ select max(dur)
+ from experimental_slice_layout
+ where filter_track_ids = '${this.config.trackIds.join(',')}'
+ `);
+ if (slowlyCountRows(maxDurResult) === 1) {
+ this.maxDurNs = maxDurResult.columns[0].longValues![0];
+ }
+ }
+
+ const rawResult = await this.query(`
+ SELECT
+ (s.ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as tsq,
+ s.ts,
+ max(s.dur) as dur,
+ s.layout_depth,
+ s.name,
+ s.id,
+ s.dur = 0 as is_instant,
+ s.dur = -1 as is_incomplete,
+ CASE
+ WHEN afs.present_type = 'Dropped' THEN '${ERROR_COLOR}'
+ WHEN not afs.on_time_finish THEN '${BAD_COLOR}'
+ WHEN jank_type = 'None' THEN '${GOOD_COLOR}'
+ ELSE '${WARNING_COLOR}'
+ END as color
+ from experimental_slice_layout s
+ join actual_frame_timeline_slice afs using(id)
+ where
+ filter_track_ids = '${this.config.trackIds.join(',')}' and
+ s.ts >= ${startNs - this.maxDurNs} and
+ s.ts <= ${endNs}
+ group by tsq, s.layout_depth
+ order by tsq, s.layout_depth
+ `);
+
+ const numRows = slowlyCountRows(rawResult);
+ const slices: Data = {
+ start,
+ end,
+ resolution,
+ length: numRows,
+ strings: [],
+ sliceIds: new Float64Array(numRows),
+ starts: new Float64Array(numRows),
+ ends: new Float64Array(numRows),
+ depths: new Uint16Array(numRows),
+ titles: new Uint16Array(numRows),
+ colors: new Uint16Array(numRows),
+ isInstant: new Uint16Array(numRows),
+ isIncomplete: new Uint16Array(numRows),
+ };
+
+ const stringIndexes = new Map<string, number>();
+ function internString(str: string) {
+ let idx = stringIndexes.get(str);
+ if (idx !== undefined) return idx;
+ idx = slices.strings.length;
+ slices.strings.push(str);
+ stringIndexes.set(str, idx);
+ return idx;
+ }
+
+ const cols = rawResult.columns;
+ for (let row = 0; row < numRows; row++) {
+ const startNsQ = +cols[0].longValues![row];
+ const startNs = +cols[1].longValues![row];
+ const durNs = +cols[2].longValues![row];
+ const endNs = startNs + durNs;
+
+ let endNsQ = Math.floor((endNs + bucketNs / 2 - 1) / bucketNs) * bucketNs;
+ endNsQ = Math.max(endNsQ, startNsQ + bucketNs);
+
+ assertTrue(startNsQ !== endNsQ);
+
+ slices.starts[row] = fromNs(startNsQ);
+ slices.ends[row] = fromNs(endNsQ);
+ slices.depths[row] = +cols[3].longValues![row];
+ slices.titles[row] = internString(cols[4].stringValues![row]);
+ slices.colors![row] = internString(cols[8].stringValues![row]);
+ slices.sliceIds[row] = +cols[5].longValues![row];
+ slices.isInstant[row] = +cols[6].longValues![row];
+ slices.isIncomplete[row] = +cols[7].longValues![row];
+ }
+ return slices;
+ }
+}
+
+
+trackControllerRegistry.register(ActualFramesSliceTrackController);
diff --git a/ui/src/tracks/actual_frames/frontend.ts b/ui/src/tracks/actual_frames/frontend.ts
new file mode 100644
index 0000000..3c91598
--- /dev/null
+++ b/ui/src/tracks/actual_frames/frontend.ts
@@ -0,0 +1,29 @@
+// 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 {TrackState} from '../../common/state';
+import {Track} from '../../frontend/track';
+import {trackRegistry} from '../../frontend/track_registry';
+import {ChromeSliceTrack} from '../chrome_slices/frontend';
+
+import {ACTUAL_FRAMES_SLICE_TRACK_KIND} from './common';
+
+export class ActualFramesSliceTrack extends ChromeSliceTrack {
+ static readonly kind = ACTUAL_FRAMES_SLICE_TRACK_KIND;
+ static create(trackState: TrackState): Track {
+ return new ActualFramesSliceTrack(trackState);
+ }
+}
+
+trackRegistry.register(ActualFramesSliceTrack);
diff --git a/ui/src/tracks/all_controller.ts b/ui/src/tracks/all_controller.ts
index 116c1b9..ca0b5c5 100644
--- a/ui/src/tracks/all_controller.ts
+++ b/ui/src/tracks/all_controller.ts
@@ -26,3 +26,5 @@
import './thread_state/controller';
import './async_slices/controller';
import './debug_slices/controller';
+import './actual_frames/controller';
+import './expected_frames/controller';
diff --git a/ui/src/tracks/all_frontend.ts b/ui/src/tracks/all_frontend.ts
index 6311137..6af4d22 100644
--- a/ui/src/tracks/all_frontend.ts
+++ b/ui/src/tracks/all_frontend.ts
@@ -26,3 +26,5 @@
import './thread_state/frontend';
import './async_slices/frontend';
import './debug_slices/frontend';
+import './actual_frames/frontend';
+import './expected_frames/frontend';
diff --git a/ui/src/tracks/chrome_slices/common.ts b/ui/src/tracks/chrome_slices/common.ts
index 5ed3cb7..41fc2f8 100644
--- a/ui/src/tracks/chrome_slices/common.ts
+++ b/ui/src/tracks/chrome_slices/common.ts
@@ -30,6 +30,7 @@
ends: Float64Array;
depths: Uint16Array;
titles: Uint16Array; // Index into strings.
+ colors?: Uint16Array; // Index into strings.
isInstant: Uint16Array;
isIncomplete: Uint16Array;
}
diff --git a/ui/src/tracks/chrome_slices/frontend.ts b/ui/src/tracks/chrome_slices/frontend.ts
index 83795ce..1fc3358 100644
--- a/ui/src/tracks/chrome_slices/frontend.ts
+++ b/ui/src/tracks/chrome_slices/frontend.ts
@@ -82,6 +82,7 @@
const isInstant = data.isInstant[i];
const isIncomplete = data.isIncomplete[i];
const title = data.strings[titleId];
+ const colorOverride = data.colors && data.strings[data.colors[i]];
if (isIncomplete) { // incomplete slice
tEnd = tStart + INCOMPLETE_SLICE_TIME_S;
}
@@ -101,8 +102,13 @@
const saturation = isSelected ? 80 : 50;
const highlighted = titleId === this.hoveredTitleId ||
globals.frontendLocalState.highlightedSliceId === sliceId;
- const color = `hsl(${hue}, ${saturation}%, ${highlighted ? 30 : 65}%)`;
+ let color: string;
+ if (colorOverride === undefined) {
+ color = `hsl(${hue}, ${saturation}%, ${highlighted ? 30 : 65}%)`;
+ } else {
+ color = colorOverride;
+ }
ctx.fillStyle = color;
// We draw instant events as upward facing chevrons starting at A:
diff --git a/ui/src/tracks/expected_frames/common.ts b/ui/src/tracks/expected_frames/common.ts
new file mode 100644
index 0000000..9245520
--- /dev/null
+++ b/ui/src/tracks/expected_frames/common.ts
@@ -0,0 +1,34 @@
+// 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 {TrackData} from '../../common/track_data';
+
+export const EXPECTED_FRAMES_SLICE_TRACK_KIND = 'ExpectedFramesSliceTrack';
+
+export interface Config {
+ maxDepth: number;
+ trackIds: number[];
+}
+
+export interface Data extends TrackData {
+ // Slices are stored in a columnar fashion. All fields have the same length.
+ strings: string[];
+ sliceIds: Float64Array;
+ starts: Float64Array;
+ ends: Float64Array;
+ depths: Uint16Array;
+ titles: Uint16Array; // Index in |strings|.
+ colors?: Uint16Array; // Index in |strings|.
+ isInstant: Uint16Array;
+ isIncomplete: Uint16Array;
+}
diff --git a/ui/src/tracks/expected_frames/controller.ts b/ui/src/tracks/expected_frames/controller.ts
new file mode 100644
index 0000000..97ab2d9
--- /dev/null
+++ b/ui/src/tracks/expected_frames/controller.ts
@@ -0,0 +1,125 @@
+// 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 {slowlyCountRows} from '../../common/query_iterator';
+import {fromNs, toNs} from '../../common/time';
+import {
+ TrackController,
+ trackControllerRegistry,
+} from '../../controller/track_controller';
+
+import {Config, Data, EXPECTED_FRAMES_SLICE_TRACK_KIND} from './common';
+
+class ExpectedFramesSliceTrackController extends TrackController<Config, Data> {
+ static readonly kind = EXPECTED_FRAMES_SLICE_TRACK_KIND;
+ private maxDurNs = 0;
+
+ async onBoundsChange(start: number, end: number, resolution: number):
+ Promise<Data> {
+ const startNs = toNs(start);
+ const endNs = toNs(end);
+
+ const pxSize = this.pxSize();
+
+ // ns per quantization bucket (i.e. ns per pixel). /2 * 2 is to force it to
+ // be an even number, so we can snap in the middle.
+ const bucketNs = Math.max(Math.round(resolution * 1e9 * pxSize / 2) * 2, 1);
+
+ if (this.maxDurNs === 0) {
+ const maxDurResult = await this.query(`
+ select max(dur)
+ from experimental_slice_layout
+ where filter_track_ids = '${this.config.trackIds.join(',')}'
+ `);
+ if (slowlyCountRows(maxDurResult) === 1) {
+ this.maxDurNs = maxDurResult.columns[0].longValues![0];
+ }
+ }
+
+ const rawResult = await this.query(`
+ SELECT
+ (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as tsq,
+ ts,
+ max(dur) as dur,
+ layout_depth,
+ name,
+ id,
+ dur = 0 as is_instant,
+ dur = -1 as is_incomplete
+ from experimental_slice_layout
+ where
+ filter_track_ids = '${this.config.trackIds.join(',')}' and
+ ts >= ${startNs - this.maxDurNs} and
+ ts <= ${endNs}
+ group by tsq, layout_depth
+ order by tsq, layout_depth
+ `);
+
+ const numRows = slowlyCountRows(rawResult);
+ const slices: Data = {
+ start,
+ end,
+ resolution,
+ length: numRows,
+ strings: [],
+ sliceIds: new Float64Array(numRows),
+ starts: new Float64Array(numRows),
+ ends: new Float64Array(numRows),
+ depths: new Uint16Array(numRows),
+ titles: new Uint16Array(numRows),
+ colors: new Uint16Array(numRows),
+ isInstant: new Uint16Array(numRows),
+ isIncomplete: new Uint16Array(numRows),
+ };
+
+ const stringIndexes = new Map<string, number>();
+ function internString(str: string) {
+ let idx = stringIndexes.get(str);
+ if (idx !== undefined) return idx;
+ idx = slices.strings.length;
+ slices.strings.push(str);
+ stringIndexes.set(str, idx);
+ return idx;
+ }
+ const greenIndex = internString('#4CAF50');
+
+ const cols = rawResult.columns;
+ for (let row = 0; row < numRows; row++) {
+ const startNsQ = +cols[0].longValues![row];
+ const startNs = +cols[1].longValues![row];
+ const durNs = +cols[2].longValues![row];
+ const endNs = startNs + durNs;
+
+ let endNsQ = Math.floor((endNs + bucketNs / 2 - 1) / bucketNs) * bucketNs;
+ endNsQ = Math.max(endNsQ, startNsQ + bucketNs);
+
+ if (startNsQ === endNsQ) {
+ throw new Error('Should never happen');
+ }
+
+ slices.starts[row] = fromNs(startNsQ);
+ slices.ends[row] = fromNs(endNsQ);
+ slices.depths[row] = +cols[3].longValues![row];
+ slices.titles[row] = internString(cols[4].stringValues![row]);
+ slices.sliceIds[row] = +cols[5].longValues![row];
+ slices.isInstant[row] = +cols[6].longValues![row];
+ slices.isIncomplete[row] = +cols[7].longValues![row];
+ slices.colors![row] = greenIndex;
+ }
+ return slices;
+ }
+}
+
+
+trackControllerRegistry.register(ExpectedFramesSliceTrackController);
diff --git a/ui/src/tracks/expected_frames/frontend.ts b/ui/src/tracks/expected_frames/frontend.ts
new file mode 100644
index 0000000..441f098
--- /dev/null
+++ b/ui/src/tracks/expected_frames/frontend.ts
@@ -0,0 +1,28 @@
+// 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 {TrackState} from '../../common/state';
+import {Track} from '../../frontend/track';
+import {trackRegistry} from '../../frontend/track_registry';
+import {ChromeSliceTrack} from '../chrome_slices/frontend';
+
+import {EXPECTED_FRAMES_SLICE_TRACK_KIND} from './common';
+
+export class ExpectedFramesSliceTrack extends ChromeSliceTrack {
+ static readonly kind = EXPECTED_FRAMES_SLICE_TRACK_KIND;
+ static create(trackState: TrackState): Track {
+ return new ExpectedFramesSliceTrack(trackState);
+ }
+}
+
+trackRegistry.register(ExpectedFramesSliceTrack);