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";
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> {
if(typeof _script_promises[url] === "object") {
return _script_promises[url];
}
const resourceRequest = loaderPerformance.logResourceRequest("script", url);
resourceRequest.markEnqueue();
return (_script_promises[url] = new Promise((resolve, reject) => {
const script_tag: HTMLScriptElement = document.createElement("script");
let error = false;
const error_handler = (event: ErrorEvent) => {
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);
return new Promise((resolve, reject) => {
const scriptTag = document.createElement("script");
scriptTag.type = "application/javascript";
scriptTag.async = true;
scriptTag.defer = true;
const cleanup = () => {
script_tag.onerror = undefined;
script_tag.onload = undefined;
scriptTag.onerror = undefined;
scriptTag.onload = undefined;
clearTimeout(timeout_handle);
window.removeEventListener('error', error_handler as any);
clearTimeout(timeoutHandle);
};
const timeout_handle = setTimeout(() => {
const timeoutHandle = setTimeout(() => {
resourceRequest.markExecuted({ status: "timeout", givenTimeout: givenTimeout });
cleanup();
reject("timeout");
}, 120 * 1000);
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();
}, givenTimeout);
if(config.verbose) console.debug("Script %o loaded", url);
setTimeout(resolve, 100);
/* TODO: Test if on syntax error the parameters contain extra info */
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;
})).then(() => {
/* cleanup memory */
_script_promises[url] = Promise.resolve(); /* this promise does not holds the whole script tag and other memory */
return _script_promises[url];
}).catch(error => {
/* cleanup memory */
_script_promises[url] = Promise.reject(error); /* this promise does not holds the whole script tag and other memory */
return _script_promises[url];
scriptTag.onloadstart = () => {
}
scriptTag.src = config.baseUrl + url;
document.getElementById("scripts").appendChild(scriptTag);
resourceRequest.markExecuting();
});
}
export interface Options {
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;
type MultipleOptions = ParallelOptions;
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(config.error) {
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";
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> {
if(typeof loadedStyles[url] === "object") {
return loadedStyles[url];
}
const resourceRequest = loaderPerformance.logResourceRequest("script", path);
resourceRequest.markEnqueue();
return (loadedStyles[url] = new Promise((resolve, reject) => {
const tag: HTMLLinkElement = document.createElement("link");
return new Promise((resolve, reject) => {
const linkTag = document.createElement("link");
let error = false;
const error_handler = (event: ErrorEvent) => {
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);
}
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";
linkTag.type = "text/css";
linkTag.rel = "stylesheet";
linkTag.href = config.baseUrl + path;
const cleanup = () => {
tag.onerror = undefined;
tag.onload = undefined;
linkTag.onerror = undefined;
linkTag.onload = undefined;
clearTimeout(timeout_handle);
window.removeEventListener('error', error_handler as any);
clearTimeout(timeoutHandle);
};
const timeout_handle = setTimeout(() => {
const errorCleanup = () => {
linkTag.remove();
cleanup();
};
const timeoutHandle = setTimeout(() => {
resourceRequest.markExecuted({ status: "timeout", givenTimeout: givenTimeout });
cleanup();
reject("timeout");
}, 5000);
}, givenTimeout);
tag.onerror = error => {
cleanup();
tag.remove();
if(config.error) {
console.error("File load error for file %s: %o", url, error);
}
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);
/* TODO: Test if on syntax error the parameters contain extra info */
linkTag.onerror = () => {
resourceRequest.markExecuted({ status: "error-event" });
errorCleanup();
reject();
};
document.getElementById("style").appendChild(tag);
tag.href = config.baseUrl + url;
})).then(() => {
/* cleanup memory */
loadedStyles[url] = Promise.resolve(); /* this promise does not holds the whole script tag and other memory */
return loadedStyles[url];
}).catch(error => {
/* cleanup memory */
loadedStyles[url] = Promise.reject(error); /* this promise does not holds the whole script tag and other memory */
return loadedStyles[url];
linkTag.onload = () => {
resourceRequest.markExecuted({ status: "success" });
cleanup();
resolve();
};
document.getElementById("style").appendChild(linkTag);
resourceRequest.markExecuting();
});
}
export interface Options {
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 type MultipleOptions = ParallelOptions;
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(config.error) {
console.error("Failed to load the following style sheets:");

View File

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

View File

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