cargo-timing-info.js

  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}