Starting to introduce a loader performance logger
parent
85acd6dd56
commit
3d04fa3f0d
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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:");
|
||||||
|
|
|
@ -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:");
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue