Skip to content

Commit 2d19d30

Browse files
committed
Make task time accounting finer-grained.
1 parent e6425ae commit 2d19d30

4 files changed

Lines changed: 117 additions & 40 deletions

File tree

builder/cpu-worker.mjs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// appropriate handler and posts back { result } or { error, stack }.
33
// See PLAN-scheduler.md §Worker for the full handler set.
44

5-
import { parentPort } from "node:worker_threads";
5+
import { parentPort, workerData } from "node:worker_threads";
66
import { compileLightScss, compileDarkScss } from "./scss.mjs";
77
import { regenerateMermaid } from "./mermaid.mjs";
88
import { captureBuildInfo } from "./build-info.mjs";
@@ -14,6 +14,10 @@ import { deriveOfflinePage, deriveOfflinePageCached,
1414
sliceNavBlock, normalizeBaseurl,
1515
posixDirname } from "./offline-rewrite.mjs";
1616

17+
// Report cold-boot time: from worker spawn (passed via workerData) to
18+
// the point where all static imports have resolved and top-level code runs.
19+
if (workerData?.spawnTime) parentPort.postMessage({ coldBoot: { start: workerData.spawnTime, end: Date.now() } });
20+
1721
// Shiki (highlight.mjs) is loaded lazily — its transitive import of the
1822
// shiki package is the heaviest single module in the worker graph. A
1923
// warmup signal from the pool triggers loading on idle workers so it
@@ -24,8 +28,9 @@ import { deriveOfflinePage, deriveOfflinePageCached,
2428
let _highlighterP = null;
2529
function ensureHighlighterInit() {
2630
if (!_highlighterP) {
31+
const warmStart = Date.now();
2732
_highlighterP = import("./highlight.mjs").then(m => m.initHighlighter());
28-
_highlighterP.then(() => parentPort.postMessage({ warmedUp: true }));
33+
_highlighterP.then(() => parentPort.postMessage({ warmedUp: true, warmBoot: { start: warmStart, end: Date.now() } }));
2934
}
3035
return _highlighterP;
3136
}

builder/gantt.mjs

Lines changed: 92 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const COLORS = {
66
Spine: { light: "#6eb5d9", dark: "#3c7db0" },
77
Render: { light: "#b09cd8", dark: "#8066a8" },
88
Write: { light: "#e8a756", dark: "#c08030" },
9+
Boot: { light: "#e57373", dark: "#c62828" },
910
Other: { light: "#bbb", dark: "#666" },
1011
};
1112

@@ -24,8 +25,32 @@ export function renderGantt(grouped) {
2425
const maxT = Math.max(...all.map(t => t.end));
2526
if (maxT <= 0) return "";
2627

27-
let rows = 0;
28-
for (const tasks of grouped.values()) rows += tasks.length;
28+
// Any task with a lane ran on a worker — pull it into the Workers
29+
// section, tagged with its original section for bar colour. Leftover
30+
// Render tasks (dispatch, prepDest) fold into Spine.
31+
const seeds = [], spine = [], write = [];
32+
const laneTasks = [];
33+
for (const [section, tasks] of grouped) {
34+
for (const t of tasks) {
35+
if (t.lane != null) { t._color = section; laneTasks.push(t); }
36+
else if (section === "Seeds") seeds.push(t);
37+
else if (section === "Spine" || section === "Render") spine.push(t);
38+
else if (section === "Write") write.push(t);
39+
}
40+
}
41+
const mainSections = [["Seeds", seeds], ["Spine", spine], ["Write", write]];
42+
43+
const lanes = new Map();
44+
for (const t of laneTasks) {
45+
if (!lanes.has(t.lane)) lanes.set(t.lane, []);
46+
lanes.get(t.lane).push(t);
47+
}
48+
for (const tasks of lanes.values())
49+
tasks.sort((a, b) => a.workerStart - b.workerStart);
50+
const sortedLanes = [...lanes.entries()].sort((a, b) => a[0] - b[0]);
51+
52+
let rows = sortedLanes.length;
53+
for (const [, tasks] of mainSections) rows += tasks.length;
2954
const h = AXIS_H + rows * ROW_H + 5;
3055
const xOf = t => SECTION_W + (t / maxT) * CHART_W;
3156

@@ -57,35 +82,79 @@ export function renderGantt(grouped) {
5782
}
5883

5984
let y = AXIS_H;
60-
for (const [section, tasks] of grouped) {
85+
86+
// Seeds, Spine (with dispatch / prepDest folded in)
87+
for (const [section, tasks] of mainSections.slice(0, 2)) {
6188
if (tasks.length === 0) continue;
89+
y = renderMainSection(o, section, tasks, y, xOf);
90+
}
91+
92+
// Workers — one row per lane, individual task bars
93+
if (sortedLanes.length > 0) {
6294
o.push(`<line x1="0" y1="${y}" x2="${SVG_W}" y2="${y}" class="gg" stroke-width=".5"/>`);
63-
const cls = `gb-${section.toLowerCase()}`;
64-
for (let i = 0; i < tasks.length; i++) {
65-
const t = tasks[i];
66-
const bx = rd(xOf(t.start));
67-
const bw = rd(Math.max(xOf(t.end) - xOf(t.start), 1));
68-
const by = rd(y + (ROW_H - BAR_H) / 2);
95+
for (let li = 0; li < sortedLanes.length; li++) {
96+
const [, tasks] = sortedLanes[li];
6997
const ty = rd(y + ROW_H / 2 + 3.5);
70-
if (i === 0) o.push(`<text x="4" y="${ty}" class="gs" font-size="12">${esc(section)}</text>`);
71-
o.push(`<rect x="${bx}" y="${by}" width="${bw}" height="${BAR_H}" class="${cls}" rx="2"/>`);
72-
const lbl = taskLabel(t);
73-
const textW = lbl.length * CHAR_W;
74-
if (textW + BAR_PAD * 2 <= bw) {
75-
o.push(`<text x="${rd(bx + BAR_PAD)}" y="${ty}" class="gl" font-size="11">${esc(lbl)}</text>`);
76-
} else if (bx + bw + 4 + textW <= SVG_W) {
77-
o.push(`<text x="${rd(bx + bw + 4)}" y="${ty}" class="gl" font-size="11">${esc(lbl)}</text>`);
78-
} else {
79-
o.push(`<text x="${rd(bx - 4)}" y="${ty}" text-anchor="end" class="gl" font-size="11">${esc(lbl)}</text>`);
98+
const by = rd(y + (ROW_H - BAR_H) / 2);
99+
if (li === 0) o.push(`<text x="4" y="${ty}" class="gs" font-size="12">Workers</text>`);
100+
const bootBars = [];
101+
for (const t of tasks) {
102+
if (t._color === "Boot") { bootBars.push(t); continue; }
103+
const bx = rd(xOf(t.workerStart));
104+
const bw = rd(Math.max(xOf(t.workerEnd) - xOf(t.workerStart), 1));
105+
o.push(`<rect x="${bx}" y="${by}" width="${bw}" height="${BAR_H}" class="gb-${(t._color || "render").toLowerCase()}" rx="2"/>`);
106+
const lbl = workerLabel(t);
107+
if (lbl.length * CHAR_W + BAR_PAD * 2 <= bw)
108+
o.push(`<text x="${rd(bx + BAR_PAD)}" y="${ty}" class="gl" font-size="11">${esc(lbl)}</text>`);
109+
}
110+
const bootH = Math.round(BAR_H * 0.75);
111+
for (const t of bootBars) {
112+
const bx = rd(xOf(t.workerStart));
113+
const bw = rd(Math.max(xOf(t.workerEnd) - xOf(t.workerStart), 1));
114+
o.push(`<rect x="${bx}" y="${by}" width="${bw}" height="${bootH}" class="gb-boot" rx="2"/>`);
115+
const lbl = workerLabel(t);
116+
if (lbl.length * CHAR_W + BAR_PAD * 2 <= bw)
117+
o.push(`<text x="${rd(bx + BAR_PAD)}" y="${ty}" class="gl" font-size="11">${esc(lbl)}</text>`);
80118
}
81119
y += ROW_H;
82120
}
83121
}
84122

123+
// Write
124+
for (const [section, tasks] of mainSections.slice(2)) {
125+
if (tasks.length === 0) continue;
126+
y = renderMainSection(o, section, tasks, y, xOf);
127+
}
128+
85129
o.push(`</g></svg>`);
86130
return o.join("\n");
87131
}
88132

133+
function renderMainSection(o, section, tasks, y, xOf) {
134+
o.push(`<line x1="0" y1="${y}" x2="${SVG_W}" y2="${y}" class="gg" stroke-width=".5"/>`);
135+
const cls = `gb-${section.toLowerCase()}`;
136+
for (let i = 0; i < tasks.length; i++) {
137+
const t = tasks[i];
138+
const bx = rd(xOf(t.start));
139+
const bw = rd(Math.max(xOf(t.end) - xOf(t.start), 1));
140+
const by = rd(y + (ROW_H - BAR_H) / 2);
141+
const ty = rd(y + ROW_H / 2 + 3.5);
142+
if (i === 0) o.push(`<text x="4" y="${ty}" class="gs" font-size="12">${esc(section)}</text>`);
143+
o.push(`<rect x="${bx}" y="${by}" width="${bw}" height="${BAR_H}" class="${cls}" rx="2"/>`);
144+
const lbl = taskLabel(t);
145+
const textW = lbl.length * CHAR_W;
146+
if (textW + BAR_PAD * 2 <= bw) {
147+
o.push(`<text x="${rd(bx + BAR_PAD)}" y="${ty}" class="gl" font-size="11">${esc(lbl)}</text>`);
148+
} else if (bx + bw + 4 + textW <= SVG_W) {
149+
o.push(`<text x="${rd(bx + bw + 4)}" y="${ty}" class="gl" font-size="11">${esc(lbl)}</text>`);
150+
} else {
151+
o.push(`<text x="${rd(bx - 4)}" y="${ty}" text-anchor="end" class="gl" font-size="11">${esc(lbl)}</text>`);
152+
}
153+
y += ROW_H;
154+
}
155+
return y;
156+
}
157+
89158
function niceInterval(max) {
90159
for (const c of [100, 200, 250, 500, 1000, 2000, 2500, 5000])
91160
if (max / c <= 10) return c;
@@ -109,5 +178,9 @@ function taskLabel(t) {
109178
return s;
110179
}
111180

181+
function workerLabel(t) {
182+
return t.id.replace(/:.*/, "").replace(/ w\d+$/, "");
183+
}
184+
112185
function rd(n) { return Math.round(n * 10) / 10; }
113186
function esc(s) { return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); }

builder/tbdocs.mjs

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -604,23 +604,6 @@ async function writeGantt(timings, outPath) {
604604
grouped.get(section).push(entry);
605605
}
606606

607-
// Condense tasks flagged consolidate into one bar per worker lane.
608-
for (const [section, tasks] of grouped) {
609-
const kept = [];
610-
const byLane = new Map();
611-
for (const entry of tasks) {
612-
if (!entry.consolidate || entry.lane == null) { kept.push(entry); continue; }
613-
const prev = byLane.get(entry.lane);
614-
if (!prev) byLane.set(entry.lane, { start: entry.start, end: entry.end });
615-
else { prev.start = Math.min(prev.start, entry.start); prev.end = Math.max(prev.end, entry.end); }
616-
}
617-
if (byLane.size === 0) continue;
618-
const lanes = [...byLane.entries()]
619-
.sort((a, b) => a[1].start - b[1].start)
620-
.map(([lane, { start, end }]) => ({ id: `${section.toLowerCase()} w${lane}`, start, end }));
621-
grouped.set(section, [...kept, ...lanes]);
622-
}
623-
624607
const lines = [
625608
"gantt",
626609
" title Build task timeline",
@@ -728,6 +711,15 @@ export async function runBuild(opts) {
728711
}
729712
console.log(scheduler.summary());
730713

714+
for (const bt of pool.bootTimings) {
715+
scheduler.timings.set(`${bt.type}:w${bt.lane}`, {
716+
start: bt.start, end: bt.end,
717+
workerStart: bt.start, workerEnd: bt.end,
718+
lane: bt.lane,
719+
ganttSection: "Boot",
720+
});
721+
}
722+
731723
const ganttPath = path.resolve(process.cwd(), opts.ganttFile ?? "build-gantt.mmd");
732724
const grouped = await writeGantt(scheduler.timings, ganttPath);
733725

builder/worker-pool.mjs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,20 @@ export class WorkerPool {
1717
this._warm = new Set(); // all workers that have signalled warmedUp
1818
this._busy = new Map(); // Worker → { resolve, reject }
1919
this._queue = []; // pending { message, transferList, resolve, reject }
20+
this.bootTimings = []; // { lane, type, start, end }[]
2021
this._workers = Array.from({ length: size }, (_, i) => this._spawn(i));
2122
}
2223

2324
_spawn(lane) {
24-
const w = new Worker(this._workerUrl);
25+
const spawnTime = Date.now();
26+
const w = new Worker(this._workerUrl, { workerData: { lane, spawnTime } });
2527
w.on("message", (msg) => {
26-
if (msg.warmedUp) { this._onWarmedUp(w); return; }
28+
if (msg.coldBoot) { this.bootTimings.push({ lane, type: "cold", ...msg.coldBoot }); return; }
29+
if (msg.warmedUp) {
30+
if (msg.warmBoot) this.bootTimings.push({ lane, type: "warm", ...msg.warmBoot });
31+
this._onWarmedUp(w);
32+
return;
33+
}
2734
const entry = this._busy.get(w);
2835
if (!entry) return;
2936
this._busy.delete(w);

0 commit comments

Comments
 (0)