Speed up cold boot: build-time gzip_static, HF Hub CDN asset routing, and parallel prefetch

#13
by Am1nadav - opened
Files changed (3) hide show
  1. Dockerfile +8 -1
  2. app/src/game/duck.js +29 -17
  3. 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
- // Same cache-buster as the GLB: force-cache would otherwise keep
34
- // serving a stale kinematics.json after the mesh list changes.
35
- const r = await fetch(signed(`${url}?v=${MESH_VERSION}`), { cache: "force-cache" });
36
- if (!r.ok) throw new Error(`kinematics fetch ${r.status}`);
37
- const k = await r.json();
38
- // The exported mesh_dir is root-absolute ("/robot/mjlab/meshes"), which
39
- // breaks when the app is served under a prefix (/play/). Rebase it like
40
- // every other asset URL (MODEL_DIR is "./robot/mjlab").
41
- if (typeof k.mesh_dir === "string" && k.mesh_dir.startsWith("/")) {
42
- k.mesh_dir = `.${k.mesh_dir}`;
43
- }
44
- // Full-resolution meshes by default (the reduction will be redone
45
- // interactively in Blender later). `?lite=1` opts into a decimated
46
- // meshes-lite/ sibling if present.
47
- if (new URLSearchParams(location.search).get("lite") === "1") {
48
- k.mesh_dir = k.mesh_dir.replace(/\/meshes$/, "/meshes-lite");
 
 
 
 
 
 
 
 
 
 
49
  }
50
- return k;
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 = (url) =>
333
- ort.InferenceSession.create(signed(url), sessionOpts).then((s) => {
334
- donePolicies.progress(`${++policiesLoaded}/7`);
335
- return s;
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(POLICIES.walk),
342
- bootPolicy(POLICIES.sitstand),
343
- bootPolicy(POLICIES.roll),
344
- bootPolicy(POLICIES.kickL),
345
- bootPolicy(POLICIES.kickR),
346
- bootPolicy(POLICIES.groundpick),
347
- bootPolicy(POLICIES.stand),
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");