Files
react-native-devtools-frontend/front_end/timeline/TimelineEventOverview.js
T
Vidal Guillermo Diazleal Ortega 87060e47d6 [Loc] Migrate timeline(Performance) to Localization V2
- Migrate timeline(Performance) to use Localization V2 APIs.
- Removed timeline grdps

Tested with en-XL.json
https://imgur.com/a/mxMQWBF

Bug: 1136655
Change-Id: I0b282c5c9efdd90fd57c376ea4f69ff8eb0d3006
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2689595
Commit-Queue: Vidal Diazleal <vidorteg@microsoft.com>
Reviewed-by: Simon Zünd <szuend@chromium.org>
2021-02-12 16:47:58 +00:00

943 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Copyright (C) 2013 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import * as Coverage from '../coverage/coverage.js';
import * as i18n from '../i18n/i18n.js';
import * as PerfUI from '../perf_ui/perf_ui.js';
import * as Platform from '../platform/platform.js';
import * as SDK from '../sdk/sdk.js'; // eslint-disable-line no-unused-vars
import * as TimelineModel from '../timeline_model/timeline_model.js';
import * as UI from '../ui/ui.js';
import {PerformanceModel} from './PerformanceModel.js'; // eslint-disable-line no-unused-vars
import {EventDispatchTypeDescriptor, TimelineCategory, TimelineRecordStyle, TimelineUIUtils} from './TimelineUIUtils.js'; // eslint-disable-line no-unused-vars
export const UIStrings = {
/**
*@description Text in Timeline Event Overview of the Performance panel
*/
net: 'NET',
/**
*@description Text in Timeline Event Overview of the Performance panel
*/
cpu: 'CPU',
/**
*@description Text in Timeline Event Overview of the Performance panel
*/
fps: 'FPS',
/**
*@description Text in Timeline Event Overview of the Performance panel
*/
heap: 'HEAP',
/**
*@description Heap size label text content in Timeline Event Overview of the Performance panel
*@example {10 MB} PH1
*@example {30 MB} PH2
*/
sSDash: '{PH1} – {PH2}',
/**
*@description Text in Timeline Event Overview of the Performance panel
*/
coverage: 'COVERAGE',
};
const str_ = i18n.i18n.registerUIStrings('timeline/TimelineEventOverview.js', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class TimelineEventOverview extends PerfUI.TimelineOverviewPane.TimelineOverviewBase {
/**
* @param {string} id
* @param {?string} title
*/
constructor(id, title) {
super();
this.element.id = 'timeline-overview-' + id;
this.element.classList.add('overview-strip');
/** @type {?PerformanceModel} */
this._model = null;
if (title) {
this.element.createChild('div', 'timeline-overview-strip-title').textContent = title;
}
}
/**
* @param {?PerformanceModel} model
*/
setModel(model) {
this._model = model;
}
/**
* @param {number} begin
* @param {number} end
* @param {number} position
* @param {number} height
* @param {string} color
*/
_renderBar(begin, end, position, height, color) {
const x = begin;
const width = end - begin;
const ctx = this.context();
ctx.fillStyle = color;
ctx.fillRect(x, position, width, height);
}
}
export class TimelineEventOverviewInput extends TimelineEventOverview {
constructor() {
super('input', null);
}
/**
* @override
*/
update() {
super.update();
if (!this._model) {
return;
}
const height = this.height();
const descriptors = TimelineUIUtils.eventDispatchDesciptors();
/** @type {!Map.<string,!EventDispatchTypeDescriptor>} */
const descriptorsByType = new Map();
let maxPriority = -1;
for (const descriptor of descriptors) {
for (const type of descriptor.eventTypes) {
descriptorsByType.set(type, descriptor);
}
maxPriority = Math.max(maxPriority, descriptor.priority);
}
const minWidth = 2 * window.devicePixelRatio;
const timeOffset = this._model.timelineModel().minimumRecordTime();
const timeSpan = this._model.timelineModel().maximumRecordTime() - timeOffset;
const canvasWidth = this.width();
const scale = canvasWidth / timeSpan;
for (let priority = 0; priority <= maxPriority; ++priority) {
for (const track of this._model.timelineModel().tracks()) {
for (let i = 0; i < track.events.length; ++i) {
const event = track.events[i];
if (event.name !== TimelineModel.TimelineModel.RecordType.EventDispatch) {
continue;
}
const descriptor = descriptorsByType.get(event.args['data']['type']);
if (!descriptor || descriptor.priority !== priority) {
continue;
}
if (event.endTime === undefined) {
continue;
}
const start =
Platform.NumberUtilities.clamp(Math.floor((event.startTime - timeOffset) * scale), 0, canvasWidth);
const end = Platform.NumberUtilities.clamp(Math.ceil((event.endTime - timeOffset) * scale), 0, canvasWidth);
const width = Math.max(end - start, minWidth);
this._renderBar(start, start + width, 0, height, descriptor.color);
}
}
}
}
}
export class TimelineEventOverviewNetwork extends TimelineEventOverview {
constructor() {
super('network', i18nString(UIStrings.net));
}
/**
* @override
*/
update() {
super.update();
if (!this._model) {
return;
}
const timelineModel = this._model.timelineModel();
const bandHeight = this.height() / 2;
const timeOffset = timelineModel.minimumRecordTime();
const timeSpan = timelineModel.maximumRecordTime() - timeOffset;
const canvasWidth = this.width();
const scale = canvasWidth / timeSpan;
const highPath = new Path2D();
const lowPath = new Path2D();
const priorities = Protocol.Network.ResourcePriority;
const highPrioritySet = new Set([priorities.VeryHigh, priorities.High, priorities.Medium]);
for (const request of timelineModel.networkRequests()) {
const path = highPrioritySet.has(request.priority) ? highPath : lowPath;
const s = Math.max(Math.floor((request.startTime - timeOffset) * scale), 0);
const e = Math.min(Math.ceil((request.endTime - timeOffset) * scale + 1), canvasWidth);
path.rect(s, 0, e - s, bandHeight - 1);
}
const ctx = this.context();
ctx.save();
ctx.fillStyle = 'hsl(214, 60%, 60%)';
ctx.fill(/** @type {?} */ (highPath));
ctx.translate(0, bandHeight);
ctx.fillStyle = 'hsl(214, 80%, 80%)';
ctx.fill(/** @type {?} */ (lowPath));
ctx.restore();
}
}
/** @type {!WeakMap<!TimelineCategory, number>} */
const categoryToIndex = new WeakMap();
export class TimelineEventOverviewCPUActivity extends TimelineEventOverview {
constructor() {
super('cpu-activity', i18nString(UIStrings.cpu));
/** @type {!HTMLCanvasElement} */
this._backgroundCanvas = /** @type {!HTMLCanvasElement} */ (this.element.createChild('canvas', 'fill background'));
}
/**
* @override
*/
resetCanvas() {
super.resetCanvas();
this._backgroundCanvas.width = this.element.clientWidth * window.devicePixelRatio;
this._backgroundCanvas.height = this.element.clientHeight * window.devicePixelRatio;
}
/**
* @override
*/
update() {
super.update();
if (!this._model) {
return;
}
const timelineModel = this._model.timelineModel();
const /** @const */ quantSizePx = 4 * window.devicePixelRatio;
const width = this.width();
const height = this.height();
const baseLine = height;
const timeOffset = timelineModel.minimumRecordTime();
const timeSpan = timelineModel.maximumRecordTime() - timeOffset;
const scale = width / timeSpan;
const quantTime = quantSizePx / scale;
const categories = TimelineUIUtils.categories();
const categoryOrder = TimelineUIUtils.getTimelineMainEventCategories();
const otherIndex = categoryOrder.indexOf('other');
const idleIndex = 0;
console.assert(idleIndex === categoryOrder.indexOf('idle'));
for (let i = idleIndex + 1; i < categoryOrder.length; ++i) {
categoryToIndex.set(categories[categoryOrder[i]], i);
}
const backgroundContext = /** @type {?CanvasRenderingContext2D} */ (this._backgroundCanvas.getContext('2d'));
if (!backgroundContext) {
throw new Error('Could not find 2d canvas');
}
for (const track of timelineModel.tracks()) {
if (track.type === TimelineModel.TimelineModel.TrackType.MainThread && track.forMainFrame) {
drawThreadEvents(this.context(), track.events);
} else {
drawThreadEvents(backgroundContext, track.events);
}
}
applyPattern(backgroundContext);
/**
* @param {!CanvasRenderingContext2D} ctx
* @param {!Array<!SDK.TracingModel.Event>} events
*/
function drawThreadEvents(ctx, events) {
const quantizer = new Quantizer(timeOffset, quantTime, drawSample);
let x = 0;
/** @type {!Array<number>} */
const categoryIndexStack = [];
/** @type {!Array<!Path2D>} */
const paths = [];
/** @type {!Array<number>} */
const lastY = [];
for (let i = 0; i < categoryOrder.length; ++i) {
paths[i] = new Path2D();
paths[i].moveTo(0, height);
lastY[i] = height;
}
/**
* @param {!Array<number>} counters
*/
function drawSample(counters) {
let y = baseLine;
for (let i = idleIndex + 1; i < categoryOrder.length; ++i) {
const h = (counters[i] || 0) / quantTime * height;
y -= h;
paths[i].bezierCurveTo(x, lastY[i], x, y, x + quantSizePx / 2, y);
lastY[i] = y;
}
x += quantSizePx;
}
/**
* @param {!SDK.TracingModel.Event} e
*/
function onEventStart(e) {
const index = categoryIndexStack.length ? categoryIndexStack[categoryIndexStack.length - 1] : idleIndex;
quantizer.appendInterval(e.startTime, /** @type {number} */ (index));
categoryIndexStack.push(categoryToIndex.get(TimelineUIUtils.eventStyle(e).category) || otherIndex);
}
/**
* @param {!SDK.TracingModel.Event} e
*/
function onEventEnd(e) {
const lastCategoryIndex = categoryIndexStack.pop();
if (e.endTime !== undefined && lastCategoryIndex) {
quantizer.appendInterval(e.endTime, lastCategoryIndex);
}
}
TimelineModel.TimelineModel.TimelineModelImpl.forEachEvent(events, onEventStart, onEventEnd);
quantizer.appendInterval(timeOffset + timeSpan + quantTime, idleIndex); // Kick drawing the last bucket.
for (let i = categoryOrder.length - 1; i > 0; --i) {
paths[i].lineTo(width, height);
ctx.fillStyle = categories[categoryOrder[i]].color;
ctx.fill(paths[i]);
}
}
/**
* @param {!CanvasRenderingContext2D} ctx
*/
function applyPattern(ctx) {
const step = 4 * window.devicePixelRatio;
ctx.save();
ctx.lineWidth = step / Math.sqrt(8);
for (let x = 0.5; x < width + height; x += step) {
ctx.moveTo(x, 0);
ctx.lineTo(x - height, height);
}
ctx.globalCompositeOperation = 'destination-out';
ctx.stroke();
ctx.restore();
}
}
}
export class TimelineEventOverviewResponsiveness extends TimelineEventOverview {
constructor() {
super('responsiveness', null);
}
/**
* @override
*/
update() {
super.update();
if (!this._model) {
return;
}
const height = this.height();
const timeOffset = this._model.timelineModel().minimumRecordTime();
const timeSpan = this._model.timelineModel().maximumRecordTime() - timeOffset;
const scale = this.width() / timeSpan;
const frames = this._model.frames();
const ctx = this.context();
const fillPath = new Path2D();
const markersPath = new Path2D();
for (let i = 0; i < frames.length; ++i) {
const frame = frames[i];
if (!frame.hasWarnings()) {
continue;
}
paintWarningDecoration(frame.startTime, frame.duration);
}
for (const track of this._model.timelineModel().tracks()) {
const events = track.events;
for (let i = 0; i < events.length; ++i) {
if (!TimelineModel.TimelineModel.TimelineData.forEvent(events[i]).warning) {
continue;
}
const duration = events[i].duration;
if (duration !== undefined) {
paintWarningDecoration(events[i].startTime, duration);
}
}
}
ctx.fillStyle = 'hsl(0, 80%, 90%)';
ctx.strokeStyle = 'red';
ctx.lineWidth = 2 * window.devicePixelRatio;
ctx.fill(fillPath);
ctx.stroke(markersPath);
/**
* @param {number} time
* @param {number} duration
*/
function paintWarningDecoration(time, duration) {
const x = Math.round(scale * (time - timeOffset));
const w = Math.round(scale * duration);
fillPath.rect(x, 0, w, height);
markersPath.moveTo(x + w, 0);
markersPath.lineTo(x + w, height);
}
}
}
export class TimelineFilmStripOverview extends TimelineEventOverview {
constructor() {
super('filmstrip', null);
/** @type {!Map<!SDK.FilmStripModel.Frame,!Promise<!HTMLImageElement>>} */
this._frameToImagePromise = new Map();
/** @type {?SDK.FilmStripModel.Frame} */
this._lastFrame = null;
/** @type {?Element} */
this._lastElement = null;
this.reset();
}
/**
* @override
*/
update() {
super.update();
const frames = this._model ? this._model.filmStripModel().frames() : [];
if (!frames.length) {
return;
}
const drawGeneration = Symbol('drawGeneration');
this._drawGeneration = drawGeneration;
this._imageByFrame(frames[0]).then(image => {
if (this._drawGeneration !== drawGeneration) {
return;
}
if (!image || !image.naturalWidth || !image.naturalHeight) {
return;
}
const imageHeight = this.height() - 2 * TimelineFilmStripOverview.Padding;
const imageWidth = Math.ceil(imageHeight * image.naturalWidth / image.naturalHeight);
const popoverScale = Math.min(200 / image.naturalWidth, 1);
this._emptyImage = new Image(image.naturalWidth * popoverScale, image.naturalHeight * popoverScale);
this._drawFrames(imageWidth, imageHeight);
});
}
/**
* @param {!SDK.FilmStripModel.Frame} frame
* @return {!Promise<?HTMLImageElement>}
*/
async _imageByFrame(frame) {
/** @type {(!Promise<?HTMLImageElement>|undefined)} */
let imagePromise = this._frameToImagePromise.get(frame);
if (!imagePromise) {
const data = await frame.imageDataPromise();
imagePromise = UI.UIUtils.loadImageFromData(data);
this._frameToImagePromise.set(frame, /** @type {!Promise<!HTMLImageElement>} */ (imagePromise));
}
return imagePromise;
}
/**
* @param {number} imageWidth
* @param {number} imageHeight
*/
_drawFrames(imageWidth, imageHeight) {
if (!imageWidth || !this._model) {
return;
}
const filmStripModel = this._model.filmStripModel();
if (!filmStripModel.frames().length) {
return;
}
const padding = TimelineFilmStripOverview.Padding;
const width = this.width();
const zeroTime = filmStripModel.zeroTime();
const spanTime = filmStripModel.spanTime();
const scale = spanTime / width;
const context = this.context();
const drawGeneration = this._drawGeneration;
context.beginPath();
for (let x = padding; x < width; x += imageWidth + 2 * padding) {
const time = zeroTime + (x + imageWidth / 2) * scale;
const frame = filmStripModel.frameByTimestamp(time);
if (!frame) {
continue;
}
context.rect(x - 0.5, 0.5, imageWidth + 1, imageHeight + 1);
this._imageByFrame(frame).then(drawFrameImage.bind(this, x));
}
context.strokeStyle = '#ddd';
context.stroke();
/**
* @param {number} x
* @param {?HTMLImageElement} image
* @this {TimelineFilmStripOverview}
*/
function drawFrameImage(x, image) {
// Ignore draws deferred from a previous update call.
if (this._drawGeneration !== drawGeneration || !image) {
return;
}
context.drawImage(image, x, 1, imageWidth, imageHeight);
}
}
/**
* @override
* @param {number} x
* @return {!Promise<?Element>}
*/
async overviewInfoPromise(x) {
if (!this._model || !this._model.filmStripModel().frames().length) {
return null;
}
const calculator = this.calculator();
if (!calculator) {
return null;
}
const time = calculator.positionToTime(x);
const frame = this._model.filmStripModel().frameByTimestamp(time);
if (frame === this._lastFrame) {
return this._lastElement;
}
const imagePromise = frame ? this._imageByFrame(frame) : Promise.resolve(this._emptyImage);
const image = await imagePromise;
const element = document.createElement('div');
element.classList.add('frame');
if (image) {
element.createChild('div', 'thumbnail').appendChild(image);
}
this._lastFrame = frame;
this._lastElement = element;
return element;
}
/**
* @override
*/
reset() {
this._lastFrame = null;
this._lastElement = null;
/** @type {!Map<!SDK.FilmStripModel.Frame,!Promise<!HTMLImageElement>>} */
this._frameToImagePromise = new Map();
this._imageWidth = 0;
}
}
TimelineFilmStripOverview.Padding = 2;
export class TimelineEventOverviewFrames extends TimelineEventOverview {
constructor() {
super('framerate', i18nString(UIStrings.fps));
}
/**
* @override
*/
update() {
super.update();
if (!this._model) {
return;
}
const frames = this._model.frames();
if (!frames.length) {
return;
}
const height = this.height();
const /** @const */ padding = Number(window.devicePixelRatio);
const /** @const */ baseFrameDurationMs = 1e3 / 60;
const visualHeight = height - 2 * padding;
const timeOffset = this._model.timelineModel().minimumRecordTime();
const timeSpan = this._model.timelineModel().maximumRecordTime() - timeOffset;
const scale = this.width() / timeSpan;
const baseY = height - padding;
const ctx = this.context();
const bottomY = baseY + 10 * window.devicePixelRatio;
let x = 0;
let y = bottomY;
const lineWidth = window.devicePixelRatio;
const offset = lineWidth & 1 ? 0.5 : 0;
const tickDepth = 1.5 * window.devicePixelRatio;
ctx.beginPath();
ctx.moveTo(0, y);
for (let i = 0; i < frames.length; ++i) {
const frame = frames[i];
x = Math.round((frame.startTime - timeOffset) * scale) + offset;
ctx.lineTo(x, y);
ctx.lineTo(x, y + tickDepth);
y = frame.idle ? bottomY :
Math.round(baseY - visualHeight * Math.min(baseFrameDurationMs / frame.duration, 1)) - offset;
ctx.lineTo(x, y + tickDepth);
ctx.lineTo(x, y);
}
const lastFrame = frames[frames.length - 1];
if (lastFrame) {
x = Math.round((lastFrame.startTime + lastFrame.duration - timeOffset) * scale) + offset;
}
ctx.lineTo(x, y);
ctx.lineTo(x, bottomY);
ctx.fillStyle = 'hsl(110, 50%, 88%)';
ctx.strokeStyle = 'hsl(110, 50%, 60%)';
ctx.lineWidth = lineWidth;
ctx.fill();
ctx.stroke();
}
}
export class TimelineEventOverviewMemory extends TimelineEventOverview {
constructor() {
super('memory', i18nString(UIStrings.heap));
this._heapSizeLabel = this.element.createChild('div', 'memory-graph-label');
}
resetHeapSizeLabels() {
this._heapSizeLabel.textContent = '';
}
/**
* @override
*/
update() {
super.update();
const ratio = window.devicePixelRatio;
if (!this._model) {
this.resetHeapSizeLabels();
return;
}
const tracks = this._model.timelineModel().tracks().filter(
track => track.type === TimelineModel.TimelineModel.TrackType.MainThread && track.forMainFrame);
const trackEvents = tracks.map(track => track.events);
const lowerOffset = 3 * ratio;
let maxUsedHeapSize = 0;
let minUsedHeapSize = 100000000000;
const minTime = this._model.timelineModel().minimumRecordTime();
const maxTime = this._model.timelineModel().maximumRecordTime();
/**
* @param {!SDK.TracingModel.Event} event
* @return {boolean}
*/
function isUpdateCountersEvent(event) {
return event.name === TimelineModel.TimelineModel.RecordType.UpdateCounters;
}
for (let i = 0; i < trackEvents.length; i++) {
trackEvents[i] = trackEvents[i].filter(isUpdateCountersEvent);
}
/**
* @param {!SDK.TracingModel.Event} event
*/
function calculateMinMaxSizes(event) {
const counters = event.args.data;
if (!counters || !counters.jsHeapSizeUsed) {
return;
}
maxUsedHeapSize = Math.max(maxUsedHeapSize, counters.jsHeapSizeUsed);
minUsedHeapSize = Math.min(minUsedHeapSize, counters.jsHeapSizeUsed);
}
for (let i = 0; i < trackEvents.length; i++) {
trackEvents[i].forEach(calculateMinMaxSizes);
}
minUsedHeapSize = Math.min(minUsedHeapSize, maxUsedHeapSize);
const lineWidth = 1;
const width = this.width();
const height = this.height() - lowerOffset;
const xFactor = width / (maxTime - minTime);
const yFactor = (height - lineWidth) / Math.max(maxUsedHeapSize - minUsedHeapSize, 1);
const histogram = new Array(width);
/**
* @param {!SDK.TracingModel.Event} event
*/
function buildHistogram(event) {
const counters = event.args.data;
if (!counters || !counters.jsHeapSizeUsed) {
return;
}
const x = Math.round((event.startTime - minTime) * xFactor);
const y = Math.round((counters.jsHeapSizeUsed - minUsedHeapSize) * yFactor);
// TODO(alph): use sum instead of max.
histogram[x] = Math.max(histogram[x] || 0, y);
}
for (let i = 0; i < trackEvents.length; i++) {
trackEvents[i].forEach(buildHistogram);
}
const ctx = this.context();
const heightBeyondView = height + lowerOffset + lineWidth;
ctx.translate(0.5, 0.5);
ctx.beginPath();
ctx.moveTo(-lineWidth, heightBeyondView);
let y = 0;
let isFirstPoint = true;
let lastX = 0;
for (let x = 0; x < histogram.length; x++) {
if (typeof histogram[x] === 'undefined') {
continue;
}
if (isFirstPoint) {
isFirstPoint = false;
y = histogram[x];
ctx.lineTo(-lineWidth, height - y);
}
const nextY = histogram[x];
if (Math.abs(nextY - y) > 2 && Math.abs(x - lastX) > 1) {
ctx.lineTo(x, height - y);
}
y = nextY;
ctx.lineTo(x, height - y);
lastX = x;
}
ctx.lineTo(width + lineWidth, height - y);
ctx.lineTo(width + lineWidth, heightBeyondView);
ctx.closePath();
ctx.fillStyle = 'hsla(220, 90%, 70%, 0.2)';
ctx.fill();
ctx.lineWidth = lineWidth;
ctx.strokeStyle = 'hsl(220, 90%, 70%)';
ctx.stroke();
this._heapSizeLabel.textContent = i18nString(UIStrings.sSDash, {
PH1: Platform.NumberUtilities.bytesToString(minUsedHeapSize),
PH2: Platform.NumberUtilities.bytesToString(maxUsedHeapSize)
});
}
}
export class Quantizer {
/**
* @param {number} startTime
* @param {number} quantDuration
* @param {function(!Array<number>):void} callback
*/
constructor(startTime, quantDuration, callback) {
this._lastTime = startTime;
this._quantDuration = quantDuration;
this._callback = callback;
/** @type {!Array<number>} */
this._counters = [];
this._remainder = quantDuration;
}
/**
* @param {number} time
* @param {number} group
*/
appendInterval(time, group) {
let interval = time - this._lastTime;
if (interval <= this._remainder) {
this._counters[group] = (this._counters[group] || 0) + interval;
this._remainder -= interval;
this._lastTime = time;
return;
}
this._counters[group] = (this._counters[group] || 0) + this._remainder;
this._callback(this._counters);
interval -= this._remainder;
while (interval >= this._quantDuration) {
const counters = [];
counters[group] = this._quantDuration;
this._callback(counters);
interval -= this._quantDuration;
}
this._counters = [];
this._counters[group] = interval;
this._lastTime = time;
this._remainder = this._quantDuration - interval;
}
}
export class TimelineEventOverviewCoverage extends TimelineEventOverview {
constructor() {
super('coverage', i18nString(UIStrings.coverage));
this._heapSizeLabel = this.element.createChild('div', 'timeline-overview-coverage-label');
}
resetHeapSizeLabels() {
this._heapSizeLabel.textContent = '';
}
/**
* @override
* @param {?PerformanceModel} model
*/
setModel(model) {
super.setModel(model);
if (model) {
const mainTarget = model.mainTarget();
if (mainTarget) {
this._coverageModel = mainTarget.model(Coverage.CoverageModel.CoverageModel);
}
}
}
/**
* @override
*/
update() {
super.update();
const ratio = window.devicePixelRatio;
if (!this._coverageModel) {
return;
}
let total = 0;
let total_used = 0;
/** @type {!Map<number, number>} */
const usedByTimestamp = new Map();
/** @type {!Map<number, !Set<!Coverage.CoverageModel.CoverageInfo>>} */
const totalByTimestamp = new Map();
for (const urlInfo of this._coverageModel.entries()) {
for (const info of urlInfo.entries()) {
total += info.size();
for (const [stamp, used] of info.usedByTimestamp()) {
total_used += used;
let uniqueTimestamps = totalByTimestamp.get(stamp);
if (uniqueTimestamps === undefined) {
uniqueTimestamps = new Set();
totalByTimestamp.set(stamp, uniqueTimestamps);
}
uniqueTimestamps.add(info);
const previousCount = usedByTimestamp.get(stamp);
if (previousCount === undefined) {
usedByTimestamp.set(stamp, used);
} else {
usedByTimestamp.set(stamp, previousCount + used);
}
}
}
}
/** @type {!Set<!Coverage.CoverageModel.CoverageInfo>} */
const seen = new Set();
/** @type {!Map<number, number>} */
const coverageByTimestamp = new Map();
let sumTotal = 0, sumUsed = 0;
const sortedByTimestamp = Array.from(totalByTimestamp.entries()).sort((a, b) => a[0] - b[0]);
for (const [stamp, infos] of sortedByTimestamp) {
for (const info of infos.values()) {
if (seen.has(info)) {
continue;
}
seen.add(info);
sumTotal += info.size();
}
sumUsed += usedByTimestamp.get(stamp) || 0;
coverageByTimestamp.set(stamp, sumUsed / sumTotal);
}
const percentUsed = total ? Math.round(100 * total_used / total) : 0;
const lowerOffset = 3 * ratio;
const millisecondsPerSecond = 1000;
if (!this._model) {
return;
}
const minTime = this._model.timelineModel().minimumRecordTime() / millisecondsPerSecond;
const maxTime = this._model.timelineModel().maximumRecordTime() / millisecondsPerSecond;
const lineWidth = 1;
const width = this.width();
const height = this.height() - lowerOffset;
const xFactor = width / (maxTime - minTime);
const yFactor = height - lineWidth;
let yOffset = 0;
const ctx = this.context();
const heightBeyondView = height + lowerOffset + lineWidth;
ctx.translate(0.5, 0.5);
ctx.beginPath();
ctx.moveTo(-lineWidth, heightBeyondView);
ctx.lineTo(-lineWidth, height - yOffset);
let previous = null;
for (const stamp of this._coverageModel.coverageUpdateTimes()) {
/** @type {?number} */
const coverage = coverageByTimestamp.get(stamp) || previous;
previous = coverage;
if (!coverage) {
continue;
}
if (stamp > maxTime) {
break;
}
const x = (stamp - minTime) * xFactor;
yOffset = coverage * yFactor;
ctx.lineTo(x, height - yOffset);
}
const white = 'hsl(0, 100%, 100%)';
const blue = 'hsl(220, 90%, 70%)';
const transparentBlue = 'hsla(220, 90%, 70%, 0.2)';
ctx.lineTo(width + lineWidth, height - yOffset);
ctx.lineTo(width + lineWidth, heightBeyondView);
ctx.closePath();
ctx.fillStyle = transparentBlue;
ctx.fill();
ctx.lineWidth = lineWidth;
ctx.strokeStyle = blue;
ctx.stroke();
previous = null;
for (const stamp of this._coverageModel.coverageUpdateTimes()) {
/** @type {?number} */
const coverage = coverageByTimestamp.get(stamp) || previous;
previous = coverage;
if (!coverage) {
continue;
}
ctx.beginPath();
const x = (stamp - minTime) * xFactor;
const y = height - coverage * yFactor;
ctx.arc(x, y, 2 * lineWidth, 0, 2 * Math.PI, false);
ctx.closePath();
ctx.stroke();
ctx.fillStyle = coverageByTimestamp.has(stamp) ? blue : white;
ctx.fill();
}
this._heapSizeLabel.textContent = `${percentUsed}% used`;
}
}