Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/common/types/profile-derived.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @flow
import type { Milliseconds } from './units';
import type { MarkerPayload } from './profile';

export type IndexIntoFuncStackTable = number;

Expand All @@ -20,8 +21,11 @@ export type TracingMarker = {
dur: Milliseconds,
name: string,
title: string|null,
data: MarkerPayload,
};

export type IndexIntoTracingMarkers = number;

export type Node = {
totalTime: string,
totalTimePercent: string,
Expand All @@ -31,3 +35,17 @@ export type Node = {
dim: boolean,
icon: string | null,
};

export type IndexIntoMarkerTiming = number;

export type MarkerTiming = {
// Start time in milliseconds.
start: number[],
// End time in milliseconds.
end: number[],
index: IndexIntoTracingMarkers[],
label: string[],
name: string,
length: number,
};
export type MarkerTimingRows = Array<MarkerTiming>
30 changes: 23 additions & 7 deletions src/common/types/profile.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,28 @@ export type ProfilerMarkerTracing = {
// TODO - Add more markers.
);

/**
* The payload for the UserTimings API. These are added through performance.measure()
* and performance.mark(). https://developer.mozilla.org/en-US/docs/Web/API/Performance
*/
export type UserTimingMarkerPayload = {
type: "UserTiming",
startTime: Milliseconds,
endTime: Milliseconds,
name: string,
entryType: "measure" | "mark",
}

/**
* The union of all the different marker payloads that perf.html knows about, this is
* not guaranteed to be all the payloads that we actually get from the profiler.
*/
export type MarkerPayload =
GPUMarkerPayload |
ProfilerMarkerTracing |
UserTimingMarkerPayload |
null;

/**
* Markers represent arbitrary events that happen within the browser. They have a
* name, time, and potentially a JSON data payload. These can come from all over the
Expand All @@ -105,13 +127,7 @@ export type ProfilerMarkerTracing = {
* perf.html to instrument their code.
*/
export type MarkersTable = {
data: (
GPUMarkerPayload |
ProfilerMarkerTracing |
Object |
null |
void
)[],
data: MarkerPayload[],
name: IndexIntoStringTable[],
time: number[],
length: number,
Expand Down
5 changes: 5 additions & 0 deletions src/common/types/units.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,8 @@ export type HorizontalViewport = {
}

export type StartEndRange = { start: Milliseconds, end: Milliseconds };

/**
* This is not really a unit, but doesn't warrant a separate file.
*/
export type NonNull = number | string | () => mixed | Object | Array<any>;
9 changes: 7 additions & 2 deletions src/content/actions/timeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ export function changeFlameChartLabelingStrategy(getLabel: GetLabel): Action {
};
}

export function changeTimelineExpandedThread(threadIndex: ThreadIndex, isExpanded: boolean): Action {
const type = 'CHANGE_TIMELINE_EXPANDED_THREAD';
export function changeTimelineFlameChartExpandedThread(threadIndex: ThreadIndex, isExpanded: boolean): Action {
const type = 'CHANGE_TIMELINE_FLAME_CHART_EXPANDED_THREAD';
return { type, threadIndex, isExpanded };
}

export function changeTimelineMarkersExpandedThread(threadIndex: ThreadIndex, isExpanded: boolean): Action {
const type = 'CHANGE_TIMELINE_MARKERS_EXPANDED_THREAD';
return { type, threadIndex, isExpanded };
}

Expand Down
3 changes: 2 additions & 1 deletion src/content/actions/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ type ReceiveProfileAction =
type TimelineAction =
{ type: 'CHANGE_FLAME_CHART_COLOR_STRATEGY', getCategory: GetCategory } |
{ type: 'CHANGE_FLAME_CHART_LABELING_STRATEGY', getLabel: GetLabel } |
{ type: 'CHANGE_TIMELINE_EXPANDED_THREAD', threadIndex: ThreadIndex, isExpanded: boolean };
{ type: 'CHANGE_TIMELINE_FLAME_CHART_EXPANDED_THREAD', threadIndex: ThreadIndex, isExpanded: boolean } |
{ type: 'CHANGE_TIMELINE_MARKERS_EXPANDED_THREAD', threadIndex: ThreadIndex, isExpanded: boolean };

type URLEnhancerAction =
{ type: "@@urlenhancer/urlSetupDone" } |
Expand Down
167 changes: 102 additions & 65 deletions src/content/components/FlameChartCanvas.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
// @flow
import React, { PureComponent } from 'react';
import { timeCode } from '../../common/time-code';
import TextMeasurement from '../../common/text-measurement';
import withTimelineViewport from './TimelineViewport';
import TimelineCanvas from './TimelineCanvas';

import type { Thread } from '../../common/types/profile';
import type { Milliseconds, CssPixels, UnitIntervalOfProfileRange, DevicePixels } from '../../common/types/units';
import type { StackTimingByDepth } from '../stack-timing';
import type { Milliseconds, CssPixels, UnitIntervalOfProfileRange } from '../../common/types/units';
import type { StackTimingByDepth, StackTimingDepth, IndexIntoStackTiming } from '../stack-timing';
import type { GetCategory } from '../color-categories';
import type { GetLabel } from '../labeling-strategies';
import type { Action, ProfileSelection } from '../actions/types';

type Props = {
thread: Thread,
Expand All @@ -25,6 +26,12 @@ type Props = {
stackFrameHeight: CssPixels,
getCategory: GetCategory,
getLabel: GetLabel,
updateProfileSelection: ProfileSelection => Action,
};

type HoveredStackTiming = {
depth: StackTimingDepth,
stackTableIndex: IndexIntoStackTiming,
};

require('./FlameChartCanvas.css');
Expand All @@ -35,62 +42,16 @@ const TEXT_OFFSET_TOP = 11;

class FlameChartCanvas extends PureComponent {

_requestedAnimationFrame: boolean
_devicePixelRatio: number
_textMeasurement: null|TextMeasurement
_ctx: null|CanvasRenderingContext2D
_textMeasurement: null | TextMeasurement;

props: Props
props: Props;

constructor(props: Props) {
super(props);
this._requestedAnimationFrame = false;
this._devicePixelRatio = 1;
this._textMeasurement = null;
}

_scheduleDraw() {
if (!this._requestedAnimationFrame) {
this._requestedAnimationFrame = true;
window.requestAnimationFrame(() => {
this._requestedAnimationFrame = false;
if (this.refs.canvas) {
timeCode('FlameChartCanvas render', () => {
this.drawCanvas();
});
}
});
}
}

componentDidMount() {
this._textMeasurement = new TextMeasurement(this.refs.canvas.getContext('2d'));
}

_prepCanvas() {
const {canvas} = this.refs;
const {containerWidth, containerHeight} = this.props;
const {devicePixelRatio} = window;
const pixelWidth: DevicePixels = containerWidth * devicePixelRatio;
const pixelHeight: DevicePixels = containerHeight * devicePixelRatio;
if (!this._ctx) {
this._ctx = canvas.getContext('2d');
}
if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
canvas.width = pixelWidth;
canvas.height = pixelHeight;
canvas.style.width = containerWidth + 'px';
canvas.style.height = containerHeight + 'px';
this._ctx.scale(this._devicePixelRatio, this._devicePixelRatio);
}
if (this._devicePixelRatio !== devicePixelRatio) {
// Make sure and multiply by the inverse of the previous ratio, as the scaling
// operates off of the previous set scale.
const scale = (1 / this._devicePixelRatio) * devicePixelRatio;
this._ctx.scale(scale, scale);
this._devicePixelRatio = devicePixelRatio;
}
return this._ctx;
(this: any)._onDoubleClickStack = this._onDoubleClickStack.bind(this);
(this: any)._getHoveredStackInfo = this._getHoveredStackInfo.bind(this);
(this: any)._drawCanvas = this._drawCanvas.bind(this);
(this: any)._hitTest = this._hitTest.bind(this);
}

/**
Expand All @@ -100,15 +61,22 @@ class FlameChartCanvas extends PureComponent {
* 0 - 1. This was done to make the calculations easier for computing various zoomed
* and translated views independent of any particular scale. See TimelineViewport.js
* for a diagram detailing the various components of this set-up.
* @param {HTMLCanvasElement} canvas - The current canvas.
* @returns {undefined}
*/
drawCanvas() {
_drawCanvas(
ctx: CanvasRenderingContext2D,
hoveredItem: HoveredStackTiming | null
) {
const { thread, rangeStart, rangeEnd, containerWidth, getLabel,
containerHeight, stackTimingByDepth, stackFrameHeight, getCategory,
viewportLeft, viewportRight, viewportTop, viewportBottom } = this.props;

const ctx = this._prepCanvas();
// Ensure the text measurement tool is created, since this is the first time
// this class has access to a ctx.
if (!this._textMeasurement) {
this._textMeasurement = new TextMeasurement(ctx);
}
const textMeasurement = this._textMeasurement;

ctx.clearRect(0, 0, containerWidth, containerHeight);

const rangeLength: Milliseconds = rangeEnd - rangeStart;
Expand Down Expand Up @@ -158,8 +126,9 @@ class FlameChartCanvas extends PureComponent {
const frameIndex = thread.stackTable.frame[stackIndex];
const text = getLabel(thread, stackIndex);
const category = getCategory(thread, frameIndex);
const isHovered = hoveredItem && depth === hoveredItem.depth && i === hoveredItem.stackTableIndex;

ctx.fillStyle = category.color;
ctx.fillStyle = isHovered ? 'Highlight' : category.color;
ctx.fillRect(x, y, w, h);
// Ensure spacing between blocks.
ctx.clearRect(x, y, 1, h);
Expand All @@ -169,10 +138,10 @@ class FlameChartCanvas extends PureComponent {
const x2: CssPixels = Math.max(x, 0) + TEXT_OFFSET_START;
const w2: CssPixels = Math.max(0, w - (x2 - x));

if (this._textMeasurement !== null && w2 > this._textMeasurement.minWidth) {
const fittedText = this._textMeasurement.getFittedText(text, w2);
if (w2 > textMeasurement.minWidth) {
const fittedText = textMeasurement.getFittedText(text, w2);
if (fittedText) {
ctx.fillStyle = 'rgb(0, 0, 0)';
ctx.fillStyle = isHovered ? 'HighlightText' : '#000000';
ctx.fillText(fittedText, x2, y + TEXT_OFFSET_TOP);
}
}
Expand All @@ -181,9 +150,77 @@ class FlameChartCanvas extends PureComponent {
}
}

_getHoveredStackInfo(
{depth, stackTableIndex}: HoveredStackTiming
): string {
const { thread, getLabel, stackTimingByDepth } = this.props;
const label = getLabel(thread, stackTimingByDepth[depth].stack[stackTableIndex]);

const duration = stackTimingByDepth[depth].end[stackTableIndex] -
stackTimingByDepth[depth].start[stackTableIndex];
let durationString;
if (duration >= 10) {
durationString = duration.toFixed(0);
} else if (duration >= 1) {
durationString = duration.toFixed(1);
} else if (duration >= 0.1) {
durationString = duration.toFixed(2);
} else {
durationString = duration.toFixed(3);
}

return `${durationString}ms - ${label}`;
}

_onDoubleClickStack({depth, stackTableIndex}: HoveredStackTiming) {
const { stackTimingByDepth, updateProfileSelection } = this.props;
updateProfileSelection({
hasSelection: true,
isModifying: false,
selectionStart: stackTimingByDepth[depth].start[stackTableIndex],
selectionEnd: stackTimingByDepth[depth].end[stackTableIndex],
});
}

_hitTest(x: CssPixels, y: CssPixels): HoveredStackTiming | null {
const {
rangeStart, rangeEnd, viewportLeft, viewportRight, viewportTop,
containerWidth, stackTimingByDepth,
} = this.props;

const rangeLength: Milliseconds = rangeEnd - rangeStart;
const viewportLength: UnitIntervalOfProfileRange = viewportRight - viewportLeft;
const unitIntervalTime: UnitIntervalOfProfileRange = viewportLeft + viewportLength * (x / containerWidth);
const time: Milliseconds = rangeStart + unitIntervalTime * rangeLength;
const depth = Math.floor((y + viewportTop) / ROW_HEIGHT);
const stackTiming = stackTimingByDepth[depth];

if (!stackTiming) {
return null;
}

for (let i = 0; i < stackTiming.length; i++) {
const start = stackTiming.start[i];
const end = stackTiming.end[i];
if (start < time && end > time) {
return { depth, stackTableIndex: i };
}
}

return null;
}


render() {
this._scheduleDraw();
return <canvas className='flameChartCanvas' ref='canvas'/>;
const { containerWidth, containerHeight } = this.props;

return <TimelineCanvas className='flameChartCanvas'
containerWidth={containerWidth}
containerHeight={containerHeight}
onDoubleClickItem={this._onDoubleClickStack}
getHoveredItemInfo={this._getHoveredStackInfo}
drawCanvas={this._drawCanvas}
hitTest={this._hitTest} />;
}
}

Expand Down
9 changes: 9 additions & 0 deletions src/content/components/TimelineCanvas.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.timelineCanvas {
position: absolute;
top: 0;
left: 0;
}

.timelineCanvas.hover {
cursor: default;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: have you tried putting the "cursor: default" on the above block ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The behavior here is to show the grab cursor on the Timeline when not hovering over a stack frame, then when you hover over a stack frame it changes to the default cursor.

}
Loading