chrome-devtools-frontend
Version:
Chrome DevTools UI
326 lines (271 loc) • 15.5 kB
text/typescript
// Copyright 2023 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Common from '../../core/common/common.js';
import * as Trace from '../../models/trace/trace.js';
import {describeWithEnvironment} from '../../testing/EnvironmentHelpers.js';
import {TraceLoader} from '../../testing/TraceLoader.js';
import type * as PerfUi from '../../ui/legacy/components/perf_ui/perf_ui.js';
import * as Timeline from './timeline.js';
describeWithEnvironment('TimelineFlameChartNetworkDataProvider', function() {
it('renders the network track correctly', async function() {
const dataProvider = new Timeline.TimelineFlameChartNetworkDataProvider.TimelineFlameChartNetworkDataProvider();
const {parsedTrace} = await TraceLoader.traceEngine(this, 'load-simple.json.gz');
const entityMapper = new Timeline.Utils.EntityMapper.EntityMapper(parsedTrace);
const minTime = Trace.Helpers.Timing.microToMilli(parsedTrace.Meta.traceBounds.min);
const maxTime = Trace.Helpers.Timing.microToMilli(parsedTrace.Meta.traceBounds.max);
dataProvider.setModel(parsedTrace, entityMapper);
dataProvider.setWindowTimes(minTime, maxTime);
// TimelineFlameChartNetworkDataProvider only has network track, so should always be one track group.
assert.lengthOf(dataProvider.timelineData().groups, 1);
const networkTrackGroup = dataProvider.timelineData().groups[0];
assert.deepEqual(dataProvider.minimumBoundary(), minTime);
assert.deepEqual(dataProvider.totalTime(), maxTime - minTime);
const networkEvents = parsedTrace.NetworkRequests.byTime;
const networkEventsStartTimes = networkEvents.map(request => Trace.Helpers.Timing.microToMilli(request.ts));
const networkEventsTotalTimes = networkEvents.map(request => {
const {startTime, endTime} = Trace.Helpers.Timing.eventTimingsMilliSeconds(request);
return endTime - startTime;
});
assert.lengthOf(dataProvider.timelineData().entryLevels, 6);
assert.deepEqual(dataProvider.timelineData().entryLevels, [0, 1, 1, 1, 1, 2]);
assertTimestampsEqual(dataProvider.timelineData().entryStartTimes, networkEventsStartTimes);
assertTimestampsEqual(dataProvider.timelineData().entryTotalTimes, networkEventsTotalTimes);
assert.deepEqual(dataProvider.maxStackDepth(), 3);
// The decorateEntry() will be handled in the TimelineFlameChartNetworkDataProvider, so this function always returns true.
assert.isTrue(dataProvider.forceDecoration(0));
assert.isFalse(dataProvider.isEmpty());
// The network track is default to collapsed.
assert.isFalse(dataProvider.isExpanded());
// The height of collapsed network track style is 17.
assert.strictEqual(dataProvider.preferredHeight(), 17);
networkTrackGroup.expanded = true;
assert.isTrue(dataProvider.isExpanded());
// The max level here is 3, so `clamp(this.#maxLevel + 1, 7, 8.5)` = 7
assert.strictEqual(dataProvider.preferredHeight(), 17 * 7);
});
it('renders initiators and clears them when events are deselected', async function() {
const dataProvider = new Timeline.TimelineFlameChartNetworkDataProvider.TimelineFlameChartNetworkDataProvider();
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz');
const entityMapper = new Timeline.Utils.EntityMapper.EntityMapper(parsedTrace);
dataProvider.setModel(parsedTrace, entityMapper);
const timelineData1 = dataProvider.timelineData();
assert.lengthOf(timelineData1.initiatorsData, 0); // no initiators by default
// A network event that has an initiator - nothing special about the exact event.
const event = parsedTrace.NetworkRequests.byId.get('90829.57');
assert.exists(event);
const index = dataProvider.indexForEvent(event);
assert.isNotNull(index);
dataProvider.buildFlowForInitiator(index);
const timelineData2 = dataProvider.timelineData();
// The selected event kicks off a chain of 3 initiators.
assert.lengthOf(timelineData2.initiatorsData, 3);
// Deselect and ensure they are removed
dataProvider.buildFlowForInitiator(-1);
const timelineData3 = dataProvider.timelineData();
assert.lengthOf(timelineData3.initiatorsData, 0);
});
it('can return the group for a given entryIndex', async function() {
const dataProvider = new Timeline.TimelineFlameChartNetworkDataProvider.TimelineFlameChartNetworkDataProvider();
const {parsedTrace} = await TraceLoader.traceEngine(this, 'load-simple.json.gz');
const entityMapper = new Timeline.Utils.EntityMapper.EntityMapper(parsedTrace);
dataProvider.setModel(parsedTrace, entityMapper);
dataProvider.timelineData();
assert.strictEqual(
dataProvider.groupForEvent(0)?.name,
'Network',
);
});
it('filters navigations to only return those that happen on the main frame', async function() {
const dataProvider = new Timeline.TimelineFlameChartNetworkDataProvider.TimelineFlameChartNetworkDataProvider();
const {parsedTrace} = await TraceLoader.traceEngine(this, 'multiple-navigations-with-iframes.json.gz');
const entityMapper = new Timeline.Utils.EntityMapper.EntityMapper(parsedTrace);
const minTime = Trace.Helpers.Timing.microToMilli(parsedTrace.Meta.traceBounds.min);
const maxTime = Trace.Helpers.Timing.microToMilli(parsedTrace.Meta.traceBounds.max);
dataProvider.setModel(parsedTrace, entityMapper);
dataProvider.setWindowTimes(minTime, maxTime);
const mainFrameID = parsedTrace.Meta.mainFrameId;
const navigationEvents = dataProvider.mainFrameNavigationStartEvents();
// Ensure that every navigation event that we return is for the main frame.
assert.isTrue(navigationEvents.every(navEvent => {
return navEvent.args.frame === mainFrameID;
}));
});
it('can provide the index for an event and the event for a given index', async function() {
const dataProvider = new Timeline.TimelineFlameChartNetworkDataProvider.TimelineFlameChartNetworkDataProvider();
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const entityMapper = new Timeline.Utils.EntityMapper.EntityMapper(parsedTrace);
dataProvider.setModel(parsedTrace, entityMapper);
const event = dataProvider.eventByIndex(0);
assert.isOk(event);
assert.strictEqual(dataProvider.indexForEvent(event), 0);
});
it('does not render the network track if there is no network requests', async function() {
const dataProvider = new Timeline.TimelineFlameChartNetworkDataProvider.TimelineFlameChartNetworkDataProvider();
const {parsedTrace} = await TraceLoader.traceEngine(this, 'basic.json.gz');
const entityMapper = new Timeline.Utils.EntityMapper.EntityMapper(parsedTrace);
const minTime = Trace.Helpers.Timing.microToMilli(parsedTrace.Meta.traceBounds.min);
const maxTime = Trace.Helpers.Timing.microToMilli(parsedTrace.Meta.traceBounds.max);
dataProvider.setModel(parsedTrace, entityMapper);
dataProvider.setWindowTimes(minTime, maxTime);
// Network track appender won't append the network track if there is no network requests.
assert.lengthOf(dataProvider.timelineData().groups, 0);
assert.deepEqual(dataProvider.minimumBoundary(), minTime);
assert.deepEqual(dataProvider.totalTime(), maxTime - minTime);
assert.deepEqual(dataProvider.timelineData().entryLevels, []);
assert.deepEqual(dataProvider.timelineData().entryStartTimes, []);
assert.deepEqual(dataProvider.timelineData().entryTotalTimes, []);
assert.deepEqual(dataProvider.maxStackDepth(), 0);
// The decorateEntry() will be handled in the TimelineFlameChartNetworkDataProvider, so this function always returns true.
assert.isTrue(dataProvider.forceDecoration(0));
// The network track won't show if it is empty.
assert.isTrue(dataProvider.isEmpty());
assert.strictEqual(dataProvider.preferredHeight(), 0);
});
it('decorate a event correctly', async function() {
const dataProvider = new Timeline.TimelineFlameChartNetworkDataProvider.TimelineFlameChartNetworkDataProvider();
const {parsedTrace} = await TraceLoader.traceEngine(this, 'cls-cluster-max-timeout.json.gz');
// The field that is important of this test:
// {
// "ts": 183752441.977,
// "dur": 183752670.454,
// "finishTime": 183752669.23299998,
// ...
// "timing": {
// "pushStart": 0,
// "receiveHeadersEnd": 218.084,
// "requestTime": 183752.449687,
// "sendEnd": 13.01,
// "sendStart": 12.792,
// ...
// },
// "priority": "VeryHigh",
// "responseTime": 1634222299.776
// ...
// }
const event = parsedTrace.NetworkRequests.byTime[1];
// So for this request:
// The earliest event belonging to this request starts at 183752441.977.
// This is used in flamechart to calculate unclippedBarX.
// Start time is 183752441.977
// End time is 183752670.454
// Finish time is 183752669.233
// request time is 183752.449687, but it is in second, so 183752449.687
// in milliseconds.
// sendStartTime is requestTime + sendStart = 183752462.479
// headersEndTime is requestTime + receiveHeadersEnd = 183752667.771
//
// To calculate the pixel of a timestamp, we substrate the begin time from
// it, then multiple the timeToPixelRatio and then add the unclippedBarX.
// Then get the floor of the pixel.
// So the pixel of sendStart is (183752462.479 - 183752441.977) + 10, in ts it will be 30.502000004053116.
// So the pixel of headersEnd is (183752667.771 - 183752441.977) + 10, in ts it will be 235.79399999976158.
// So the pixel of finish is (183752669.233 - 183752441.977) + 10, in ts it will be 237.25600001215935.
// So the pixel of start is (183752441.977 - 183752441.977) + 10 = 10.
// So the pixel of end is (183752670.454 - 183752441.977) + 10, in ts it will be 238.47699999809265.
assert.deepEqual(dataProvider.getDecorationPixels(event, /* unclippedBarX= */ 10, /* timeToPixelRatio= */ 1), {
sendStart: (183752462.479 - 183752441.977) + 10,
headersEnd: (183752667.771 - 183752441.977) + 10,
finish: (183752669.233 - 183752441.977) + 10,
start: 10,
end: (183752670.454 - 183752441.977) + 10,
});
});
it('can search for entries within a given time-range', async function() {
const dataProvider = new Timeline.TimelineFlameChartNetworkDataProvider.TimelineFlameChartNetworkDataProvider();
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz');
const entityMapper = new Timeline.Utils.EntityMapper.EntityMapper(parsedTrace);
dataProvider.setModel(parsedTrace, entityMapper);
const boundsMs = Trace.Helpers.Timing.traceWindowMilliSeconds(parsedTrace.Meta.traceBounds);
dataProvider.setWindowTimes(boundsMs.min, boundsMs.max);
const filter = new Timeline.TimelineFilters.TimelineRegExp(/app\.js/i);
const results = dataProvider.search(parsedTrace.Meta.traceBounds, filter);
assert.lengthOf(results, 1);
assert.deepEqual(results[0], {index: 8, startTimeMilli: 122411056.533, provider: 'network'});
});
it('delete annotations associated with an event', async function() {
const dataProvider = new Timeline.TimelineFlameChartNetworkDataProvider.TimelineFlameChartNetworkDataProvider();
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz');
const entityMapper = new Timeline.Utils.EntityMapper.EntityMapper(parsedTrace);
dataProvider.setModel(parsedTrace, entityMapper);
const entryIndex = 0;
const eventToFindAssociatedEntriesFor = dataProvider.eventByIndex(entryIndex);
const event = dataProvider.eventByIndex(1);
assert.exists(eventToFindAssociatedEntriesFor);
assert.exists(event);
// This link annotation should be deleted
Timeline.ModificationsManager.ModificationsManager.activeManager()?.createAnnotation({
type: 'ENTRIES_LINK',
entryFrom: eventToFindAssociatedEntriesFor,
entryTo: event,
state: Trace.Types.File.EntriesLinkState.CONNECTED,
});
Timeline.ModificationsManager.ModificationsManager.activeManager()?.createAnnotation({
type: 'ENTRY_LABEL',
entry: event,
label: 'label',
});
dataProvider.deleteAnnotationsForEntry(entryIndex);
// Make sure one of the annotations was deleted
assert.deepEqual(Timeline.ModificationsManager.ModificationsManager.activeManager()?.getAnnotations().length, 1);
});
it('correctly identifies if an event has annotations', async function() {
const dataProvider = new Timeline.TimelineFlameChartNetworkDataProvider.TimelineFlameChartNetworkDataProvider();
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz');
const entityMapper = new Timeline.Utils.EntityMapper.EntityMapper(parsedTrace);
dataProvider.setModel(parsedTrace, entityMapper);
const eventIndex = 0;
const event = dataProvider.eventByIndex(eventIndex);
const event2 = dataProvider.eventByIndex(1);
assert.exists(event);
assert.exists(event2);
// Create a link between events
Timeline.ModificationsManager.ModificationsManager.activeManager()?.createAnnotation({
type: 'ENTRIES_LINK',
entryFrom: event,
entryTo: event2,
state: Trace.Types.File.EntriesLinkState.CONNECTED,
});
// Made sure the event has annotations
assert.isTrue(dataProvider.entryHasAnnotations(eventIndex));
// Delete annotations for the event
dataProvider.deleteAnnotationsForEntry(eventIndex);
// Made sure the event does not have annotations
assert.isFalse(dataProvider.entryHasAnnotations(eventIndex));
});
it('persists track configurations to the setting if it is provided with one', async function() {
const {Settings} = Common.Settings;
const setting =
Settings.instance().createSetting<PerfUi.FlameChart.PersistedConfigPerTrace>('persist-flame-config', {});
const dataProvider = new Timeline.TimelineFlameChartNetworkDataProvider.TimelineFlameChartNetworkDataProvider();
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz');
const entityMapper = new Timeline.Utils.EntityMapper.EntityMapper(parsedTrace);
dataProvider.setModel(parsedTrace, entityMapper);
dataProvider.setPersistedGroupConfigSetting(setting);
const groups = dataProvider.timelineData().groups;
assert.lengthOf(groups, 1);
assert.isUndefined(groups[0].expanded);
// Pretend the user has expanded the group
groups[0].expanded = true;
dataProvider.handleTrackConfigurationChange(groups, [0]);
const newSetting = setting.get();
const traceKey = Timeline.TrackConfiguration.keyForTraceConfig(parsedTrace);
assert.deepEqual(newSetting[traceKey], [
{
expanded: true,
hidden: false,
originalIndex: 0,
visualIndex: 0,
},
]);
});
});
function assertTimestampEqual(actual: number, expected: number): void {
assert.strictEqual(actual.toFixed(2), expected.toFixed(2));
}
function assertTimestampsEqual(actual: number[]|Float32Array|Float64Array, expected: number[]): void {
assert.strictEqual(actual.length, expected.length);
for (let i = 0; i < actual.length; i++) {
assertTimestampEqual(actual[i], expected[i]);
}
}