| #!/usr/bin/env python3 |
| # Copyright (C) 2026 Apple Inc. All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions |
| # are met: |
| # 1. Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # 2. Redistributions in binary form must reproduce the above copyright |
| # notice, this list of conditions and the following disclaimer in the |
| # documentation and/or other materials provided with the distribution. |
| # |
| # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| # THE POSSIBILITY OF SUCH DAMAGE. |
| |
| """ |
| cmake-build-analysis -- whole-build timing analysis for a CMake/Ninja tree. |
| |
| Companion to analyze-ftime-trace. That tool answers "why is this translation |
| unit slow?" (headers, templates, codegen) from clang's per-TU -ftime-trace |
| output. This tool answers "why is the whole ninja invocation slow?" from |
| .ninja_log + build.ninja: which edges dominate, where parallelism collapses, |
| and -- the actionable number -- the critical path that bounds wall time |
| regardless of core count. |
| |
| The analysis assumes .ninja_log covers exactly one cold build: |
| |
| rm WebKitBuild/cmake-mac/ASan/.ninja_log |
| ninja -C WebKitBuild/cmake-mac/ASan |
| Tools/Scripts/cmake-build-analysis WebKitBuild/cmake-mac/ASan |
| open WebKitBuild/cmake-mac/ASan/build-analysis.html |
| |
| Multi-run logs are refused rather than guessed at: ninja compacts the log in |
| place (discarding completion order), so once two runs are interleaved there is |
| no reliable way to separate them, and a merged timeline silently |
| double-counts. Detection is two-pronged: duplicate outputs in the raw log |
| (catches the uncompacted case) and CPU-seconds > wall x cores (catches a |
| compacted multi-run log whose merged intervals over-subscribe the machine). |
| |
| Critical path comes from build.ninja, not .ninja_log -- the log has timings |
| but no dependency edges. CMake-generated ninja writes fully-expanded paths in |
| `build` statements (variables appear only in FLAGS/DEFINES blocks), so a |
| single pass that handles `$\\n` line continuations and `$$ $: $ ` escapes is |
| sufficient; no general ninja-variable evaluator is needed. Order-only deps are |
| included in the graph because for codegen edges (DerivedSources stamps, |
| forwarding headers) they are exactly what serializes the early build, which is |
| the thing this tool exists to surface. Edges that produced no .ninja_log entry |
| (phony, or skipped by restat) contribute zero weight but still propagate the |
| path so a phony aggregate doesn't break the chain. |
| |
| The HTML is a single self-contained file: edge data inlined as JSON, drawn on |
| a <canvas> so ~35k rectangles stay smooth where the equivalent SVG would not. |
| X is wall time, each row is one greedily-packed parallelism slot, colour is |
| category, hatched edges are on the critical path. Wheel zooms, drag pans, |
| hover names the edge. Serialization is visually obvious as the chart narrowing |
| to a few rows -- no legend-reading required. |
| """ |
| |
| import argparse |
| import heapq |
| import json |
| import os |
| import re |
| import sys |
| from collections import defaultdict |
| from dataclasses import dataclass |
| from pathlib import Path |
| |
| |
| @dataclass(eq=False) |
| class Edge: |
| start: int # ms |
| end: int # ms |
| out: str |
| cat: str = "" |
| lane: int = -1 |
| crit: bool = False |
| |
| @property |
| def dur(self): |
| return self.end - self.start |
| |
| |
| # ── .ninja_log ─────────────────────────────────────────────────────────────── |
| |
| def parse_ninja_log(path: Path): |
| raw = [] |
| with path.open() as f: |
| header = f.readline() |
| if not header.startswith("# ninja log v"): |
| sys.exit(f"error: {path} is not a ninja log") |
| for line in f: |
| p = line.rstrip("\n").split("\t") |
| if len(p) < 5: |
| continue |
| raw.append((int(p[0]), int(p[1]), p[3])) |
| |
| # An edge with N outputs produces N consecutive log lines sharing one |
| # (start,end) pair; collapse those to a single edge keyed by the timing so |
| # multi-output codegen rules aren't N-times over-counted. |
| by_key = {} |
| out_seen = defaultdict(int) |
| for s, e, o in raw: |
| out_seen[o] += 1 |
| by_key.setdefault((s, e), []).append(o) |
| |
| rebuilds = sum(1 for n in out_seen.values() if n > 1) |
| if rebuilds: |
| sys.exit( |
| f"error: {path} contains {rebuilds} output(s) built more than once.\n" |
| f" This log spans multiple ninja invocations; analysis would mix runs.\n" |
| f" For a clean measurement:\n" |
| f" rm {path}\n" |
| f" ninja -C {path.parent}\n" |
| ) |
| |
| edges = [Edge(s, e, min(outs, key=len)) for (s, e), outs in by_key.items()] |
| edges.sort(key=lambda x: (x.start, x.end)) |
| for ed in edges: |
| ed.cat = categorize(ed.out) |
| return edges |
| |
| |
| def sanity_check(edges, ncpu): |
| wall = max(e.end for e in edges) - min(e.start for e in edges) |
| cpu = sum(e.dur for e in edges) |
| if cpu > wall * ncpu * 1.2: |
| sys.exit( |
| "error: CPU-seconds ({:.0f}s) exceed wall x cores ({:.0f}s x {}).\n" |
| " The log almost certainly spans multiple compacted runs.\n" |
| " rm .ninja_log and rebuild before analysing." |
| .format(cpu / 1000, wall / 1000, ncpu) |
| ) |
| return wall, cpu |
| |
| |
| # ── categorization ─────────────────────────────────────────────────────────── |
| |
| _CAT_RULES = ( |
| (re.compile(r"\.swift\.o$|\.swiftmodule$|\.swiftinterface$|/Swift/"), "swift"), |
| (re.compile(r"\.o$|\.obj$"), "compile"), |
| (re.compile(r"\.dSYM/|\.dSYM$"), "dsym"), |
| (re.compile(r"\.a$"), "archive"), |
| (re.compile(r"\.dylib$|\.framework/Versions/.*/[^/.]+$|/bin/[^/.]+$"), "link"), |
| (re.compile(r"DerivedSources|\.stamp$|Headers/|PrivateHeaders/|\.h$|\.cpp$|\.mm$"), "codegen"), |
| ) |
| |
| |
| def categorize(out): |
| for rx, cat in _CAT_RULES: |
| if rx.search(out): |
| return cat |
| base = out.rsplit("/", 1)[-1] |
| return "link" if "." not in base else "other" |
| |
| |
| # ── build.ninja dependency graph ───────────────────────────────────────────── |
| |
| # `build` statements only -- enough to recover the edge DAG. CMake never emits |
| # `subninja` for the main graph (the lone CMakeFiles/rules.ninja `include` |
| # carries rule bodies, not edges) and never uses ninja variables in paths, so |
| # this stays a flat-file scan rather than a ninja evaluator. |
| _BUILD_RE = re.compile(r"^build\s+(.+?)\s*:\s*\S+\s*(.*)$") |
| |
| |
| def _unescape(tok): |
| return tok.replace("$$", "$").replace("$:", ":").replace("$ ", " ") |
| |
| |
| def _split(spec): |
| # Split on whitespace but keep `$ `-escaped spaces inside a token. |
| return [_unescape(t) for t in re.split(r"(?<!\$)\s+", spec) if t] |
| |
| |
| def parse_build_graph(path: Path): |
| text = path.read_text() |
| text = re.sub(r"\$\n[ \t]*", "", text) # join $-continuations |
| inputs_of = {} |
| for line in text.splitlines(): |
| m = _BUILD_RE.match(line) |
| if not m: |
| continue |
| outs_spec, ins_spec = m.groups() |
| outs = _split(re.sub(r"(?<!\$)\|.*$", "", outs_spec)) # drop implicit-out marker |
| ins = _split(ins_spec.replace("||", " ").replace("|", " ")) |
| for o in outs: |
| inputs_of[o] = ins |
| return inputs_of |
| |
| |
| def critical_path(edges, inputs_of): |
| """Longest-weighted path through the build DAG, where the weight of a node |
| is its measured duration (zero for phony / restat-skipped nodes that have a |
| `build` statement but no .ninja_log entry). Computed by memoised DFS rather |
| than an explicit topological sort: the graph is a DAG by construction (ninja |
| refuses cycles), recursion depth is bounded by the longest dependency chain |
| (~100 for WebKit, well under the interpreter limit), and memoisation makes |
| it O(V+E).""" |
| dur = {e.out: e.dur for e in edges} |
| best = {} |
| sys.setrecursionlimit(max(10000, len(inputs_of) + 100)) |
| |
| def dp(node): |
| if node in best: |
| return best[node][0] |
| d = dur.get(node, 0) |
| ins = inputs_of.get(node, ()) |
| pred, pred_cost = None, 0 |
| for i in ins: |
| if i not in inputs_of and i not in dur: |
| continue # source file |
| c = dp(i) |
| if c > pred_cost: |
| pred_cost, pred = c, i |
| best[node] = (pred_cost + d, pred) |
| return pred_cost + d |
| |
| end, end_cost = None, -1 |
| for e in edges: |
| c = dp(e.out) |
| if c > end_cost: |
| end_cost, end = c, e.out |
| |
| path = [] |
| node = end |
| while node is not None: |
| if node in dur: |
| path.append(node) |
| node = best.get(node, (0, None))[1] |
| path.reverse() |
| crit = set(path) |
| for e in edges: |
| if e.out in crit: |
| e.crit = True |
| return [(o, dur[o]) for o in path], end_cost |
| |
| |
| # ── layout & windows ───────────────────────────────────────────────────────── |
| |
| def pack_lanes(edges): |
| """Greedy interval packing with a min-heap of (free_at, lane). Choosing the |
| lane that has been free the longest -- rather than first-fit -- spreads work |
| evenly across rows so the chart's silhouette reads as instantaneous |
| parallelism: where it is N rows tall, N jobs were running.""" |
| heap = [] # (free_at, lane) |
| next_lane = 0 |
| for e in sorted(edges, key=lambda x: x.start): |
| if heap and heap[0][0] <= e.start: |
| _, lane = heapq.heappop(heap) |
| else: |
| lane = next_lane |
| next_lane += 1 |
| e.lane = lane |
| heapq.heappush(heap, (e.end, lane)) |
| return next_lane |
| |
| |
| def serialization_windows(edges, ncpu, threshold=None, min_ms=2000): |
| threshold = threshold or max(1, ncpu // 2) |
| events = [] |
| for e in edges: |
| events.append((e.start, 1, e)) |
| events.append((e.end, -1, e)) |
| events.sort(key=lambda x: (x[0], -x[1])) |
| windows = [] |
| running = 0 |
| t0 = None |
| active = set() |
| sample = None |
| for t, d, e in events: |
| prev = running |
| running += d |
| if d > 0: |
| active.add(e) |
| else: |
| active.discard(e) |
| if prev >= threshold > running and running > 0: |
| t0 = t |
| sample = max(active, key=lambda x: x.dur, default=None) |
| elif prev < threshold <= running and t0 is not None: |
| if t - t0 >= min_ms: |
| windows.append((t0, t, prev, sample)) |
| t0 = None |
| return windows |
| |
| |
| # ── reporting ──────────────────────────────────────────────────────────────── |
| |
| def fmt_ms(ms): |
| s = ms / 1000 |
| if s < 60: |
| return f"{s:6.1f}s" |
| m, s = divmod(s, 60) |
| if m < 60: |
| return f"{int(m):3d}m{int(s):02d}s" |
| h, m = divmod(m, 60) |
| return f"{int(h):2d}h{int(m):02d}m " |
| |
| |
| def short(out, n=70): |
| return out if len(out) <= n else "…" + out[-(n - 1):] |
| |
| |
| def terminal_report(edges, wall, cpu, ncpu, peak, crit_path, crit_ms, windows, top): |
| eff = cpu / (wall * ncpu) if wall else 0 |
| print(f"wall {fmt_ms(wall)} cpu {fmt_ms(cpu)} " |
| f"cores {ncpu} peak {peak} efficiency {eff:4.0%}") |
| |
| print("\n── slowest edges " + "─" * 60) |
| for e in sorted(edges, key=lambda x: -x.dur)[:top]: |
| mark = "*" if e.crit else " " |
| print(f" {mark}{fmt_ms(e.dur)} {e.cat:<8} {short(e.out)}") |
| print(" (* = on critical path)") |
| |
| print(f"\n── critical path ({fmt_ms(crit_ms)}) " + "─" * 47) |
| t = 0 |
| for o, d in crit_path: |
| if d < 50: |
| continue |
| print(f" {fmt_ms(t)} +{fmt_ms(d)} {categorize(o):<8} {short(o)}") |
| t += d |
| |
| print(f"\n── serialization windows (parallelism < {ncpu // 2}) " + "─" * 36) |
| if not windows: |
| print(" (none ≥ 2s)") |
| for t0, t1, n, e in windows: |
| what = short(e.out, 50) if e else "" |
| print(f" {fmt_ms(t0)}–{fmt_ms(t1)} {n} job(s) {what}") |
| |
| print("\n── by category " + "─" * 62) |
| by_cat = defaultdict(int) |
| for e in edges: |
| by_cat[e.cat] += e.dur |
| for cat, ms in sorted(by_cat.items(), key=lambda kv: -kv[1]): |
| print(f" {cat:<8} {fmt_ms(ms)} ({ms / cpu:4.0%})") |
| |
| |
| # ── HTML ───────────────────────────────────────────────────────────────────── |
| |
| _CAT_COLOR = { |
| "compile": "#4e79a7", "link": "#e15759", "swift": "#f28e2b", |
| "codegen": "#76b7b2", "archive": "#59a14f", "dsym": "#edc948", |
| "other": "#bab0ac", |
| } |
| |
| _HTML = r"""<!doctype html><meta charset=utf-8> |
| <title>__TITLE__</title> |
| <style> |
| html,body{margin:0;font:12px -apple-system,system-ui,sans-serif;background:#1e1e1e;color:#ccc} |
| #hdr{padding:8px 12px;background:#252526;position:sticky;top:0;z-index:5} |
| #hdr .row{display:flex;gap:18px;align-items:center} |
| #hdr b{color:#fff} |
| #hdr .sub{opacity:.65;font-size:11px;margin-top:4px} |
| .lg{display:inline-flex;align-items:center;gap:4px}.lg i{width:10px;height:10px;display:inline-block} |
| .lg.crit i{background:none;border:1px solid #fff;width:9px;height:9px} |
| #tip{position:fixed;background:#2d2d30;border:1px solid #444;padding:4px 6px;pointer-events:none; |
| display:none;white-space:pre;z-index:10;max-width:60vw} |
| canvas{display:block} |
| </style> |
| <div id=hdr> |
| <div class=row> |
| <b>__TITLE__</b> |
| <span>wall <b>__WALL__</b></span><span>cpu <b>__CPU__</b></span> |
| <span>peak <b>__PEAK__</b></span><span>eff <b>__EFF__</b></span> |
| __LEGEND__ |
| <span class="lg crit"><i></i>critical path</span> |
| <span style="margin-left:auto;opacity:.7">wheel: zoom · drag: pan · hover: details</span> |
| </div> |
| <div class=sub> |
| x-axis: wall-clock time since ninja started · |
| top strip: jobs running at that instant (red line = __NCPU__ cores) · |
| each row below: one parallel slot (one box = one ninja edge; where the chart narrows, the build serialized) |
| </div> |
| </div> |
| <canvas id=c></canvas><div id=tip></div> |
| <script> |
| const E=__DATA__; |
| const COL=__COLORS__; |
| const WALL=__WALL_MS__, LANES=__LANES__, NCPU=__NCPU__, ROW=14; |
| const STRIP=40, AXIS=14, TOP=STRIP+AXIS; |
| const cv=document.getElementById('c'),cx=cv.getContext('2d'),tip=document.getElementById('tip'); |
| let sx=innerWidth/WALL, ox=0; |
| // Per-lane index for O(log n) hover hit-test instead of scanning all ~35k edges. |
| const byLane=[];for(let i=0;i<LANES;i++)byLane.push([]); |
| for(const e of E)byLane[e[3]].push(e); |
| for(const L of byLane)L.sort((a,b)=>a[0]-b[0]); |
| // Parallelism timeline: sweep-line over start/end events -> [t, jobs_after_t]. |
| const ev=[];for(const e of E){ev.push([e[0],1]);ev.push([e[0]+e[1],-1]);} |
| ev.sort((a,b)=>a[0]-b[0]||b[1]-a[1]); |
| const PT=[];{let n=0;for(const[t,d]of ev){n+=d;PT.push([t,n]);}} |
| const PMAX=Math.max(NCPU,...PT.map(p=>p[1])); |
| function fmt(ms){const s=ms/1000;if(s<60)return s.toFixed(s<10?1:0)+'s'; |
| const m=Math.floor(s/60);if(m<60)return m+'m'+String(Math.floor(s%60)).padStart(2,'0'); |
| return Math.floor(m/60)+'h'+String(m%60).padStart(2,'0')+'m';} |
| function draw(){ |
| const w=innerWidth,h=TOP+LANES*ROW+4;cv.width=w;cv.height=h; |
| cx.fillStyle='#1e1e1e';cx.fillRect(0,0,w,h); |
| // time grid + axis labels |
| const span=w/sx, step=[.1,.5,1,5,10,30,60,300,600,1800,3600].find(s=>span/s<12)*1000||3600000; |
| cx.font='10px -apple-system'; |
| for(let t=Math.ceil((-ox/sx)/step)*step;t*sx+ox<w;t+=step){ |
| const x=t*sx+ox;if(x<0)continue; |
| cx.fillStyle='#2a2a2a';cx.fillRect(x,0,1,h); |
| cx.fillStyle='#888';cx.fillText(fmt(t),x+3,STRIP+11);} |
| // parallelism strip: filled area = jobs running, red line = core count |
| cx.fillStyle='#3a3d41'; |
| cx.beginPath();cx.moveTo(ox,STRIP); |
| for(const[t,n]of PT){const x=t*sx+ox,y=STRIP-(n/PMAX)*STRIP;cx.lineTo(x,y);} |
| cx.lineTo(WALL*sx+ox,STRIP);cx.closePath();cx.fill(); |
| const cy=STRIP-(NCPU/PMAX)*STRIP; |
| cx.strokeStyle='#e15759';cx.setLineDash([4,3]);cx.beginPath(); |
| cx.moveTo(0,cy);cx.lineTo(w,cy);cx.stroke();cx.setLineDash([]); |
| cx.fillStyle='#e15759';cx.fillText(NCPU+' cores',4,cy-2); |
| cx.fillStyle='#555';cx.fillRect(0,STRIP,w,1); |
| // edges |
| for(const e of E){ |
| const x=e[0]*sx+ox,ww=Math.max(1,e[1]*sx);if(x+ww<0||x>w)continue; |
| const y=TOP+e[3]*ROW; |
| cx.fillStyle=COL[e[4]]||'#888';cx.fillRect(x,y,ww,ROW-2); |
| if(e[5]){cx.strokeStyle='#fff';cx.lineWidth=1;cx.strokeRect(x+.5,y+.5,ww-1,ROW-3);} |
| } |
| } |
| function hit(mx,my){ |
| if(my<TOP)return null; |
| const lane=Math.floor((my-TOP)/ROW);if(lane<0||lane>=LANES)return null; |
| const t=(mx-ox)/sx,L=byLane[lane]; |
| let lo=0,hi=L.length;while(lo<hi){const m=(lo+hi)>>1;if(L[m][0]+L[m][1]<t)lo=m+1;else hi=m;} |
| const e=L[lo];return e&&e[0]<=t&&t<=e[0]+e[1]?e:null; |
| } |
| function stripHit(mx,my){ |
| if(my>=STRIP)return null;const t=(mx-ox)/sx; |
| let lo=0,hi=PT.length;while(lo<hi){const m=(lo+hi)>>1;if(PT[m][0]<t)lo=m+1;else hi=m;} |
| return PT[Math.max(0,lo-1)]; |
| } |
| cv.onmousemove=ev=>{ |
| const e=hit(ev.offsetX,ev.offsetY),s=stripHit(ev.offsetX,ev.offsetY); |
| if(e){tip.style.display='block';tip.style.left=ev.clientX+12+'px';tip.style.top=ev.clientY+12+'px'; |
| tip.textContent=e[2]+'\n'+(e[1]/1000).toFixed(2)+'s '+e[4]+(e[5]?' [critical path]':'');} |
| else if(s){tip.style.display='block';tip.style.left=ev.clientX+12+'px';tip.style.top=ev.clientY+12+'px'; |
| tip.textContent=fmt(s[0])+' '+s[1]+' job'+(s[1]==1?'':'s')+' running';} |
| else tip.style.display='none'; |
| if(drag){ox+=ev.clientX-drag;drag=ev.clientX;draw();} |
| }; |
| cv.onwheel=ev=>{ev.preventDefault();const f=ev.deltaY<0?1.15:1/1.15; |
| const t=(ev.offsetX-ox)/sx;sx*=f;ox=ev.offsetX-t*sx;draw();}; |
| let drag=0;cv.onmousedown=ev=>drag=ev.clientX;onmouseup=()=>drag=0; |
| cv.onmouseleave=()=>tip.style.display='none'; |
| onresize=draw;draw(); |
| </script> |
| """ |
| |
| |
| def write_html(path: Path, edges, wall, cpu, ncpu, peak, build_dir): |
| data = [[e.start, e.dur, e.out, e.lane, e.cat, 1 if e.crit else 0] for e in edges] |
| legend = " ".join( |
| f'<span class=lg><i style="background:{c}"></i>{k}</span>' |
| for k, c in _CAT_COLOR.items() |
| ) |
| html = (_HTML |
| .replace("__TITLE__", f"ninja build — {build_dir.name}") |
| .replace("__WALL__", fmt_ms(wall).strip()) |
| .replace("__CPU__", fmt_ms(cpu).strip()) |
| .replace("__PEAK__", str(peak)) |
| .replace("__EFF__", f"{cpu / (wall * ncpu):.0%}") |
| .replace("__LEGEND__", legend) |
| .replace("__WALL_MS__", str(wall)) |
| .replace("__LANES__", str(peak)) |
| .replace("__NCPU__", str(ncpu)) |
| .replace("__COLORS__", json.dumps(_CAT_COLOR)) |
| .replace("__DATA__", json.dumps(data, separators=(",", ":")))) |
| path.write_text(html) |
| |
| |
| # ── main ───────────────────────────────────────────────────────────────────── |
| |
| def main(): |
| ap = argparse.ArgumentParser(description=__doc__, |
| formatter_class=argparse.RawDescriptionHelpFormatter) |
| ap.add_argument("build_dir", nargs="?", default="WebKitBuild/cmake-mac/ASan") |
| ap.add_argument("--top", type=int, default=20, help="rows in slowest-edge table") |
| ap.add_argument("--html", metavar="PATH", |
| help="write Gantt to PATH (default: <build-dir>/build-analysis.html)") |
| ap.add_argument("--no-html", action="store_true") |
| ap.add_argument("--no-critical-path", action="store_true", |
| help="skip build.ninja parse (faster, but loses the only number " |
| "that tells you what to fix)") |
| ap.add_argument("--open", action="store_true", help="open the HTML when done") |
| args = ap.parse_args() |
| |
| build_dir = Path(args.build_dir).resolve() |
| log = build_dir / ".ninja_log" |
| if not log.exists(): |
| sys.exit(f"error: {log} not found") |
| |
| edges = parse_ninja_log(log) |
| if not edges: |
| sys.exit("error: .ninja_log is empty") |
| ncpu = os.cpu_count() or 1 |
| wall, cpu = sanity_check(edges, ncpu) |
| peak = pack_lanes(edges) |
| |
| crit_path, crit_ms = [], 0 |
| if not args.no_critical_path: |
| graph = parse_build_graph(build_dir / "build.ninja") |
| crit_path, crit_ms = critical_path(edges, graph) |
| |
| windows = serialization_windows(edges, ncpu) |
| terminal_report(edges, wall, cpu, ncpu, peak, crit_path, crit_ms, windows, args.top) |
| |
| if not args.no_html: |
| out = Path(args.html) if args.html else build_dir / "build-analysis.html" |
| write_html(out, edges, wall, cpu, ncpu, peak, build_dir) |
| print(f"\n open {out}") |
| if args.open: |
| os.system(f"open {out}") |
| |
| |
| if __name__ == "__main__": |
| main() |