UNPKG

chrome-devtools-frontend

Version:
703 lines (628 loc) 27.7 kB
/* * 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. */ /* eslint-disable rulesdir/no-imperative-dom-api */ import * as i18n from '../../core/i18n/i18n.js'; import * as Trace from '../../models/trace/trace.js'; import * as TraceBounds from '../../services/trace_bounds/trace_bounds.js'; import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as VisualLogging from '../../ui/visual_logging/visual_logging.js'; import * as Utils from './utils/utils.js'; const UIStrings = { /** *@description Short for Network. Label for the network requests section 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 */ 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}', } as const; const str_ = i18n.i18n.registerUIStrings('panels/timeline/TimelineEventOverview.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export abstract class TimelineEventOverview extends PerfUI.TimelineOverviewPane.TimelineOverviewBase { constructor(id: string, title: string|null) { super(); this.element.id = 'timeline-overview-' + id; this.element.classList.add('overview-strip'); if (title) { this.element.createChild('div', 'timeline-overview-strip-title').textContent = title; } } renderBar(begin: number, end: number, position: number, height: number, color: string): void { const x = begin; const width = end - begin; const ctx = this.context(); ctx.fillStyle = color; ctx.fillRect(x, position, width, height); } } export class TimelineEventOverviewNetwork extends TimelineEventOverview { #parsedTrace: Trace.Handlers.Types.ParsedTrace; constructor(parsedTrace: Trace.Handlers.Types.ParsedTrace) { super('network', i18nString(UIStrings.net)); this.#parsedTrace = parsedTrace; } override update(start?: Trace.Types.Timing.Milli, end?: Trace.Types.Timing.Milli): void { this.resetCanvas(); this.#renderWithParsedTrace(start, end); } #renderWithParsedTrace(start?: Trace.Types.Timing.Milli, end?: Trace.Types.Timing.Milli): void { if (!this.#parsedTrace) { return; } // Because the UI is in milliseconds, we work with milliseconds through // this function to get the right scale and sizing const traceBoundsMilli = (start && end) ? { min: start, max: end, range: end - start, } : Trace.Helpers.Timing.traceWindowMilliSeconds(this.#parsedTrace.Meta.traceBounds); // We draw two paths, so each can take up half the height const pathHeight = this.height() / 2; const canvasWidth = this.width(); const scale = canvasWidth / traceBoundsMilli.range; // We draw network requests in two chunks: // Requests with a priority of Medium or higher go onto the first path // Other requests go onto the second path. const highPath = new Path2D(); const lowPath = new Path2D(); for (const request of this.#parsedTrace.NetworkRequests.byTime) { const path = Trace.Helpers.Network.isSyntheticNetworkRequestHighPriority(request) ? highPath : lowPath; const {startTime, endTime} = Trace.Helpers.Timing.eventTimingsMilliSeconds(request); const rectStart = Math.max(Math.floor((startTime - traceBoundsMilli.min) * scale), 0); const rectEnd = Math.min(Math.ceil((endTime - traceBoundsMilli.min) * scale + 1), canvasWidth); path.rect(rectStart, 0, rectEnd - rectStart, pathHeight - 1); } const ctx = this.context(); ctx.save(); // Draw the high path onto the canvas. ctx.fillStyle = 'hsl(214, 60%, 60%)'; ctx.fill(highPath); // Now jump down by the height of the high path, and then draw the low path. ctx.translate(0, pathHeight); ctx.fillStyle = 'hsl(214, 80%, 80%)'; ctx.fill(lowPath); ctx.restore(); } } const categoryToIndex = new WeakMap<Utils.EntryStyles.TimelineCategory, number>(); export class TimelineEventOverviewCPUActivity extends TimelineEventOverview { private backgroundCanvas: HTMLCanvasElement; #parsedTrace: Trace.Handlers.Types.ParsedTrace; #drawn = false; #start: Trace.Types.Timing.Milli; #end: Trace.Types.Timing.Milli; constructor(parsedTrace: Trace.Handlers.Types.ParsedTrace) { // During the sync tracks migration this component can use either legacy // Performance Model data or the new engine's data. Once the migration is // complete this will be updated to only use the new engine and mentions of // the PerformanceModel will be removed. super('cpu-activity', i18nString(UIStrings.cpu)); this.#parsedTrace = parsedTrace; this.backgroundCanvas = this.element.createChild('canvas', 'fill background'); this.#start = Trace.Helpers.Timing.traceWindowMilliSeconds(parsedTrace.Meta.traceBounds).min; this.#end = Trace.Helpers.Timing.traceWindowMilliSeconds(parsedTrace.Meta.traceBounds).max; } #entryCategory(entry: Trace.Types.Events.Event): Utils.EntryStyles.EventCategory|undefined { // Special case: in CPU Profiles we get a lot of ProfileCalls that // represent Idle time. We typically represent ProfileCalls in the // Scripting Category, but if they represent idle time, we do not want // that. if (Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === '(idle)') { return Utils.EntryStyles.EventCategory.IDLE; } if (Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === '(program)') { return Utils.EntryStyles.EventCategory.OTHER; } const eventStyle = Utils.EntryStyles.getEventStyle(entry.name as Trace.Types.Events.Name)?.category || Utils.EntryStyles.getCategoryStyles().other; const categoryName = eventStyle.name; return categoryName; } override resetCanvas(): void { super.resetCanvas(); this.#drawn = false; this.backgroundCanvas.width = this.element.clientWidth * window.devicePixelRatio; this.backgroundCanvas.height = this.element.clientHeight * window.devicePixelRatio; } #draw(parsedTrace: Trace.Handlers.Types.ParsedTrace): void { const quantSizePx = 4 * window.devicePixelRatio; const width = this.width(); const height = this.height(); const baseLine = height; const timeRange = this.#end - this.#start; const scale = width / timeRange; const quantTime = quantSizePx / scale; const categories = Utils.EntryStyles.getCategoryStyles(); const categoryOrder = Utils.EntryStyles.getTimelineMainEventCategories(); const otherIndex = categoryOrder.indexOf(Utils.EntryStyles.EventCategory.OTHER); const idleIndex = 0; console.assert(idleIndex === categoryOrder.indexOf(Utils.EntryStyles.EventCategory.IDLE)); for (let i = 0; i < categoryOrder.length; ++i) { categoryToIndex.set(categories[categoryOrder[i]], i); } const drawThreadEntries = (context: CanvasRenderingContext2D, threadData: Trace.Handlers.Threads.ThreadData): void => { const quantizer = new Quantizer(this.#start, quantTime, drawSample); let x = 0; const categoryIndexStack: number[] = []; const paths: Path2D[] = []; const lastY: number[] = []; for (let i = 0; i < categoryOrder.length; ++i) { paths[i] = new Path2D(); paths[i].moveTo(0, height); lastY[i] = height; } function drawSample(counters: number[]): void { 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; } const onEntryStart = (entry: Trace.Types.Events.Event): void => { const category = this.#entryCategory(entry); if (!category || category === 'idle') { // Idle event won't show in CPU activity, so just skip them. return; } const startTimeMilli = Trace.Helpers.Timing.microToMilli(entry.ts); const index = categoryIndexStack.length ? categoryIndexStack[categoryIndexStack.length - 1] : idleIndex; quantizer.appendInterval(startTimeMilli, index); const categoryIndex = categoryOrder.indexOf(category); categoryIndexStack.push(categoryIndex || otherIndex); }; function onEntryEnd(entry: Trace.Types.Events.Event): void { const endTimeMilli = Trace.Helpers.Timing.microToMilli(entry.ts) + Trace.Helpers.Timing.microToMilli(Trace.Types.Timing.Micro(entry.dur || 0)); const lastCategoryIndex = categoryIndexStack.pop(); if (endTimeMilli !== undefined && lastCategoryIndex) { quantizer.appendInterval(endTimeMilli, lastCategoryIndex); } } const startMicro = Trace.Helpers.Timing.milliToMicro(this.#start); const endMicro = Trace.Helpers.Timing.milliToMicro(this.#end); const bounds = { min: startMicro, max: endMicro, range: Trace.Types.Timing.Micro(endMicro - startMicro), }; // Filter out tiny events - they don't make a visual impact to the // canvas as they are so small, but they do impact the time it takes // to walk the tree and render the events. // However, if the entire range we are showing is 200ms or less, then show all events. const minDuration = Trace.Types.Timing.Micro( bounds.range > 200_000 ? 16_000 : 0, ); Trace.Helpers.TreeHelpers.walkEntireTree( threadData.entryToNode, threadData.tree, onEntryStart, onEntryEnd, bounds, minDuration); quantizer.appendInterval(this.#start + timeRange + quantTime, idleIndex); // Kick drawing the last bucket. for (let i = categoryOrder.length - 1; i > 0; --i) { paths[i].lineTo(width, height); const computedColorValue = categories[categoryOrder[i]].getComputedColorValue(); context.fillStyle = computedColorValue; context.fill(paths[i]); context.strokeStyle = 'white'; context.lineWidth = 1; context.stroke(paths[i]); } }; const backgroundContext = (this.backgroundCanvas.getContext('2d')); if (!backgroundContext) { throw new Error('Could not find 2d canvas'); } const threads = Trace.Handlers.Threads.threadsInTrace(parsedTrace); const mainThreadContext = this.context(); for (const thread of threads) { // We treat CPU_PROFILE as main thread because in a CPU Profile trace there is only ever one thread. const isMainThread = thread.type === Trace.Handlers.Threads.ThreadType.MAIN_THREAD || thread.type === Trace.Handlers.Threads.ThreadType.CPU_PROFILE; if (isMainThread) { drawThreadEntries(mainThreadContext, thread); } else { drawThreadEntries(backgroundContext, thread); } } function applyPattern(ctx: CanvasRenderingContext2D): void { 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(); } applyPattern(backgroundContext); } override update(): void { const traceBoundsState = TraceBounds.TraceBounds.BoundsManager.instance().state(); const bounds = traceBoundsState?.milli.minimapTraceBounds; if (!bounds) { return; } if (bounds.min === this.#start && bounds.max === this.#end && this.#drawn) { return; } this.#start = bounds.min; this.#end = bounds.max; // Order matters here, resetCanvas will set this.#drawn to false. this.resetCanvas(); this.#drawn = true; this.#draw(this.#parsedTrace); } } export class TimelineEventOverviewResponsiveness extends TimelineEventOverview { #parsedTrace: Trace.Handlers.Types.ParsedTrace; constructor(parsedTrace: Trace.Handlers.Types.ParsedTrace) { super('responsiveness', null); this.#parsedTrace = parsedTrace; } #gatherEventsWithRelevantWarnings(): Set<Trace.Types.Events.Event> { const {topLevelRendererIds} = this.#parsedTrace.Meta; // All the warnings that we care about regarding responsiveness and want to represent on the overview. const warningsForResponsiveness = new Set<Trace.Handlers.ModelHandlers.Warnings.Warning>([ 'LONG_TASK', 'FORCED_REFLOW', 'IDLE_CALLBACK_OVER_TIME', ]); const allWarningEvents = new Set<Trace.Types.Events.Event>(); for (const warning of warningsForResponsiveness) { const eventsForWarning = this.#parsedTrace.Warnings.perWarning.get(warning); if (!eventsForWarning) { continue; } for (const event of eventsForWarning) { // Only keep events whose PID is a top level renderer, which means it // was on the main thread. This avoids showing issues from iframes or // other sub-frames in the minimap overview. if (topLevelRendererIds.has(event.pid)) { allWarningEvents.add(event); } } } return allWarningEvents; } override update(start?: Trace.Types.Timing.Milli, end?: Trace.Types.Timing.Milli): void { this.resetCanvas(); const height = this.height(); const visibleTimeWindow = !(start && end) ? this.#parsedTrace.Meta.traceBounds : { min: Trace.Helpers.Timing.milliToMicro(start), max: Trace.Helpers.Timing.milliToMicro(end), range: Trace.Helpers.Timing.milliToMicro(Trace.Types.Timing.Milli(end - start)), }; const timeSpan = visibleTimeWindow.range; const scale = this.width() / timeSpan; const ctx = this.context(); const fillPath = new Path2D(); const markersPath = new Path2D(); const eventsWithWarning = this.#gatherEventsWithRelevantWarnings(); for (const event of eventsWithWarning) { paintWarningDecoration(event); } ctx.fillStyle = 'hsl(0, 80%, 90%)'; ctx.strokeStyle = 'red'; ctx.lineWidth = 2 * window.devicePixelRatio; ctx.fill(fillPath); ctx.stroke(markersPath); function paintWarningDecoration(event: Trace.Types.Events.Event): void { const {startTime, duration} = Trace.Helpers.Timing.eventTimingsMicroSeconds(event); const x = Math.round(scale * (startTime - visibleTimeWindow.min)); const width = Math.round(scale * duration); fillPath.rect(x, 0, width, height); markersPath.moveTo(x + width, 0); markersPath.lineTo(x + width, height); } } } export class TimelineFilmStripOverview extends TimelineEventOverview { private frameToImagePromise: Map<Trace.Extras.FilmStrip.Frame, Promise<HTMLImageElement>>; private lastFrame: Trace.Extras.FilmStrip.Frame|null = null; private lastElement: Element|null; private drawGeneration?: symbol; private emptyImage?: HTMLImageElement; #filmStrip: Trace.Extras.FilmStrip.Data|null = null; constructor(filmStrip: Trace.Extras.FilmStrip.Data) { super('filmstrip', null); this.element.setAttribute('jslog', `${VisualLogging.section('film-strip')}`); this.frameToImagePromise = new Map(); this.#filmStrip = filmStrip; this.lastFrame = null; this.lastElement = null; this.reset(); } override update(customStartTime?: Trace.Types.Timing.Milli, customEndTime?: Trace.Types.Timing.Milli): void { this.resetCanvas(); const frames = this.#filmStrip ? this.#filmStrip.frames : []; if (!frames.length) { return; } if (this.height() === 0) { // Height of 0 causes the maths below to get off and generate very large // negative numbers that cause an extremely long loop when attempting to // draw images by frame. Rather than that, let's warn and exist early. console.warn('TimelineFilmStrip could not be drawn as its canvas height is 0'); return; } const drawGeneration = Symbol('drawGeneration'); this.drawGeneration = drawGeneration; void this.imageByFrame(frames[0]).then(image => { if (this.drawGeneration !== drawGeneration) { return; } if (!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, customStartTime, customEndTime); }); } private async imageByFrame(frame: Trace.Extras.FilmStrip.Frame): Promise<HTMLImageElement|null> { let imagePromise: Promise<HTMLImageElement|null>|undefined = this.frameToImagePromise.get(frame); if (!imagePromise) { // TODO(paulirish): Adopt Util.ImageCache const uri = Trace.Handlers.ModelHandlers.Screenshots.screenshotImageDataUri(frame.screenshotEvent); imagePromise = UI.UIUtils.loadImage(uri); this.frameToImagePromise.set(frame, (imagePromise as Promise<HTMLImageElement>)); } return await imagePromise; } private drawFrames( imageWidth: number, imageHeight: number, customStartTime?: Trace.Types.Timing.Milli, customEndTime?: Trace.Types.Timing.Milli): void { if (!imageWidth) { return; } if (!this.#filmStrip || this.#filmStrip.frames.length < 1) { return; } const padding = TimelineFilmStripOverview.Padding; const width = this.width(); const zeroTime = customStartTime ?? Trace.Helpers.Timing.microToMilli(this.#filmStrip.zeroTime); const spanTime = customEndTime ? customEndTime - zeroTime : Trace.Helpers.Timing.microToMilli(this.#filmStrip.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 = Trace.Types.Timing.Milli(zeroTime + (x + imageWidth / 2) * scale); const timeMicroSeconds = Trace.Helpers.Timing.milliToMicro(time); const frame = Trace.Extras.FilmStrip.frameClosestToTimestamp(this.#filmStrip, timeMicroSeconds); if (!frame) { continue; } context.rect(x - 0.5, 0.5, imageWidth + 1, imageHeight + 1); void this.imageByFrame(frame).then(drawFrameImage.bind(this, x)); } context.strokeStyle = '#ddd'; context.stroke(); function drawFrameImage(this: TimelineFilmStripOverview, x: number, image: HTMLImageElement|null): void { // Ignore draws deferred from a previous update call. if (this.drawGeneration !== drawGeneration || !image) { return; } context.drawImage(image, x, 1, imageWidth, imageHeight); } } override async overviewInfoPromise(x: number): Promise<Element|null> { if (!this.#filmStrip || this.#filmStrip.frames.length === 0) { return null; } const calculator = this.calculator(); if (!calculator) { return null; } const timeMilliSeconds = calculator.positionToTime(x); const timeMicroSeconds = Trace.Helpers.Timing.milliToMicro(timeMilliSeconds); const frame = Trace.Extras.FilmStrip.frameClosestToTimestamp(this.#filmStrip, timeMicroSeconds); 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(): void { this.lastFrame = null; this.lastElement = null; this.frameToImagePromise = new Map(); } // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // eslint-disable-next-line @typescript-eslint/naming-convention static readonly Padding = 2; } export class TimelineEventOverviewMemory extends TimelineEventOverview { private heapSizeLabel: HTMLElement; #parsedTrace: Trace.Handlers.Types.ParsedTrace; constructor(parsedTrace: Trace.Handlers.Types.ParsedTrace) { super('memory', i18nString(UIStrings.heap)); this.heapSizeLabel = this.element.createChild('div', 'memory-graph-label'); this.#parsedTrace = parsedTrace; } resetHeapSizeLabels(): void { this.heapSizeLabel.textContent = ''; } override update(start?: Trace.Types.Timing.Milli, end?: Trace.Types.Timing.Milli): void { this.resetCanvas(); const ratio = window.devicePixelRatio; if (this.#parsedTrace.Memory.updateCountersByProcess.size === 0) { this.resetHeapSizeLabels(); return; } const mainRendererIds = Array.from(this.#parsedTrace.Meta.topLevelRendererIds); const counterEventsPerTrack = mainRendererIds.map(pid => this.#parsedTrace.Memory.updateCountersByProcess.get(pid) || []) .filter(eventsPerRenderer => eventsPerRenderer.length > 0); const lowerOffset = 3 * ratio; let maxUsedHeapSize = 0; let minUsedHeapSize = 100000000000; const boundsMs = (start && end) ? { min: start, max: end, range: end - start, } : Trace.Helpers.Timing.traceWindowMilliSeconds(this.#parsedTrace.Meta.traceBounds); const minTime = boundsMs.min; const maxTime = boundsMs.max; function calculateMinMaxSizes(event: Trace.Types.Events.UpdateCounters): void { 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 < counterEventsPerTrack.length; i++) { counterEventsPerTrack[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); function buildHistogram(event: Trace.Types.Events.UpdateCounters): void { const counters = event.args.data; if (!counters || !counters.jsHeapSizeUsed) { return; } const {startTime} = Trace.Helpers.Timing.eventTimingsMilliSeconds(event); const x = Math.round((startTime - minTime) * xFactor); const y = Math.round((counters.jsHeapSizeUsed - minUsedHeapSize) * yFactor); histogram[x] = Math.max(histogram[x] || 0, y); } for (let i = 0; i < counterEventsPerTrack.length; i++) { counterEventsPerTrack[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: i18n.ByteUtilities.bytesToString(minUsedHeapSize), PH2: i18n.ByteUtilities.bytesToString(maxUsedHeapSize), }); } } export class Quantizer { private lastTime: number; private quantDuration: number; private readonly callback: (arg0: number[]) => void; private counters: number[]; private remainder: number; constructor(startTime: number, quantDuration: number, callback: (arg0: number[]) => void) { this.lastTime = startTime; this.quantDuration = quantDuration; this.callback = callback; this.counters = []; this.remainder = quantDuration; } appendInterval(time: number, group: number): void { 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; } }