Starting to introduce a loader performance logger

master
WolverinDEV 2021-03-18 19:44:46 +01:00
parent 85acd6dd56
commit 3d04fa3f0d
6 changed files with 214 additions and 158 deletions

View File

@ -0,0 +1,128 @@
export type ResourceRequestResult = {
status: "success"
} | {
status: "unknown-error",
message: string
} | {
status: "error-event"
} | {
status: "timeout",
givenTimeout: number
};
export type ResourceType = "script" | "css" | "json";
export class ResourceRequest {
private readonly type: ResourceType;
private readonly name: string;
private status: "unset" | "pending" | "executing" | "executed";
private result: ResourceRequestResult | undefined;
private timestampEnqueue: number;
private timestampExecuting: number;
private timestampExecuted: number;
constructor(type: ResourceType, name: string) {
this.type = type;
this.name = name;
this.status = "unset";
}
markEnqueue() {
if(this.status !== "unset") {
console.warn("ResourceRequest %s status isn't unset.", this.name);
return;
}
this.timestampEnqueue = performance.now();
this.status = "pending";
}
markExecuting() {
switch (this.status) {
case "unset":
/* the markEnqueue() invoke has been skipped */
break;
case "pending":
break;
default:
console.warn("ResourceRequest %s has invalid status to call markExecuting.", this.name);
return;
}
this.timestampExecuting = performance.now();
this.status = "executing";
}
markExecuted(result: ResourceRequestResult) {
switch (this.status) {
case "unset":
/* the markEnqueue() invoke has been skipped */
break;
case "pending":
/* the markExecuting() invoke has been skipped */
break;
case "executing":
break;
default:
console.warn("ResourceRequest %s has invalid status to call markExecuted.", this.name);
return;
}
this.result = result;
this.timestampExecuted = performance.now();
this.status = "executed";
}
generateReportString() {
let timeEnqueued, timeExecuted;
if(this.timestampEnqueue === 0) {
timeEnqueued = "unknown";
} else {
let endTimestamp = Math.min(this.timestampExecuting, this.timestampExecuted);
if (endTimestamp === 0) {
timeEnqueued = "pending";
} else {
timeEnqueued = endTimestamp - this.timestampEnqueue;
}
}
if(this.timestampExecuted === 0) {
timeExecuted = "unknown";
} else {
if( this.timestampExecuted === 0) {
timeExecuted = "pending";
} else {
timeExecuted = this.timestampExecuted - this.timestampEnqueue;
}
}
return `ResourceRequest{ type: ${this.type}, time enqueued: ${timeEnqueued}, time executed: ${timeExecuted}, name: ${this.name} }`;
}
}
export class LoaderPerformanceLogger {
private readonly resourceTimings: ResourceRequest[] = [];
private eventTimeBase: number;
constructor() {
this.eventTimeBase = performance.now();
}
getResourceTimings() : ResourceRequest[] {
return this.resourceTimings;
}
logResourceRequest(type: ResourceType, name: string) : ResourceRequest {
const request = new ResourceRequest(type, name);
this.resourceTimings.push(request);
return request;
}
}

View File

@ -1,80 +1,57 @@
import {config, critical_error, SourcePath} from "./loader"; import {config, critical_error, loaderPerformance, SourcePath} from "./loader";
import {executeParallelLoad, LoadCallback, LoadSyntaxError, ParallelOptions} from "./Utils"; import {executeParallelLoad, LoadCallback, LoadSyntaxError, ParallelOptions} from "./Utils";
let _script_promises: {[key: string]: Promise<void>} = {}; export function loadScript(url: SourcePath) : Promise<void> {
const givenTimeout = 120 * 1000;
function load_script_url(url: string) : Promise<void> { const resourceRequest = loaderPerformance.logResourceRequest("script", url);
if(typeof _script_promises[url] === "object") { resourceRequest.markEnqueue();
return _script_promises[url];
}
return (_script_promises[url] = new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const script_tag: HTMLScriptElement = document.createElement("script"); const scriptTag = document.createElement("script");
scriptTag.type = "application/javascript";
let error = false; scriptTag.async = true;
const error_handler = (event: ErrorEvent) => { scriptTag.defer = true;
if(event.filename == script_tag.src && event.message.indexOf("Illegal constructor") == -1) { //Our tag throw an uncaught error
if(config.verbose) console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error);
window.removeEventListener('error', error_handler as any);
reject(new LoadSyntaxError(event.error));
event.preventDefault();
error = true;
}
};
window.addEventListener('error', error_handler as any);
const cleanup = () => { const cleanup = () => {
script_tag.onerror = undefined; scriptTag.onerror = undefined;
script_tag.onload = undefined; scriptTag.onload = undefined;
clearTimeout(timeout_handle); clearTimeout(timeoutHandle);
window.removeEventListener('error', error_handler as any);
}; };
const timeout_handle = setTimeout(() => {
const timeoutHandle = setTimeout(() => {
resourceRequest.markExecuted({ status: "timeout", givenTimeout: givenTimeout });
cleanup(); cleanup();
reject("timeout"); reject("timeout");
}, 120 * 1000); }, givenTimeout);
script_tag.type = "application/javascript";
script_tag.async = true;
script_tag.defer = true;
script_tag.onerror = error => {
cleanup();
script_tag.remove();
reject(error);
};
script_tag.onload = () => {
cleanup();
if(config.verbose) console.debug("Script %o loaded", url); /* TODO: Test if on syntax error the parameters contain extra info */
setTimeout(resolve, 100); scriptTag.onerror = () => {
resourceRequest.markExecuted({ status: "error-event" });
scriptTag.remove();
cleanup();
reject();
}; };
document.getElementById("scripts").appendChild(script_tag); scriptTag.onload = () => {
resourceRequest.markExecuted({ status: "success" });
cleanup();
resolve();
};
script_tag.src = config.baseUrl + url; scriptTag.onloadstart = () => {
})).then(() => { }
/* cleanup memory */
_script_promises[url] = Promise.resolve(); /* this promise does not holds the whole script tag and other memory */ scriptTag.src = config.baseUrl + url;
return _script_promises[url]; document.getElementById("scripts").appendChild(scriptTag);
}).catch(error => { resourceRequest.markExecuting();
/* cleanup memory */
_script_promises[url] = Promise.reject(error); /* this promise does not holds the whole script tag and other memory */
return _script_promises[url];
}); });
} }
export interface Options { type MultipleOptions = ParallelOptions;
cache_tag?: string;
}
export async function loadScript(path: SourcePath, options: Options) : Promise<void> {
await load_script_url(path + (options.cache_tag || ""));
}
type MultipleOptions = Options | ParallelOptions;
export async function loadScripts(paths: SourcePath[], options: MultipleOptions, callback?: LoadCallback<SourcePath>) : Promise<void> { export async function loadScripts(paths: SourcePath[], options: MultipleOptions, callback?: LoadCallback<SourcePath>) : Promise<void> {
const result = await executeParallelLoad<SourcePath>(paths, e => loadScript(e, options), e => e, options, callback); const result = await executeParallelLoad<SourcePath>(paths, e => loadScript(e), e => e, options, callback);
if(result.failed.length > 0) { if(result.failed.length > 0) {
if(config.error) { if(config.error) {
console.error("Failed to load the following scripts:"); console.error("Failed to load the following scripts:");

View File

@ -1,112 +1,58 @@
import {config, critical_error, SourcePath} from "./loader"; import {config, critical_error, loaderPerformance, SourcePath} from "./loader";
import {executeParallelLoad, LoadCallback, LoadSyntaxError, ParallelOptions} from "./Utils"; import {executeParallelLoad, LoadCallback, LoadSyntaxError, ParallelOptions} from "./Utils";
let loadedStyles: {[key: string]: Promise<void>} = {}; export function loadStyle(path: SourcePath) : Promise<void> {
const givenTimeout = 120 * 1000;
function load_style_url(url: string) : Promise<void> { const resourceRequest = loaderPerformance.logResourceRequest("script", path);
if(typeof loadedStyles[url] === "object") { resourceRequest.markEnqueue();
return loadedStyles[url];
}
return (loadedStyles[url] = new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const tag: HTMLLinkElement = document.createElement("link"); const linkTag = document.createElement("link");
let error = false; linkTag.type = "text/css";
const error_handler = (event: ErrorEvent) => { linkTag.rel = "stylesheet";
if(config.verbose) { linkTag.href = config.baseUrl + path;
console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error);
}
if(event.filename == tag.href) { //FIXME!
window.removeEventListener('error', error_handler as any);
reject(new SyntaxError(event.error));
event.preventDefault();
error = true;
}
};
window.addEventListener('error', error_handler as any);
tag.type = "text/css";
tag.rel = "stylesheet";
const cleanup = () => { const cleanup = () => {
tag.onerror = undefined; linkTag.onerror = undefined;
tag.onload = undefined; linkTag.onload = undefined;
clearTimeout(timeout_handle); clearTimeout(timeoutHandle);
window.removeEventListener('error', error_handler as any);
}; };
const timeout_handle = setTimeout(() => { const errorCleanup = () => {
linkTag.remove();
cleanup();
};
const timeoutHandle = setTimeout(() => {
resourceRequest.markExecuted({ status: "timeout", givenTimeout: givenTimeout });
cleanup(); cleanup();
reject("timeout"); reject("timeout");
}, 5000); }, givenTimeout);
tag.onerror = error => { /* TODO: Test if on syntax error the parameters contain extra info */
cleanup(); linkTag.onerror = () => {
tag.remove(); resourceRequest.markExecuted({ status: "error-event" });
if(config.error) { errorCleanup();
console.error("File load error for file %s: %o", url, error); reject();
}
reject("failed to load file " + url);
};
tag.onload = () => {
cleanup();
{
const css: CSSStyleSheet = tag.sheet as CSSStyleSheet;
const rules = css.cssRules;
const rules_remove: number[] = [];
const rules_add: string[] = [];
for(let index = 0; index < rules.length; index++) {
const rule = rules.item(index);
let rule_text = rule.cssText;
if(rule.cssText.indexOf("%%base_path%%") != -1) {
rules_remove.push(index);
rules_add.push(rule_text.replace("%%base_path%%", document.location.origin + document.location.pathname));
}
}
for(const index of rules_remove.sort((a, b) => b > a ? 1 : 0)) {
if(css.removeRule)
css.removeRule(index);
else
css.deleteRule(index);
}
for(const rule of rules_add)
css.insertRule(rule, rules_remove[0]);
}
if(config.verbose) console.debug("Style sheet %o loaded", url);
setTimeout(resolve, 100);
}; };
document.getElementById("style").appendChild(tag); linkTag.onload = () => {
tag.href = config.baseUrl + url; resourceRequest.markExecuted({ status: "success" });
})).then(() => { cleanup();
/* cleanup memory */ resolve();
loadedStyles[url] = Promise.resolve(); /* this promise does not holds the whole script tag and other memory */ };
return loadedStyles[url];
}).catch(error => { document.getElementById("style").appendChild(linkTag);
/* cleanup memory */ resourceRequest.markExecuting();
loadedStyles[url] = Promise.reject(error); /* this promise does not holds the whole script tag and other memory */
return loadedStyles[url];
}); });
} }
export interface Options { export type MultipleOptions = ParallelOptions;
cache_tag?: string;
}
export async function loadStyle(path: SourcePath, options: Options) : Promise<void> {
await load_style_url(path + (options.cache_tag || ""));
}
export type MultipleOptions = Options | ParallelOptions;
export async function loadStyles(paths: SourcePath[], options: MultipleOptions, callback?: LoadCallback<SourcePath>) : Promise<void> { export async function loadStyles(paths: SourcePath[], options: MultipleOptions, callback?: LoadCallback<SourcePath>) : Promise<void> {
const result = await executeParallelLoad<SourcePath>(paths, e => loadStyle(e, options), e => e, options, callback); const result = await executeParallelLoad<SourcePath>(paths, e => loadStyle(e), e => e, options, callback);
if(result.failed.length > 0) { if(result.failed.length > 0) {
if(config.error) { if(config.error) {
console.error("Failed to load the following style sheets:"); console.error("Failed to load the following style sheets:");

View File

@ -1,5 +1,3 @@
import {Options} from "./ScriptLoader";
export const getUrlParameter = key => { export const getUrlParameter = key => {
const match = location.search.match(new RegExp("(.*[?&]|^)" + key + "=([^&]+)($|&.*)")); const match = location.search.match(new RegExp("(.*[?&]|^)" + key + "=([^&]+)($|&.*)"));
if(!match) { if(!match) {
@ -16,7 +14,7 @@ export class LoadSyntaxError {
} }
} }
export interface ParallelOptions extends Options { export interface ParallelOptions {
maxParallelRequests?: number maxParallelRequests?: number
} }

View File

@ -1,5 +1,6 @@
import * as Animation from "../animation"; import * as Animation from "../animation";
import {getUrlParameter} from "./Utils"; import {getUrlParameter} from "./Utils";
import {LoaderPerformanceLogger} from "./Performance";
export interface ApplicationLoader { export interface ApplicationLoader {
execute(); execute();
@ -329,4 +330,6 @@ export function critical_error_handler(handler?: ErrorHandler, override?: boolea
} }
/* loaders */ /* loaders */
export type SourcePath = string; export type SourcePath = string;
export let loaderPerformance = new LoaderPerformanceLogger();

View File

@ -1,5 +1,5 @@
import * as loader from "./loader/loader"; import * as loader from "./loader/loader";
import {config} from "./loader/loader"; import {config, loaderPerformance} from "./loader/loader";
import {loadStyles} from "./loader/StyleLoader"; import {loadStyles} from "./loader/StyleLoader";
import {loadScripts} from "./loader/ScriptLoader"; import {loadScripts} from "./loader/ScriptLoader";
@ -31,14 +31,19 @@ export async function loadManifest() : Promise<ApplicationManifest> {
return manifest; return manifest;
} }
const requestResource = loaderPerformance.logResourceRequest("json", "manifest.json");
try { try {
requestResource.markExecuting();
const response = await fetch(config.baseUrl + "/manifest.json?_date=" + Date.now()); const response = await fetch(config.baseUrl + "/manifest.json?_date=" + Date.now());
if(!response.ok) { if(!response.ok) {
requestResource.markExecuted({ status: "unknown-error", message: response.status + " " + response.statusText });
throw response.status + " " + response.statusText; throw response.status + " " + response.statusText;
} }
manifest = await response.json(); manifest = await response.json();
requestResource.markExecuted({ status: "success" });
} catch(error) { } catch(error) {
requestResource.markExecuted({ status: "error-event" });
console.error("Failed to load javascript manifest: %o", error); console.error("Failed to load javascript manifest: %o", error);
loader.critical_error("Failed to load manifest.json", error); loader.critical_error("Failed to load manifest.json", error);
throw "failed to load manifest.json"; throw "failed to load manifest.json";
@ -62,9 +67,9 @@ export async function loadManifestTarget(chunkName: string, taskId: number) {
modules: manifest.chunks[chunkName].modules modules: manifest.chunks[chunkName].modules
}); });
const kMaxRequests = 4;
await loadStyles(manifest.chunks[chunkName].css_files.map(e => e.file), { await loadStyles(manifest.chunks[chunkName].css_files.map(e => e.file), {
cache_tag: undefined, maxParallelRequests: kMaxRequests
maxParallelRequests: 4
}, (entry, state) => { }, (entry, state) => {
if (state !== "loading") { if (state !== "loading") {
return; return;
@ -74,8 +79,7 @@ export async function loadManifestTarget(chunkName: string, taskId: number) {
}); });
await loadScripts(manifest.chunks[chunkName].files.map(e => e.file), { await loadScripts(manifest.chunks[chunkName].files.map(e => e.file), {
cache_tag: undefined, maxParallelRequests: kMaxRequests
maxParallelRequests: 4
}, (script, state) => { }, (script, state) => {
if(state !== "loading") { if(state !== "loading") {
return; return;