1#!/usr/bin/env node
2
3const fs = require("fs");
4const os = require("os");
5const path = require("path");
6
7function getZedDataDir() {
8 const platform = process.platform;
9
10 if (platform === "darwin") {
11 // macOS: ~/Library/Application Support/Zed
12 return path.join(os.homedir(), "Library", "Application Support", "Zed");
13 } else if (platform === "linux" || platform === "freebsd") {
14 // Linux/FreeBSD: $FLATPAK_XDG_DATA_HOME or XDG_DATA_HOME/zed
15 if (process.env.FLATPAK_XDG_DATA_HOME) {
16 return path.join(process.env.FLATPAK_XDG_DATA_HOME, "zed");
17 }
18 const xdgDataHome = process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share");
19 return path.join(xdgDataHome, "zed");
20 } else if (platform === "win32") {
21 // Windows: LocalAppData/Zed
22 const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
23 return path.join(localAppData, "Zed");
24 } else {
25 // Fallback to XDG config dir
26 const xdgConfigHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
27 return path.join(xdgConfigHome, "zed");
28 }
29}
30
31function extractUnitData(htmlContent) {
32 // Find the UNIT_DATA array in the file
33 const unitDataMatch = htmlContent.match(/const\s+UNIT_DATA\s*=\s*(\[[\s\S]*?\]);/);
34 if (!unitDataMatch) {
35 throw new Error("Could not find UNIT_DATA in the file");
36 }
37
38 try {
39 return JSON.parse(unitDataMatch[1]);
40 } catch (e) {
41 throw new Error(`Failed to parse UNIT_DATA as JSON: ${e.message}`);
42 }
43}
44
45function formatTime(seconds) {
46 if (seconds < 60) {
47 return `${seconds.toFixed(2)}s`;
48 }
49 const minutes = Math.floor(seconds / 60);
50 const remainingSeconds = seconds % 60;
51 return `${minutes}m ${remainingSeconds.toFixed(2)}s`;
52}
53
54function formatUnit(unit) {
55 let name = `${unit.name} v${unit.version}`;
56 if (unit.target && unit.target.trim()) {
57 name += ` (${unit.target.trim()})`;
58 }
59 return name;
60}
61
62function parseTimestampFromFilename(filePath) {
63 const basename = path.basename(filePath);
64 // Format: cargo-timing-20260219T161555.879263Z.html
65 const match = basename.match(/cargo-timing-(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})\.(\d+)Z\.html/);
66 if (!match) {
67 return null;
68 }
69 const [, year, month, day, hour, minute, second, microseconds] = match;
70 // Convert to ISO 8601 format
71 const milliseconds = Math.floor(parseInt(microseconds) / 1000);
72 return `${year}-${month}-${day}T${hour}:${minute}:${second}.${milliseconds.toString().padStart(3, "0")}Z`;
73}
74
75function writeBuildTimingJson(filePath, durationMs, firstCrate, target, blockedMs, command) {
76 const buildTimingsDir = path.join(getZedDataDir(), "build_timings");
77
78 // Create directory if it doesn't exist
79 if (!fs.existsSync(buildTimingsDir)) {
80 fs.mkdirSync(buildTimingsDir, { recursive: true });
81 }
82
83 // Parse timestamp from filename, or use file modification time as fallback
84 let startedAt = parseTimestampFromFilename(filePath);
85 if (!startedAt) {
86 const stats = fs.statSync(filePath);
87 startedAt = stats.mtime.toISOString();
88 }
89
90 const buildTiming = {
91 started_at: startedAt,
92 duration_ms: durationMs,
93 first_crate: firstCrate,
94 target: target,
95 blocked_ms: blockedMs,
96 command: command,
97 };
98
99 const jsonPath = path.join(buildTimingsDir, `build-timing-${startedAt}.json`);
100 fs.writeFileSync(jsonPath, JSON.stringify(buildTiming, null, 2) + "\n");
101 console.log(`\nWrote build timing JSON to: ${jsonPath}`);
102}
103
104function analyzeTimings(filePath, command) {
105 // Read the file
106 const htmlContent = fs.readFileSync(filePath, "utf-8");
107
108 // Extract UNIT_DATA
109 const unitData = extractUnitData(htmlContent);
110
111 if (unitData.length === 0) {
112 console.log("No units found in UNIT_DATA");
113 return;
114 }
115
116 // Find the unit that finishes last (start + duration)
117 let lastFinishingUnit = unitData[0];
118 let maxEndTime = unitData[0].start + unitData[0].duration;
119
120 for (const unit of unitData) {
121 const endTime = unit.start + unit.duration;
122 if (endTime > maxEndTime) {
123 maxEndTime = endTime;
124 lastFinishingUnit = unit;
125 }
126 }
127
128 // Find the first crate that had to be rebuilt (earliest start time)
129 // Sort by start time to find the first one
130 const sortedByStart = [...unitData].sort((a, b) => a.start - b.start);
131 const firstRebuilt = sortedByStart[0];
132
133 // The minimum start time indicates time spent blocked (e.g. waiting for cargo lock)
134 const blockedTime = firstRebuilt.start;
135
136 // Find the last item being built (the one that was still building when the build finished)
137 // This is the unit with the latest end time (which we already found)
138 const lastBuilding = lastFinishingUnit;
139
140 console.log("=== Cargo Timing Analysis ===\n");
141 console.log(`File: ${path.basename(filePath)}\n`);
142 console.log(`Total build time: ${formatTime(maxEndTime)}`);
143 console.log(`Time blocked: ${formatTime(blockedTime)}`);
144 console.log(`Total crates compiled: ${unitData.length}\n`);
145 console.log(`First crate rebuilt: ${formatUnit(firstRebuilt)}`);
146 console.log(` Started at: ${formatTime(firstRebuilt.start)}`);
147 console.log(` Duration: ${formatTime(firstRebuilt.duration)}\n`);
148 console.log(`Last item being built: ${formatUnit(lastBuilding)}`);
149 console.log(` Started at: ${formatTime(lastBuilding.start)}`);
150 console.log(` Duration: ${formatTime(lastBuilding.duration)}`);
151 console.log(` Finished at: ${formatTime(lastBuilding.start + lastBuilding.duration)}`);
152
153 // Write JSON file for BuildTiming struct
154 const durationMs = maxEndTime * 1000;
155 const blockedMs = blockedTime * 1000;
156 const firstCrateName = firstRebuilt.name;
157 const targetName = lastBuilding.name;
158 writeBuildTimingJson(filePath, durationMs, firstCrateName, targetName, blockedMs, command);
159}
160
161// Main execution
162const args = process.argv.slice(2);
163
164if (args.length === 0) {
165 console.error("Usage: cargo-timing-info.js <path-to-cargo-timing.html> [command]");
166 console.error("");
167 console.error("Example:");
168 console.error(" cargo-timing-info.js target/cargo-timings/cargo-timing-20260219T161555.879263Z.html");
169 process.exit(1);
170}
171
172const filePath = args[0];
173const command = args[1] || null;
174
175if (!fs.existsSync(filePath)) {
176 console.error(`Error: File not found: ${filePath}`);
177 process.exit(1);
178}
179
180try {
181 analyzeTimings(filePath, command);
182} catch (e) {
183 console.error(`Error: ${e.message}`);
184 process.exit(1);
185}