Last active
March 27, 2026 19:30
-
-
Save hSATAC/ed6fd6f245ae443cbb8909bb3d84bada to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| const fs = require("fs"); | |
| const COMPILE_DETAIL_TYPES = new Set([ | |
| "cCompilation", | |
| "compileAssetsCatalog", | |
| "compileStoryboard", | |
| ]); | |
| const COMPILE_SIGNATURE_PREFIXES = ["SwiftCompile ", "SwiftGeneratePch "]; | |
| function isCompileStep(step) { | |
| const detailStepType = step.detailStepType ?? ""; | |
| const signature = step.signature ?? ""; | |
| if (COMPILE_DETAIL_TYPES.has(detailStepType)) { | |
| return true; | |
| } | |
| return COMPILE_SIGNATURE_PREFIXES.some((prefix) => signature.startsWith(prefix)); | |
| } | |
| function collectTopLevelCompileSteps(step, results = []) { | |
| for (const child of step.subSteps ?? []) { | |
| if (isCompileStep(child)) { | |
| results.push(child); | |
| continue; | |
| } | |
| collectTopLevelCompileSteps(child, results); | |
| } | |
| return results; | |
| } | |
| function intervalFromStep(step) { | |
| if (typeof step.startTimestamp !== "number" || typeof step.endTimestamp !== "number") { | |
| return null; | |
| } | |
| return { | |
| start: step.startTimestamp, | |
| end: step.endTimestamp, | |
| }; | |
| } | |
| function computeActiveWallTime(intervals) { | |
| if (intervals.length === 0) { | |
| return 0; | |
| } | |
| const sortedIntervals = [...intervals].sort((a, b) => a.start - b.start); | |
| let activeWallTime = 0; | |
| let currentStart = sortedIntervals[0].start; | |
| let currentEnd = sortedIntervals[0].end; | |
| for (const interval of sortedIntervals.slice(1)) { | |
| if (interval.start <= currentEnd) { | |
| currentEnd = Math.max(currentEnd, interval.end); | |
| continue; | |
| } | |
| activeWallTime += currentEnd - currentStart; | |
| currentStart = interval.start; | |
| currentEnd = interval.end; | |
| } | |
| activeWallTime += currentEnd - currentStart; | |
| return activeWallTime; | |
| } | |
| function buildTargetMetrics(data, { minTaskSeconds = 0.5 } = {}) { | |
| const results = []; | |
| for (const step of data.subSteps ?? []) { | |
| const title = step.title ?? ""; | |
| if (!title.startsWith("Build target ")) continue; | |
| const moduleName = title.replace("Build target ", ""); | |
| const compileSteps = collectTopLevelCompileSteps(step); | |
| const taskSeconds = compileSteps.reduce((sum, compileStep) => sum + (compileStep.duration ?? 0), 0); | |
| if (taskSeconds < minTaskSeconds) continue; | |
| const intervals = compileSteps.map(intervalFromStep).filter(Boolean); | |
| const minStart = intervals.length > 0 ? Math.min(...intervals.map((interval) => interval.start)) : 0; | |
| const maxEnd = intervals.length > 0 ? Math.max(...intervals.map((interval) => interval.end)) : 0; | |
| const wallSpan = intervals.length > 0 ? maxEnd - minStart : 0; | |
| const activeWallTime = computeActiveWallTime(intervals); | |
| results.push({ | |
| moduleName, | |
| taskSeconds, | |
| wallSpan, | |
| activeWallTime, | |
| }); | |
| } | |
| results.sort((a, b) => { | |
| if (b.taskSeconds !== a.taskSeconds) return b.taskSeconds - a.taskSeconds; | |
| if (b.wallSpan !== a.wallSpan) return b.wallSpan - a.wallSpan; | |
| return a.moduleName.localeCompare(b.moduleName); | |
| }); | |
| const totalTaskSeconds = results.reduce((sum, result) => sum + result.taskSeconds, 0); | |
| return { | |
| results, | |
| totalTaskSeconds, | |
| }; | |
| } | |
| function formatSeconds(value) { | |
| return `${value.toFixed(1)}s`; | |
| } | |
| function formatReport(data) { | |
| const { results, totalTaskSeconds } = buildTargetMetrics(data); | |
| const nameWidth = Math.max(6, ...results.map((result) => result.moduleName.length)) + 2; | |
| const taskWidth = 10; | |
| const pctWidth = 8; | |
| const wallWidth = 10; | |
| const barMaxWidth = 20; | |
| const maxTaskSeconds = results[0]?.taskSeconds ?? 1; | |
| const header = | |
| "┌─" + | |
| "─".repeat(nameWidth) + | |
| "─┬────────────┬──────────┬────────────┬─" + | |
| "─".repeat(barMaxWidth) + | |
| "─┐"; | |
| const divider = | |
| "├─" + | |
| "─".repeat(nameWidth) + | |
| "─┼────────────┼──────────┼────────────┼─" + | |
| "─".repeat(barMaxWidth) + | |
| "─┤"; | |
| const footer = | |
| "└─" + | |
| "─".repeat(nameWidth) + | |
| "─┴────────────┴──────────┴────────────┴─" + | |
| "─".repeat(barMaxWidth) + | |
| "─┘"; | |
| const lines = []; | |
| lines.push(""); | |
| lines.push("Xcode Build — Compile Metrics by Module"); | |
| lines.push(`Schema: ${data.schema ?? "unknown"}`); | |
| lines.push("% Task = share of total Compile Task Seconds"); | |
| lines.push("Wall = first compile start to last compile end"); | |
| lines.push(""); | |
| lines.push(header); | |
| lines.push( | |
| "│ " + | |
| "Module".padEnd(nameWidth) + | |
| " │ " + | |
| "Task".padStart(taskWidth) + | |
| " │ " + | |
| "% Task".padStart(pctWidth) + | |
| " │ " + | |
| "Wall".padStart(wallWidth) + | |
| " │ " + | |
| " ".repeat(barMaxWidth) + | |
| " │" | |
| ); | |
| lines.push(divider); | |
| for (const result of results) { | |
| const pct = totalTaskSeconds === 0 ? 0 : (result.taskSeconds / totalTaskSeconds) * 100; | |
| const barLength = Math.round((result.taskSeconds / maxTaskSeconds) * barMaxWidth); | |
| const bar = "█".repeat(barLength) + "░".repeat(barMaxWidth - barLength); | |
| lines.push( | |
| "│ " + | |
| result.moduleName.padEnd(nameWidth) + | |
| " │ " + | |
| formatSeconds(result.taskSeconds).padStart(taskWidth) + | |
| " │ " + | |
| `${pct.toFixed(1)}%`.padStart(pctWidth) + | |
| " │ " + | |
| formatSeconds(result.wallSpan).padStart(wallWidth) + | |
| " │ " + | |
| bar + | |
| " │" | |
| ); | |
| } | |
| lines.push(divider); | |
| lines.push( | |
| "│ " + | |
| "TOTAL".padEnd(nameWidth) + | |
| " │ " + | |
| formatSeconds(totalTaskSeconds).padStart(taskWidth) + | |
| " │ " + | |
| "100.0%".padStart(pctWidth) + | |
| " │ " + | |
| "n/a".padStart(wallWidth) + | |
| " │ " + | |
| " ".repeat(barMaxWidth) + | |
| " │" | |
| ); | |
| lines.push(footer); | |
| lines.push(""); | |
| return lines.join("\n"); | |
| } | |
| function main() { | |
| const filePath = process.argv[2]; | |
| if (!filePath) { | |
| console.error("Usage: node parse-xcactivitylog.js <path-to-xcactivitylog.json>"); | |
| process.exit(1); | |
| } | |
| const data = JSON.parse(fs.readFileSync(filePath, "utf-8")); | |
| console.log(formatReport(data)); | |
| } | |
| if (require.main === module) { | |
| main(); | |
| } | |
| module.exports = { | |
| buildTargetMetrics, | |
| collectTopLevelCompileSteps, | |
| formatReport, | |
| isCompileStep, | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment