blob: f533bfe8a2d124e54e59cfca299aebfc38aecf83 [file] [log] [blame]
/**
* 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_underscored_properties */
import * as Common from '../core/common/common.js';
import * as Host from '../core/host/host.js';
import * as i18n from '../core/i18n/i18n.js';
import * as Platform from '../core/platform/platform.js';
import * as Root from '../core/root/root.js';
import * as SDK from '../core/sdk/sdk.js'; // eslint-disable-line no-unused-vars
import * as ThemeSupport from '../theme_support/theme_support.js';
import * as TimelineModel from '../timeline_model/timeline_model.js'; // eslint-disable-line no-unused-vars
import * as UI from '../ui/legacy/legacy.js';
import {ChartViewport, ChartViewportDelegate} from './ChartViewport.js'; // eslint-disable-line no-unused-vars
import {Calculator, TimelineGrid} from './TimelineGrid.js'; // eslint-disable-line no-unused-vars
const UIStrings = {
/**
*@description Aria accessible name in Flame Chart of the Performance panel
*/
flameChart: 'Flame Chart',
/**
*@description Text for the screen reader to announce a hovered group
*@example {Network} PH1
*/
sHovered: '{PH1} hovered',
/**
*@description Text for screen reader to announce a selected group.
*@example {Network} PH1
*/
sSelected: '{PH1} selected',
/**
*@description Text for screen reader to announce an expanded group
*@example {Network} PH1
*/
sExpanded: '{PH1} expanded',
/**
*@description Text for screen reader to announce a collapsed group
*@example {Network} PH1
*/
sCollapsed: '{PH1} collapsed',
};
const str_ = i18n.i18n.registerUIStrings('perf_ui/FlameChart.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class FlameChartDelegate {
windowChanged(_startTime: number, _endTime: number, _animate: boolean): void {
}
updateRangeSelection(_startTime: number, _endTime: number): void {
}
updateSelectedGroup(_flameChart: FlameChart, _group: Group|null): void {
}
}
interface GroupExpansionState {
[key: string]: boolean;
}
export class FlameChart extends UI.Widget.VBox implements Calculator, ChartViewportDelegate {
_groupExpansionSetting?: Common.Settings.Setting<GroupExpansionState>;
_groupExpansionState: GroupExpansionState;
_flameChartDelegate: FlameChartDelegate;
_useWebGL: boolean;
_chartViewport: ChartViewport;
_dataProvider: FlameChartDataProvider;
_candyStripeCanvas: HTMLCanvasElement;
_viewportElement: HTMLElement;
_canvasGL!: HTMLCanvasElement;
_canvas: HTMLCanvasElement;
_entryInfo: HTMLElement;
_markerHighlighElement: HTMLElement;
_highlightElement: HTMLElement;
_selectedElement: HTMLElement;
_rulerEnabled: boolean;
_rangeSelectionStart: number;
_rangeSelectionEnd: number;
_barHeight: number;
_textBaseline: number;
_textPadding: number;
_markerRadius: number;
_headerLeftPadding: number;
_arrowSide: number;
_expansionArrowIndent: number;
_headerLabelXPadding: number;
_headerLabelYPadding: number;
_highlightedMarkerIndex: number;
_highlightedEntryIndex: number;
_selectedEntryIndex: number;
_rawTimelineDataLength: number;
_textWidth: Map<string, Map<string, number>>;
_markerPositions: Map<number, {
x: number,
width: number,
}>;
_lastMouseOffsetX: number;
_selectedGroup: number;
_keyboardFocusedGroup: number;
_selectedGroupBackroundColor: string;
_selectedGroupBorderColor: string;
_offsetWidth!: number;
_offsetHeight!: number;
_dragStartX!: number;
_dragStartY!: number;
_lastMouseOffsetY!: number;
_minimumBoundary!: number;
_maxDragOffset!: number;
_shaderProgram?: WebGLProgram|null;
_vertexBuffer?: WebGLBuffer|null;
_colorBuffer?: WebGLBuffer|null;
_uScalingFactor?: WebGLUniformLocation|null;
_uShiftVector?: WebGLUniformLocation|null;
_aVertexPosition?: number;
_aVertexColor?: number;
_vertexCount?: number;
_prevTimelineData?: TimelineData;
_timelineLevels?: number[][]|null;
_visibleLevelOffsets?: Uint32Array|null;
_visibleLevels?: Uint16Array|null;
_groupOffsets?: Uint32Array|null;
_rawTimelineData?: TimelineData|null;
_forceDecorationCache?: Int8Array|null;
_entryColorsCache?: string[]|null;
_visibleLevelHeights?: Uint32Array;
_totalTime?: number;
constructor(
dataProvider: FlameChartDataProvider, flameChartDelegate: FlameChartDelegate,
groupExpansionSetting?: Common.Settings.Setting<GroupExpansionState>) {
super(true);
this.registerRequiredCSS('perf_ui/flameChart.css', {enableLegacyPatching: false});
this.contentElement.classList.add('flame-chart-main-pane');
this._groupExpansionSetting = groupExpansionSetting;
this._groupExpansionState = groupExpansionSetting && groupExpansionSetting.get() || {};
this._flameChartDelegate = flameChartDelegate;
this._useWebGL = Root.Runtime.experiments.isEnabled('timelineWebGL');
this._chartViewport = new ChartViewport(this);
this._chartViewport.show(this.contentElement);
this._dataProvider = dataProvider;
this._candyStripeCanvas = (document.createElement('canvas') as HTMLCanvasElement);
this._createCandyStripePattern();
this._viewportElement = this._chartViewport.viewportElement;
if (this._useWebGL) {
this._canvasGL = (this._viewportElement.createChild('canvas', 'fill') as HTMLCanvasElement);
this._initWebGL();
}
this._canvas = (this._viewportElement.createChild('canvas', 'fill') as HTMLCanvasElement);
this._canvas.tabIndex = 0;
UI.ARIAUtils.setAccessibleName(this._canvas, i18nString(UIStrings.flameChart));
UI.ARIAUtils.markAsTree(this._canvas);
this.setDefaultFocusedElement(this._canvas);
this._canvas.classList.add('flame-chart-canvas');
this._canvas.addEventListener('mousemove', this._onMouseMove.bind(this), false);
this._canvas.addEventListener('mouseout', this._onMouseOut.bind(this), false);
this._canvas.addEventListener('click', this._onClick.bind(this), false);
this._canvas.addEventListener('keydown', this._onKeyDown.bind(this), false);
this._entryInfo = this._viewportElement.createChild('div', 'flame-chart-entry-info');
this._markerHighlighElement = this._viewportElement.createChild('div', 'flame-chart-marker-highlight-element');
this._highlightElement = this._viewportElement.createChild('div', 'flame-chart-highlight-element');
this._selectedElement = this._viewportElement.createChild('div', 'flame-chart-selected-element');
this._canvas.addEventListener('focus', () => {
this.dispatchEventToListeners(Events.CanvasFocused);
}, false);
UI.UIUtils.installDragHandle(
this._viewportElement, this._startDragging.bind(this), this._dragging.bind(this), this._endDragging.bind(this),
null);
this._rulerEnabled = true;
this._rangeSelectionStart = 0;
this._rangeSelectionEnd = 0;
this._barHeight = 17;
this._textBaseline = 5;
this._textPadding = 5;
this._markerRadius = 6;
this._chartViewport.setWindowTimes(
dataProvider.minimumBoundary(), dataProvider.minimumBoundary() + dataProvider.totalTime());
this._headerLeftPadding = 6;
this._arrowSide = 8;
this._expansionArrowIndent = this._headerLeftPadding + this._arrowSide / 2;
this._headerLabelXPadding = 3;
this._headerLabelYPadding = 2;
this._highlightedMarkerIndex = -1;
this._highlightedEntryIndex = -1;
this._selectedEntryIndex = -1;
this._rawTimelineDataLength = 0;
this._textWidth = new Map();
this._markerPositions = new Map();
this._lastMouseOffsetX = 0;
this._selectedGroup = -1;
// Keyboard focused group is used to navigate groups irrespective of whether they are selectable or not
this._keyboardFocusedGroup = -1;
this._selectedGroupBackroundColor = ThemeSupport.ThemeSupport.instance().patchColorText(
Colors.SelectedGroupBackground, ThemeSupport.ThemeSupport.ColorUsage.Background);
this._selectedGroupBorderColor = ThemeSupport.ThemeSupport.instance().patchColorText(
Colors.SelectedGroupBorder, ThemeSupport.ThemeSupport.ColorUsage.Background);
}
willHide(): void {
this.hideHighlight();
}
setBarHeight(value: number): void {
this._barHeight = value;
}
setTextBaseline(value: number): void {
this._textBaseline = value;
}
setTextPadding(value: number): void {
this._textPadding = value;
}
enableRuler(enable: boolean): void {
this._rulerEnabled = enable;
}
alwaysShowVerticalScroll(): void {
this._chartViewport.alwaysShowVerticalScroll();
}
disableRangeSelection(): void {
this._chartViewport.disableRangeSelection();
}
highlightEntry(entryIndex: number): void {
if (this._highlightedEntryIndex === entryIndex) {
return;
}
if (!this._dataProvider.entryColor(entryIndex)) {
return;
}
this._highlightedEntryIndex = entryIndex;
this._updateElementPosition(this._highlightElement, this._highlightedEntryIndex);
this.dispatchEventToListeners(Events.EntryHighlighted, entryIndex);
}
hideHighlight(): void {
this._entryInfo.removeChildren();
this._highlightedEntryIndex = -1;
this._updateElementPosition(this._highlightElement, this._highlightedEntryIndex);
this.dispatchEventToListeners(Events.EntryHighlighted, -1);
}
_createCandyStripePattern(): void {
// Set the candy stripe pattern to 17px so it repeats well.
const size = 17;
this._candyStripeCanvas.width = size;
this._candyStripeCanvas.height = size;
const ctx = this._candyStripeCanvas.getContext('2d');
if (!ctx) {
return;
}
// Rotate the stripe by 45deg to the right.
ctx.translate(size * 0.5, size * 0.5);
ctx.rotate(Math.PI * 0.25);
ctx.translate(-size * 0.5, -size * 0.5);
ctx.fillStyle = 'rgba(255, 0, 0, 0.4)';
for (let x = -size; x < size * 2; x += 3) {
ctx.fillRect(x, -size, 1, size * 3);
}
}
_resetCanvas(): void {
const ratio = window.devicePixelRatio;
const width = Math.round(this._offsetWidth * ratio);
const height = Math.round(this._offsetHeight * ratio);
this._canvas.width = width;
this._canvas.height = height;
this._canvas.style.width = `${width / ratio}px`;
this._canvas.style.height = `${height / ratio}px`;
if (this._useWebGL) {
this._canvasGL.width = width;
this._canvasGL.height = height;
this._canvasGL.style.width = `${width / ratio}px`;
this._canvasGL.style.height = `${height / ratio}px`;
}
}
windowChanged(startTime: number, endTime: number, animate: boolean): void {
this._flameChartDelegate.windowChanged(startTime, endTime, animate);
}
updateRangeSelection(startTime: number, endTime: number): void {
this._flameChartDelegate.updateRangeSelection(startTime, endTime);
}
setSize(width: number, height: number): void {
this._offsetWidth = width;
this._offsetHeight = height;
}
_startDragging(event: MouseEvent): boolean {
this.hideHighlight();
this._maxDragOffset = 0;
this._dragStartX = event.pageX;
this._dragStartY = event.pageY;
return true;
}
_dragging(event: MouseEvent): void {
const dx = event.pageX - this._dragStartX;
const dy = event.pageY - this._dragStartY;
this._maxDragOffset = Math.max(this._maxDragOffset, Math.sqrt(dx * dx + dy * dy));
}
_endDragging(_event: MouseEvent): void {
this._updateHighlight();
}
_timelineData(): TimelineData|null {
if (!this._dataProvider) {
return null;
}
const timelineData = this._dataProvider.timelineData();
if (timelineData !== this._rawTimelineData ||
(timelineData && timelineData.entryStartTimes.length !== this._rawTimelineDataLength)) {
this._processTimelineData(timelineData);
}
return this._rawTimelineData || null;
}
_revealEntry(entryIndex: number): void {
const timelineData = this._timelineData();
if (!timelineData) {
return;
}
const timeLeft = this._chartViewport.windowLeftTime();
const timeRight = this._chartViewport.windowRightTime();
const entryStartTime = timelineData.entryStartTimes[entryIndex];
const entryTotalTime = timelineData.entryTotalTimes[entryIndex];
const entryEndTime = entryStartTime + entryTotalTime;
let minEntryTimeWindow = Math.min(entryTotalTime, timeRight - timeLeft);
const level = timelineData.entryLevels[entryIndex];
this._chartViewport.setScrollOffset(this._levelToOffset(level), this._levelHeight(level));
const minVisibleWidthPx = 30;
const futurePixelToTime = (timeRight - timeLeft) / this._offsetWidth;
minEntryTimeWindow = Math.max(minEntryTimeWindow, futurePixelToTime * minVisibleWidthPx);
if (timeLeft > entryEndTime) {
const delta = timeLeft - entryEndTime + minEntryTimeWindow;
this.windowChanged(timeLeft - delta, timeRight - delta, /* animate */ true);
} else if (timeRight < entryStartTime) {
const delta = entryStartTime - timeRight + minEntryTimeWindow;
this.windowChanged(timeLeft + delta, timeRight + delta, /* animate */ true);
}
}
setWindowTimes(startTime: number, endTime: number, animate?: boolean): void {
this._chartViewport.setWindowTimes(startTime, endTime, animate);
this._updateHighlight();
}
_onMouseMove(event: Event): void {
const mouseEvent = (event as MouseEvent);
this._lastMouseOffsetX = mouseEvent.offsetX;
this._lastMouseOffsetY = mouseEvent.offsetY;
if (!this._enabled()) {
return;
}
if (this._chartViewport.isDragging()) {
return;
}
if (this._coordinatesToGroupIndex(mouseEvent.offsetX, mouseEvent.offsetY, true /* headerOnly */) >= 0) {
this.hideHighlight();
this._viewportElement.style.cursor = 'pointer';
return;
}
this._updateHighlight();
}
_updateHighlight(): void {
const entryIndex = this._coordinatesToEntryIndex(this._lastMouseOffsetX, this._lastMouseOffsetY);
if (entryIndex === -1) {
this.hideHighlight();
const group =
this._coordinatesToGroupIndex(this._lastMouseOffsetX, this._lastMouseOffsetY, false /* headerOnly */);
if (group >= 0 && this._rawTimelineData && this._rawTimelineData.groups &&
this._rawTimelineData.groups[group].selectable) {
this._viewportElement.style.cursor = 'pointer';
} else {
this._viewportElement.style.cursor = 'default';
}
return;
}
if (this._chartViewport.isDragging()) {
return;
}
this._updatePopover(entryIndex);
this._viewportElement.style.cursor = this._dataProvider.canJumpToEntry(entryIndex) ? 'pointer' : 'default';
this.highlightEntry(entryIndex);
}
_onMouseOut(): void {
this._lastMouseOffsetX = -1;
this._lastMouseOffsetY = -1;
this.hideHighlight();
}
_updatePopover(entryIndex: number): void {
if (entryIndex === this._highlightedEntryIndex) {
this._updatePopoverOffset();
return;
}
this._entryInfo.removeChildren();
const popoverElement = this._dataProvider.prepareHighlightedEntryInfo(entryIndex);
if (popoverElement) {
this._entryInfo.appendChild(popoverElement);
this._updatePopoverOffset();
}
}
_updatePopoverOffset(): void {
const mouseX = this._lastMouseOffsetX;
const mouseY = this._lastMouseOffsetY;
const parentWidth = this._entryInfo.parentElement ? this._entryInfo.parentElement.clientWidth : 0;
const parentHeight = this._entryInfo.parentElement ? this._entryInfo.parentElement.clientHeight : 0;
const infoWidth = this._entryInfo.clientWidth;
const infoHeight = this._entryInfo.clientHeight;
const /** @const */ offsetX = 10;
const /** @const */ offsetY = 6;
let x;
let y;
for (let quadrant = 0; quadrant < 4; ++quadrant) {
const dx = quadrant & 2 ? -offsetX - infoWidth : offsetX;
const dy = quadrant & 1 ? -offsetY - infoHeight : offsetY;
x = Platform.NumberUtilities.clamp(mouseX + dx, 0, parentWidth - infoWidth);
y = Platform.NumberUtilities.clamp(mouseY + dy, 0, parentHeight - infoHeight);
if (x >= mouseX || mouseX >= x + infoWidth || y >= mouseY || mouseY >= y + infoHeight) {
break;
}
}
this._entryInfo.style.left = x + 'px';
this._entryInfo.style.top = y + 'px';
}
_onClick(event: Event): void {
const mouseEvent = (event as MouseEvent);
this.focus();
// onClick comes after dragStart and dragEnd events.
// So if there was drag (mouse move) in the middle of that events
// we skip the click. Otherwise we jump to the sources.
const clickThreshold = 5;
if (this._maxDragOffset > clickThreshold) {
return;
}
this._selectGroup(this._coordinatesToGroupIndex(mouseEvent.offsetX, mouseEvent.offsetY, false /* headerOnly */));
this._toggleGroupExpand(
this._coordinatesToGroupIndex(mouseEvent.offsetX, mouseEvent.offsetY, true /* headerOnly */));
const timelineData = this._timelineData();
if (mouseEvent.shiftKey && this._highlightedEntryIndex !== -1 && timelineData) {
const start = timelineData.entryStartTimes[this._highlightedEntryIndex];
const end = start + timelineData.entryTotalTimes[this._highlightedEntryIndex];
this._chartViewport.setRangeSelection(start, end);
} else {
this._chartViewport.onClick(mouseEvent);
this.dispatchEventToListeners(Events.EntryInvoked, this._highlightedEntryIndex);
}
}
_selectGroup(groupIndex: number): void {
if (groupIndex < 0 || this._selectedGroup === groupIndex) {
return;
}
if (!this._rawTimelineData) {
return;
}
const groups = this._rawTimelineData.groups;
if (!groups) {
return;
}
this._keyboardFocusedGroup = groupIndex;
this._scrollGroupIntoView(groupIndex);
const groupName = groups[groupIndex].name;
if (!groups[groupIndex].selectable) {
this._deselectAllGroups();
UI.ARIAUtils.alert(i18nString(UIStrings.sHovered, {PH1: groupName}), this._canvas);
} else {
this._selectedGroup = groupIndex;
this._flameChartDelegate.updateSelectedGroup(this, groups[groupIndex]);
this._resetCanvas();
this._draw();
UI.ARIAUtils.alert(i18nString(UIStrings.sSelected, {PH1: groupName}), this._canvas);
}
}
_deselectAllGroups(): void {
this._selectedGroup = -1;
this._flameChartDelegate.updateSelectedGroup(this, null);
this._resetCanvas();
this._draw();
}
_deselectAllEntries(): void {
this._selectedEntryIndex = -1;
this._resetCanvas();
this._draw();
}
_isGroupFocused(index: number): boolean {
return index === this._selectedGroup || index === this._keyboardFocusedGroup;
}
_scrollGroupIntoView(index: number): void {
if (index < 0) {
return;
}
if (!this._rawTimelineData) {
return;
}
const groups = this._rawTimelineData.groups;
const groupOffsets = this._groupOffsets;
if (!groupOffsets || !groups) {
return;
}
const groupTop = groupOffsets[index];
let nextOffset = groupOffsets[index + 1];
if (index === groups.length - 1) {
nextOffset += groups[index].style.padding;
}
// For the top group, scroll all the way to the top of the chart
// to accommodate the bar with time markers
const scrollTop = index === 0 ? 0 : groupTop;
const scrollHeight = Math.min(nextOffset - scrollTop, this._chartViewport.chartHeight());
this._chartViewport.setScrollOffset(scrollTop, scrollHeight);
}
_toggleGroupExpand(groupIndex: number): void {
if (groupIndex < 0 || !this._isGroupCollapsible(groupIndex)) {
return;
}
if (!this._rawTimelineData || !this._rawTimelineData.groups) {
return;
}
this._expandGroup(groupIndex, !this._rawTimelineData.groups[groupIndex].expanded /* setExpanded */);
}
_expandGroup(groupIndex: number, setExpanded: boolean|undefined = true, propagatedExpand: boolean|undefined = false):
void {
if (groupIndex < 0 || !this._isGroupCollapsible(groupIndex)) {
return;
}
if (!this._rawTimelineData) {
return;
}
const groups = this._rawTimelineData.groups;
if (!groups) {
return;
}
const group = groups[groupIndex];
group.expanded = setExpanded;
this._groupExpansionState[group.name] = group.expanded;
if (this._groupExpansionSetting) {
this._groupExpansionSetting.set(this._groupExpansionState);
}
this._updateLevelPositions();
this._updateHighlight();
if (!group.expanded) {
const timelineData = this._timelineData();
if (timelineData) {
const level = timelineData.entryLevels[this._selectedEntryIndex];
if (this._selectedEntryIndex >= 0 && level >= group.startLevel &&
(groupIndex >= groups.length - 1 || groups[groupIndex + 1].startLevel > level)) {
this._selectedEntryIndex = -1;
}
}
}
this._updateHeight();
this._resetCanvas();
this._draw();
this._scrollGroupIntoView(groupIndex);
// We only want to read expanded/collapsed state on user inputted expand/collapse
if (!propagatedExpand) {
const groupName = groups[groupIndex].name;
const content = group.expanded ? i18nString(UIStrings.sExpanded, {PH1: groupName}) :
i18nString(UIStrings.sCollapsed, {PH1: groupName});
UI.ARIAUtils.alert(content, this._canvas);
}
}
_onKeyDown(e: KeyboardEvent): void {
if (!UI.KeyboardShortcut.KeyboardShortcut.hasNoModifiers(e) || !this._timelineData()) {
return;
}
const eventHandled = this._handleSelectionNavigation(e);
// Handle keyboard navigation in groups
if (!eventHandled && this._rawTimelineData && this._rawTimelineData.groups) {
this._handleKeyboardGroupNavigation(e);
}
}
bindCanvasEvent(eventName: string, onEvent: (arg0: Event) => void): void {
this._canvas.addEventListener(eventName, onEvent);
}
_handleKeyboardGroupNavigation(event: Event): void {
const keyboardEvent = (event as KeyboardEvent);
let handled = false;
let entrySelected = false;
if (keyboardEvent.code === 'ArrowUp') {
handled = this._selectPreviousGroup();
} else if (keyboardEvent.code === 'ArrowDown') {
handled = this._selectNextGroup();
} else if (keyboardEvent.code === 'ArrowLeft') {
if (this._keyboardFocusedGroup >= 0) {
this._expandGroup(this._keyboardFocusedGroup, false /* setExpanded */);
handled = true;
}
} else if (keyboardEvent.code === 'ArrowRight') {
if (this._keyboardFocusedGroup >= 0) {
this._expandGroup(this._keyboardFocusedGroup, true /* setExpanded */);
this._selectFirstChild();
handled = true;
}
} else if (keyboardEvent.key === 'Enter') {
entrySelected = this._selectFirstEntryInCurrentGroup();
handled = entrySelected;
}
if (handled && !entrySelected) {
this._deselectAllEntries();
}
if (handled) {
keyboardEvent.consume(true);
}
}
_selectFirstEntryInCurrentGroup(): boolean {
if (!this._rawTimelineData) {
return false;
}
const allGroups = this._rawTimelineData.groups;
if (this._keyboardFocusedGroup < 0 || !allGroups) {
return false;
}
const group = allGroups[this._keyboardFocusedGroup];
const startLevelInGroup = group.startLevel;
// Return if no levels in this group
if (startLevelInGroup < 0) {
return false;
}
// Make sure this is the innermost nested group with this startLevel
// This is because a parent group also contains levels of all its child groups
// So check if the next group has the same level, if it does, user should
// go to that child group to select this entry
if (this._keyboardFocusedGroup < allGroups.length - 1 &&
allGroups[this._keyboardFocusedGroup + 1].startLevel === startLevelInGroup) {
return false;
}
if (!this._timelineLevels) {
return false;
}
// Get first (default) entry in startLevel of selected group
const firstEntryIndex = this._timelineLevels[startLevelInGroup][0];
this._expandGroup(this._keyboardFocusedGroup, true /* setExpanded */);
this.setSelectedEntry(firstEntryIndex);
return true;
}
_selectPreviousGroup(): boolean {
if (this._keyboardFocusedGroup <= 0) {
return false;
}
const groupIndexToSelect = this._getGroupIndexToSelect(-1 /* offset */);
this._selectGroup(groupIndexToSelect);
return true;
}
_selectNextGroup(): boolean {
if (!this._rawTimelineData || !this._rawTimelineData.groups) {
return false;
}
if (this._keyboardFocusedGroup >= this._rawTimelineData.groups.length - 1) {
return false;
}
const groupIndexToSelect = this._getGroupIndexToSelect(1 /* offset */);
this._selectGroup(groupIndexToSelect);
return true;
}
_getGroupIndexToSelect(offset: number): number {
if (!this._rawTimelineData || !this._rawTimelineData.groups) {
throw new Error('No raw timeline data');
}
const allGroups = this._rawTimelineData.groups;
let groupIndexToSelect = this._keyboardFocusedGroup;
let groupName, groupWithSubNestingLevel;
do {
groupIndexToSelect += offset;
groupName = this._rawTimelineData.groups[groupIndexToSelect].name;
groupWithSubNestingLevel = this._keyboardFocusedGroup !== -1 &&
allGroups[groupIndexToSelect].style.nestingLevel > allGroups[this._keyboardFocusedGroup].style.nestingLevel;
} while (groupIndexToSelect > 0 && groupIndexToSelect < allGroups.length - 1 &&
(!groupName || groupWithSubNestingLevel));
return groupIndexToSelect;
}
_selectFirstChild(): void {
if (!this._rawTimelineData || !this._rawTimelineData.groups) {
return;
}
const allGroups = this._rawTimelineData.groups;
if (this._keyboardFocusedGroup < 0 || this._keyboardFocusedGroup >= allGroups.length - 1) {
return;
}
const groupIndexToSelect = this._keyboardFocusedGroup + 1;
if (allGroups[groupIndexToSelect].style.nestingLevel > allGroups[this._keyboardFocusedGroup].style.nestingLevel) {
this._selectGroup(groupIndexToSelect);
}
}
_handleSelectionNavigation(event: KeyboardEvent): boolean {
if (this._selectedEntryIndex === -1) {
return false;
}
const timelineData = this._timelineData();
if (!timelineData) {
return false;
}
function timeComparator(time: number, entryIndex: number): number {
if (!timelineData) {
throw new Error('No timeline data');
}
return time - timelineData.entryStartTimes[entryIndex];
}
function entriesIntersect(entry1: number, entry2: number): boolean {
if (!timelineData) {
throw new Error('No timeline data');
}
const start1 = timelineData.entryStartTimes[entry1];
const start2 = timelineData.entryStartTimes[entry2];
const end1 = start1 + timelineData.entryTotalTimes[entry1];
const end2 = start2 + timelineData.entryTotalTimes[entry2];
return start1 < end2 && start2 < end1;
}
const keyboardEvent = (event as KeyboardEvent);
const keys = UI.KeyboardShortcut.Keys;
if (keyboardEvent.keyCode === keys.Left.code || keyboardEvent.keyCode === keys.Right.code) {
const level = timelineData.entryLevels[this._selectedEntryIndex];
const levelIndexes = this._timelineLevels ? this._timelineLevels[level] : [];
let indexOnLevel = Platform.ArrayUtilities.lowerBound(levelIndexes, this._selectedEntryIndex, (a, b) => a - b);
indexOnLevel += keyboardEvent.keyCode === keys.Left.code ? -1 : 1;
event.consume(true);
if (indexOnLevel >= 0 && indexOnLevel < levelIndexes.length) {
this.dispatchEventToListeners(Events.EntrySelected, levelIndexes[indexOnLevel]);
}
return true;
}
if (keyboardEvent.keyCode === keys.Up.code || keyboardEvent.keyCode === keys.Down.code) {
let level = timelineData.entryLevels[this._selectedEntryIndex];
level += keyboardEvent.keyCode === keys.Up.code ? -1 : 1;
if (level < 0 || (this._timelineLevels && level >= this._timelineLevels.length)) {
this._deselectAllEntries();
keyboardEvent.consume(true);
return true;
}
const entryTime = timelineData.entryStartTimes[this._selectedEntryIndex] +
timelineData.entryTotalTimes[this._selectedEntryIndex] / 2;
const levelIndexes = this._timelineLevels ? this._timelineLevels[level] : [];
let indexOnLevel = Platform.ArrayUtilities.upperBound(levelIndexes, entryTime, timeComparator) - 1;
if (!entriesIntersect(this._selectedEntryIndex, levelIndexes[indexOnLevel])) {
++indexOnLevel;
if (indexOnLevel >= levelIndexes.length ||
!entriesIntersect(this._selectedEntryIndex, levelIndexes[indexOnLevel])) {
if (keyboardEvent.code === 'ArrowDown') {
return false;
}
// Stay in the current group and give focus to the parent group instead of entries
this._deselectAllEntries();
keyboardEvent.consume(true);
return true;
}
}
keyboardEvent.consume(true);
this.dispatchEventToListeners(Events.EntrySelected, levelIndexes[indexOnLevel]);
return true;
}
if (event.key === 'Enter') {
event.consume(true);
this.dispatchEventToListeners(Events.EntryInvoked, this._selectedEntryIndex);
return true;
}
return false;
}
_coordinatesToEntryIndex(x: number, y: number): number {
if (x < 0 || y < 0) {
return -1;
}
const timelineData = this._timelineData();
if (!timelineData) {
return -1;
}
y += this._chartViewport.scrollOffset();
if (!this._visibleLevelOffsets) {
throw new Error('No visible level offsets');
}
const cursorLevel =
Platform.ArrayUtilities.upperBound(this._visibleLevelOffsets, y, Platform.ArrayUtilities.DEFAULT_COMPARATOR) -
1;
if (cursorLevel < 0 || (this._visibleLevels && !this._visibleLevels[cursorLevel])) {
return -1;
}
const offsetFromLevel = y - this._visibleLevelOffsets[cursorLevel];
if (offsetFromLevel > this._levelHeight(cursorLevel)) {
return -1;
}
// Check markers first.
for (const [index, pos] of this._markerPositions) {
if (timelineData.entryLevels[index] !== cursorLevel) {
continue;
}
if (pos.x <= x && x < pos.x + pos.width) {
return /** @type {number} */ index as number;
}
}
// Check regular entries.
const entryStartTimes = timelineData.entryStartTimes;
const entriesOnLevel: number[] = this._timelineLevels ? this._timelineLevels[cursorLevel] : [];
if (!entriesOnLevel || !entriesOnLevel.length) {
return -1;
}
const cursorTime = this._chartViewport.pixelToTime(x);
const indexOnLevel = Math.max(
Platform.ArrayUtilities.upperBound(
entriesOnLevel, cursorTime, (time, entryIndex) => time - entryStartTimes[entryIndex]) -
1,
0);
function checkEntryHit(this: FlameChart, entryIndex: number|undefined): boolean {
if (entryIndex === undefined) {
return false;
}
if (!timelineData) {
return false;
}
const startTime = entryStartTimes[entryIndex];
const duration = timelineData.entryTotalTimes[entryIndex];
const startX = this._chartViewport.timeToPosition(startTime);
const endX = this._chartViewport.timeToPosition(startTime + duration);
const barThresholdPx = 3;
return startX - barThresholdPx < x && x < endX + barThresholdPx;
}
let entryIndex: number = entriesOnLevel[indexOnLevel];
if (checkEntryHit.call(this, entryIndex)) {
return entryIndex;
}
entryIndex = entriesOnLevel[indexOnLevel + 1];
if (checkEntryHit.call(this, entryIndex)) {
return entryIndex;
}
return -1;
}
_coordinatesToGroupIndex(x: number, y: number, headerOnly: boolean): number {
if (!this._rawTimelineData || !this._rawTimelineData.groups || !this._groupOffsets) {
return -1;
}
if (x < 0 || y < 0) {
return -1;
}
y += this._chartViewport.scrollOffset();
const groups = this._rawTimelineData.groups || [];
const group =
Platform.ArrayUtilities.upperBound(this._groupOffsets, y, Platform.ArrayUtilities.DEFAULT_COMPARATOR) - 1;
if (group < 0 || group >= groups.length) {
return -1;
}
const height = headerOnly ? groups[group].style.height : this._groupOffsets[group + 1] - this._groupOffsets[group];
if (y - this._groupOffsets[group] >= height) {
return -1;
}
if (!headerOnly) {
return group;
}
const context = (this._canvas.getContext('2d') as CanvasRenderingContext2D);
context.save();
context.font = groups[group].style.font;
const right = this._headerLeftPadding + this._labelWidthForGroup(context, groups[group]);
context.restore();
if (x > right) {
return -1;
}
return group;
}
_markerIndexAtPosition(x: number): number {
const timelineData = this._timelineData();
if (!timelineData) {
return -1;
}
const markers = timelineData.markers;
if (!markers) {
return -1;
}
const /** @const */ accurracyOffsetPx = 4;
const time = this._chartViewport.pixelToTime(x);
const leftTime = this._chartViewport.pixelToTime(x - accurracyOffsetPx);
const rightTime = this._chartViewport.pixelToTime(x + accurracyOffsetPx);
const left = this._markerIndexBeforeTime(leftTime);
let markerIndex = -1;
let distance: number = Infinity;
for (let i = left; i < markers.length && markers[i].startTime() < rightTime; i++) {
const nextDistance = Math.abs(markers[i].startTime() - time);
if (nextDistance < distance) {
markerIndex = i;
distance = nextDistance;
}
}
return markerIndex;
}
_markerIndexBeforeTime(time: number): number {
const timelineData = this._timelineData();
if (!timelineData) {
throw new Error('No timeline data');
}
const markers = timelineData.markers;
if (!markers) {
throw new Error('No timeline markers');
}
return Platform.ArrayUtilities.lowerBound(
timelineData.markers, time, (markerTimestamp, marker) => markerTimestamp - marker.startTime());
}
_draw(): void {
const timelineData = this._timelineData();
if (!timelineData) {
return;
}
const visibleLevelOffsets = this._visibleLevelOffsets ? this._visibleLevelOffsets : new Uint32Array();
const width = this._offsetWidth;
const height = this._offsetHeight;
const context = (this._canvas.getContext('2d') as CanvasRenderingContext2D);
context.save();
const ratio = window.devicePixelRatio;
const top = this._chartViewport.scrollOffset();
context.scale(ratio, ratio);
context.fillStyle = 'rgba(0, 0, 0, 0)';
context.fillRect(0, 0, width, height);
context.translate(0, -top);
const defaultFont = '11px ' + Host.Platform.fontFamily();
context.font = defaultFont;
const candyStripePattern = context.createPattern(this._candyStripeCanvas, 'repeat');
const entryTotalTimes = timelineData.entryTotalTimes;
const entryStartTimes = timelineData.entryStartTimes;
const entryLevels = timelineData.entryLevels;
const timeToPixel = this._chartViewport.timeToPixel();
const titleIndices = [];
const markerIndices = [];
const textPadding = this._textPadding;
const minTextWidth = 2 * textPadding + UI.UIUtils.measureTextWidth(context, '…');
const minTextWidthDuration = this._chartViewport.pixelToTimeOffset(minTextWidth);
const minVisibleBarLevel = Math.max(
Platform.ArrayUtilities.upperBound(visibleLevelOffsets, top, Platform.ArrayUtilities.DEFAULT_COMPARATOR) - 1,
0);
this._markerPositions.clear();
let mainThreadTopLevel = -1;
// Find the main thread so that we can mark tasks longer than 50ms.
if ('groups' in timelineData && Array.isArray(timelineData.groups)) {
const mainThread = timelineData.groups.find(group => {
if (!group.track) {
return false;
}
return group.track.name === 'CrRendererMain';
});
if (mainThread) {
mainThreadTopLevel = mainThread.startLevel;
}
}
const colorBuckets = new Map<string, {
indexes: number[],
}>();
for (let level = minVisibleBarLevel; level < this._dataProvider.maxStackDepth(); ++level) {
if (this._levelToOffset(level) > top + height) {
break;
}
if (!this._visibleLevels || !this._visibleLevels[level]) {
continue;
}
if (!this._timelineLevels) {
continue;
}
// Entries are ordered by start time within a level, so find the last visible entry.
const levelIndexes = this._timelineLevels[level];
const rightIndexOnLevel = Platform.ArrayUtilities.lowerBound(
levelIndexes, this._chartViewport.windowRightTime(),
(time, entryIndex) => time - entryStartTimes[entryIndex]) -
1;
let lastDrawOffset = Infinity;
for (let entryIndexOnLevel = rightIndexOnLevel; entryIndexOnLevel >= 0; --entryIndexOnLevel) {
const entryIndex = levelIndexes[entryIndexOnLevel];
const duration = entryTotalTimes[entryIndex];
if (isNaN(duration)) {
markerIndices.push(entryIndex);
continue;
}
if (duration >= minTextWidthDuration ||
(this._forceDecorationCache && this._forceDecorationCache[entryIndex])) {
titleIndices.push(entryIndex);
}
const entryStartTime = entryStartTimes[entryIndex];
const entryOffsetRight = entryStartTime + duration;
if (entryOffsetRight <= this._chartViewport.windowLeftTime()) {
break;
}
if (this._useWebGL) {
continue;
}
const barX = this._timeToPositionClipped(entryStartTime);
// Check if the entry entirely fits into an already drawn pixel, we can just skip drawing it.
if (barX >= lastDrawOffset) {
continue;
}
lastDrawOffset = barX;
if (this._entryColorsCache) {
const color = this._entryColorsCache[entryIndex];
let bucket = colorBuckets.get(color);
if (!bucket) {
bucket = {indexes: []};
colorBuckets.set(color, bucket);
}
bucket.indexes.push(entryIndex);
}
}
}
if (this._useWebGL) {
this._drawGL();
} else {
context.save();
this._forEachGroupInViewport((offset, index, group, isFirst, groupHeight) => {
if (this._isGroupFocused(index)) {
context.fillStyle = this._selectedGroupBackroundColor;
context.fillRect(0, offset, width, groupHeight - group.style.padding);
}
});
context.restore();
for (const [color, {indexes}] of colorBuckets) {
context.beginPath();
for (let i = 0; i < indexes.length; ++i) {
const entryIndex = indexes[i];
const duration = entryTotalTimes[entryIndex];
if (isNaN(duration)) {
continue;
}
const entryStartTime = entryStartTimes[entryIndex];
const barX = this._timeToPositionClipped(entryStartTime);
const barLevel = entryLevels[entryIndex];
const barHeight = this._levelHeight(barLevel);
const barY = this._levelToOffset(barLevel);
const barRight = this._timeToPositionClipped(entryStartTime + duration);
const barWidth = Math.max(barRight - barX, 1);
context.rect(barX, barY, barWidth - 0.4, barHeight - 1);
}
context.fillStyle = color;
context.fill();
// Draw long task regions.
context.beginPath();
for (let i = 0; i < indexes.length; ++i) {
const entryIndex = indexes[i];
const duration = entryTotalTimes[entryIndex];
const showLongDurations = entryLevels[entryIndex] === mainThreadTopLevel;
if (!showLongDurations) {
continue;
}
if (isNaN(duration) || duration < 50) {
continue;
}
const entryStartTime = entryStartTimes[entryIndex];
const barX = this._timeToPositionClipped(entryStartTime + 50);
const barLevel = entryLevels[entryIndex];
const barHeight = this._levelHeight(barLevel);
const barY = this._levelToOffset(barLevel);
const barRight = this._timeToPositionClipped(entryStartTime + duration);
const barWidth = Math.max(barRight - barX, 1);
context.rect(barX, barY, barWidth - 0.4, barHeight - 1);
}
if (candyStripePattern) {
context.fillStyle = candyStripePattern;
context.fill();
}
}
}
context.textBaseline = 'alphabetic';
context.beginPath();
let lastMarkerLevel = -1;
let lastMarkerX: number = -Infinity;
// Markers are sorted top to bottom, right to left.
for (let m = markerIndices.length - 1; m >= 0; --m) {
const entryIndex = markerIndices[m];
const title = this._dataProvider.entryTitle(entryIndex);
if (!title) {
continue;
}
const entryStartTime = entryStartTimes[entryIndex];
const level = entryLevels[entryIndex];
if (lastMarkerLevel !== level) {
lastMarkerX = -Infinity;
}
const x = Math.max(this._chartViewport.timeToPosition(entryStartTime), lastMarkerX);
const y = this._levelToOffset(level);
const h = this._levelHeight(level);
const padding = 4;
const width = Math.ceil(UI.UIUtils.measureTextWidth(context, title)) + 2 * padding;
lastMarkerX = x + width + 1;
lastMarkerLevel = level;
this._markerPositions.set(entryIndex, {x, width});
context.fillStyle = this._dataProvider.entryColor(entryIndex);
context.fillRect(x, y, width, h - 1);
context.fillStyle = 'white';
context.fillText(title, x + padding, y + h - this._textBaseline);
}
context.strokeStyle = 'rgba(0, 0, 0, 0.2)';
context.stroke();
for (let i = 0; i < titleIndices.length; ++i) {
const entryIndex = titleIndices[i];
const entryStartTime = entryStartTimes[entryIndex];
const barX = this._timeToPositionClipped(entryStartTime);
const barRight = Math.min(this._timeToPositionClipped(entryStartTime + entryTotalTimes[entryIndex]), width) + 1;
const barWidth = barRight - barX;
const barLevel = entryLevels[entryIndex];
const barY = this._levelToOffset(barLevel);
let text = this._dataProvider.entryTitle(entryIndex);
if (text && text.length) {
context.font = this._dataProvider.entryFont(entryIndex) || defaultFont;
text = UI.UIUtils.trimTextMiddle(context, text, barWidth - 2 * textPadding);
}
const unclippedBarX = this._chartViewport.timeToPosition(entryStartTime);
const barHeight = this._levelHeight(barLevel);
if (this._dataProvider.decorateEntry(
entryIndex, context, text, barX, barY, barWidth, barHeight, unclippedBarX, timeToPixel)) {
continue;
}
if (!text || !text.length) {
continue;
}
context.fillStyle = this._dataProvider.textColor(entryIndex);
context.fillText(text, barX + textPadding, barY + barHeight - this._textBaseline);
}
context.restore();
this._drawGroupHeaders(width, height);
this._drawFlowEvents(context, width, height);
this._drawMarkers();
const dividersData = TimelineGrid.calculateGridOffsets(this);
const navStartTimes = Array.from(this._dataProvider.navStartTimes().values());
let navStartTimeIndex = 0;
const drawAdjustedTime = (time: number): string => {
if (navStartTimes.length === 0) {
return this.formatValue(time, dividersData.precision);
}
// Track when the time crosses the boundary to the next nav start record,
// and when it does, move the nav start array index accordingly.
const hasNextNavStartTime = navStartTimes.length > navStartTimeIndex + 1;
if (hasNextNavStartTime && time > navStartTimes[navStartTimeIndex + 1].startTime) {
navStartTimeIndex++;
}
// Adjust the time by the nearest nav start marker's value.
const nearestMarker = navStartTimes[navStartTimeIndex];
if (nearestMarker) {
time -= nearestMarker.startTime - this.zeroTime();
}
return this.formatValue(time, dividersData.precision);
};
TimelineGrid.drawCanvasGrid(context, dividersData);
if (this._rulerEnabled) {
TimelineGrid.drawCanvasHeaders(context, dividersData, drawAdjustedTime, 3, HeaderHeight);
}
this._updateElementPosition(this._highlightElement, this._highlightedEntryIndex);
this._updateElementPosition(this._selectedElement, this._selectedEntryIndex);
this._updateMarkerHighlight();
}
_initWebGL(): void {
const gl = (this._canvasGL.getContext('webgl') as WebGLRenderingContext | null);
if (!gl) {
console.error('Failed to obtain WebGL context.');
this._useWebGL = false; // Fallback to use canvas.
return;
}
const vertexShaderSource = `
attribute vec2 aVertexPosition;
attribute float aVertexColor;
uniform vec2 uScalingFactor;
uniform vec2 uShiftVector;
varying mediump vec2 vPalettePosition;
void main() {
vec2 shiftedPosition = aVertexPosition - uShiftVector;
gl_Position = vec4(shiftedPosition * uScalingFactor + vec2(-1.0, 1.0), 0.0, 1.0);
vPalettePosition = vec2(aVertexColor, 0.5);
}`;
const fragmentShaderSource = `
varying mediump vec2 vPalettePosition;
uniform sampler2D uSampler;
void main() {
gl_FragColor = texture2D(uSampler, vPalettePosition);
}`;
function loadShader(gl: WebGLRenderingContext, type: number, source: string): WebGLShader|null {
const shader = gl.createShader(type);
if (!shader) {
return null;
}
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
return shader;
}
console.error('Shader compile error: ' + gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
const shaderProgram = gl.createProgram();
if (!shaderProgram || !vertexShader || !fragmentShader) {
return;
}
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
if (gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
this._shaderProgram = shaderProgram;
gl.useProgram(shaderProgram);
} else {
this._shaderProgram = null;
throw new Error('Unable to initialize the shader program: ' + gl.getProgramInfoLog(shaderProgram));
}
this._vertexBuffer = gl.createBuffer();
this._colorBuffer = gl.createBuffer();
this._uScalingFactor = gl.getUniformLocation(shaderProgram, 'uScalingFactor');
this._uShiftVector = gl.getUniformLocation(shaderProgram, 'uShiftVector');
const uSampler = gl.getUniformLocation(shaderProgram, 'uSampler');
gl.uniform1i(uSampler, 0);
this._aVertexPosition = gl.getAttribLocation(this._shaderProgram, 'aVertexPosition');
this._aVertexColor = gl.getAttribLocation(this._shaderProgram, 'aVertexColor');
gl.enableVertexAttribArray(this._aVertexPosition);
gl.enableVertexAttribArray(this._aVertexColor);
}
_setupGLGeometry(): void {
const gl = (this._canvasGL.getContext('webgl') as WebGLRenderingContext | null);
if (!gl) {
return;
}
const timelineData = this._timelineData();
if (!timelineData) {
return;
}
const entryTotalTimes = timelineData.entryTotalTimes;
const entryStartTimes = timelineData.entryStartTimes;
const entryLevels = timelineData.entryLevels;
const verticesPerBar = 6;
const vertexArray = new Float32Array(entryTotalTimes.length * verticesPerBar * 2);
let colorArray = new Uint8Array(entryTotalTimes.length * verticesPerBar);
let vertex = 0;
const parsedColorCache = new Map<string, number>();
const colors: number[] = [];
const visibleLevels = this._visibleLevels || [];
const rawTimelineData = this._rawTimelineData || {groups: []};
const collapsedOverviewLevels = new Array(visibleLevels.length);
const groups = rawTimelineData.groups || [];
this._forEachGroup((offset, index, group) => {
if (group.style.useFirstLineForOverview || !this._isGroupCollapsible(index) || group.expanded) {
return;
}
let nextGroup = index + 1;
while (nextGroup < groups.length && groups[nextGroup].style.nestingLevel > group.style.nestingLevel) {
++nextGroup;
}
const endLevel = nextGroup < groups.length ? groups[nextGroup].startLevel : this._dataProvider.maxStackDepth();
for (let i = group.startLevel; i < endLevel; ++i) {
collapsedOverviewLevels[i] = offset;
}
});
for (let i = 0; i < entryTotalTimes.length; ++i) {
const level = entryLevels[i];
const collapsedGroupOffset = collapsedOverviewLevels[level];
if (!visibleLevels[level] && !collapsedGroupOffset) {
continue;
}
if (!this._entryColorsCache) {
continue;
}
const color = this._entryColorsCache[i];
if (!color) {
continue;
}
let colorIndex = parsedColorCache.get(color);
if (colorIndex === undefined) {
const parsedColor = Common.Color.Color.parse(color);
if (parsedColor) {
const rgba = parsedColor.canonicalRGBA();
rgba[3] = Math.round(rgba[3] * 255);
colorIndex = colors.length / 4;
colors.push(...rgba);
if (colorIndex === 256) {
colorArray = new Uint8Array(colorArray);
}
}
if (colorIndex) {
parsedColorCache.set(color, colorIndex);
}
}
for (let j = 0; j < verticesPerBar; ++j) {
if (colorIndex) {
colorArray[vertex + j] = colorIndex;
}
}
const vpos = vertex * 2;
const x0 = entryStartTimes[i] - this._minimumBoundary;
const x1 = x0 + entryTotalTimes[i];
const y0 = collapsedGroupOffset || this._levelToOffset(level);
const y1 = y0 + this._levelHeight(level) - 1;
vertexArray[vpos + 0] = x0;
vertexArray[vpos + 1] = y0;
vertexArray[vpos + 2] = x1;
vertexArray[vpos + 3] = y0;
vertexArray[vpos + 4] = x0;
vertexArray[vpos + 5] = y1;
vertexArray[vpos + 6] = x0;
vertexArray[vpos + 7] = y1;
vertexArray[vpos + 8] = x1;
vertexArray[vpos + 9] = y0;
vertexArray[vpos + 10] = x1;
vertexArray[vpos + 11] = y1;
vertex += verticesPerBar;
}
this._vertexCount = vertex;
const paletteTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, paletteTexture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.activeTexture(gl.TEXTURE0);
const numColors = colors.length / 4;
const useShortForColors = numColors >= 256;
const width = !useShortForColors ? 256 : Math.min(1 << 16, gl.getParameter(gl.MAX_TEXTURE_SIZE));
console.assert(numColors <= width, 'Too many colors');
const height = 1;
const colorIndexType = useShortForColors ? gl.UNSIGNED_SHORT : gl.UNSIGNED_BYTE;
if (useShortForColors) {
const factor = (1 << 16) / width;
for (let i = 0; i < vertex; ++i) {
colorArray[i] *= factor;
}
}
const pixels = new Uint8Array(width * 4);
pixels.set(colors);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
if (this._vertexBuffer && this._aVertexPosition) {
gl.bindBuffer(gl.ARRAY_BUFFER, this._vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertexArray, gl.STATIC_DRAW);
gl.vertexAttribPointer(this._aVertexPosition, /* vertexComponents */ 2, gl.FLOAT, false, 0, 0);
}
if (this._colorBuffer && this._aVertexColor) {
gl.bindBuffer(gl.ARRAY_BUFFER, this._colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, colorArray, gl.STATIC_DRAW);
gl.vertexAttribPointer(this._aVertexColor, /* colorComponents */ 1, colorIndexType, true, 0, 0);
}
}
_drawGL(): void {
const gl = (this._canvasGL.getContext('webgl') as WebGLRenderingContext | null);
if (!gl) {
return;
}
const timelineData = this._timelineData();
if (!timelineData) {
return;
}
if (!this._prevTimelineData || timelineData.entryTotalTimes !== this._prevTimelineData.entryTotalTimes) {
this._prevTimelineData = timelineData;
this._setupGLGeometry();
}
gl.viewport(0, 0, this._canvasGL.width, this._canvasGL.height);
if (!this._vertexCount) {
return;
}
const viewportScale = [2.0 / this.boundarySpan(), -2.0 * window.devicePixelRatio / this._canvasGL.height];
const viewportShift = [this.minimumBoundary() - this.zeroTime(), this._chartViewport.scrollOffset()];
if (this._uScalingFactor) {
gl.uniform2fv(this._uScalingFactor, viewportScale);
}
if (this._uShiftVector) {
gl.uniform2fv(this._uShiftVector, viewportShift);
}
gl.drawArrays(gl.TRIANGLES, 0, this._vertexCount);
}
_drawGroupHeaders(width: number, height: number): void {
const context = (this._canvas.getContext('2d') as CanvasRenderingContext2D);
const top = this._chartViewport.scrollOffset();
const ratio = window.devicePixelRatio;
if (!this._rawTimelineData) {
return;
}
const groups = this._rawTimelineData.groups || [];
if (!groups.length) {
return;
}
const groupOffsets = this._groupOffsets;
if (groupOffsets === null || groupOffsets === undefined) {
return;
}
const lastGroupOffset = groupOffsets[groupOffsets.length - 1];
const colorUsage = ThemeSupport.ThemeSupport.ColorUsage;
context.save();
context.scale(ratio, ratio);
context.translate(0, -top);
const defaultFont = '11px ' + Host.Platform.fontFamily();
context.font = defaultFont;
context.fillStyle = ThemeSupport.ThemeSupport.instance().patchColorText('#fff', colorUsage.Background);
this._forEachGroupInViewport((offset, index, group) => {
const paddingHeight = group.style.padding;
if (paddingHeight < 5) {
return;
}
context.fillRect(0, offset - paddingHeight + 2, width, paddingHeight - 4);
});
if (groups.length && lastGroupOffset < top + height) {
context.fillRect(0, lastGroupOffset + 2, width, top + height - lastGroupOffset);
}
context.strokeStyle = ThemeSupport.ThemeSupport.instance().patchColorText('#eee', colorUsage.Background);
context.beginPath();
this._forEachGroupInViewport((offset, index, group, isFirst) => {
if (isFirst || group.style.padding < 4) {
return;
}
hLine(offset - 2.5);
});
hLine(lastGroupOffset + 1.5);
context.stroke();
this._forEachGroupInViewport((offset, index, group) => {
if (group.style.useFirstLineForOverview) {
return;
}
if (!this._isGroupCollapsible(index) || group.expanded) {
if (!group.style.shareHeaderLine && this._isGroupFocused(index)) {
context.fillStyle = group.style.backgroundColor;
context.fillRect(0, offset, width, group.style.height);
}
return;
}
if (this._useWebGL) {
return;
}
let nextGroup = index + 1;
while (nextGroup < groups.length && groups[nextGroup].style.nestingLevel > group.style.nestingLevel) {
nextGroup++;
}
const endLevel = nextGroup < groups.length ? groups[nextGroup].startLevel : this._dataProvider.maxStackDepth();
this._drawCollapsedOverviewForGroup(group, offset, endLevel);
});
context.save();
this._forEachGroupInViewport((offset, index, group) => {
context.font = group.style.font;
if (this._isGroupCollapsible(index) && !group.expanded || group.style.shareHeaderLine) {
const width = this._labelWidthForGroup(context, group) + 2;
if (this._isGroupFocused(index)) {
context.fillStyle = this._selectedGroupBackroundColor;
} else {
const parsedColor = Common.Color.Color.parse(group.style.backgroundColor);
if (parsedColor) {
context.fillStyle = (parsedColor.setAlpha(0.8).asString(null) as string);
}
}
context.fillRect(
this._headerLeftPadding - this._headerLabelXPadding, offset + this._headerLabelYPadding, width,
group.style.height - 2 * this._headerLabelYPadding);
}
context.fillStyle = group.style.color;
context.fillText(
group.name, Math.floor(this._expansionArrowIndent * (group.style.nestingLevel + 1) + this._arrowSide),
offset + group.style.height - this._textBaseline);
});
context.restore();
context.fillStyle = ThemeSupport.ThemeSupport.instance().patchColorText('#6e6e6e', colorUsage.Foreground);
context.beginPath();
this._forEachGroupInViewport((offset, index, group) => {
if (this._isGroupCollapsible(index)) {
drawExpansionArrow.call(
this, this._expansionArrowIndent * (group.style.nestingLevel + 1),
offset + group.style.height - this._textBaseline - this._arrowSide / 2, Boolean(group.expanded));
}
});
context.fill();
context.strokeStyle = ThemeSupport.ThemeSupport.instance().patchColorText('#ddd', colorUsage.Background);
context.beginPath();
context.stroke();
this._forEachGroupInViewport((offset, index, group, isFirst, groupHeight) => {
if (this._isGroupFocused(index)) {
const lineWidth = 2;
const bracketLength = 10;
context.fillStyle = this._selectedGroupBorderColor;
context.fillRect(0, offset - lineWidth, lineWidth, groupHeight - group.style.padding + 2 * lineWidth);
context.fillRect(0, offset - lineWidth, bracketLength, lineWidth);
context.fillRect(0, offset + groupHeight - group.style.padding, bracketLength, lineWidth);
}
});
context.restore();
function hLine(y: number): void {
context.moveTo(0, y);
context.lineTo(width, y);
}
function drawExpansionArrow(this: FlameChart, x: number, y: number, expanded: boolean): void {
const arrowHeight = this._arrowSide * Math.sqrt(3) / 2;
const arrowCenterOffset = Math.round(arrowHeight / 2);
context.save();
context.translate(x, y);
context.rotate(expanded ? Math.PI / 2 : 0);
context.moveTo(-arrowCenterOffset, -this._arrowSide / 2);
context.lineTo(-arrowCenterOffset, this._arrowSide / 2);
context.lineTo(arrowHeight - arrowCenterOffset, 0);
context.restore();
}
}
_forEachGroup(callback: (arg0: number, arg1: number, arg2: Group, arg3: boolean, arg4: number) => void): void {
if (!this._rawTimelineData) {
return;
}
const groups = this._rawTimelineData.groups || [];
if (!groups.length) {
return;
}
const groupOffsets = this._groupOffsets;
if (!groupOffsets) {
return;
}
const groupStack: {
nestingLevel: number,
visible: boolean,
}[] = [{nestingLevel: -1, visible: true}];
for (let i = 0; i < groups.length; ++i) {
const groupTop = groupOffsets[i];
const group = groups[i];
let firstGroup = true;
let last: {
nestingLevel: number,
visible: boolean,
} = groupStack[groupStack.length - 1];
while (last && last.nestingLevel >= group.style.nestingLevel) {
groupStack.pop();
firstGroup = false;
last = groupStack[groupStack.length - 1];
}
last = groupStack[groupStack.length - 1];
const parentGroupVisible = last ? last.visible : false;
const thisGroupVisible = parentGroupVisible && (!this._isGroupCollapsible(i) || group.expanded);
groupStack.push({nestingLevel: group.style.nestingLevel, visible: Boolean(thisGroupVisible)});
const nextOffset = i === groups.length - 1 ? groupOffsets[i + 1] + group.style.padding : groupOffsets[i + 1];
if (!parentGroupVisible) {
continue;
}
callback(groupTop, i, group, firstGroup, nextOffset - groupTop);
}
}
_forEachGroupInViewport(callback: (arg0: number, arg1: number, arg2: Group, arg3: boolean, arg4: number) => void):
void {
const top = this._chartViewport.scrollOffset();
this._forEachGroup((groupTop, index, group, firstGroup, height) => {
if (groupTop - group.style.padding > top + this._offsetHeight) {
return;
}
if (groupTop + height < top) {
return;
}
callback(groupTop, index, group, firstGroup, height);
});
}
_labelWidthForGroup(context: CanvasRenderingContext2D, group: Group): number {
return UI.UIUtils.measureTextWidth(context, group.name) +
this._expansionArrowIndent * (group.style.nestingLevel + 1) + 2 * this._headerLabelXPadding;
}
_drawCollapsedOverviewForGroup(group: Group, y: number, endLevel: number): void {
const range = new Common.SegmentedRange.SegmentedRange(mergeCallback);
const timeWindowLeft = this._chartViewport.windowLeftTime();
const timeWindowRight = this._chartViewport.windowRightTime();
const context = (this._canvas.getContext('2d') as CanvasRenderingContext2D);
const barHeight = group.style.height;
if (!this._rawTimelineData) {
return;
}
const entryStartTimes = this._rawTimelineData.entryStartTimes;
const entryTotalTimes = this._rawTimelineData.entryTotalTimes;
const timeToPixel = this._chartViewport.timeToPixel();
for (let level = group.startLevel; level < endLevel; ++level) {
const levelIndexes: number[] = this._timelineLevels ? this._timelineLevels[level] : [];
const rightIndexOnLevel =
Platform.ArrayUtilities.lowerBound(
levelIndexes, timeWindowRight, (time, entryIndex) => time - entryStartTimes[entryIndex]) -
1;
let lastDrawOffset: number = Infinity;
for (let entryIndexOnLevel = rightIndexOnLevel; entryIndexOnLevel >= 0; --entryIndexOnLevel) {
const entryIndex = levelIndexes[entryIndexOnLevel];
const entryStartTime = entryStartTimes[entryIndex];
const barX = this._timeToPositionClipped(entryStartTime);
const entryEndTime = entryStartTime + entryTotalTimes[entryIndex];
if (isNaN(entryEndTime) || barX >= lastDrawOffset) {
continue;
}
if (entryEndTime <= timeWindowLeft) {
break;
}
lastDrawOffset = barX;
const color = this._entryColorsCache ? this._entryColorsCache[entryIndex] : '';
const endBarX = this._timeToPositionClipped(entryEndTime);
if (group.style.useDecoratorsForOverview && this._dataProvider.forceDecoration(entryIndex)) {
const unclippedBarX = this._chartViewport.timeToPosition(entryStartTime);
const barWidth = endBarX - barX;
context.beginPath();
context.fillStyle = color;
context.fillRect(barX, y, barWidth, barHeight - 1);
this._dataProvider.decorateEntry(
entryIndex, context, '', barX, y, barWidth, barHeight, unclippedBarX, timeToPixel);
continue;
}
range.append(new Common.SegmentedRange.Segment(barX, endBarX, color));
}
}
const segments = range.segments().slice().sort((a, b) => a.data.localeCompare(b.data));
let lastColor;
context.beginPath();
for (let i = 0; i < segments.length; ++i) {
const segment = segments[i];
if (lastColor !== segments[i].data) {
context.fill();
context.beginPath();
lastColor = segments[i].data;
context.fillStyle = lastColor;
}
context.rect(segment.begin, y, segment.end - segment.begin, barHeight);
}
context.fill();
function mergeCallback(
a: Common.SegmentedRange.Segment, b: Common.SegmentedRange.Segment): Common.SegmentedRange.Segment|null {
return a.data === b.data && a.end + 0.4 > b.end ? a : null;
}
}
_drawFlowEvents(context: CanvasRenderingContext2D, _width: number, _height: number): void {
context.save();
const ratio = window.devicePixelRatio;
const top = this._chartViewport.scrollOffset();
const arrowWidth = 6;
context.scale(ratio, ratio);
context.translate(0, -top);
context.fillStyle = '#7f5050';
context.strokeStyle = '#7f5050';
const td = this._timelineData();
if (!td) {
return;
}
const endIndex = Platform.ArrayUtilities.lowerBound(
td.flowStartTimes, this._chartViewport.windowRightTime(), Platform.ArrayUtilities.DEFAULT_COMPARATOR);
context.lineWidth = 0.5;
for (let i = 0; i < endIndex; ++i) {
if (!td.flowEndTimes[i] || td.flowEndTimes[i] < this._chartViewport.windowLeftTime()) {
continue;
}
const startX = this._chartViewport.timeToPosition(td.flowStartTimes[i]);
const endX = this._chartViewport.timeToPosition(td.flowEndTimes[i]);
const startLevel = td.flowStartLevels[i];
const endLevel = td.flowEndLevels[i];
const startY = this._levelToOffset(startLevel) + this._levelHeight(startLevel) / 2;
const endY = this._levelToOffset(endLevel) + this._levelHeight(endLevel) / 2;
const segment = Math.min((endX - startX) / 4, 40);
const distanceTime = td.flowEndTimes[i] - td.flowStartTimes[i];
const distanceY = (endY - startY) / 10;
const spread = 30;
const lineY = distanceTime < 1 ? startY : spread + Math.max(0, startY + distanceY * (i % spread));
const p = [];
p.push({x: startX, y: startY});
p.push({x: startX + arrowWidth, y: startY});
p.push({x: startX + segment + 2 * arrowWidth, y: startY});
p.push({x: startX + segment, y: lineY});
p.push({x: startX + segment * 2, y: lineY});
p.push({x: endX - segment * 2, y: lineY});
p.push({x: endX - segment, y: lineY});
p.push({x: endX - segment - 2 * arrowWidth, y: endY});
p.push({x: endX - arrowWidth, y: endY});
context.beginPath();
context.moveTo(p[0].x, p[0].y);
context.lineTo(p[1].x, p[1].y);
context.bezierCurveTo(p[2].x, p[2].y, p[3].x, p[3].y, p[4].x, p[4].y);
context.lineTo(p[5].x, p[5].y);
context.bezierCurveTo(p[6].x, p[6].y, p[7].x, p[7].y, p[8].x, p[8].y);
context.stroke();
context.beginPath();
context.arc(startX, startY, 2, -Math.PI / 2, Math.PI / 2, false);
context.fill();
context.beginPath();
context.moveTo(endX, endY);
context.lineTo(endX - arrowWidth, endY - 3);
context.lineTo(endX - arrowWidth, endY + 3);
context.fill();
}
context.restore();
}
_drawMarkers(): void {
const timelineData = this._timelineData();
if (!timelineData) {
return;
}
const markers = timelineData.markers;
const left = this._markerIndexBeforeTime(this.minimumBoundary());
const rightBoundary = this.maximumBoundary();
const timeToPixel = this._chartViewport.timeToPixel();
const context = (this._canvas.getContext('2d') as CanvasRenderingContext2D);
context.save();
const ratio = window.devicePixelRatio;
context.scale(ratio, ratio);
context.translate(0, 3);
const height = HeaderHeight - 1;
for (let i = left; i < markers.length; i++) {
const timestamp = markers[i].startTime();
if (timestamp > rightBoundary) {
break;
}
markers[i].draw(context, this._chartViewport.timeToPosition(timestamp), height, timeToPixel);
}
context.restore();
}
_updateMarkerHighlight(): void {
const element = this._markerHighlighElement;
if (element.parentElement) {
element.remove();
}
const markerIndex = this._highlightedMarkerIndex;
if (markerIndex === -1) {
return;
}
const timelineData = this._timelineData();
if (!timelineData) {
return;
}
const marker = timelineData.markers[markerIndex];
const barX = this._timeToPositionClipped(marker.startTime());
UI.Tooltip.Tooltip.install(element, marker.title() || '');
const style = element.style;
style.left = barX + 'px';
style.backgroundColor = marker.color();
this._viewportElement.appendChild(element);
}
_processTimelineData(timelineData: TimelineData|null): void {
if (!timelineData) {
this._timelineLevels = null;
this._visibleLevelOffsets = null;
this._visibleLevels = null;
this._groupOffsets = null;
this._rawTimelineData = null;
this._forceDecorationCache = null;
this._entryColorsCache = null;
this._rawTimelineDataLength = 0;
this._selectedGroup = -1;
this._keyboardFocusedGroup = -1;
this._flameChartDelegate.updateSelectedGroup(this, null);
return;
}
this._rawTimelineData = timelineData;
this._rawTimelineDataLength = timelineData.entryStartTimes.length;
this._forceDecorationCache = new Int8Array(this._rawTimelineDataLength);
this._entryColorsCache = new Array(this._rawTimelineDataLength);
for (let i = 0; i < this._rawTimelineDataLength; ++i) {
this._forceDecorationCache[i] = this._dataProvider.forceDecoration(i) ? 1 : 0;
this._entryColorsCache[i] = this._dataProvider.entryColor(i);
}
const entryCounters = new Uint32Array(this._dataProvider.maxStackDepth() + 1);
for (let i = 0; i < timelineData.entryLevels.length; ++i) {
++entryCounters[timelineData.entryLevels[i]];
}
const levelIndexes = new Array(entryCounters.length);
for (let i = 0; i < levelIndexes.length; ++i) {
levelIndexes[i] = new Uint32Array(entryCounters[i]);
entryCounters[i] = 0;
}
for (let i = 0; i < timelineData.entryLevels.length; ++i) {
const level = timelineData.entryLevels[i];
levelIndexes[level][entryCounters[level]++] = i;
}
this._timelineLevels = levelIndexes;
const groups = this._rawTimelineData.groups || [];
for (let i = 0; i < groups.length; ++i) {
const expanded = this._groupExpansionState[groups[i].name];
if (expanded !== undefined) {
groups[i].expanded = expanded;
}
}
this._updateLevelPositions();
this._updateHeight();
this._selectedGroup = timelineData.selectedGroup ? groups.indexOf(timelineData.selectedGroup) : -1;
this._keyboardFocusedGroup = this._selectedGroup;
this._flameChartDelegate.updateSelectedGroup(this, timelineData.selectedGroup);
}
_updateLevelPositions(): void {
const levelCount = this._dataProvider.maxStackDepth();
const groups = this._rawTimelineData ? (this._rawTimelineData.groups || []) : [];
this._visibleLevelOffsets = new Uint32Array(levelCount + 1);
this._visibleLevelHeights = new Uint32Array(levelCount);
this._visibleLevels = new Uint16Array(levelCount);
this._groupOffsets = new Uint32Array(groups.length + 1);
let groupIndex = -1;
let currentOffset = this._rulerEnabled ? HeaderHeight + 2 : 2;
let visible = true;
const groupStack: {
nestingLevel: number,
visible: boolean,
}[] = [{nestingLevel: -1, visible: true}];
const lastGroupLevel =
Math.max(levelCount, groups.length ? (groups[groups.length - 1] as Group).startLevel + 1 : 0);
let level;
for (level = 0; level < lastGroupLevel; ++level) {
let parentGroupIsVisible = true;
let style;
while (groupIndex < groups.length - 1 && level === groups[groupIndex + 1].startLevel) {
++groupIndex;
style = groups[groupIndex].style;
let nextLevel = true;
let last: {
nestingLevel: number,
visible: boolean,
} = groupStack[groupStack.length - 1];
while (last && last.nestingLevel >= style.nestingLevel) {
groupStack.pop();
nextLevel = false;
last = groupStack[groupStack.length - 1];
}
const thisGroupIsVisible =
groupIndex >= 0 && this._isGroupCollapsible(groupIndex) ? groups[groupIndex].expanded : true;
last = groupStack[groupStack.length - 1];
parentGroupIsVisible = last ? last.visible : false;
visible = Boolean(thisGroupIsVisible) && parentGroupIsVisible;
groupStack.push({nestingLevel: style.nestingLevel, visible: visible});
if (parentGroupIsVisible) {
currentOffset += nextLevel ? 0 : style.padding;
}
this._groupOffsets[groupIndex] = currentOffset;
if (parentGroupIsVisible && !style.shareHeaderLine) {
currentOffset += style.height;
}
}
if (level >= levelCount) {
continue;
}
const isFirstOnLevel = groupIndex >= 0 && level === groups[groupIndex].startLevel;
const thisLevelIsVisible =
parentGroupIsVisible && (visible || isFirstOnLevel && groups[groupIndex].style.useFirstLineForOverview);
let height;
if (groupIndex >= 0) {
const group = groups[groupIndex];
const styleB = group.style;
height = isFirstOnLevel && !styleB.shareHeaderLine || (styleB.collapsible && !group.expanded) ?
styleB.height :
(styleB.itemsHeight || this._barHeight);
} else {
height = this._barHeight;
}
this._visibleLevels[level] = thisLevelIsVisible ? 1 : 0;
this._visibleLevelOffsets[level] = currentOffset;
this._visibleLevelHeights[level] = height;
if (thisLevelIsVisible || (parentGroupIsVisible && style && style.shareHeaderLine && isFirstOnLevel)) {
currentOffset += this._visibleLevelHeights[level];
}
}
if (groupIndex >= 0) {
this._groupOffsets[groupIndex + 1] = currentOffset;
}
this._visibleLevelOffsets[level] = currentOffset;
if (this._useWebGL) {
this._setupGLGeometry();
}
}
_isGroupCollapsible(index: number): boolean|undefined {
if (!this._rawTimelineData) {
return;
}
const groups = this._rawTimelineData.groups || [];
const style = groups[index].style;
if (!style.shareHeaderLine || !style.collapsible) {
return Boolean(style.collapsible);
}
const isLastGroup = index + 1 >= groups.length;
if (!isLastGroup && groups[index + 1].style.nestingLevel > style.nestingLevel) {
return true;
}
const nextGroupLevel = isLastGroup ? this._dataProvider.maxStackDepth() : groups[index + 1].startLevel;
if (nextGroupLevel !== groups[index].startLevel + 1) {
return true;
}
// For groups that only have one line and share header line, pretend these are not collapsible
// unless the itemsHeight does not match the headerHeight
return style.height !== style.itemsHeight;
}
setSelectedEntry(entryIndex: number): void {
if (this._selectedEntryIndex === entryIndex) {
return;
}
if (entryIndex !== -1) {
this._chartViewport.hideRangeSelection();
}
this._selectedEntryIndex = entryIndex;
this._revealEntry(entryIndex);
this._updateElementPosition(this._selectedElement, this._selectedEntryIndex);
}
_updateElementPosition(element: Element, entryIndex: number): void {
const elementMinWidthPx = 2;
element.classList.add('hidden');
if (entryIndex === -1) {
return;
}
const timelineData = this._timelineData();
if (!timelineData) {
return;
}
const startTime = timelineData.entryStartTimes[entryIndex];
const duration = timelineData.entryTotalTimes[entryIndex];
let barX = 0;
let barWidth = 0;
let visible = true;
if (Number.isNaN(duration)) {
const position = this._markerPositions.get(entryIndex);
if (position) {
barX = position.x;
barWidth = position.width;
} else {
visible = false;
}
} else {
barX = this._chartViewport.timeToPosition(startTime);
barWidth = duration * this._chartViewport.timeToPixel();
}
if (barX + barWidth <= 0 || barX >= this._offsetWidth) {
return;
}
const barCenter = barX + barWidth / 2;
barWidth = Math.max(barWidth, elementMinWidthPx);
barX = barCenter - barWidth / 2;
const entryLevel = timelineData.entryLevels[entryIndex];
const barY = this._levelToOffset(entryLevel) - this._chartViewport.scrollOffset();
const barHeight = this._levelHeight(entryLevel);
const style = (element as HTMLElement).style;
style.left = barX + 'px';
style.top = barY + 'px';
style.width = barWidth + 'px';
style.height = barHeight - 1 + 'px';
element.classList.toggle('hidden', !visible);
this._viewportElement.appendChild(element);
}
_timeToPositionClipped(time: number): number {
return Platform.NumberUtilities.clamp(this._chartViewport.timeToPosition(time), 0, this._offsetWidth);
}
_levelToOffset(level: number): number {
if (!this._visibleLevelOffsets) {
throw new Error('No visible level offsets');
}
return this._visibleLevelOffsets[level];
}
_levelHeight(level: number): number {
if (!this._visibleLevelHeights) {
throw new Error('No visible level heights');
}
return this._visibleLevelHeights[level];
}
_updateBoundaries(): void {
this._totalTime = this._dataProvider.totalTime();
this._minimumBoundary = this._dataProvider.minimumBoundary();
this._chartViewport.setBoundaries(this._minimumBoundary, this._totalTime);
}
_updateHeight(): void {
const height = this._levelToOffset(this._dataProvider.maxStackDepth()) + 2;
this._chartViewport.setContentHeight(height);
}
onResize(): void {
this.scheduleUpdate();
}
update(): void {
if (!this._timelineData()) {
return;
}
this._resetCanvas();
this._updateHeight();
this._updateBoundaries();
this._draw();
if (!this._chartViewport.isDragging()) {
this._updateHighlight();
}
}
reset(): void {
this._chartViewport.reset();
this._rawTimelineData = null;
this._rawTimelineDataLength = 0;
this._highlightedMarkerIndex = -1;
this._highlightedEntryIndex = -1;
this._selectedEntryIndex = -1;
/** @type {!Map<string,!Map<string,number>>} */
this._textWidth = new Map();
this._chartViewport.scheduleUpdate();
}
scheduleUpdate(): void {
this._chartViewport.scheduleUpdate();
}
_enabled(): boolean {
return this._rawTimelineDataLength !== 0;
}
computePosition(time: number): number {
return this._chartViewport.timeToPosition(time);
}
formatValue(value: number, precision?: number): string {
return this._dataProvider.formatValue(value - this.zeroTime(), precision);
}
maximumBoundary(): number {
return this._chartViewport.windowRightTime();
}
minimumBoundary(): number {
return this._chartViewport.windowLeftTime();
}
zeroTime(): number {
return this._dataProvider.minimumBoundary();
}
boundarySpan(): number {
return this.maximumBoundary() - this.minimumBoundary();
}
}
export const HeaderHeight = 15;
export const MinimalTimeWindowMs = 0.5;
export class TimelineData {
entryLevels: number[]|Uint16Array;
entryTotalTimes: number[]|Float32Array;
entryStartTimes: number[]|Float64Array;
groups: Group[];
markers: FlameChartMarker[];
flowStartTimes: number[];
flowStartLevels: number[];
flowEndTimes: number[];
flowEndLevels: number[];
selectedGroup: Group|null;
constructor(
entryLevels: number[]|Uint16Array, entryTotalTimes: number[]|Float32Array, entryStartTimes: number[]|Float64Array,
groups: Group[]|null) {
this.entryLevels = entryLevels;
this.entryTotalTimes = entryTotalTimes;
this.entryStartTimes = entryStartTimes;
this.groups = groups || [];
this.markers = [];
this.flowStartTimes = [];
this.flowStartLevels = [];
this.flowEndTimes = [];
this.flowEndLevels = [];
this.selectedGroup = null;
}
}
/**
* @interface
*/
export interface FlameChartDataProvider {
minimumBoundary(): number;
totalTime(): number;
formatValue(value: number, precision?: number): string;
maxStackDepth(): number;
timelineData(): TimelineData|null;
prepareHighlightedEntryInfo(entryIndex: number): Element|null;
canJumpToEntry(entryIndex: number): boolean;
entryTitle(entryIndex: number): string|null;
entryFont(entryIndex: number): string|null;
entryColor(entryIndex: number): string;
decorateEntry(
entryIndex: number, context: CanvasRenderingContext2D, text: string|null, barX: number, barY: number,
barWidth: number, barHeight: number, unclippedBarX: number, timeToPixelRatio: number): boolean;
forceDecoration(entryIndex: number): boolean;
textColor(entryIndex: number): string;
navStartTimes(): Map<string, SDK.TracingModel.Event>;
}
export interface FlameChartMarker {
startTime(): number;
color(): string;
title(): string|null;
draw(context: CanvasRenderingContext2D, x: number, height: number, pixelsPerMillisecond: number): void;
}
// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum
export enum Events {
CanvasFocused = 'CanvasFocused',
EntryInvoked = 'EntryInvoked',
EntrySelected = 'EntrySelected',
EntryHighlighted = 'EntryHighlighted',
}
export const Colors = {
SelectedGroupBackground: 'hsl(215, 85%, 98%)',
SelectedGroupBorder: 'hsl(216, 68%, 54%)',
};
export interface Group {
name: Common.UIString.LocalizedString;
startLevel: number;
expanded?: boolean;
selectable?: boolean;
style: {
height: number,
padding: number,
collapsible: boolean,
font: string,
color: string,
backgroundColor: string,
nestingLevel: number,
itemsHeight?: number,
shareHeaderLine?: boolean,
useFirstLineForOverview?: boolean,
useDecoratorsForOverview?: boolean,
};
track?: TimelineModel.TimelineModel.Track|null;
}
export interface GroupStyle {
height: number;
padding: number;
collapsible: boolean;
font: string;
color: string;
backgroundColor: string;
nestingLevel: number;
itemsHeight?: number;
shareHeaderLine?: boolean;
useFirstLineForOverview?: boolean;
useDecoratorsForOverview?: boolean;
}