Skip to content

Commit

Permalink
#184 add option to render userTiming Marker and durations
Browse files Browse the repository at this point in the history
  • Loading branch information
Michael Mrowetz committed Mar 19, 2017
1 parent b0f7114 commit e2ff9fc
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 58 deletions.
4 changes: 4 additions & 0 deletions src/css-raw/perf-cascade.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
.water-fall-chart .line-end.active,
.water-fall-chart .line-start.active {display: block;}

.water-fall-chart .line-mark {fill: #0aa; opacity: 0.01; stroke-width: 0;}
.water-fall-chart .line-holder.active .line-mark { opacity: 0.5;}
.water-fall-chart .line-label-holder {cursor: pointer;}

.water-fall-chart .mark-holder text {writing-mode:vertical-lr;}

.left-fixed-holder .label-full-bg {fill: #fff; opacity: 0.9}
Expand Down
17 changes: 14 additions & 3 deletions src/ts/helpers/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,21 @@ export function roundNumber(num: number, decimals: number = 2) {
/**
*
* Checks if `status` code is `>= lowerBound` and `<= upperBound`
* @param {number} entry
* @param {number} lowerBound - inclusive lower bound
* @param {number} upperBound - inclusive upper bound
* @param {number} status HTTP status code
* @param {number} lowerBound inclusive lower bound
* @param {number} upperBound inclusive upper bound
*/
export function isInStatusCodeRange(status: number, lowerBound: number, upperBound: number) {
return status >= lowerBound && status <= upperBound;
}

/** precompiled regex */
const cssClassRegEx = /[^a-z-]/g;

/**
* Converts a seed string to a CSS class by stripping out invalid characters
* @param {string} seed string to base the CSS class off
*/
export function toCssClass(seed: string) {
return seed.toLowerCase().replace(cssClassRegEx, "");
}
16 changes: 13 additions & 3 deletions src/ts/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { validateOptions } from "./helpers/parse";
import { makeLegend } from "./legend/legend";
import Paging from "./paging/paging";
import * as HarTransformer from "./transformers/har";
import { ChartOptions } from "./typing/options";
import { ChartOptions, HarTransformerOptions } from "./typing/options";
import { WaterfallDocs } from "./typing/waterfall";
import { createWaterfallSvg } from "./waterfall/svg-chart";

Expand All @@ -20,6 +20,12 @@ const defaultOptions: Readonly<ChartOptions> = {
showMimeTypeIcon: true,
};

/** default options to use if not set in `options` parameter */
const defaultHarTransformerOptions: Readonly<HarTransformerOptions> = {
showUserTiming: false,
showUserTimingEndMarker: false,
};

function PerfCascade(waterfallDocsData: WaterfallDocs, chartOptions: Partial<ChartOptions> = {}): SVGSVGElement {
const options: ChartOptions = validateOptions({ ...defaultOptions, ...chartOptions });

Expand Down Expand Up @@ -53,8 +59,12 @@ function PerfCascade(waterfallDocsData: WaterfallDocs, chartOptions: Partial<Cha
* @param {ChartOptions} options - PerfCascade options object
* @returns {SVGSVGElement} - Chart SVG Element
*/
function fromHar(harData: Har, options: Partial<ChartOptions> = {}): SVGSVGElement {
const data = HarTransformer.transformDoc(harData);
function fromHar(harData: Har, options: Partial<ChartOptions & HarTransformerOptions> = {}): SVGSVGElement {
const harTransformerOptions: HarTransformerOptions = {
...defaultHarTransformerOptions,
...options as Partial<HarTransformerOptions>
};
const data = HarTransformer.transformDoc(harData, harTransformerOptions);
if (typeof options.onParsed === "function") {
options.onParsed(data);
}
Expand Down
110 changes: 88 additions & 22 deletions src/ts/transformers/har.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import {
} from "har-format";
import { roundNumber } from "../helpers/misc";
import { toInt } from "../helpers/parse";
import { HarTransformerOptions } from "../typing/options";
import {
Mark,
TimingType,
UserTiming,
WaterfallData,
WaterfallDocs,
WaterfallEntryIndicator,
Expand All @@ -28,16 +30,17 @@ import {

/**
* Transforms the full HAR doc, including all pages
* @param {Har} harData - raw hhar object
* @param {Har} harData - raw HAR object
* @param {HarTransformerOptions} options - HAR-parser-specific options
* @returns WaterfallDocs
*/
export function transformDoc(harData: Har | Log): WaterfallDocs {
export function transformDoc(harData: Har | Log, options: HarTransformerOptions): WaterfallDocs {
// make sure it's the *.log base node
let data = (harData["log"] !== undefined ? harData["log"] : harData) as Log;
const pages = getPages(data);

return {
pages: pages.map((_page, i) => this.transformPage(data, i)),
pages: pages.map((_page, i) => this.transformPage(data, i, options)),
};
}

Expand Down Expand Up @@ -65,7 +68,7 @@ function toWaterFallEntry(entry: Entry, index: number, startRelative: number, is
}

/** retuns the page or a mock page object */
function getPages(data: Log) {
const getPages = (data: Log) => {
if (data.pages && data.pages.length > 0) {
return data.pages;
}
Expand All @@ -80,15 +83,18 @@ function getPages(data: Log) {
startedDateTime: statedTime,
title: "n/a",
} as Page];
}
};

/**
* Transforms a HAR object into the format needed to render the PerfCascade
* @param {Har} harData - HAR document
* @param {number=0} pageIndex - page to parse (for multi-page HAR)
* @param {HarTransformerOptions} options - HAR-parser-specific options
* @returns WaterfallData
*/
export function transformPage(harData: Har | Log, pageIndex: number = 0): WaterfallData {
export function transformPage(harData: Har | Log,
pageIndex: number = 0,
options: HarTransformerOptions): WaterfallData {
// make sure it's the *.log base node
let data = (harData["log"] !== undefined ? harData["log"] : harData) as Log;

Expand Down Expand Up @@ -116,17 +122,7 @@ export function transformPage(harData: Har | Log, pageIndex: number = 0): Waterf
return toWaterFallEntry(entry, index, startRelative, isTLS);
});

const marks = Object.keys(pageTimings)
.filter((k: keyof PageTiming) => (typeof pageTimings[k] === "number" && pageTimings[k] >= 0))
.sort((a: string, b: string) => pageTimings[a] > pageTimings[b] ? 1 : -1)
.map((k) => {
const startRelative: number = pageTimings[k];
doneTime = Math.max(doneTime, startRelative);
return {
"name": `${k.replace(/^[_]/, "")} (${roundNumber(startRelative, 0)} ms)`,
"startTime": startRelative,
} as Mark;
});
const marks = getMarks(pageTimings, currPage, options);

// Add 100ms margin to make room for labels
doneTime += 100;
Expand All @@ -136,19 +132,89 @@ export function transformPage(harData: Har | Log, pageIndex: number = 0): Waterf
durationMs: doneTime,
entries,
marks,
lines: [],
title: currPage.title,
};
}

/**
* Extract all `Mark`s based on `PageTiming` and `UserTiming`
* @param {PageTiming} pageTimings - HARs `PageTiming` object
* @param {Page} currPage - active page
* @param {HarTransformerOptions} options - HAR-parser-specific options
*/
const getMarks = (pageTimings: PageTiming, currPage: Page, options: HarTransformerOptions) => {
const sortFn = (a: Mark, b: Mark) => a.startTime - b.startTime;
const marks = Object.keys(pageTimings)
.filter((k: keyof PageTiming) => (typeof pageTimings[k] === "number" && pageTimings[k] >= 0))
.map((k) => ({
name: `${k.replace(/^[_]/, "")} (${roundNumber(pageTimings[k], 0)} ms)`,
startTime: pageTimings[k],
} as Mark));

if (!options.showUserTiming) {
return marks.sort(sortFn);
}

return getUserTimimngs(currPage, options)
.concat(marks)
.sort(sortFn);
};

/**
* Extract all `Mark`s based on `UserTiming`
* @param {Page} currPage - active page
* @param {HarTransformerOptions} options - HAR-parser-specific options
*/
const getUserTimimngs = (currPage: Page, options: HarTransformerOptions) => {
let baseFilter = options.showUserTimingEndMarker ?
(k: string) => k.indexOf("_userTime.") === 0 :
(k: string) => k.indexOf("_userTime.") === 0 && k.indexOf("_userTime.endTimer-") !== 0;
let filterFn = baseFilter;

if (Array.isArray(options.showUserTiming)) {
let findTimings = options.showUserTiming;
filterFn = (k: string) => (
baseFilter(k) &&
findTimings.indexOf(k.replace(/^_userTime\./, "")) >= 0
);
}

const findName = /^_userTime\.((?:startTimer-)?(.+))$/;

const extractUserTiming = (k: string) => {
let name: string;
let fullName: string;
let duration: number;
[, fullName, name] = findName.exec(k);

if (fullName !== name && currPage[`_userTime.endTimer-${name}`]) {
duration = currPage[`_userTime.endTimer-${name}`] - currPage[k];
return {
name: fullName,
duration,
startTime: currPage[k],
// x: currPage[k],
} as UserTiming;
}
return {
name: fullName,
startTime: currPage[k],
} as UserTiming;
};

return Object.keys(currPage)
.filter(filterFn)
.map(extractUserTiming);
};

/**
* Create `WaterfallEntry`s to represent the subtimings of a request
* ("blocked", "dns", "connect", "send", "wait", "receive")
* @param {number} startRelative - Number of milliseconds since page load started (`page.startedDateTime`)
* @param {Entry} harEntry
* @returns Array
*/
function buildDetailTimingBlocks(startRelative: number, harEntry: Entry): WaterfallEntryTiming[] {
const buildDetailTimingBlocks = (startRelative: number, harEntry: Entry): WaterfallEntryTiming[] => {
let t = harEntry.timings;
return ["blocked", "dns", "connect", "send", "wait", "receive"].reduce((collect: WaterfallEntryTiming[],
key: TimingType) => {
Expand All @@ -172,7 +238,7 @@ function buildDetailTimingBlocks(startRelative: number, harEntry: Entry): Waterf

return collect.concat([createWaterfallEntryTiming(key, Math.round(time.start), Math.round(time.end))]);
}, []);
}
};

/**
* Returns Object containing start and end time of `collect`
Expand All @@ -183,7 +249,7 @@ function buildDetailTimingBlocks(startRelative: number, harEntry: Entry): Waterf
* @param {number} startRelative - Number of milliseconds since page load started (`page.startedDateTime`)
* @returns {Object}
*/
function getTimePair(key: string, harEntry: Entry, collect: WaterfallEntryTiming[], startRelative: number) {
const getTimePair =(key: string, harEntry: Entry, collect: WaterfallEntryTiming[], startRelative: number) => {
let wptKey;
switch (key) {
case "wait": wptKey = "ttfb"; break;
Expand All @@ -209,7 +275,7 @@ function getTimePair(key: string, harEntry: Entry, collect: WaterfallEntryTiming
* @param {WaterfallEntryIndicator[]} indicators
* @returns WaterfallResponseDetails
*/
function createResponseDetails(entry: Entry, indicators: WaterfallEntryIndicator[]): WaterfallResponseDetails {
const createResponseDetails = (entry: Entry, indicators: WaterfallEntryIndicator[]): WaterfallResponseDetails => {
const requestType = mimeToRequestType(entry.response.content.mimeType);
return {
icon: makeMimeTypeIcon(entry.response.status, entry.response.statusText, requestType, entry.response.redirectURL),
Expand Down
6 changes: 3 additions & 3 deletions src/ts/transformers/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** Helpers that are not file-fromat specific */
import { isInStatusCodeRange } from "../helpers/misc";
import { isInStatusCodeRange, toCssClass } from "../helpers/misc";
import { escapeHtml } from "../helpers/parse";
import { RequestType } from "../typing/waterfall";
import {
Expand All @@ -19,8 +19,8 @@ export function makeDefinitionList(dlKeyValues: KvTuple[], addClass: boolean = f
if (!addClass) {
return "";
}
let className = key.toLowerCase().replace(/[^a-z-]/g, "");
return `class="${className || "no-colour"}"`;
let className = toCssClass(key) || "no-colour";
return `class="${className}"`;
};
return dlKeyValues
.filter((tuple: KvTuple) => tuple[1] !== undefined)
Expand Down
12 changes: 12 additions & 0 deletions src/ts/typing/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,15 @@ export interface ChartOptions {
/** Callback called when the HAR doc has been parsed into PerfCascases */
onParsed: (data: WaterfallDocs) => void;
}

export interface HarTransformerOptions {
/** Should UserTimings in WPT be used and rendered as Mark (default: false) */
showUserTiming: boolean | string[];
/**
* If this is enabled, the `endTimer-*` marker are shown,
* and both start and end show the full `startTimer-*` and `endTimer-*` name. (default: false)
*
* _requires `showUserTiming` to be `true`_
*/
showUserTimingEndMarker: boolean;
}
10 changes: 9 additions & 1 deletion src/ts/typing/waterfall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ export type RequestType = "other" | "image" | "video" | "audio" | "font" | "svg"

export type IndicatorType = "error" | "warning" | "info";

/** Typing for a event, e.g. UserTiming API performance mark from WPT */
export interface UserTiming {
duration?: number;
name: string;
startTime: number;
}

/** Type for a time-marker, e.g. the fireing of an event */
export interface Mark extends UserTiming {
/** custom data to store x position */
x?: number;
Expand Down Expand Up @@ -86,17 +88,23 @@ export interface WaterfallResponseDetails {
requestType: RequestType;
}

/** Type data used to rendering a single waterfall diagram */
export interface WaterfallData {
/** Page title */
title: string;
/** time to load all contained entries (in ms) */
durationMs: number;
/** Array of requests */
entries: WaterfallEntry[];
/** special time marker e.g. `onLoad` */
marks: Mark[];
lines: WaterfallEntry[];
/** indicates if the parent document is loaded with TLS or SSL */
docIsTLS: boolean;
}

/** Type for a series of waterfall diagrams */
export interface WaterfallDocs {
/** Series of waterfalls (e.g. multiple page-views) */
pages: WaterfallData[];
}

Expand Down
16 changes: 0 additions & 16 deletions src/ts/waterfall/sub-components/svg-general-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@

import { roundNumber } from "../../helpers/misc";
import * as svg from "../../helpers/svg";
import { requestTypeToCssClass } from "../../transformers/styling-converters";
import { Context } from "../../typing/context";
import { OverlayChangeEvent } from "../../typing/open-overlay";
import { WaterfallEntry } from "../../typing/waterfall";

/**
* Renders a per-second marker line and appends it to `timeHolder`
Expand Down Expand Up @@ -85,17 +83,3 @@ export function createTimeScale(context: Context, durationMs: number): SVGGEleme
}
return timeHolder;
}

// TODO: Implement - data for this not parsed yet
export function createBgRect(context: Context, entry: WaterfallEntry): SVGRectElement {
let rect = svg.newRect({
"height": context.diagramHeight,
"width": ((entry.total || 1) / context.unit) + "%",
"x": ((entry.start || 0.001) / context.unit) + "%",
"y": 0,
}, requestTypeToCssClass(entry.responseDetails.requestType));

rect.appendChild(svg.newTitle(entry.url)); // Add tile to wedge path

return rect;
}
Loading

0 comments on commit e2ff9fc

Please sign in to comment.