Files

102 lines
3.2 KiB
JavaScript

// A Linux-boot-style log of the client's map setup, shown while a game loads.
// The client regenerates the whole world from the seed and uploads it to the
// GPU, which can take several seconds; this shows each step and how long it
// took. It records nothing unless a log has been started, so importing modules
// (and the test suite) pay no cost.
const now = () =>
(typeof performance !== "undefined" ? performance.now() : Date.now());
export class LoadingLog {
constructor() {
this.$root = null;
this.$log = null;
this.active = false;
this.startedAt = 0;
this.lastAt = 0;
}
bind($root) {
this.$root = $root;
this.$log = $root && $root.length ? $root.find(".loading-log") : null;
return this;
}
// Clears the log and shows the overlay. Safe to call when the overlay is not
// in the page: everything then no-ops.
begin() {
if (!this.$root || !this.$root.length) return;
this.active = true;
this.startedAt = now();
this.lastAt = this.startedAt;
this.$log.empty();
this.$root.removeClass("hidden").removeClass("done");
this.step("Booting the game client");
}
// Records a step, timed from the previous step/measure.
step(label) {
if (!this.active) return;
const time = now();
this._line(time - this.lastAt, label);
this.lastAt = time;
}
// Runs `fn`, logs how long it took under `label` and returns its result.
measure(label, fn) {
if (!this.active) return fn();
const start = now();
try {
return fn();
} finally {
const time = now();
this._line(time - start, label);
this.lastAt = time;
}
}
// `onHidden` runs once the overlay has actually left the screen, so callers
// can show something (the capability report) without it landing behind the
// fading loading panel. It fires at once when the log was never bound or was
// already finished.
finish(onHidden) {
if (!this.active) {
if (onHidden) onHidden();
return;
}
this.step("Ready");
this._line(now() - this.startedAt, "Total map setup", true);
this.active = false;
const notify = () => { if (onHidden) onHidden(); };
if (this.$root) {
const $root = this.$root;
$root.addClass("done");
// Fade out after a beat so the last lines can be read. Hiding is deferred
// through requestAnimationFrame, which the test harness stubs out, so a
// finished log never touches the DOM after the test window is closed.
const hide = () => { $root.addClass("hidden"); notify(); };
if (typeof requestAnimationFrame === "function") {
setTimeout(() => requestAnimationFrame(hide), 2500);
} else {
hide();
}
} else {
notify();
}
}
_line(milliseconds, label, total = false) {
if (!this.$log || !this.$log.length) return;
const value = milliseconds < 1000
? `${milliseconds.toFixed(1)} ms`
: `${(milliseconds / 1000).toFixed(2)} s`;
const line = document.createElement("div");
line.className = total ? "loading-line loading-total" : "loading-line";
line.textContent = `[ ${value.padStart(7, " ")} ] ${label}`;
this.$log[0].appendChild(line);
this.$log[0].scrollTop = this.$log[0].scrollHeight;
}
}
export const bootLog = new LoadingLog();