File size: 4,249 Bytes
c7592d1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
/**
 * WebLLM reports model-load progress in three independent phases, each of
 * which restarts its own fraction at 0:
 *
 *   1. "Fetching param cache[i/N]"       - one report per param shard
 *   2. "Loading model from cache[i/N]"   - one report per shard, resets to 0
 *   3. "Loading GPU shader modules[i/N]" - throttled to one report per second
 *
 * Painting those raw fractions on a single bar makes it fill, snap back to
 * zero, then fill again, which reads as "stuck, then it jumped". This module
 * folds them into one monotonic 0..1 bar, keeps a per-phase ceiling so the UI
 * can creep between WebLLM's chunky reports without ever entering the next
 * phase early, and parses the real megabyte figure out of WebLLM's own text
 * (the fraction alone carries no byte count).
 */

export type LoadPhase = "fetch" | "cache" | "shaders" | "unknown";

export interface LoadProgressView {
  phase: LoadPhase;
  /** Monotonic 0..1 completion across all three phases. Never decreases. */
  overall: number;
  /** Highest 0..1 the current phase can justify; creep never passes it. */
  ceiling: number;
  /** Real MB WebLLM reported, or null when its text carries no figure. */
  loadedMb: number | null;
}

/** Share of the whole load that each phase accounts for, as [start, end]. */
const BANDS: Record<LoadPhase, [number, number]> = {
  fetch: [0, 0.6],
  cache: [0.6, 0.85],
  shaders: [0.85, 1],
  unknown: [0, 0],
};

/**
 * How far past the last real report the bar may creep. Small on purpose: it
 * buys visible motion between reports without ever overstating the load by
 * more than this much before WebLLM's next report corrects it.
 */
export const CREEP_LOOKAHEAD = 0.02;

/** Identify the phase from WebLLM's progress text. */
export function loadPhase(text: string): LoadPhase {
  const t = text.toLowerCase();
  // "shader" first: the shader phase's text also contains "loading".
  if (t.includes("shader")) return "shaders";
  if (t.includes("loading model from cache")) return "cache";
  if (t.includes("fetch")) return "fetch";
  return "unknown";
}

/** WebLLM phrases its figure as "NNNMB fetched" or "NNNMB loaded". */
export function parseLoadedMb(text: string): number | null {
  const m = /(\d+)\s*MB\s+(?:fetched|loaded)/i.exec(text);
  return m ? Number(m[1]) : null;
}

export function initialLoadProgress(): LoadProgressView {
  return { phase: "unknown", overall: 0, ceiling: 0, loadedMb: null };
}

function clamp01(n: number): number {
  if (!Number.isFinite(n)) return 0;
  return Math.min(1, Math.max(0, n));
}

/**
 * Fold one WebLLM report into the view. Pure, and monotonic by construction so
 * a phase change (or a re-sent shard report) can never drag the bar backwards.
 */
export function reduceLoadProgress(
  prev: LoadProgressView,
  report: { text: string; progress: number }
): LoadProgressView {
  const phase = loadPhase(report.text);
  const [start, end] = BANDS[phase];
  const frac = clamp01(report.progress);
  const overall = Math.max(prev.overall, start + frac * (end - start));
  const ceiling = Math.max(prev.ceiling, phase === "unknown" ? overall : end);
  return { phase, overall, ceiling, loadedMb: parseLoadedMb(report.text) ?? prev.loadedMb };
}

/** Where the displayed bar may creep to: just ahead of the last real report. */
export function creepTarget(view: LoadProgressView): number {
  return Math.min(view.ceiling, view.overall + CREEP_LOOKAHEAD);
}

/**
 * Advance a displayed 0..1 value toward `target` at a steady rate, so the bar
 * keeps moving between WebLLM's chunky reports. Never runs backwards and never
 * passes `target`.
 */
export function creepToward(
  displayed: number,
  target: number,
  dtMs: number,
  ratePerSec = 0.01
): number {
  if (!(dtMs > 0)) return displayed;
  if (displayed >= target) return target;
  return Math.min(target, displayed + ratePerSec * (dtMs / 1000));
}

/** Human-readable byte pair for the progress readout, e.g. "85 MB / 1.4 GB". */
export function formatMbPair(loadedMb: number | null, totalMb: number): string | null {
  if (loadedMb === null || !(totalMb > 0)) return null;
  const one = (n: number) => (n >= 1024 ? `${(n / 1024).toFixed(2)} GB` : `${Math.round(n)} MB`);
  return `${one(loadedMb)} / ${one(totalMb)}`;
}