| /* |
| * 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. |
| */ |
| |
| /** |
| * @unrestricted |
| */ |
| Timeline.TimelineEventOverview = class extends PerfUI.TimelineOverviewBase { |
| /** |
| * @param {string} id |
| * @param {?string} title |
| */ |
| constructor(id, title) { |
| super(); |
| this.element.id = 'timeline-overview-' + id; |
| this.element.classList.add('overview-strip'); |
| /** @type {?Timeline.PerformanceModel} */ |
| this._model = null; |
| if (title) { |
| this.element.createChild('div', 'timeline-overview-strip-title').textContent = title; |
| } |
| } |
| |
| /** |
| * @param {?Timeline.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); |
| } |
| }; |
| |
| /** |
| * @unrestricted |
| */ |
| Timeline.TimelineEventOverviewInput = class extends Timeline.TimelineEventOverview { |
| constructor() { |
| super('input', null); |
| } |
| |
| /** |
| * @override |
| */ |
| update() { |
| super.update(); |
| if (!this._model) { |
| return; |
| } |
| const height = this.height(); |
| const descriptors = Timeline.TimelineUIUtils.eventDispatchDesciptors(); |
| /** @type {!Map.<string,!Timeline.TimelineUIUtils.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; |
| } |
| const start = Number.constrain(Math.floor((event.startTime - timeOffset) * scale), 0, canvasWidth); |
| const end = Number.constrain(Math.ceil((event.endTime - timeOffset) * scale), 0, canvasWidth); |
| const width = Math.max(end - start, minWidth); |
| this._renderBar(start, start + width, 0, height, descriptor.color); |
| } |
| } |
| } |
| } |
| }; |
| |
| /** |
| * @unrestricted |
| */ |
| Timeline.TimelineEventOverviewNetwork = class extends Timeline.TimelineEventOverview { |
| constructor() { |
| super('network', Common.UIString('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(); |
| } |
| }; |
| |
| /** |
| * @unrestricted |
| */ |
| Timeline.TimelineEventOverviewCPUActivity = class extends Timeline.TimelineEventOverview { |
| constructor() { |
| super('cpu-activity', Common.UIString('CPU')); |
| this._backgroundCanvas = 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 = Timeline.TimelineUIUtils.categories(); |
| const categoryOrder = Timeline.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) { |
| categories[categoryOrder[i]]._overviewIndex = i; |
| } |
| |
| const backgroundContext = this._backgroundCanvas.getContext('2d'); |
| 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 Timeline.Quantizer(timeOffset, quantTime, drawSample); |
| let x = 0; |
| const categoryIndexStack = []; |
| const paths = []; |
| 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.peekLast() : idleIndex; |
| quantizer.appendInterval(e.startTime, index); |
| categoryIndexStack.push(Timeline.TimelineUIUtils.eventStyle(e).category._overviewIndex || otherIndex); |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} e |
| */ |
| function onEventEnd(e) { |
| quantizer.appendInterval(e.endTime, categoryIndexStack.pop()); |
| } |
| |
| TimelineModel.TimelineModel.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(); |
| } |
| } |
| }; |
| |
| /** |
| * @unrestricted |
| */ |
| Timeline.TimelineEventOverviewResponsiveness = class extends Timeline.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(); |
| // This is due to usage of new signatures of fill() and storke() that closure compiler does not recognize. |
| const ctx = /** @type {!Object} */ (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.TimelineData.forEvent(events[i]).warning) { |
| continue; |
| } |
| paintWarningDecoration(events[i].startTime, events[i].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); |
| } |
| } |
| }; |
| |
| /** |
| * @unrestricted |
| */ |
| Timeline.TimelineFilmStripOverview = class extends Timeline.TimelineEventOverview { |
| constructor() { |
| super('filmstrip', 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 * Timeline.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>} |
| */ |
| _imageByFrame(frame) { |
| let imagePromise = this._frameToImagePromise.get(frame); |
| if (!imagePromise) { |
| imagePromise = frame.imageDataPromise().then(data => UI.loadImageFromData(data)); |
| this._frameToImagePromise.set(frame, 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 = Timeline.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 {Timeline.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>} |
| */ |
| overviewInfoPromise(x) { |
| if (!this._model || !this._model.filmStripModel().frames().length) { |
| return Promise.resolve(/** @type {?Element} */ (null)); |
| } |
| |
| const time = this.calculator().positionToTime(x); |
| const frame = this._model.filmStripModel().frameByTimestamp(time); |
| if (frame === this._lastFrame) { |
| return Promise.resolve(this._lastElement); |
| } |
| const imagePromise = frame ? this._imageByFrame(frame) : Promise.resolve(this._emptyImage); |
| return imagePromise.then(createFrameElement.bind(this)); |
| |
| /** |
| * @this {Timeline.TimelineFilmStripOverview} |
| * @param {?HTMLImageElement} image |
| * @return {?Element} |
| */ |
| function createFrameElement(image) { |
| const element = createElementWithClass('div', 'frame'); |
| if (image) { |
| element.createChild('div', 'thumbnail').appendChild(image); |
| } |
| this._lastFrame = frame; |
| this._lastElement = element; |
| return element; |
| } |
| } |
| |
| /** |
| * @override |
| */ |
| reset() { |
| this._lastFrame = undefined; |
| this._lastElement = null; |
| /** @type {!Map<!SDK.FilmStripModel.Frame,!Promise<!HTMLImageElement>>} */ |
| this._frameToImagePromise = new Map(); |
| this._imageWidth = 0; |
| } |
| }; |
| |
| Timeline.TimelineFilmStripOverview.Padding = 2; |
| |
| /** |
| * @unrestricted |
| */ |
| Timeline.TimelineEventOverviewFrames = class extends Timeline.TimelineEventOverview { |
| constructor() { |
| super('framerate', Common.UIString('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 = 1 * 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.peekLast(); |
| 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(); |
| } |
| }; |
| |
| /** |
| * @unrestricted |
| */ |
| Timeline.TimelineEventOverviewMemory = class extends Timeline.TimelineEventOverview { |
| constructor() { |
| super('memory', Common.UIString('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 = |
| Common.UIString('%s \u2013 %s', Number.bytesToString(minUsedHeapSize), Number.bytesToString(maxUsedHeapSize)); |
| } |
| }; |
| |
| /** |
| * @unrestricted |
| */ |
| Timeline.Quantizer = class { |
| /** |
| * @param {number} startTime |
| * @param {number} quantDuration |
| * @param {function(!Array<number>)} callback |
| */ |
| constructor(startTime, quantDuration, callback) { |
| this._lastTime = startTime; |
| this._quantDuration = quantDuration; |
| this._callback = callback; |
| 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; |
| } |
| }; |
| |
| /** |
| * @unrestricted |
| */ |
| Timeline.TimelineEventOverviewCoverage = class extends Timeline.TimelineEventOverview { |
| constructor() { |
| super('coverage', Common.UIString('COVERAGE')); |
| this._heapSizeLabel = this.element.createChild('div', 'timeline-overview-coverage-label'); |
| } |
| |
| resetHeapSizeLabels() { |
| this._heapSizeLabel.textContent = ''; |
| } |
| |
| /** |
| * @override |
| * @param {?Timeline.PerformanceModel} model |
| */ |
| setModel(model) { |
| super.setModel(model); |
| if (this._model) { |
| this._coverageModel = model.mainTarget().model(Coverage.CoverageModel); |
| } |
| } |
| |
| /** |
| * @override |
| */ |
| update() { |
| super.update(); |
| const ratio = window.devicePixelRatio; |
| |
| if (!this._coverageModel) { |
| return; |
| } |
| |
| let total = 0; |
| let total_used = 0; |
| /** @type {!Map<!Coverage.CoverageInfo>} */ |
| const usedByTimestamp = new Map(); |
| /** @type {!Map<!Coverage.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; |
| |
| if (!totalByTimestamp.has(stamp)) { |
| totalByTimestamp.set(stamp, new Set()); |
| } |
| |
| totalByTimestamp.get(stamp).add(info); |
| |
| if (!usedByTimestamp.has(stamp)) { |
| usedByTimestamp.set(stamp, used); |
| } else { |
| usedByTimestamp.set(stamp, usedByTimestamp.get(stamp) + used); |
| } |
| } |
| } |
| } |
| |
| /** @type {!Set<!Coverage.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); |
| coverageByTimestamp.set(stamp, sumUsed / sumTotal); |
| } |
| |
| const percentUsed = total ? Math.round(100 * total_used / total) : 0; |
| const lowerOffset = 3 * ratio; |
| |
| const minTime = this._model.recordStartTime(); |
| const maxTime = |
| minTime + (this._model.timelineModel().maximumRecordTime() - this._model.timelineModel().minimumRecordTime()); |
| |
| 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); |
| |
| for (const [stamp, coverage] of coverageByTimestamp) { |
| if (stamp > maxTime) { |
| break; |
| } |
| const x = (stamp - minTime) * xFactor; |
| yOffset = coverage * yFactor; |
| ctx.lineTo(x, height - yOffset); |
| } |
| |
| ctx.lineTo(width + lineWidth, height - yOffset); |
| 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(); |
| |
| for (const [stamp, coverage] of coverageByTimestamp) { |
| if (stamp > maxTime) { |
| break; |
| } |
| 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.fill(); |
| } |
| |
| this._heapSizeLabel.textContent = `${percentUsed}% used`; |
| } |
| }; |