Speed up cold boot: build-time gzip_static, HF Hub CDN asset routing, and parallel prefetch
#13
by Am1nadav - opened
- Dockerfile +8 -1
- app/src/game/duck.js +29 -17
- app/src/game/game.js +38 -12
Dockerfile
CHANGED
|
@@ -8,8 +8,15 @@ WORKDIR /build
|
|
| 8 |
COPY app/package.json app/package-lock.json ./
|
| 9 |
RUN npm ci
|
| 10 |
COPY app/ ./
|
| 11 |
-
RUN npm run build
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
FROM nginxinc/nginx-unprivileged:alpine
|
| 14 |
COPY --from=build /build/dist /usr/share/nginx/html
|
|
|
|
| 15 |
EXPOSE 8080
|
|
|
|
| 8 |
COPY app/package.json app/package-lock.json ./
|
| 9 |
RUN npm ci
|
| 10 |
COPY app/ ./
|
| 11 |
+
RUN npm run build && \
|
| 12 |
+
find /build/dist -type f \( \
|
| 13 |
+
-name '*.wasm' -o -name '*.js' -o -name '*.css' -o \
|
| 14 |
+
-name '*.json' -o -name '*.xml' -o -name '*.glb' -o \
|
| 15 |
+
-name '*.stl' -o -name '*.onnx' -o -name '*.svg' \
|
| 16 |
+
\) -exec gzip -9 -k {} + && \
|
| 17 |
+
printf 'gzip_static on;\ngzip on;\ngzip_vary on;\ngzip_proxied any;\ngzip_types application/wasm application/javascript text/css application/json application/xml text/xml model/gltf-binary model/stl application/octet-stream image/svg+xml;\n' > /build/gzip.conf
|
| 18 |
|
| 19 |
FROM nginxinc/nginx-unprivileged:alpine
|
| 20 |
COPY --from=build /build/dist /usr/share/nginx/html
|
| 21 |
+
COPY --from=build /build/gzip.conf /etc/nginx/conf.d/gzip.conf
|
| 22 |
EXPOSE 8080
|
app/src/game/duck.js
CHANGED
|
@@ -29,25 +29,37 @@ import { signed } from "./signed.js";
|
|
| 29 |
// `?all=1` for debugging.
|
| 30 |
const HIDDEN_MESHES = new Set([]);
|
| 31 |
|
|
|
|
|
|
|
| 32 |
export async function loadKinematics(url) {
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
}
|
| 50 |
-
return
|
| 51 |
}
|
| 52 |
|
| 53 |
// One GLB, named meshes matching kinematics geom.mesh (e.g. "left_shell.stl").
|
|
|
|
| 29 |
// `?all=1` for debugging.
|
| 30 |
const HIDDEN_MESHES = new Set([]);
|
| 31 |
|
| 32 |
+
const kinematicsPromises = new Map();
|
| 33 |
+
|
| 34 |
export async function loadKinematics(url) {
|
| 35 |
+
const lite = new URLSearchParams(location.search).get("lite") === "1";
|
| 36 |
+
const key = `${url}|${lite ? "1" : "0"}`;
|
| 37 |
+
if (!kinematicsPromises.has(key)) {
|
| 38 |
+
kinematicsPromises.set(
|
| 39 |
+
key,
|
| 40 |
+
(async () => {
|
| 41 |
+
// Same cache-buster as the GLB: force-cache would otherwise keep
|
| 42 |
+
// serving a stale kinematics.json after the mesh list changes.
|
| 43 |
+
const r = await fetch(signed(`${url}?v=${MESH_VERSION}`), { cache: "force-cache" });
|
| 44 |
+
if (!r.ok) throw new Error(`kinematics fetch ${r.status}`);
|
| 45 |
+
const k = await r.json();
|
| 46 |
+
// The exported mesh_dir is root-absolute ("/robot/mjlab/meshes"), which
|
| 47 |
+
// breaks when the app is served under a prefix (/play/). Rebase it like
|
| 48 |
+
// every other asset URL (MODEL_DIR is "./robot/mjlab").
|
| 49 |
+
if (typeof k.mesh_dir === "string" && k.mesh_dir.startsWith("/")) {
|
| 50 |
+
k.mesh_dir = `.${k.mesh_dir}`;
|
| 51 |
+
}
|
| 52 |
+
// Full-resolution meshes by default (the reduction will be redone
|
| 53 |
+
// interactively in Blender later). `?lite=1` opts into a decimated
|
| 54 |
+
// meshes-lite/ sibling if present.
|
| 55 |
+
if (lite) {
|
| 56 |
+
k.mesh_dir = k.mesh_dir.replace(/\/meshes$/, "/meshes-lite");
|
| 57 |
+
}
|
| 58 |
+
return k;
|
| 59 |
+
})(),
|
| 60 |
+
);
|
| 61 |
}
|
| 62 |
+
return kinematicsPromises.get(key);
|
| 63 |
}
|
| 64 |
|
| 65 |
// One GLB, named meshes matching kinematics geom.mesh (e.g. "left_shell.stl").
|
app/src/game/game.js
CHANGED
|
@@ -144,6 +144,30 @@ async function boot({ scene, camera, renderer }) {
|
|
| 144 |
);
|
| 145 |
};
|
| 146 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
// ── Runtimes (vendored, lazy chunks) ─────────────────────────────────
|
| 148 |
const [{ default: loadMujocoFactory }, ort] = await traced(
|
| 149 |
"RUNTIME MODULES",
|
|
@@ -329,22 +353,24 @@ async function boot({ scene, camera, renderer }) {
|
|
| 329 |
const donePolicies = bootLine("LOADING POLICIES");
|
| 330 |
const sessionOpts = { executionProviders: ["wasm"] };
|
| 331 |
let policiesLoaded = 0;
|
| 332 |
-
const bootPolicy = (
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
|
|
|
|
|
|
| 337 |
try {
|
| 338 |
[sessions.walk, sessions.sitstand, sessions.roll, sessions.kickL, sessions.kickR,
|
| 339 |
sessions.groundpick, sessions.stand] =
|
| 340 |
await Promise.all([
|
| 341 |
-
bootPolicy(
|
| 342 |
-
bootPolicy(
|
| 343 |
-
bootPolicy(
|
| 344 |
-
bootPolicy(
|
| 345 |
-
bootPolicy(
|
| 346 |
-
bootPolicy(
|
| 347 |
-
bootPolicy(
|
| 348 |
]);
|
| 349 |
} catch (err) {
|
| 350 |
donePolicies("FAILED");
|
|
|
|
| 144 |
);
|
| 145 |
};
|
| 146 |
|
| 147 |
+
// ── Prefetch static assets in parallel with WASM runtime chunks ─────
|
| 148 |
+
// Kick off GLB + ONNX policy downloads immediately so they overlap with
|
| 149 |
+
// the @mujoco/mujoco + onnxruntime-web chunk and WASM downloads instead
|
| 150 |
+
// of waiting in a serial waterfall behind MjVFS setup. Attach no-op
|
| 151 |
+
// catch handlers so a early network error only surfaces when awaited at
|
| 152 |
+
// its traced BIOS step.
|
| 153 |
+
const glbPrefetch = loadGlbGeometries();
|
| 154 |
+
glbPrefetch.catch(() => {});
|
| 155 |
+
const fetchPolicyBytes = (url) =>
|
| 156 |
+
fetch(signed(url)).then((r) => {
|
| 157 |
+
if (!r.ok) throw new Error(`policy fetch ${r.status}: ${url}`);
|
| 158 |
+
return r.arrayBuffer();
|
| 159 |
+
});
|
| 160 |
+
const policyPrefetch = {
|
| 161 |
+
walk: fetchPolicyBytes(POLICIES.walk),
|
| 162 |
+
sitstand: fetchPolicyBytes(POLICIES.sitstand),
|
| 163 |
+
roll: fetchPolicyBytes(POLICIES.roll),
|
| 164 |
+
kickL: fetchPolicyBytes(POLICIES.kickL),
|
| 165 |
+
kickR: fetchPolicyBytes(POLICIES.kickR),
|
| 166 |
+
groundpick: fetchPolicyBytes(POLICIES.groundpick),
|
| 167 |
+
stand: fetchPolicyBytes(POLICIES.stand),
|
| 168 |
+
};
|
| 169 |
+
for (const p of Object.values(policyPrefetch)) p.catch(() => {});
|
| 170 |
+
|
| 171 |
// ── Runtimes (vendored, lazy chunks) ─────────────────────────────────
|
| 172 |
const [{ default: loadMujocoFactory }, ort] = await traced(
|
| 173 |
"RUNTIME MODULES",
|
|
|
|
| 353 |
const donePolicies = bootLine("LOADING POLICIES");
|
| 354 |
const sessionOpts = { executionProviders: ["wasm"] };
|
| 355 |
let policiesLoaded = 0;
|
| 356 |
+
const bootPolicy = (bufPromise) =>
|
| 357 |
+
bufPromise
|
| 358 |
+
.then((buf) => ort.InferenceSession.create(new Uint8Array(buf), sessionOpts))
|
| 359 |
+
.then((s) => {
|
| 360 |
+
donePolicies.progress(`${++policiesLoaded}/7`);
|
| 361 |
+
return s;
|
| 362 |
+
});
|
| 363 |
try {
|
| 364 |
[sessions.walk, sessions.sitstand, sessions.roll, sessions.kickL, sessions.kickR,
|
| 365 |
sessions.groundpick, sessions.stand] =
|
| 366 |
await Promise.all([
|
| 367 |
+
bootPolicy(policyPrefetch.walk),
|
| 368 |
+
bootPolicy(policyPrefetch.sitstand),
|
| 369 |
+
bootPolicy(policyPrefetch.roll),
|
| 370 |
+
bootPolicy(policyPrefetch.kickL),
|
| 371 |
+
bootPolicy(policyPrefetch.kickR),
|
| 372 |
+
bootPolicy(policyPrefetch.groundpick),
|
| 373 |
+
bootPolicy(policyPrefetch.stand),
|
| 374 |
]);
|
| 375 |
} catch (err) {
|
| 376 |
donePolicies("FAILED");
|