/* * Copyright (C) 2019 Apple 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: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. 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. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS 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 APPLE INC. OR ITS 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. */ WI.CPUTimelineOverviewGraph = class CPUTimelineOverviewGraph extends WI.TimelineOverviewGraph { constructor(timeline, timelineOverview) { console.assert(timeline instanceof WI.Timeline); console.assert(timeline.type === WI.TimelineRecord.Type.CPU, timeline); super(timelineOverview); this.element.classList.add("cpu"); this._cpuTimeline = timeline; this._cpuTimeline.addEventListener(WI.Timeline.Event.RecordAdded, this._cpuTimelineRecordAdded, this); let size = new WI.Size(0, this.height); this._chart = new WI.StackedColumnChart(size); this._chart.initializeSections(["main-thread-usage", "worker-thread-usage", "total-usage"]); this.addSubview(this._chart); this.element.appendChild(this._chart.element); this._chart.element.addEventListener("click", this._handleChartClick.bind(this)); this._legendElement = this.element.appendChild(document.createElement("div")); this._legendElement.classList.add("legend"); this._lastSelectedRecordInLayout = null; this.reset(); for (let record of this._cpuTimeline.records) this._processRecord(record); } // Protected get height() { return 60; } reset() { super.reset(); this._maxUsage = 0; this._cachedMaxUsage = undefined; this._lastSelectedRecordInLayout = null; this._updateLegend(); this._chart.clear(); this._chart.needsLayout(); } layout() { super.layout(); if (this.hidden) return; this._updateLegend(); this._chart.clear(); let graphWidth = this.timelineOverview.scrollContainerWidth; if (isNaN(graphWidth)) return; this._lastSelectedRecordInLayout = this.selectedRecord; if (this._chart.size.width !== graphWidth || this._chart.size.height !== this.height) this._chart.size = new WI.Size(graphWidth, this.height); let graphStartTime = this.startTime; let visibleEndTime = Math.min(this.endTime, this.currentTime); let secondsPerPixel = this.timelineOverview.secondsPerPixel; let maxCapacity = Math.max(20, this._maxUsage * 1.05); // Add 5% for padding. function xScale(time) { return (time - graphStartTime) / secondsPerPixel; } let height = this.height; function yScale(size) { return (size / maxCapacity) * height; } let visibleRecords = this._cpuTimeline.recordsInTimeRange(graphStartTime, visibleEndTime, { includeRecordBeforeStart: true, }); if (!visibleRecords.length) return; const minimumDisplayHeight = 4; for (let record of visibleRecords) { let additionalClass = record === this.selectedRecord ? "selected" : undefined; let w = (record.endTime - record.startTime) / secondsPerPixel; let x = xScale(record.startTime); let h1 = Math.max(minimumDisplayHeight, yScale(record.mainThreadUsage)); let h2 = Math.max(minimumDisplayHeight, yScale(record.mainThreadUsage + record.workerThreadUsage)); let h3 = Math.max(minimumDisplayHeight, yScale(record.usage)); this._chart.addColumnSet(x, height, w, [h1, h2, h3], additionalClass); } } updateSelectedRecord() { super.updateSelectedRecord(); if (this._lastSelectedRecordInLayout !== this.selectedRecord) { // Since we don't have the exact element to re-style with a selected appearance // we trigger another layout to re-layout the graph and provide additional // styles for the column for the selected record. this.needsLayout(); } } // Private _updateLegend() { if (this._cachedMaxUsage === this._maxUsage) return; this._cachedMaxUsage = this._maxUsage; if (!this._maxUsage) { this._legendElement.hidden = true; this._legendElement.textContent = ""; } else { this._legendElement.hidden = false; this._legendElement.textContent = WI.UIString("Maximum CPU Usage: %s").format(Number.percentageString(this._maxUsage / 100)); } } _graphPositionForMouseEvent(event) { // Only trigger if clicking on a rect, not anywhere in the graph. let elements = document.elementsFromPoint(event.pageX, event.pageY); let rectElement = elements.find((x) => x.localName === "rect"); if (!rectElement) return NaN; let chartElement = rectElement.closest(".stacked-column-chart"); if (!chartElement) return NaN; let rect = chartElement.getBoundingClientRect(); let position = event.pageX - rect.left; if (WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL) return rect.width - position; return position; } _handleChartClick(event) { let position = this._graphPositionForMouseEvent(event); if (isNaN(position)) return; let secondsPerPixel = this.timelineOverview.secondsPerPixel; let graphClickTime = position * secondsPerPixel; let graphStartTime = this.startTime; let clickTime = graphStartTime + graphClickTime; let record = this._cpuTimeline.closestRecordTo(clickTime); if (!record) return; // Ensure that the container "click" listener added by `WI.TimelineOverview` isn't called. event.__timelineRecordClickEventHandled = true; this.selectedRecord = record; this.needsLayout(); } _cpuTimelineRecordAdded(event) { let cpuTimelineRecord = event.data.record; this._processRecord(cpuTimelineRecord); this.needsLayout(); } _processRecord(cpuTimelineRecord) { this._maxUsage = Math.max(this._maxUsage, cpuTimelineRecord.usage); } };