Skip to content

Commit e2ff9fc

Browse files
author
Michael Mrowetz
committed
#184 add option to render userTiming Marker and durations
1 parent b0f7114 commit e2ff9fc

File tree

10 files changed

+174
-58
lines changed

10 files changed

+174
-58
lines changed

Diff for: src/css-raw/perf-cascade.css

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
.water-fall-chart .line-end.active,
2020
.water-fall-chart .line-start.active {display: block;}
2121

22+
.water-fall-chart .line-mark {fill: #0aa; opacity: 0.01; stroke-width: 0;}
23+
.water-fall-chart .line-holder.active .line-mark { opacity: 0.5;}
24+
.water-fall-chart .line-label-holder {cursor: pointer;}
25+
2226
.water-fall-chart .mark-holder text {writing-mode:vertical-lr;}
2327

2428
.left-fixed-holder .label-full-bg {fill: #fff; opacity: 0.9}

Diff for: src/ts/helpers/misc.ts

+14-3
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,21 @@ export function roundNumber(num: number, decimals: number = 2) {
6868
/**
6969
*
7070
* Checks if `status` code is `>= lowerBound` and `<= upperBound`
71-
* @param {number} entry
72-
* @param {number} lowerBound - inclusive lower bound
73-
* @param {number} upperBound - inclusive upper bound
71+
* @param {number} status HTTP status code
72+
* @param {number} lowerBound inclusive lower bound
73+
* @param {number} upperBound inclusive upper bound
7474
*/
7575
export function isInStatusCodeRange(status: number, lowerBound: number, upperBound: number) {
7676
return status >= lowerBound && status <= upperBound;
7777
}
78+
79+
/** precompiled regex */
80+
const cssClassRegEx = /[^a-z-]/g;
81+
82+
/**
83+
* Converts a seed string to a CSS class by stripping out invalid characters
84+
* @param {string} seed string to base the CSS class off
85+
*/
86+
export function toCssClass(seed: string) {
87+
return seed.toLowerCase().replace(cssClassRegEx, "");
88+
}

Diff for: src/ts/main.ts

+13-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { validateOptions } from "./helpers/parse";
33
import { makeLegend } from "./legend/legend";
44
import Paging from "./paging/paging";
55
import * as HarTransformer from "./transformers/har";
6-
import { ChartOptions } from "./typing/options";
6+
import { ChartOptions, HarTransformerOptions } from "./typing/options";
77
import { WaterfallDocs } from "./typing/waterfall";
88
import { createWaterfallSvg } from "./waterfall/svg-chart";
99

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

23+
/** default options to use if not set in `options` parameter */
24+
const defaultHarTransformerOptions: Readonly<HarTransformerOptions> = {
25+
showUserTiming: false,
26+
showUserTimingEndMarker: false,
27+
};
28+
2329
function PerfCascade(waterfallDocsData: WaterfallDocs, chartOptions: Partial<ChartOptions> = {}): SVGSVGElement {
2430
const options: ChartOptions = validateOptions({ ...defaultOptions, ...chartOptions });
2531

@@ -53,8 +59,12 @@ function PerfCascade(waterfallDocsData: WaterfallDocs, chartOptions: Partial<Cha
5359
* @param {ChartOptions} options - PerfCascade options object
5460
* @returns {SVGSVGElement} - Chart SVG Element
5561
*/
56-
function fromHar(harData: Har, options: Partial<ChartOptions> = {}): SVGSVGElement {
57-
const data = HarTransformer.transformDoc(harData);
62+
function fromHar(harData: Har, options: Partial<ChartOptions & HarTransformerOptions> = {}): SVGSVGElement {
63+
const harTransformerOptions: HarTransformerOptions = {
64+
...defaultHarTransformerOptions,
65+
...options as Partial<HarTransformerOptions>
66+
};
67+
const data = HarTransformer.transformDoc(harData, harTransformerOptions);
5868
if (typeof options.onParsed === "function") {
5969
options.onParsed(data);
6070
}

Diff for: src/ts/transformers/har.ts

+88-22
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import {
77
} from "har-format";
88
import { roundNumber } from "../helpers/misc";
99
import { toInt } from "../helpers/parse";
10+
import { HarTransformerOptions } from "../typing/options";
1011
import {
1112
Mark,
1213
TimingType,
14+
UserTiming,
1315
WaterfallData,
1416
WaterfallDocs,
1517
WaterfallEntryIndicator,
@@ -28,16 +30,17 @@ import {
2830

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

3942
return {
40-
pages: pages.map((_page, i) => this.transformPage(data, i)),
43+
pages: pages.map((_page, i) => this.transformPage(data, i, options)),
4144
};
4245
}
4346

@@ -65,7 +68,7 @@ function toWaterFallEntry(entry: Entry, index: number, startRelative: number, is
6568
}
6669

6770
/** retuns the page or a mock page object */
68-
function getPages(data: Log) {
71+
const getPages = (data: Log) => {
6972
if (data.pages && data.pages.length > 0) {
7073
return data.pages;
7174
}
@@ -80,15 +83,18 @@ function getPages(data: Log) {
8083
startedDateTime: statedTime,
8184
title: "n/a",
8285
} as Page];
83-
}
86+
};
8487

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

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

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

131127
// Add 100ms margin to make room for labels
132128
doneTime += 100;
@@ -136,19 +132,89 @@ export function transformPage(harData: Har | Log, pageIndex: number = 0): Waterf
136132
durationMs: doneTime,
137133
entries,
138134
marks,
139-
lines: [],
140135
title: currPage.title,
141136
};
142137
}
143138

139+
/**
140+
* Extract all `Mark`s based on `PageTiming` and `UserTiming`
141+
* @param {PageTiming} pageTimings - HARs `PageTiming` object
142+
* @param {Page} currPage - active page
143+
* @param {HarTransformerOptions} options - HAR-parser-specific options
144+
*/
145+
const getMarks = (pageTimings: PageTiming, currPage: Page, options: HarTransformerOptions) => {
146+
const sortFn = (a: Mark, b: Mark) => a.startTime - b.startTime;
147+
const marks = Object.keys(pageTimings)
148+
.filter((k: keyof PageTiming) => (typeof pageTimings[k] === "number" && pageTimings[k] >= 0))
149+
.map((k) => ({
150+
name: `${k.replace(/^[_]/, "")} (${roundNumber(pageTimings[k], 0)} ms)`,
151+
startTime: pageTimings[k],
152+
} as Mark));
153+
154+
if (!options.showUserTiming) {
155+
return marks.sort(sortFn);
156+
}
157+
158+
return getUserTimimngs(currPage, options)
159+
.concat(marks)
160+
.sort(sortFn);
161+
};
162+
163+
/**
164+
* Extract all `Mark`s based on `UserTiming`
165+
* @param {Page} currPage - active page
166+
* @param {HarTransformerOptions} options - HAR-parser-specific options
167+
*/
168+
const getUserTimimngs = (currPage: Page, options: HarTransformerOptions) => {
169+
let baseFilter = options.showUserTimingEndMarker ?
170+
(k: string) => k.indexOf("_userTime.") === 0 :
171+
(k: string) => k.indexOf("_userTime.") === 0 && k.indexOf("_userTime.endTimer-") !== 0;
172+
let filterFn = baseFilter;
173+
174+
if (Array.isArray(options.showUserTiming)) {
175+
let findTimings = options.showUserTiming;
176+
filterFn = (k: string) => (
177+
baseFilter(k) &&
178+
findTimings.indexOf(k.replace(/^_userTime\./, "")) >= 0
179+
);
180+
}
181+
182+
const findName = /^_userTime\.((?:startTimer-)?(.+))$/;
183+
184+
const extractUserTiming = (k: string) => {
185+
let name: string;
186+
let fullName: string;
187+
let duration: number;
188+
[, fullName, name] = findName.exec(k);
189+
190+
if (fullName !== name && currPage[`_userTime.endTimer-${name}`]) {
191+
duration = currPage[`_userTime.endTimer-${name}`] - currPage[k];
192+
return {
193+
name: fullName,
194+
duration,
195+
startTime: currPage[k],
196+
// x: currPage[k],
197+
} as UserTiming;
198+
}
199+
return {
200+
name: fullName,
201+
startTime: currPage[k],
202+
} as UserTiming;
203+
};
204+
205+
return Object.keys(currPage)
206+
.filter(filterFn)
207+
.map(extractUserTiming);
208+
};
209+
144210
/**
145211
* Create `WaterfallEntry`s to represent the subtimings of a request
146212
* ("blocked", "dns", "connect", "send", "wait", "receive")
147213
* @param {number} startRelative - Number of milliseconds since page load started (`page.startedDateTime`)
148214
* @param {Entry} harEntry
149215
* @returns Array
150216
*/
151-
function buildDetailTimingBlocks(startRelative: number, harEntry: Entry): WaterfallEntryTiming[] {
217+
const buildDetailTimingBlocks = (startRelative: number, harEntry: Entry): WaterfallEntryTiming[] => {
152218
let t = harEntry.timings;
153219
return ["blocked", "dns", "connect", "send", "wait", "receive"].reduce((collect: WaterfallEntryTiming[],
154220
key: TimingType) => {
@@ -172,7 +238,7 @@ function buildDetailTimingBlocks(startRelative: number, harEntry: Entry): Waterf
172238

173239
return collect.concat([createWaterfallEntryTiming(key, Math.round(time.start), Math.round(time.end))]);
174240
}, []);
175-
}
241+
};
176242

177243
/**
178244
* Returns Object containing start and end time of `collect`
@@ -183,7 +249,7 @@ function buildDetailTimingBlocks(startRelative: number, harEntry: Entry): Waterf
183249
* @param {number} startRelative - Number of milliseconds since page load started (`page.startedDateTime`)
184250
* @returns {Object}
185251
*/
186-
function getTimePair(key: string, harEntry: Entry, collect: WaterfallEntryTiming[], startRelative: number) {
252+
const getTimePair =(key: string, harEntry: Entry, collect: WaterfallEntryTiming[], startRelative: number) => {
187253
let wptKey;
188254
switch (key) {
189255
case "wait": wptKey = "ttfb"; break;
@@ -209,7 +275,7 @@ function getTimePair(key: string, harEntry: Entry, collect: WaterfallEntryTiming
209275
* @param {WaterfallEntryIndicator[]} indicators
210276
* @returns WaterfallResponseDetails
211277
*/
212-
function createResponseDetails(entry: Entry, indicators: WaterfallEntryIndicator[]): WaterfallResponseDetails {
278+
const createResponseDetails = (entry: Entry, indicators: WaterfallEntryIndicator[]): WaterfallResponseDetails => {
213279
const requestType = mimeToRequestType(entry.response.content.mimeType);
214280
return {
215281
icon: makeMimeTypeIcon(entry.response.status, entry.response.statusText, requestType, entry.response.redirectURL),

Diff for: src/ts/transformers/helpers.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/** Helpers that are not file-fromat specific */
2-
import { isInStatusCodeRange } from "../helpers/misc";
2+
import { isInStatusCodeRange, toCssClass } from "../helpers/misc";
33
import { escapeHtml } from "../helpers/parse";
44
import { RequestType } from "../typing/waterfall";
55
import {
@@ -19,8 +19,8 @@ export function makeDefinitionList(dlKeyValues: KvTuple[], addClass: boolean = f
1919
if (!addClass) {
2020
return "";
2121
}
22-
let className = key.toLowerCase().replace(/[^a-z-]/g, "");
23-
return `class="${className || "no-colour"}"`;
22+
let className = toCssClass(key) || "no-colour";
23+
return `class="${className}"`;
2424
};
2525
return dlKeyValues
2626
.filter((tuple: KvTuple) => tuple[1] !== undefined)

Diff for: src/ts/typing/options.ts

+12
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,15 @@ export interface ChartOptions {
2020
/** Callback called when the HAR doc has been parsed into PerfCascases */
2121
onParsed: (data: WaterfallDocs) => void;
2222
}
23+
24+
export interface HarTransformerOptions {
25+
/** Should UserTimings in WPT be used and rendered as Mark (default: false) */
26+
showUserTiming: boolean | string[];
27+
/**
28+
* If this is enabled, the `endTimer-*` marker are shown,
29+
* and both start and end show the full `startTimer-*` and `endTimer-*` name. (default: false)
30+
*
31+
* _requires `showUserTiming` to be `true`_
32+
*/
33+
showUserTimingEndMarker: boolean;
34+
}

Diff for: src/ts/typing/waterfall.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ export type RequestType = "other" | "image" | "video" | "audio" | "font" | "svg"
44

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

7+
/** Typing for a event, e.g. UserTiming API performance mark from WPT */
78
export interface UserTiming {
89
duration?: number;
910
name: string;
1011
startTime: number;
1112
}
1213

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

91+
/** Type data used to rendering a single waterfall diagram */
8992
export interface WaterfallData {
93+
/** Page title */
9094
title: string;
95+
/** time to load all contained entries (in ms) */
9196
durationMs: number;
97+
/** Array of requests */
9298
entries: WaterfallEntry[];
99+
/** special time marker e.g. `onLoad` */
93100
marks: Mark[];
94-
lines: WaterfallEntry[];
95101
/** indicates if the parent document is loaded with TLS or SSL */
96102
docIsTLS: boolean;
97103
}
98104

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

Diff for: src/ts/waterfall/sub-components/svg-general-components.ts

-16
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@
44

55
import { roundNumber } from "../../helpers/misc";
66
import * as svg from "../../helpers/svg";
7-
import { requestTypeToCssClass } from "../../transformers/styling-converters";
87
import { Context } from "../../typing/context";
98
import { OverlayChangeEvent } from "../../typing/open-overlay";
10-
import { WaterfallEntry } from "../../typing/waterfall";
119

1210
/**
1311
* Renders a per-second marker line and appends it to `timeHolder`
@@ -85,17 +83,3 @@ export function createTimeScale(context: Context, durationMs: number): SVGGEleme
8583
}
8684
return timeHolder;
8785
}
88-
89-
// TODO: Implement - data for this not parsed yet
90-
export function createBgRect(context: Context, entry: WaterfallEntry): SVGRectElement {
91-
let rect = svg.newRect({
92-
"height": context.diagramHeight,
93-
"width": ((entry.total || 1) / context.unit) + "%",
94-
"x": ((entry.start || 0.001) / context.unit) + "%",
95-
"y": 0,
96-
}, requestTypeToCssClass(entry.responseDetails.requestType));
97-
98-
rect.appendChild(svg.newTitle(entry.url)); // Add tile to wedge path
99-
100-
return rect;
101-
}

0 commit comments

Comments
 (0)