// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview ARC Graphics Tracing UI.
*/
// Namespace of SVG elements
const svgNS = 'http://www.w3.org/2000/svg';
// Background color for the band with events.
const bandColor = '#d3d3d3';
// Color that should never appear on UI.
const unusedColor = '#ff0000';
// Supported zooms, mcs per pixel
const zooms = [
2.5,
5.0,
10.0,
25.0,
50.0,
100.0,
250.0,
500.0,
1000.0,
2500.0,
5000.0,
10000.0,
25000.0,
];
// Active zoom level, as index in |zooms|. By default 100 mcs per pixel.
let zoomLevel = 5;
// Graphics event types which are used in the model JSON data. These must match
// the graphics event types in
// chrome/browser/ash/arc/tracing/arc_tracing_graphics_model.h. To aid in
// maintaining consistency, do not modify values once added - deprecation and
// removal are allowed.
const kExoSurfaceCommit = 206;
const kExoSurfaceCommitJank = 207;
const kChromeOSPresentationDone = 503;
const kChromeOSSwapDone = 504;
const kChromeOSPerceivedJank = 506;
const kChromeOSSwapJank = 507;
/**
* Keep in sync with ArcTracingGraphicsModel::EventType
* See chrome/browser/ash/arc/tracing/arc_tracing_graphics_model.h.
* Describes how events should be rendered. |color| specifies color of the
* event, |name| is used in tooltips. |width| defines the width in case it is
* rendered as a line and |radius| defines the radius in case it is rendered as
* a circle.
*
* TODO(matvore): Only kIdleIn and kIdleOut are used in bands. Verify and clean
* up.
*/
const eventAttributes = {
// kIdleIn
0: {color: bandColor, name: 'idle'},
// kIdleOut
1: {color: '#ffbf00', name: 'active'},
// kBufferQueueDequeueStart
100: {color: '#99cc00', name: 'app requests buffer'},
// kBufferQueueDequeueDone
101: {color: '#669999', name: 'app fills buffer'},
// kBufferQueueQueueStart
102: {color: '#cccc00', name: 'app queues buffer'},
// kBufferQueueQueueDone
103: {color: unusedColor, name: 'buffer is queued'},
// kBufferQueueAcquire
104: {color: '#66ffcc', name: 'use buffer'},
// kBufferQueueReleased
105: {color: unusedColor, name: 'buffer released'},
// kBufferFillJank
106: {color: '#ff0000', name: 'buffer filling jank', width: 1.0, radius: 4.0},
[kExoSurfaceCommitJank]: {name: 'commit jank', radius: 4.0},
// kChromeBarrierOrder.
300: {color: '#ff9933', name: 'barrier order'},
// kChromeBarrierFlush
301: {color: unusedColor, name: 'barrier flush'},
// kSurfaceFlingerInvalidationStart
401: {color: '#ff9933', name: 'invalidation start'},
// kSurfaceFlingerInvalidationDone
402: {color: unusedColor, name: 'invalidation done'},
// kSurfaceFlingerCompositionStart
403: {color: '#3399ff', name: 'composition start'},
// kSurfaceFlingerCompositionDone
404: {color: unusedColor, name: 'composition done'},
// kChromeOSDraw
500: {color: '#3399ff', name: 'draw'},
// kChromeOSSwap
501: {color: '#cc9900', name: 'swap'},
// kChromeOSWaitForAck
502: {color: '#ccffff', name: 'wait for ack'},
// kChromeOSPresentationDone
503: {color: '#ffbf00', name: 'presentation done'},
// kChromeOSSwapDone
504: {color: '#65f441', name: 'swap done'},
// kChromeOSJank
505: {
color: '#ff0000',
name: 'Chrome composition jank',
width: 1.0,
radius: 4.0,
},
[kChromeOSPerceivedJank]: {
name: 'perceived jank',
radius: 4.0,
},
[kChromeOSSwapJank]: {
name: 'swap jank',
radius: 4.0,
},
// kCustomEvent
600: {color: '#7cb342', name: 'Custom event', width: 1.0, radius: 4.0},
// Service events.
// kTimeMark
10000: {color: '#888', name: 'Time mark', width: 0.75},
// kTimeMarkSmall
10001: {color: '#888', name: 'Time mark', width: 0.15},
};
/**
* Defines the map of events that can be treated as the end of event sequence.
* Time after such events is considered as idle time until the next event
* starts. Key of |endSequenceEvents| is event type as defined in
* ArcTracingGraphicsModel::EventType and value is the list of event
* types that should follow after the tested event to consider it as end of
* sequence. Empty list means that tested event is certainly end of the
* sequence.
*/
const endSequenceEvents = {
// kIdleIn
0: [],
// kBufferQueueQueueDone
103: [],
// kBufferQueueReleased
105: [],
// kChromeBarrierFlush
301: [],
// kSurfaceFlingerInvalidationDone
402: [],
// kSurfaceFlingerCompositionDone
404: [],
// kChromeOSPresentationDone. Chrome does not define exactly which event
// is the last. Different
// pipelines may produce different sequences. Both event type may indicate
// the end of the
// sequence.
503: [500 /* kChromeOSDraw */],
// kChromeOSSwapDone
504: [500 /* kChromeOSDraw */],
};
/**
* Keep in sync with ArcValueEvent::Type
* See chrome/browser/ash/arc/tracing/arc_value_event.h.
* Describes how value events should be rendered in charts. |color| specifies
* color of the event, |name| is used in tooltips, |width| specify width of
* the line in chart, |scale| is used to convert actual value to rendered value.
* When rendered, min and max values and determined and used as a range where
* chart is drawn. However, in the case range is small, let say 1 mb for
* |kMemUsed| this may lead to user confusion that huge amount of memory was
* allocated. To prevent this scanario, |minRange| defines the minimum range of
* values and is set in scaled units.
*/
const valueAttributes = {
// kMemUsed.
1: {
color: '#ff3d00',
minRange: 512.0,
name: 'used mb',
scale: 1.0 / 1024.0,
width: 1.0,
},
// kSwapRead.
2: {
color: '#ffc400',
minRange: 32.0,
name: 'swap read sectors',
scale: 1.0,
width: 1.0,
},
// kSwapWrite.
3: {
color: '#ff9100',
minRange: 32.0,
name: 'swap write sectors',
scale: 1.0,
width: 1.0,
},
// kGemObjects.
5: {
color: '#3d5afe',
minRange: 1000,
name: 'geom. objects',
scale: 1.0,
width: 1.0,
},
// kGemSize.
6: {
color: '#7c4dff',
minRange: 256.0,
name: 'geom. size mb',
scale: 1.0 / 1024.0,
width: 1.0,
},
// kGpuFrequency.
7: {
color: '#01579b',
minRange: 300.0,
name: 'GPU frequency mhz',
scale: 1.0,
width: 1.0,
},
// kCpuTemperature.
8: {
color: '#ff3d00',
minRange: 20.0,
name: 'CPU celsius.',
scale: 1.0 / 1000.0,
width: 1.0,
},
// kCpuFrequency.
9: {
color: '#ff80ab',
minRange: 300.0,
name: 'CPU Mhz.',
scale: 1.0 / 1000.0,
width: 1.0,
},
// kCpuPower.
10: {
color: '#dd2c00',
minRange: 0.0,
name: 'CPU milli-watts.',
scale: 1.0,
width: 1.0,
},
// kGpuPower.
11: {
color: '#dd2c00',
minRange: 0.0,
name: 'GPU milli-watts.',
scale: 1.0,
width: 1.0,
},
// kMemoryPower.
12: {
color: '#dd2c00',
minRange: 0.0,
name: 'Memory milli-watts.',
scale: 1.0,
width: 1.0,
},
// kPackagePowerConstraint.
13: {
color: '#dd0050',
minRange: 0.0,
name: 'CPU package constraint milli-watts.',
scale: 1.0,
width: 1.0,
},
};
/**
* @type {function()}.
* Callback when UI has to be updted.
*/
let updateUiCallback = null;
/**
* @type {DetailedInfoView}.
* Currently active detailed view.
*/
let activeDetailedInfoView = null;
/**
* Discards active detailed view if it exists.
*/
function discardDetailedInfo() {
if (activeDetailedInfoView) {
activeDetailedInfoView.discard();
activeDetailedInfoView = null;
}
}
/**
* Shows detailed view for |eventBand| in response to mouse click event
* |mouseEvent|.
*/
function showDetailedInfoForBand(eventBand, mouseEvent) {
discardDetailedInfo();
activeDetailedInfoView = eventBand.showDetailedInfo(mouseEvent);
mouseEvent.preventDefault();
}
/**
* Returns text representation of timestamp in milliseconds with one number
* after the decimal point.
*
* @param {number} timestamp in microseconds.
*/
function timestampToMsText(timestamp) {
return (timestamp / 1000.0).toFixed(1);
}
/**
* Changes zoom. |delta| specifies how many zoom levels to adjust. Negative
* |delta| means zoom in and positive zoom out.
*/
function updateZoom(delta) {
const newZoomLevel = zoomLevel + delta;
if (newZoomLevel < 0 || newZoomLevel >= zooms.length) {
return;
}
zoomLevel = newZoomLevel;
updateUiCallback();
}
/**
* Initialises common tracing UI.
* * handle zoom.
* * handle load request
* * set keyboard and mouse listeners to discard detailed view overlay.
*/
function initializeUi(initZoomLevel, callback) {
zoomLevel = initZoomLevel;
updateUiCallback = callback;
document.body.onkeydown = function(event) {
// Escape and Enter.
if (event.key === 'Escape' || event.key === 'Enter') {
discardDetailedInfo();
} else if (event.key === 'w') {
// Zoom in.
updateZoom(-1 /* delta */);
} else if (event.key === 's') {
// Zoom out.
updateZoom(1 /* delta */);
}
};
window.onclick = function(event) {
// Detect click outside the detailed view.
if (event.defaultPrevented || activeDetailedInfoView == null) {
return;
}
if (!activeDetailedInfoView.overlay.contains(event.target)) {
discardDetailedInfo();
}
};
if ($('arc-tracing-load')) {
$('arc-tracing-load').onclick = function(event) {
const fileElement = document.createElement('input');
fileElement.type = 'file';
fileElement.onchange = function(event) {
const reader = new FileReader();
reader.onload = function(response) {
chrome.send('loadFromText', [response.target.result]);
};
reader.readAsText(event.target.files[0]);
};
fileElement.click();
};
}
}
/**
* Updates current status.
* @param {string} statusText text to set as a status.
*/
function setStatus(statusText) {
$('arc-tracing-status').textContent = statusText;
}
/** Factory class for SVG elements. */
class SVG {
// Creates rectangle element in the |svg| with provided attributes.
static addRect(svg, x, y, width, height, color, opacity) {
const rect = document.createElementNS(svgNS, 'rect');
rect.setAttributeNS(null, 'x', x);
rect.setAttributeNS(null, 'y', y);
rect.setAttributeNS(null, 'width', width);
rect.setAttributeNS(null, 'height', height);
rect.setAttributeNS(null, 'fill', color);
rect.setAttributeNS(null, 'stroke', 'none');
if (opacity) {
rect.setAttributeNS(null, 'fill-opacity', opacity);
}
svg.appendChild(rect);
return rect;
}
// Creates line element in the |svg| with provided attributes.
static addLine(svg, x1, y1, x2, y2, color, width) {
const line = document.createElementNS(svgNS, 'line');
line.setAttributeNS(null, 'x1', x1);
line.setAttributeNS(null, 'y1', y1);
line.setAttributeNS(null, 'x2', x2);
line.setAttributeNS(null, 'y2', y2);
line.setAttributeNS(null, 'stroke', color);
line.setAttributeNS(null, 'stroke-width', width);
svg.appendChild(line);
return line;
}
// Creates polyline element in the |svg| with provided attributes.
static addPolyline(svg, points, color, width) {
const polyline = document.createElementNS(svgNS, 'polyline');
polyline.setAttributeNS(null, 'points', points.join(' '));
polyline.setAttributeNS(null, 'stroke', color);
polyline.setAttributeNS(null, 'stroke-width', width);
polyline.setAttributeNS(null, 'fill', 'none');
svg.appendChild(polyline);
return polyline;
}
// Creates circle element in the |svg| with provided attributes.
static addCircle(svg, x, y, radius, strokeWidth, color, strokeColor) {
const circle = document.createElementNS(svgNS, 'circle');
circle.setAttributeNS(null, 'cx', x);
circle.setAttributeNS(null, 'cy', y);
circle.setAttributeNS(null, 'r', radius);
circle.setAttributeNS(null, 'fill', color);
circle.setAttributeNS(null, 'stroke', strokeColor);
circle.setAttributeNS(null, 'stroke-width', strokeWidth);
svg.appendChild(circle);
return circle;
}
// Creates text element in the |svg| with provided attributes.
static addText(svg, x, y, fontSize, textContent, anchor, transform) {
const lines = textContent.split('\n');
let text;
for (let i = 0; i < lines.length; ++i) {
text = document.createElementNS(svgNS, 'text');
text.setAttributeNS(null, 'x', x);
text.setAttributeNS(null, 'y', y);
text.setAttributeNS(null, 'fill', 'black');
text.setAttributeNS(null, 'font-size', fontSize);
if (anchor) {
text.setAttributeNS(null, 'text-anchor', anchor);
}
if (transform) {
text.setAttributeNS(null, 'transform', transform);
}
text.appendChild(document.createTextNode(lines[i]));
svg.appendChild(text);
y += fontSize;
}
return text;
}
}
/**
* Represents title for events bands that can collapse/expand controlled
* content.
*/
class EventBandTitle {
constructor(parent, anchor, title, className, opt_iconContent) {
this.div = document.createElement('div');
this.div.classList.add(className);
if (opt_iconContent) {
const icon = document.createElement('img');
icon.src = 'data:image/png;base64,' + opt_iconContent;
this.div.appendChild(icon);
}
const span = document.createElement('span');
span.appendChild(document.createTextNode(title));
this.div.appendChild(span);
this.controlledItems = [];
this.div.onclick = this.onClick_.bind(this);
this.parent = parent;
if (anchor && anchor.nextSibling) {
this.parent.insertBefore(this.div, anchor.nextSibling);
} else {
this.parent.appendChild(this.div);
}
}
/**
* Adds extra HTML element under the control. This element will be
* automatically expanded/collapsed together with this title.
*
* @param {HTMLElement} item svg element to control.
*/
addContolledItems(item) {
this.controlledItems.push(item);
}
onClick_() {
this.div.classList.toggle('hidden');
for (let i = 0; i < this.controlledItems.length; ++i) {
this.controlledItems[i].classList.toggle('hidden');
}
}
}
/** Represents container for event bands. */
class EventBands {
/**
* Creates container for the event bands.
*
* @param {EventBandTitle} title controls visibility of this band.
* @param {string} className class name of the svg element that represents
* this band. 'arc-events-top-band' is used for top-level events and
* 'arc-events-inner-band' is used for per-buffer events.
* @param {number} resolution the resolution of bands microseconds per 1
* pixel.
* @param {number} minTimestamp the minimum timestamp to display on bands.
* @param {number} minTimestamp the maximum timestamp to display on bands.
*/
constructor(title, className, resolution, minTimestamp, maxTimestamp) {
// Keep information about bands and charts and their bounds.
this.bands = [];
this.charts = [];
this.globalEvents = [];
this.tooltips = [];
this.resolution = resolution;
this.minTimestamp = minTimestamp;
this.maxTimestamp = maxTimestamp;
this.height = 0;
// Offset of the next band of events.
this.nextYOffset = 0;
this.svg = document.createElementNS(svgNS, 'svg');
this.svg.setAttributeNS(
'http://www.w3.org/2000/xmlns/', 'xmlns:xlink',
'http://www.w3.org/1999/xlink');
this.setBandOffsetX(0);
this.setWidth(0);
this.svg.setAttribute('height', this.height + 'px');
this.svg.classList.add(className);
this.setTooltip_();
this.title = title;
title.addContolledItems(this.svg);
title.parent.insertBefore(this.svg, title.div.nextSibling);
// Set of constants, used for rendering content.
this.fontSize = 12;
this.verticalGap = 5;
this.horizontalGap = 10;
this.lineHeight = 16;
this.iconOffset = 24;
this.iconRadius = 4;
this.textOffset = 32;
}
/**
* Sets the horizontal offset to render bands.
* @param {number} offsetX offset in pixels.
*/
setBandOffsetX(offsetX) {
this.bandOffsetX = offsetX;
}
/**
* Sets the widths of event bands.
* @param {number} width width in pixels.
*/
setWidth(width) {
this.width = width;
this.svg.setAttribute('width', this.width + 'px');
}
/**
* Converts timestamp into pixel offset. 1 pixel corresponds resolution
* microseconds.
*
* @param {number} timestamp in microseconds.
*/
timestampToOffset(timestamp) {
return (timestamp - this.minTimestamp) / this.resolution;
}
/**
* Opposite conversion of |timestampToOffset|
*
* @param {number} offset in pixels.
*/
offsetToTime(offset) {
return offset * this.resolution + this.minTimestamp;
}
/**
* This adds new band of events. Height of svg container is automatically
* adjusted to fit the new content.
*
* @param {Events} eventBand event band to add.
* @param {number} height of the band.
* @param {number} padding to separate from the next band or chart.
*/
addBand(eventBand, height, padding) {
let currentColor = unusedColor;
let addToBand = false;
let x = this.bandOffsetX;
let eventIndex = eventBand.getFirstAfter(this.minTimestamp);
while (eventIndex >= 0) {
const event = eventBand.events[eventIndex];
if (event[1] >= this.maxTimestamp) {
break;
}
const nextX = this.timestampToOffset(event[1]) + this.bandOffsetX;
if (addToBand) {
SVG.addRect(
this.svg, x, this.nextYOffset, nextX - x, height, currentColor);
}
if (eventBand.isEndOfSequence(eventIndex)) {
currentColor = unusedColor;
addToBand = false;
} else {
currentColor = eventAttributes[event[0]].color;
addToBand = true;
}
x = nextX;
eventIndex = eventBand.getNextEvent(eventIndex, 1 /* direction */);
}
if (addToBand) {
SVG.addRect(
this.svg, x, this.nextYOffset,
this.timestampToOffset(this.maxTimestamp) - x + this.bandOffsetX,
height, currentColor);
}
this.bands.push({
band: eventBand,
top: this.nextYOffset,
bottom: this.nextYOffset + height,
});
this.updateHeight(height, padding);
}
/**
* This adds horizontal separator at |nextYOffset|.
*
* @param {number} padding to separate from the next band or chart.
*/
addBandSeparator(padding) {
SVG.addLine(
this.svg, 0, this.nextYOffset, this.width, this.nextYOffset, '#888',
0.25);
this.updateHeight(0 /* height */, padding);
}
/**
* This adds new chart. Height of svg container is automatically adjusted to
* fit the new content. This creates empty chart and one or more calls
* |addChartSources| are expected to add actual content to the chart.
*
* @param {number} height of the chart.
* @param {number} padding to separate from the next band or chart.
*/
addChart(height, padding) {
this.charts.push({
sourcesWithBounds: [],
top: this.nextYOffset,
bottom: this.nextYOffset + height,
});
this.updateHeight(height, padding);
}
/**
* This adds new chart into existing area.
* @param {number} top position of chart.
* @param {number} bottom position of chart.
*/
addChartToExistingArea(top, bottom) {
this.charts.push({sourcesWithBounds: [], top: top, bottom: bottom});
}
/**
* This adds sources of events to the last chart.
*
* @param {Events[]} sources is array of groupped source of events to add.
* These events are logically linked to each other and represented as a
* separate line.
* @param {boolean} smooth if set to true then indicates that chart should
* display value interpolated, otherwise values are changed
* discretely.
* @param {Object=} opt_attributes optional argument that defines view of
* the chart. If not set then it is automatically selected
* based on event type.
*/
addChartSources(sources, smooth, opt_attributes) {
const chart = this.charts[this.charts.length - 1];
// Calculate min/max for sources and event indices.
let minValue = null;
let maxValue = null;
const eventIndicesForAll = [];
let eventDetected = false;
let attributes = opt_attributes;
const autoDetectRange = !attributes ||
typeof attributes.minValue === 'undefined' ||
typeof attributes.maxValue === 'undefined';
for (let i = 0; i < sources.length; ++i) {
const source = sources[i];
let eventIndex = source.getFirstAfter(this.minTimestamp);
if (eventIndex < 0 || source.events[eventIndex][1] > this.maxTimestamp) {
eventIndicesForAll.push([]);
continue;
}
if (autoDetectRange && !minValue) {
minValue = source.events[eventIndex][2];
maxValue = source.events[eventIndex][2];
}
const eventIndices = [];
while (eventIndex >= 0 &&
source.events[eventIndex][1] <= this.maxTimestamp) {
eventIndices.push(eventIndex);
eventDetected = true;
if (autoDetectRange) {
minValue = Math.min(minValue, source.events[eventIndex][2]);
maxValue = Math.max(maxValue, source.events[eventIndex][2]);
}
if (!attributes) {
attributes = valueAttributes[source.events[eventIndex][0]];
}
eventIndex = source.getNextEvent(eventIndex, 1 /* direction */);
}
eventIndicesForAll.push(eventIndices);
}
if (!eventDetected) {
// no one event to render.
return;
}
if (autoDetectRange) {
// Ensure minimum value range.
if (maxValue - minValue < attributes.minRange / attributes.scale) {
maxValue = minValue + attributes.minRange / attributes.scale;
}
// Add +-1% to bounds.
const dif = maxValue - minValue;
minValue -= dif * 0.01;
maxValue += dif * 0.01;
} else {
minValue = attributes.minValue;
maxValue = attributes.maxValue;
}
const divider = 1.0 / (maxValue - minValue);
// Render now.
const height = chart.bottom - chart.top;
for (let i = 0; i < sources.length; ++i) {
const source = sources[i];
const eventIndices = eventIndicesForAll[i];
if (eventIndices.length === 0) {
continue;
}
// Determine type using first element.
const eventType = source.events[eventIndices[0]][0];
const points = [];
let lastY = 0;
for (let j = 0; j < eventIndices.length; ++j) {
const event = source.events[eventIndices[j]];
const x = this.timestampToOffset(event[1]);
const y = height * (maxValue - event[2]) * divider;
if (!smooth && j !== 0) {
points.push([x, lastY]);
}
points.push([x, y]);
lastY = y;
}
chart.sourcesWithBounds.push({
attributes: attributes,
minValue: minValue,
maxValue: maxValue,
source: source,
smooth: smooth,
});
SVG.addPolyline(this.svg, points, attributes.color, attributes.width);
}
}
/**
* This adds text to chart.
*
* @param {string} text to display.
* @param {number} x horizontal position in the chart.
* @param {number} y vertical position the chart.
* @param {string} anchor align of the text relative to x.
*/
addChartText(text, x, y, anchor) {
SVG.addText(this.svg, x, y, this.fontSize, text, anchor);
}
/**
* This adds bar as rectangle to the chart.
*
* @param {number} x horizontal position in the chart.
* @param {number} y vertical position in the chart.
* @param {number} width horizontal dimension of the bar.
* @param {number} height vertical dimension of the bar.
* @param {string} color of the bar.
*/
addChartBar(x, y, width, height, color) {
SVG.addRect(this.svg, x, y, width, height, color, 1.0 /* opacity */);
}
/**
* This adds popup tooltip for the fixed area in the chart.
*
* @param {number} x horizontal position of the tooltip area in the chart.
* @param {number} y vertical position of the tooltip area in the chart.
* @param {number} width of the tooltip area.
* @param {number} height of the tooltip area.
* @param {string} tooltip text to display.
* @param {number} tooltipWidth of the tooltip window.
* @param {number} tooltipHeight of the tooltip window.
*/
addChartTooltip(x, y, width, height, tooltip, tooltipWidth, tooltipHeight) {
this.tooltips.push({
left: x,
top: y,
right: x + width - 1,
bottom: y + height - 1,
tooltip: tooltip,
tooltipWidth: tooltipWidth,
tooltipHeight: tooltipHeight,
});
}
/**
* This adds HTML input element to the title of the section.
*
* @param {string} type specifies input type, like radio.
* @param {string} text label for the input.
* @param {boolean} checked if radio should be initially checked.
* @param {function} handler callback for input change.
*/
createTitleInput(type, text, checked, handler) {
const input = document.createElement('input');
input.onclick = handler;
input.setAttribute('type', type);
if (type === 'button') {
input.setAttribute('value', text);
}
if (checked) {
input.checked = true;
}
this.title.addContolledItems(input);
this.title.div.appendChild(input);
if (type === 'button') {
return;
}
const label = document.createElement('label');
label.onclick = handler;
label.appendChild(document.createTextNode(text));
this.title.addContolledItems(label);
this.title.div.appendChild(label);
}
/**
* This adds sources of events to the last chart as a bars.
*
* @param {Events[]} sources is array of groupped source of events to add.
* These events are logically linked to each other and represented as a
* bar where each bar has color corresponded the value of the event.
* @param {Object=} attributes dictionary to resolve the color of the bar.
* @param {number} y vertical offset of bars.
* @param {number} height height of bars.
*/
addBarSource(source, attributes, y, height) {
const chart = this.charts[this.charts.length - 1];
let eventIndex = source.getFirstAfter(this.minTimestamp);
if (eventIndex < 0 || source.events[eventIndex][1] > this.maxTimestamp) {
return;
}
while (eventIndex >= 0 &&
source.events[eventIndex][1] <= this.maxTimestamp) {
const eventIndexNext = source.getNextEvent(eventIndex, 1 /* direction */);
const event = source.events[eventIndex];
const x = this.timestampToOffset(event[1]);
const color = attributes[event[2]].color;
let nextTimestamp = 0;
if (eventIndexNext >= 0 &&
source.events[eventIndexNext][1] <= this.maxTimestamp) {
nextTimestamp = source.events[eventIndexNext][1];
} else {
nextTimestamp = this.maxTimestamp;
}
const width = this.timestampToOffset(nextTimestamp);
eventIndex = eventIndexNext;
SVG.addRect(this.svg, x, y, width, height, color, 1.0 /* opacity */);
}
chart.sourcesWithBounds.push(
{attributes: attributes, source: source, perValue: true});
}
addChartGridLine(y) {
SVG.addLine(this.svg, 0, y, this.width, y, '#ccc', 0.5);
}
/**
* Updates height of svg container.
*
* @param {number} new height of the chart.
* @param {number} padding to separate from the next band or chart.
*/
updateHeight(height, padding) {
this.nextYOffset += height;
this.height = this.nextYOffset;
this.svg.setAttribute('height', this.height + 'px');
this.nextYOffset += padding;
}
/**
* This adds events as a global events that do not belong to any band.
*
* @param {Events} events to add.
* @param {string} renderType defines how to render events, can be underfined
* for default or set to 'circle'.
* @param {string} color the color (fill color if rendered as a circle), or
* omitted to use the color defined in eventAttributes for
* each event type.
* @param {number} y for circles, the y position, as a fraction of this band's
* height, such as 0.5 for vertically-centered, or 0.95 for
* close to the bottom.
*/
addGlobal(events, renderType, color, y) {
let eventIndex = -1;
if (typeof y === 'undefined') {
y = 0.5;
}
while (true) {
eventIndex = events.getNextEvent(eventIndex, 1 /* direction */);
if (eventIndex < 0) {
break;
}
const event = events.events[eventIndex];
const attributes = events.getEventAttributes(eventIndex);
const x = this.timestampToOffset(event[1]) + this.bandOffsetX;
const evColor = color || attributes.color;
if (renderType === 'circle') {
SVG.addCircle(
this.svg, x, this.height * y, attributes.radius,
1 /* strokeWidth */, evColor, 'black' /* strokeColor */);
} else {
SVG.addLine(this.svg, x, 0, x, this.height, evColor, attributes.width);
}
}
this.globalEvents.push(events);
}
/** Initializes tooltip support by observing mouse events */
setTooltip_() {
this.tooltip = $('arc-event-band-tooltip');
this.svg.onmouseover = this.showToolTip_.bind(this);
this.svg.onmouseout = this.hideToolTip_.bind(this);
this.svg.onmousemove = this.updateToolTip_.bind(this);
this.svg.onclick = (event) => {
showDetailedInfoForBand(this, event);
};
}
/** Updates tooltip and shows it for this band. */
showToolTip_(event) {
this.updateToolTip_(event);
}
/** Hides the tooltip. */
hideToolTip_() {
this.tooltip.classList.remove('active');
}
/**
* Finds global events around |timestamp| and not farther than |distance|.
*
* @param {number} timestamp to search.
* @param {number} distance to search.
* @returns {Array} array of events sorted by distance to |timestamp| from
* closest to farthest.
*/
findGlobalEvents_(timestamp, distance) {
const events = [];
const leftBorder = timestamp - distance;
const rightBorder = timestamp + distance;
for (let i = 0; i < this.globalEvents.length; ++i) {
let index = this.globalEvents[i].getFirstAfter(leftBorder);
while (index >= 0) {
const event = this.globalEvents[i].events[index];
if (event[1] > rightBorder) {
break;
}
events.push(event);
index = this.globalEvents[i].getNextEvent(index, 1 /* direction */);
}
}
events.sort(function(a, b) {
const distanceA = Math.abs(timestamp - a[1]);
const distanceB = Math.abs(timestamp - b[1]);
return distanceA - distanceB;
});
return events;
}
/**
* Updates tool tip based on event under the current cursor.
*
* @param {Object} event mouse event.
*/
updateToolTip_(event) {
// Clear previous content.
this.tooltip.textContent = '';
const svgStyle = window.getComputedStyle(this.svg, null);
const paddingLeft = parseFloat(svgStyle.getPropertyValue('padding-left'));
const paddingTop = parseFloat(svgStyle.getPropertyValue('padding-top'));
const eventX = event.offsetX - paddingLeft;
const eventY = event.offsetY - paddingTop;
const svg = document.createElementNS(svgNS, 'svg');
svg.setAttributeNS(
'http://www.w3.org/2000/xmlns/', 'xmlns:xlink',
'http://www.w3.org/1999/xlink');
this.tooltip.appendChild(svg);
for (const areaTooltip of this.tooltips) {
if (areaTooltip.left <= eventX && areaTooltip.top <= eventY &&
areaTooltip.right >= eventX && areaTooltip.bottom >= eventY) {
SVG.addText(
svg, this.horizontalGap, this.verticalGap + this.fontSize,
this.fontSize, areaTooltip.tooltip);
this.showTooltipForEvent_(
event, svg, areaTooltip.tooltipHeight, areaTooltip.tooltipWidth);
return;
}
}
if (eventX < this.bandOffsetX) {
this.tooltip.classList.remove('active');
return;
}
const eventTimestamp = this.offsetToTime(eventX - this.bandOffsetX);
const width = 220;
let yOffset = this.verticalGap;
// In case of global events are not available, render tooltip for band and
// chart.
yOffset =
this.updateToolTipForGlobalEvents_(event, svg, eventTimestamp, yOffset);
if (yOffset === this.verticalGap) {
// Find band for this mouse event.
for (let i = 0; i < this.bands.length; ++i) {
if (this.bands[i].top <= eventY && this.bands[i].bottom > eventY) {
yOffset = this.updateToolTipForBand_(
event, svg, eventTimestamp, this.bands[i].band, yOffset);
break;
}
}
// Find chart for this mouse event.
for (let i = 0; i < this.charts.length; ++i) {
if (this.charts[i].top <= eventY && this.charts[i].bottom > eventY) {
yOffset = this.updateToolTipForChart_(
event, svg, eventTimestamp, this.charts[i], yOffset);
break;
}
}
}
if (yOffset > this.verticalGap) {
// Content was added.
if (this.canShowDetailedInfo()) {
yOffset += this.lineHeight;
SVG.addText(
svg, this.horizontalGap, yOffset, this.fontSize,
'Click for detailed info');
}
yOffset += this.verticalGap;
this.showTooltipForEvent_(event, svg, yOffset, width);
} else {
this.tooltip.classList.remove('active');
}
}
/**
* Adds time information for |eventTimestamp| to the tooltip. Global time is
* added first and VSYNC relative time is added next in case VSYNC event could
* be found.
*
* @param {Object} svg tooltip container.
* @param {number} yOffset current vertical offset.
* @param {number} eventTimestamp timestamp of the event.
* @returns {number} vertical position of the next element.
*/
addTimeInfoToTooltip_(svg, yOffset, eventTimestamp) {
const text = timestampToMsText(eventTimestamp) + ' ms';
yOffset += this.lineHeight;
SVG.addText(svg, this.horizontalGap, yOffset, this.fontSize, text);
return yOffset;
}
/**
* Creates and shows tooltip for global events in case they are found around
* mouse event position.
*
* @param {Object} mouse event.
* @param {Object} svg tooltip content.
* @param (number} eventTimestamp timestamp of event.
* @param {number} yOffset starting vertical position to fill this content.
* @returns {number} next vertical position to fill the next content.
*/
updateToolTipForGlobalEvents_(event, svg, eventTimestamp, yOffset) {
// Try to find closest global events in the range -3..3 pixels. Several
// events may stick close each other so let diplay up to 3 closest events.
const distanceMcs = 3 * this.resolution;
const globalEvents = this.findGlobalEvents_(eventTimestamp, distanceMcs);
if (globalEvents.length === 0) {
return yOffset;
}
// Show the global events info.
const globalEventCnt = Math.min(globalEvents.length, 3);
for (let i = 0; i < globalEventCnt; ++i) {
const globalEvent = globalEvents[i];
const globalEventType = globalEvent[0];
const globalEventTimestamp = globalEvent[1];
yOffset = this.addTimeInfoToTooltip_(svg, yOffset, globalEventTimestamp);
const attributes = eventAttributes[globalEventType];
yOffset += this.lineHeight;
SVG.addCircle(
svg, this.iconOffset, yOffset - this.iconRadius, this.iconRadius, 1,
attributes.color, 'black');
SVG.addText(
svg, this.textOffset, yOffset, this.fontSize, attributes.name);
// Render content if exists.
if (globalEvent.length > 2) {
yOffset += this.lineHeight;
SVG.addText(
svg, this.textOffset + this.horizontalGap, yOffset, this.fontSize,
globalEvent[2]);
}
}
yOffset += this.verticalGap;
return yOffset;
}
/**
* Creates and shows tooltip for event band for the position under |event|.
*
* @param {Object} mouse event.
* @param {Object} svg tooltip content.
* @param (number} eventTimestamp timestamp of event.
* @param {Object} active event band.
* @param {number} yOffset starting vertical position to fill this content.
* @returns {number} next vertical position to fill the next content.
*/
updateToolTipForBand_(event, svg, eventTimestamp, eventBand, yOffset) {
// Find the event under the cursor. |index| points to the current event
// and |nextIndex| points to the next event.
let nextIndex = eventBand.getFirstEvent();
while (nextIndex >= 0) {
if (eventBand.events[nextIndex][1] > eventTimestamp) {
break;
}
nextIndex = eventBand.getNextEvent(nextIndex, 1 /* direction */);
}
let index = eventBand.getNextEvent(nextIndex, -1 /* direction */);
if (index < 0 || eventBand.isEndOfSequence(index)) {
// In case cursor points to idle event, show its interval.
yOffset += this.lineHeight;
const startIdle = index < 0 ? 0 : eventBand.events[index][1];
const endIdle =
nextIndex < 0 ? this.maxTimestamp : eventBand.events[nextIndex][1];
SVG.addText(
svg, this.horizontalGap, yOffset, this.fontSize,
'Idle ' + timestampToMsText(startIdle) + '...' +
timestampToMsText(endIdle) + ' chart time ms.');
} else {
// Show the sequence of non-idle events.
// Find the start of the non-idle sequence.
while (true) {
const prevIndex = eventBand.getNextEvent(index, -1 /* direction */);
if (prevIndex < 0 || eventBand.isEndOfSequence(prevIndex)) {
break;
}
index = prevIndex;
}
const sequenceTimestamp = eventBand.events[index][1];
yOffset = this.addTimeInfoToTooltip_(svg, yOffset, sequenceTimestamp);
let lastTimestamp = sequenceTimestamp;
// Scan for the entries to show.
const entriesToShow = [];
while (index >= 0) {
const attributes = eventBand.getEventAttributes(index);
const eventTimestamp = eventBand.events[index][1];
const entryToShow = {};
entryToShow.color = attributes.color;
if (eventBand.events[index].length > 2) {
entryToShow.text = attributes.name + ' ' + eventBand.events[index][2];
} else {
entryToShow.text = attributes.name;
}
if (entriesToShow.length > 0) {
entriesToShow[entriesToShow.length - 1].text +=
' [' + timestampToMsText(eventTimestamp - lastTimestamp) + ' ms]';
}
entriesToShow.push(entryToShow);
if (eventBand.isEndOfSequence(index)) {
break;
}
lastTimestamp = eventTimestamp;
index = eventBand.getNextEvent(index, 1 /* direction */);
}
// Last element is end of sequence, use bandColor for the icon.
if (entriesToShow.length > 0) {
entriesToShow[entriesToShow.length - 1].color = bandColor;
}
for (let i = 0; i < entriesToShow.length; ++i) {
const entryToShow = entriesToShow[i];
yOffset += this.lineHeight;
SVG.addCircle(
svg, this.iconOffset, yOffset - this.iconRadius, this.iconRadius, 1,
entryToShow.color, 'black');
SVG.addText(
svg, this.textOffset, yOffset, this.fontSize, entryToShow.text);
}
}
yOffset += this.verticalGap;
return yOffset;
}
/**
* Creates and show tooltip for event chart for the position under |event|.
*
* @param {Object} mouse event.
* @param {Object} svg tooltip content.
* @param (number} eventTimestamp timestamp of event.
* @param {Object} active event chart.
* @param {number} yOffset starting vertical position to fill this content.
* @returns {number} next vertical position to fill the next content.
*/
updateToolTipForChart_(event, svg, eventTimestamp, chart, yOffset) {
const valueOffset = 32;
let contentAdded = false;
for (let i = 0; i < chart.sourcesWithBounds.length; ++i) {
const sourceWithBounds = chart.sourcesWithBounds[i];
let color;
let text;
if (sourceWithBounds.perValue) {
// Tooltip per value
const index = sourceWithBounds.source.getLastBefore(eventTimestamp);
if (index < 0) {
continue;
}
const event = sourceWithBounds.source.events[index];
color = sourceWithBounds.attributes[event[2]].color;
text = sourceWithBounds.attributes[event[2]].name;
} else {
// Interpolate results.
const indexAfter =
sourceWithBounds.source.getFirstAfter(eventTimestamp);
if (indexAfter < 0) {
continue;
}
const indexBefore = sourceWithBounds.source.getNextEvent(
indexAfter, -1 /* direction */);
if (indexBefore < 0) {
continue;
}
const eventBefore = sourceWithBounds.source.events[indexBefore];
const eventAfter = sourceWithBounds.source.events[indexAfter];
let factor = (eventTimestamp - eventBefore[1]) /
(eventAfter[1] - eventBefore[1]);
if (!sourceWithBounds.smooth) {
// Clamp to before value.
if (factor < 1.0) {
factor = 0.0;
}
}
const value = factor * eventAfter[2] + (1.0 - factor) * eventBefore[2];
text = (value * sourceWithBounds.attributes.scale).toFixed(1) + ' ' +
sourceWithBounds.attributes.name;
color = sourceWithBounds.attributes.color;
}
if (!contentAdded) {
yOffset = this.addTimeInfoToTooltip_(svg, yOffset, eventTimestamp);
contentAdded = true;
}
yOffset += this.lineHeight;
SVG.addCircle(
svg, this.iconOffset, yOffset - this.iconRadius, this.iconRadius, 1,
color, 'black');
SVG.addText(svg, valueOffset, yOffset, this.fontSize, text);
}
if (contentAdded) {
yOffset += this.verticalGap;
}
return yOffset;
}
/**
* Helper that shows tooltip after filling its content.
*
* @param {Object} mouse event, used to determine the position of tooltip.
* @param {Object} svg content of tooltip.
* @param {number} height of the tooltip view.
* @param {number} width of the tooltip view.
*/
showTooltipForEvent_(event, svg, height, width) {
svg.setAttribute('height', height + 'px');
svg.setAttribute('width', width + 'px');
this.tooltip.style.left = event.pageX + 'px';
this.tooltip.style.top = event.pageY + 'px';
this.tooltip.style.height = height + 'px';
this.tooltip.style.width = width + 'px';
this.tooltip.classList.add('active');
}
/**
* Returns true in case band can show detailed info.
*/
canShowDetailedInfo() {
return false;
}
/**
* Shows detailed info for the position under mouse event |event|. By default
* it creates nothing.
*/
showDetailedInfo(event) {
return null;
}
}
/**
* Base class for detailed info view.
*/
class DetailedInfoView {
discard() {}
}
/**
* CPU detailed info view. Renders 4x zoomed CPU events split by processes and
* threads.
*/
class CpuDetailedInfoView extends DetailedInfoView {
create(overviewBand) {
this.overlay = $('arc-detailed-view-overlay');
const overviewRect = overviewBand.svg.getBoundingClientRect();
// Clear previous content.
this.overlay.textContent = '';
// UI constants to render.
const columnNameWidth = 130;
const columnUsageWidth = 40;
const scrollBarWidth = 3;
const zoomFactor = 4.0;
const cpuBandHeight = 14;
const processInfoHeight = 14;
const padding = 2;
const fontSize = 12;
const processInfoPadding = 2;
const threadInfoPadding = 12;
const cpuUsagePadding = 2;
const columnsWidth = columnNameWidth + columnUsageWidth;
// Use minimum 80% of inner width or 600 pixels to display detailed view
// zoomed |zoomFactor| times.
let availableWidthPixels =
window.innerWidth * 0.8 - columnsWidth - scrollBarWidth;
availableWidthPixels = Math.max(availableWidthPixels, 600);
const availableForHalfBandMcs = Math.floor(
overviewBand.offsetToTime(availableWidthPixels) / (2.0 * zoomFactor));
// Determine the interval to display.
const eventTimestamp =
overviewBand.offsetToTime(event.offsetX - overviewBand.bandOffsetX);
const minTimestamp = eventTimestamp - availableForHalfBandMcs;
const maxTimestamp = eventTimestamp + availableForHalfBandMcs + 1;
const duration = maxTimestamp - minTimestamp;
// Construct sub-model of active/idle events per each thread, active within
// this interval.
const eventsPerTid = {};
for (let cpuId = 0; cpuId < overviewBand.model.system.cpu.length; cpuId++) {
const activeEvents = new Events(
overviewBand.model.system.cpu[cpuId], 3 /* kActive */,
3 /* kActive */);
// Assume we have an idle before minTimestamp.
let activeTid = 0;
let index = activeEvents.getFirstAfter(minTimestamp);
let activeStartTimestamp = minTimestamp;
// Check if previous event goes over minTimestamp, in that case extract
// the active thread.
if (index > 0) {
const lastBefore = activeEvents.getNextEvent(index, -1 /* direction */);
if (lastBefore >= 0) {
// This may be idle (tid=0) or real thread.
activeTid = activeEvents.events[lastBefore][2];
}
}
while (index >= 0 && activeEvents.events[index][1] < maxTimestamp) {
this.addActivityTime_(
eventsPerTid, activeTid, activeStartTimestamp,
activeEvents.events[index][1]);
activeTid = activeEvents.events[index][2];
activeStartTimestamp = activeEvents.events[index][1];
index = activeEvents.getNextEvent(index, 1 /* direction */);
}
this.addActivityTime_(
eventsPerTid, activeTid, activeStartTimestamp, maxTimestamp - 1);
}
// The same thread might be executed on different CPU cores. Sort events.
for (const tid in eventsPerTid) {
eventsPerTid[tid].events.sort(function(a, b) {
return a[1] - b[1];
});
}
// Group threads by process.
const threadsPerPid = {};
const pids = [];
let totalTime = 0;
for (const tid in eventsPerTid) {
const thread = eventsPerTid[tid];
const pid = overviewBand.model.system.threads[tid].pid;
if (!(pid in threadsPerPid)) {
pids.push(pid);
threadsPerPid[pid] = {};
threadsPerPid[pid].totalTime = 0;
threadsPerPid[pid].threads = [];
}
threadsPerPid[pid].totalTime += thread.totalTime;
threadsPerPid[pid].threads.push(thread);
totalTime += thread.totalTime;
}
// Sort processes per time usage.
pids.sort(function(a, b) {
return threadsPerPid[b].totalTime - threadsPerPid[a].totalTime;
});
const totalUsage = 100.0 * totalTime / duration;
const cpuInfo = 'CPU view. ' + pids.length + '/' +
Object.keys(eventsPerTid).length +
' active processes/threads. Total cpu usage: ' + totalUsage.toFixed(2) +
'%.';
const title = new EventBandTitle(
this.overlay, undefined /* anchor */, cpuInfo, 'arc-cpu-view-title');
const bands = new EventBands(
title, 'arc-events-cpu-detailed-band',
overviewBand.resolution / zoomFactor, minTimestamp, maxTimestamp);
bands.setBandOffsetX(columnsWidth);
const bandsWidth = bands.timestampToOffset(maxTimestamp);
const totalWidth = bandsWidth + columnsWidth;
bands.setWidth(totalWidth);
for (let i = 0; i < pids.length; i++) {
const pid = pids[i];
const threads = threadsPerPid[pid].threads;
let processName;
if (pid in overviewBand.model.system.threads) {
processName = overviewBand.model.system.threads[pid].name;
} else {
processName = 'Others';
}
const processCpuUsage = 100.0 * threadsPerPid[pid].totalTime / duration;
const processInfo = processName + ' <' + pid + '>';
const processInfoTextLine =
bands.nextYOffset + processInfoHeight - padding;
SVG.addText(
bands.svg, processInfoPadding, processInfoTextLine, fontSize,
processInfo);
SVG.addText(
bands.svg, columnsWidth - cpuUsagePadding, processInfoTextLine,
fontSize, processCpuUsage.toFixed(2), 'end' /* anchor */);
// Sort threads per time usage.
threads.sort(function(a, b) {
return eventsPerTid[b.tid].totalTime - eventsPerTid[a.tid].totalTime;
});
// In case we have only one main thread add CPU info to process.
if (threads.length === 1 && threads[0].tid === pid) {
bands.addBand(
new Events(eventsPerTid[pid].events, 0, 1), cpuBandHeight, padding);
bands.addBandSeparator(2 /* padding */);
continue;
}
bands.nextYOffset += (processInfoHeight + padding);
for (let j = 0; j < threads.length; j++) {
const tid = threads[j].tid;
bands.addBand(
new Events(eventsPerTid[tid].events, 0, 1), cpuBandHeight, padding);
const threadName = overviewBand.model.system.threads[tid].name;
const threadCpuUsage = 100.0 * threads[j].totalTime / duration;
SVG.addText(
bands.svg, threadInfoPadding, bands.nextYOffset - padding, fontSize,
threadName);
SVG.addText(
bands.svg, columnsWidth - cpuUsagePadding,
bands.nextYOffset - 2 * padding, fontSize,
threadCpuUsage.toFixed(2), 'end' /* anchor */);
}
bands.addBandSeparator(2 /* padding */);
}
// Add center and boundary lines.
const kTimeMark = 10000;
const timeEvents = [
[kTimeMark, minTimestamp],
[kTimeMark, eventTimestamp],
[kTimeMark, maxTimestamp - 1],
];
bands.addGlobal(new Events(timeEvents, kTimeMark, kTimeMark));
SVG.addLine(
bands.svg, columnNameWidth, 0, columnNameWidth, bands.height, '#888',
0.25);
SVG.addLine(
bands.svg, columnsWidth, 0, columnsWidth, bands.height, '#888', 0.25);
// Mark zoomed interval in overview.
const overviewX = overviewBand.timestampToOffset(minTimestamp);
const overviewWidth =
overviewBand.timestampToOffset(maxTimestamp) - overviewX;
this.bandSelection = SVG.addRect(
overviewBand.svg, overviewX, 0, overviewWidth, overviewBand.height,
'#000' /* color */, 0.1 /* opacity */);
// Prevent band selection to capture mouse events that would lead to
// incorrect anchor position computation for detailed view.
this.bandSelection.classList.add('arc-no-mouse-events');
// Align position in overview and middle line here if possible.
const left = Math.max(
Math.min(
Math.round(event.clientX - columnsWidth - bandsWidth * 0.5),
window.innerWidth - totalWidth),
0);
this.overlay.style.left = left + 'px';
// Place below the overview with small gap.
this.overlay.style.top = (overviewRect.bottom + window.scrollY + 2) + 'px';
this.overlay.classList.add('active');
}
discard() {
this.overlay.classList.remove('active');
this.bandSelection.remove();
}
/**
* Helper that adds kIdleIn/kIdleOut events into the dictionary.
*
* @param {Object} eventsPerTid dictionary to fill. Key is thread id and
* value is object that contains all events for thread with related
* information.
* @param {number} tid thread id.
* @param {number} timestampFrom start time of thread activity.
* @param {number} timestampTo end time of thread activity.
*/
addActivityTime_(eventsPerTid, tid, timestampFrom, timestampTo) {
if (tid === 0) {
// Don't process idle thread.
return;
}
if (!(tid in eventsPerTid)) {
// Create the band for the new thread.
eventsPerTid[tid] = {};
eventsPerTid[tid].totalTime = 0;
eventsPerTid[tid].events = [];
eventsPerTid[tid].tid = tid;
}
eventsPerTid[tid].events.push([1 /* kIdleOut */, timestampFrom]);
eventsPerTid[tid].events.push([0 /* kIdleIn */, timestampTo]);
// Update total time for this thread.
eventsPerTid[tid].totalTime += (timestampTo - timestampFrom);
}
}
class CpuEventBands extends EventBands {
setModel(model) {
this.model = model;
this.showDetailedInfo = true;
const bandHeight = 6;
const padding = 2;
for (let cpuId = 0; cpuId < this.model.system.cpu.length; cpuId++) {
this.addBand(
new Events(this.model.system.cpu[cpuId], 0, 1), bandHeight, padding);
}
}
canShowDetailedInfo() {
return this.showDetailedInfo;
}
showDetailedInfo(event) {
const view = new CpuDetailedInfoView();
view.create(this);
return view;
}
}
/** Represents one band with events. */
class Events {
/**
* Assigns events for this band. Events with type between |eventTypeMin| and
* |eventTypeMax| are only displayed on the band.
*
* @param {Object[]} events non-filtered list of all events. Each has array
* where first element is type and second is timestamp.
* @param {number} eventTypeMin minimum inclusive type of the event to be
* displayed on this band.
* @param {number=} opt_eventTypeMax maximum inclusive type of the event to be
* displayed on this band. It is optional and in case is not set then
* range of one event type eventTypeMin is used.
*/
constructor(events, eventTypeMin, opt_eventTypeMax) {
this.events = events;
this.eventTypeMin = eventTypeMin;
if (opt_eventTypeMax) {
this.eventTypeMax = opt_eventTypeMax;
} else {
this.eventTypeMax = eventTypeMin;
}
}
/**
* Helper that finds next or previous event. Events that pass filter are
* only processed.
*
* @param {number} index starting index for the search, not inclusive.
* @param {direction} direction to search, 1 means to find the next event and
* -1 means the previous event.
* @returns {number} index of the next or previous event or -1 in case not
* found.
*/
getNextEvent(index, direction) {
while (true) {
index += direction;
if (index < 0 || index >= this.events.length) {
return -1;
}
if (this.events[index][0] >= this.eventTypeMin &&
this.events[index][0] <= this.eventTypeMax) {
return index;
}
}
}
/**
* Helper that finds first event. Events that pass filter are only processed.
*
* @returns {number} index of the first event or -1 in case not found.
*/
getFirstEvent() {
return this.getNextEvent(-1 /* index */, 1 /* direction */);
}
/**
* Helper that returns render attributes for the event.
*
* @param {number} index element index in |this.events|.
*/
getEventAttributes(index) {
return eventAttributes[this.events[index][0]];
}
/**
* Returns true if the tested event denotes end of event sequence.
*
* @param {number} index element index in |this.events|.
*/
isEndOfSequence(index) {
const nextEventTypes = endSequenceEvents[this.events[index][0]];
if (!nextEventTypes) {
return false;
}
if (nextEventTypes.length === 0) {
return true;
}
const nextIndex = this.getNextEvent(index, 1 /* direction */);
if (nextIndex < 0) {
// No more events after and it is listed as possible end of sequence.
return true;
}
return nextEventTypes.includes(this.events[nextIndex][0]);
}
/**
* Returns the index of closest event to the requested |timestamp|.
*
* @param {number} timestamp to search.
*/
getClosest(timestamp) {
if (this.events.length === 0) {
return -1;
}
if (this.events[0][1] >= timestamp) {
return this.getFirstEvent();
}
if (this.events[this.events.length - 1][1] <= timestamp) {
return this.getNextEvent(
this.events.length /* index */, -1 /* direction */);
}
// At this moment |firstBefore| and |firstAfter| points to any event.
let firstBefore = 0;
let firstAfter = this.events.length - 1;
while (firstBefore + 1 !== firstAfter) {
const candidateIndex = Math.ceil((firstBefore + firstAfter) / 2);
if (this.events[candidateIndex][1] < timestamp) {
firstBefore = candidateIndex;
} else {
firstAfter = candidateIndex;
}
}
// Point |firstBefore| and |firstAfter| to the supported event types.
firstBefore =
this.getNextEvent(firstBefore + 1 /* index */, -1 /* direction */);
firstAfter =
this.getNextEvent(firstAfter - 1 /* index */, 1 /* direction */);
if (firstBefore < 0) {
return firstAfter;
} else if (firstAfter < 0) {
return firstBefore;
} else {
const diffBefore = timestamp - this.events[firstBefore][1];
const diffAfter = this.events[firstAfter][1] - timestamp;
if (diffBefore < diffAfter) {
return firstBefore;
} else {
return firstAfter;
}
}
}
/**
* Returns the index of the first event after or on requested |timestamp|.
*
* @param {number} timestamp to search.
*/
getFirstAfter(timestamp) {
const closest = this.getClosest(timestamp);
if (closest < 0) {
return -1;
}
if (this.events[closest][1] >= timestamp) {
return closest;
}
return this.getNextEvent(closest, 1 /* direction */);
}
/**
* Returns the index of the last event before or on requested |timestamp|.
*
* @param {number} timestamp to search.
*/
getLastBefore(timestamp) {
const closest = this.getClosest(timestamp);
if (closest < 0) {
return -1;
}
if (this.events[closest][1] <= timestamp) {
return closest;
}
return this.getNextEvent(closest, -1 /* direction */);
}
}
/**
* Helper function that creates chart with required attributes.
*
* @param {HTMLElement} parent container for the newly created chart.
* @param {string} title of the chart.
* @param {number} resolution scale of the chart in microseconds per pixel.
* @param {number} duration length of the chart in microseconds.
* @param {number} height of the chart in pixels.
* @param {number} gridLinesCount number of extra intermediate grid lines, 0 i
* not required.
* @param {HTMLElement} anchor insert point. View will be added after this.
* may be optional.
*
*/
function createChart(
parent, title, resolution, duration, height, gridLinesCount, anchor) {
const titleBands =
new EventBandTitle(parent, anchor, title, 'arc-events-band-title');
const bands =
new EventBands(titleBands, 'arc-events-band', resolution, 0, duration);
bands.setWidth(bands.timestampToOffset(duration));
bands.addChart(height, 4 /* padding */);
for (let i = 0; i < gridLinesCount; i++) {
bands.addChartGridLine((i + 1) * height / (gridLinesCount + 1));
}
return bands;
}