From d29b85bc1dbdf9681523dee03ead66c080c5d73d Mon Sep 17 00:00:00 2001 From: Antoine van der Lee <4329185+AvdLee@users.noreply.github.com> Date: Tue, 24 Mar 2026 19:07:02 +0100 Subject: [PATCH 1/2] Bundle scripts, references, and schemas inside each skill for standalone installation Skills referenced shared resources (scripts/, references/, schemas/) via paths outside their own folder. When installed standalone via `npx skills add --skill` or `cp -R skills/`, those external paths broke because only the skill folder is copied. Each skill now bundles its own copies of the scripts, references, and schemas it needs. Root-level canonical copies remain for repo documentation and CI. CONTRIBUTING.md documents which root files map to which skills. --- AGENTS.md | 6 +- CONTRIBUTING.md | 20 +- README.md | 39 +- skills/spm-build-analysis/SKILL.md | 4 +- .../references/build-optimization-sources.md | 159 ++++++ .../references/recommendation-format.md | 85 +++ .../scripts/check_spm_pins.py | 118 ++++ skills/xcode-build-benchmark/SKILL.md | 4 +- .../references/benchmark-artifacts.md | 94 +++ .../schemas/build-benchmark.schema.json | 230 ++++++++ .../scripts/benchmark_builds.py | 308 ++++++++++ skills/xcode-build-fixer/SKILL.md | 6 +- .../build-settings-best-practices.md | 216 +++++++ .../references/recommendation-format.md | 85 +++ .../scripts/benchmark_builds.py | 308 ++++++++++ skills/xcode-build-orchestrator/SKILL.md | 6 +- .../references/benchmark-artifacts.md | 94 +++ .../build-settings-best-practices.md | 216 +++++++ .../references/recommendation-format.md | 85 +++ .../scripts/benchmark_builds.py | 308 ++++++++++ .../scripts/diagnose_compilation.py | 273 +++++++++ .../scripts/generate_optimization_report.py | 533 ++++++++++++++++++ skills/xcode-compilation-analyzer/SKILL.md | 4 +- .../references/build-optimization-sources.md | 159 ++++++ .../references/recommendation-format.md | 85 +++ .../scripts/diagnose_compilation.py | 273 +++++++++ skills/xcode-project-analyzer/SKILL.md | 10 +- .../references/build-optimization-sources.md | 159 ++++++ .../build-settings-best-practices.md | 216 +++++++ .../references/project-audit-checks.md | 2 +- .../references/recommendation-format.md | 85 +++ 31 files changed, 4161 insertions(+), 29 deletions(-) create mode 100644 skills/spm-build-analysis/references/build-optimization-sources.md create mode 100644 skills/spm-build-analysis/references/recommendation-format.md create mode 100755 skills/spm-build-analysis/scripts/check_spm_pins.py create mode 100644 skills/xcode-build-benchmark/references/benchmark-artifacts.md create mode 100644 skills/xcode-build-benchmark/schemas/build-benchmark.schema.json create mode 100755 skills/xcode-build-benchmark/scripts/benchmark_builds.py create mode 100644 skills/xcode-build-fixer/references/build-settings-best-practices.md create mode 100644 skills/xcode-build-fixer/references/recommendation-format.md create mode 100755 skills/xcode-build-fixer/scripts/benchmark_builds.py create mode 100644 skills/xcode-build-orchestrator/references/benchmark-artifacts.md create mode 100644 skills/xcode-build-orchestrator/references/build-settings-best-practices.md create mode 100644 skills/xcode-build-orchestrator/references/recommendation-format.md create mode 100755 skills/xcode-build-orchestrator/scripts/benchmark_builds.py create mode 100755 skills/xcode-build-orchestrator/scripts/diagnose_compilation.py create mode 100644 skills/xcode-build-orchestrator/scripts/generate_optimization_report.py create mode 100644 skills/xcode-compilation-analyzer/references/build-optimization-sources.md create mode 100644 skills/xcode-compilation-analyzer/references/recommendation-format.md create mode 100755 skills/xcode-compilation-analyzer/scripts/diagnose_compilation.py create mode 100644 skills/xcode-project-analyzer/references/build-optimization-sources.md create mode 100644 skills/xcode-project-analyzer/references/build-settings-best-practices.md create mode 100644 skills/xcode-project-analyzer/references/recommendation-format.md diff --git a/AGENTS.md b/AGENTS.md index 5aac72b..b67523e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,8 +4,8 @@ This is a multi-skill Xcode build optimization repository. ## Layout -- `skills/` contains six installable Agent Skills, each with a `SKILL.md` entrypoint. -- `references/`, `schemas/`, and `scripts/` at the repo root are shared support files used by the skills. +- `skills/` contains six installable Agent Skills, each with a `SKILL.md` entrypoint. Each skill bundles its own scripts, references, and schemas so it works after standalone installation. +- `references/`, `schemas/`, and `scripts/` at the repo root are canonical copies of the shared support files. Changes to these must be synced into the skill folders that use them (see CONTRIBUTING.md). - `.claude-plugin/` contains plugin and marketplace metadata. ## Skills @@ -27,7 +27,7 @@ This is a multi-skill Xcode build optimization repository. - Benchmark before optimizing. Use `.build-benchmark/` artifacts as evidence. - Treat clean and incremental builds as separate metrics. - The orchestrator (`xcode-build-orchestrator`) is the primary entrypoint for end-to-end work. -- Shared references and schemas live at the repo root, not inside individual skills. +- Each skill bundles its own copies of scripts, references, and schemas for standalone installation. Root-level `scripts/`, `references/`, and `schemas/` are the canonical copies; keep both layers in sync. ## Handoff Between Skills diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a30735f..834350b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,8 +6,8 @@ Thanks for helping improve this repository. Contributions are welcome when they This is a multi-skill repository that follows the Agent Skills open format: -- Skills live under `skills/`, each with a `SKILL.md` entrypoint. -- Shared reference material lives in `references/`, `schemas/`, and `scripts/` at the repo root. +- Skills live under `skills/`, each with a `SKILL.md` entrypoint. Each skill bundles its own scripts, references, and schemas so it works after standalone installation. +- Canonical copies of shared files live in `references/`, `schemas/`, and `scripts/` at the repo root. When a root-level file changes, update the copies inside every skill that bundles it (see "Keeping shared files in sync" below). - The skills are intentionally recommend-first. They should not make project or source changes without explicit developer approval. ## Recommended Workflow @@ -57,6 +57,22 @@ Avoid broad iOS architecture guidance, CI platform evangelism, or product docume - Avoid network calls in scripts and GitHub Actions unless they are clearly required. - If you add or rename reference files, update the README structure block or let the sync workflow do it. +### Keeping shared files in sync + +Each skill bundles copies of the scripts and references it needs. The root-level `scripts/`, `references/`, and `schemas/` directories hold the canonical versions. When you change a root-level file, copy the updated version into every skill folder that includes it: + +| Root file | Bundled in skills | +|-----------|-------------------| +| `scripts/benchmark_builds.py` | xcode-build-benchmark, xcode-build-orchestrator, xcode-build-fixer | +| `scripts/diagnose_compilation.py` | xcode-compilation-analyzer, xcode-build-orchestrator | +| `scripts/generate_optimization_report.py` | xcode-build-orchestrator | +| `scripts/check_spm_pins.py` | spm-build-analysis | +| `references/benchmark-artifacts.md` | xcode-build-benchmark, xcode-build-orchestrator | +| `references/build-settings-best-practices.md` | xcode-project-analyzer, xcode-build-orchestrator, xcode-build-fixer | +| `references/recommendation-format.md` | xcode-compilation-analyzer, xcode-project-analyzer, spm-build-analysis, xcode-build-orchestrator, xcode-build-fixer | +| `references/build-optimization-sources.md` | xcode-compilation-analyzer, xcode-project-analyzer, spm-build-analysis | +| `schemas/build-benchmark.schema.json` | xcode-build-benchmark | + ## Typical Contribution Types - Improve one of the six skill entrypoints. diff --git a/README.md b/README.md index 61ec4cb..84e0de7 100644 --- a/README.md +++ b/README.md @@ -206,13 +206,13 @@ That distinction is central to this repo and follows both Apple's Xcode guidance ## Shared Support Layer -The skills share: +Each skill bundles its own copies of the scripts, references, and schemas it needs so it works after standalone installation. The canonical copies live at the repo root: -- a common `.build-benchmark/` artifact contract -- a shared JSON schema for benchmark output -- helper scripts for benchmarking, timing-summary parsing, compilation diagnostics, report generation, and recommendation rendering -- a build settings best practices reference for the pass/fail audit -- a single source summary file so README and skill guidance stay aligned +- `scripts/` -- helper scripts for benchmarking, timing-summary parsing, compilation diagnostics, report generation, and recommendation rendering +- `references/` -- build settings best practices, artifact format, recommendation format, and source citations +- `schemas/` -- JSON schema for benchmark output + +When a root-level file changes, the corresponding copies inside each skill that uses it must be updated (see [CONTRIBUTING.md](CONTRIBUTING.md)). ## Skill Structure @@ -220,28 +220,55 @@ The skills share: skills/ xcode-build-benchmark/ SKILL.md + scripts/ + benchmark_builds.py references/ benchmarking-workflow.md + benchmark-artifacts.md + schemas/ + build-benchmark.schema.json xcode-compilation-analyzer/ SKILL.md + scripts/ + diagnose_compilation.py references/ code-compilation-checks.md + recommendation-format.md + build-optimization-sources.md xcode-project-analyzer/ SKILL.md references/ project-audit-checks.md + build-settings-best-practices.md + recommendation-format.md + build-optimization-sources.md spm-build-analysis/ SKILL.md + scripts/ + check_spm_pins.py references/ spm-analysis-checks.md + recommendation-format.md + build-optimization-sources.md xcode-build-orchestrator/ SKILL.md + scripts/ + benchmark_builds.py + diagnose_compilation.py + generate_optimization_report.py references/ orchestration-report-template.md + benchmark-artifacts.md + recommendation-format.md + build-settings-best-practices.md xcode-build-fixer/ SKILL.md + scripts/ + benchmark_builds.py references/ fix-patterns.md + build-settings-best-practices.md + recommendation-format.md ``` diff --git a/skills/spm-build-analysis/SKILL.md b/skills/spm-build-analysis/SKILL.md index 033cf27..34ce475 100644 --- a/skills/spm-build-analysis/SKILL.md +++ b/skills/spm-build-analysis/SKILL.md @@ -88,5 +88,5 @@ If the main problem is not package-related, hand off to [`xcode-project-analyzer ## Additional Resources - For the detailed audit checklist, see [references/spm-analysis-checks.md](references/spm-analysis-checks.md) -- For the shared recommendation structure, see [../../references/recommendation-format.md](../../references/recommendation-format.md) -- For source citations, see [../../references/build-optimization-sources.md](../../references/build-optimization-sources.md) +- For the shared recommendation structure, see [references/recommendation-format.md](references/recommendation-format.md) +- For source citations, see [references/build-optimization-sources.md](references/build-optimization-sources.md) diff --git a/skills/spm-build-analysis/references/build-optimization-sources.md b/skills/spm-build-analysis/references/build-optimization-sources.md new file mode 100644 index 0000000..dc29040 --- /dev/null +++ b/skills/spm-build-analysis/references/build-optimization-sources.md @@ -0,0 +1,159 @@ +# Build Optimization Sources + +This file stores the external sources that the README and skill docs should cite consistently. + +## Apple: Improving the speed of incremental builds + +Source: + +- + +Key takeaways: + +- Measure first with `Build With Timing Summary` or `xcodebuild -showBuildTimingSummary`. +- Accurate target dependencies improve correctness and parallelism. +- Run scripts should declare inputs and outputs so Xcode can skip unnecessary work. +- `.xcfilelist` files are appropriate when scripts have many inputs or outputs. +- Custom frameworks and libraries benefit from module maps, typically by enabling `DEFINES_MODULE`. +- Module reuse is strongest when related sources compile with consistent options. +- Breaking monolithic targets into better-scoped modules can reduce unnecessary rebuilds. + +## Apple: Improving build efficiency with good coding practices + +Source: + +- + +Key takeaways: + +- Use framework-qualified imports when module maps are available. +- Keep Objective-C bridging surfaces narrow. +- Prefer explicit type information when inference becomes expensive. +- Use explicit delegate protocols instead of overly generic delegate types. +- Simplify complex expressions that are hard for the compiler to type-check. + +## Apple: Building your project with explicit module dependencies + +Source: + +- + +Key takeaways: + +- Explicit module builds make module work visible in the build log and improve scheduling. +- Repeated builds of the same module often point to avoidable module variants. +- Inconsistent build options across targets can force duplicate module builds. +- Timing summaries can reveal option drift that prevents module reuse. + +## SwiftLee: Build performance analysis for speeding up Xcode builds + +Source: + +- + +Key takeaways: + +- Clean and incremental builds should both be measured because they reveal different problems. +- Build Timeline and Build Timing Summary are practical starting points for build optimization. +- Build scripts often produce large incremental-build wins when guarded correctly. +- `-warn-long-function-bodies` and `-warn-long-expression-type-checking` help surface compile hotspots. +- Typical debug and release build setting mismatches are worth auditing, especially in older projects. + +## Apple: Xcode Release Notes -- Compilation Caching + +Source: + +- Xcode Release Notes (149700201) + +Key takeaways: + +- Compilation caching is an opt-in feature for Swift and C-family languages. +- It caches prior compilation results and reuses them when the same source inputs are recompiled. +- Branch switching and clean builds benefit the most. +- Can be enabled via the "Enable Compilation Caching" build setting or per-user project settings. + +## Apple: Demystify explicitly built modules (WWDC24) + +Source: + +- + +Key takeaways: + +- Explains how explicitly built modules divide compilation into scan, module build, and source compile stages. +- Unrelated modules build in parallel, improving CPU utilization. +- Module variant duplication is a key bottleneck -- uniform compiler options across targets prevent it. +- The build log shows each module as a discrete task, making it easier to diagnose scheduling issues. + +## Stackademic: Improving Swift Compile-Time Performance -- 14 Tips + +Source: + +- + +Key takeaways: + +- Mark classes `final` to eliminate virtual dispatch overhead and help the compiler optimize. +- Use `private`/`fileprivate` for symbols not used outside their scope. +- Prefer `struct` and `enum` over `class` when reference semantics are not needed. +- Avoid long method chains without intermediate type annotations -- even simple-looking chains can take seconds to compile. +- Add explicit return types to closures passed to generic functions. +- Break large SwiftUI view bodies into smaller composed subviews. + +## Bitrise: Demystifying Explicitly Built Modules for Xcode + +Source: + +- + +Key takeaways: + +- Explicit module builds give `xcodebuild` visibility into smaller compilation tasks for better parallelism. +- Enabled by default for C/Objective-C in Xcode 16+; experimental for Swift. +- Minimizing module variants by aligning build options is the primary optimization lever. +- Some projects see regressions from dependency scanning overhead -- benchmark before and after. + +## Bitrise: Xcode Compilation Cache FAQ + +Source: + +- + +Key takeaways: + +- Granular caching is controlled by `SWIFT_ENABLE_COMPILE_CACHE` and `CLANG_ENABLE_COMPILE_CACHE`, under the umbrella `COMPILATION_CACHING` setting. +- Non-cacheable tasks include `CompileStoryboard`, `CompileXIB`, `CompileAssetCatalogVariant`, `PhaseScriptExecution`, `DataModelCompile`, `CopyPNGFile`, `GenerateDSYMFile`, and `Ld`. +- SPM dependencies are not yet cacheable as of Xcode 26 beta. + +## RocketSim Docs: Build Insights + +Sources: + +- +- + +Key takeaways: + +- RocketSim automatically tracks clean vs incremental builds over time without build scripts. +- It reports build counts, duration trends, and percentile-based metrics such as p75 and p95. +- Team Build Insights adds machine, Xcode, and macOS comparisons for cross-team visibility. +- This repository is best positioned as the point-in-time analyze-and-improve toolkit, while RocketSim is the monitor-over-time companion. + +## Swift Forums: Slow incremental builds because of planning swift module + +Source: + +- + +Key takeaways: + +- "Planning Swift module" can dominate incremental builds (up to 30s per module), sometimes exceeding clean build time. +- Replanning every module without scheduling compiles is a sign that build inputs are being modified unexpectedly (e.g., a misconfigured linter touching file timestamps). +- Enable **Task Backtraces** (Xcode 16.4+: Scheme Editor > Build > Build Debugging) to see why each task re-ran in an incremental build. +- Heavy Swift macro usage (e.g., TCA / swift-syntax) can cause trivial changes to cascade into near-full rebuilds. +- `swift-syntax` builds universally (all architectures) when no prebuilt binary is available, adding significant overhead. +- `SwiftEmitModule` can take 60s+ after a single-line change in large modules. +- Asset catalog compilation is single-threaded per target; splitting assets into separate bundles across targets enables parallel compilation. +- Multi-platform targets (e.g., adding watchOS) can cause SPM packages to build 3x (iOS arm64, iOS x86_64, watchOS arm64). +- Zero-change incremental builds still incur ~10s of fixed overhead: compute dependencies, send project description, create build description, script phases, codesigning, and validation. +- Codesigning and validation run even when output has not changed. diff --git a/skills/spm-build-analysis/references/recommendation-format.md b/skills/spm-build-analysis/references/recommendation-format.md new file mode 100644 index 0000000..46affd6 --- /dev/null +++ b/skills/spm-build-analysis/references/recommendation-format.md @@ -0,0 +1,85 @@ +# Recommendation Format + +All optimization skills should report recommendations in a shared structure so the orchestrator can merge and prioritize them cleanly. + +## Required Fields + +Each recommendation should include: + +- `title` +- `wait_time_impact` -- plain-language statement of expected wall-clock impact, e.g. "Expected to reduce your clean build by ~3s", "Reduces parallel compile work but unlikely to reduce build wait time", or "Impact on wait time is uncertain -- re-benchmark to confirm" +- `actionability` -- classifies how fixable the issue is from the project (see values below) +- `category` +- `observed_evidence` +- `estimated_impact` +- `confidence` +- `approval_required` +- `benchmark_verification_status` + +### Actionability Values + +Every recommendation must include an `actionability` classification: + +- `repo-local` -- Fix lives entirely in project files, source code, or local configuration. The developer can apply it without side effects outside the repo. +- `package-manager` -- Requires CocoaPods or SPM configuration changes that may have broad side effects (e.g., linkage mode, dependency restructuring). These should be benchmarked before and after. +- `xcode-behavior` -- Observed cost is driven by Xcode internals and is not suppressible from the project. Report the finding for awareness but do not promise a fix. +- `upstream` -- Requires changes in a third-party dependency or external tool. The developer cannot fix it locally. + +## Suggested Optional Fields + +- `scope` +- `affected_files` +- `affected_targets` +- `affected_packages` +- `implementation_notes` +- `risk_level` + +## JSON Example + +```json +{ + "recommendations": [ + { + "title": "Guard a release-only symbol upload script", + "wait_time_impact": "Expected to reduce your incremental build by approximately 6 seconds.", + "actionability": "repo-local", + "category": "project", + "observed_evidence": [ + "Incremental builds spend 6.3 seconds in a run script phase.", + "The script runs for Debug builds even though the output is only needed in Release." + ], + "estimated_impact": "High incremental-build improvement", + "confidence": "High", + "approval_required": true, + "benchmark_verification_status": "Not yet verified", + "scope": "Target build phase", + "risk_level": "Low" + } + ] +} +``` + +## Markdown Rendering Guidance + +When rendering for human review, preserve the same field order: + +1. title +2. wait-time impact +3. actionability +4. observed evidence +5. estimated impact +6. confidence +7. approval required +8. benchmark verification status + +That makes it easier for the developer to approve or reject specific items quickly. + +## Verification Status Values + +Recommended values: + +- `Not yet verified` +- `Queued for verification` +- `Verified improvement` +- `No measurable improvement` +- `Inconclusive due to benchmark noise` diff --git a/skills/spm-build-analysis/scripts/check_spm_pins.py b/skills/spm-build-analysis/scripts/check_spm_pins.py new file mode 100755 index 0000000..fd11a58 --- /dev/null +++ b/skills/spm-build-analysis/scripts/check_spm_pins.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 + +"""Scan a project.pbxproj for branch-pinned SPM dependencies and check tag availability.""" + +import argparse +import json +import re +import subprocess +import sys +from pathlib import Path +from typing import Dict, List, Optional + + +_PKG_REF_RE = re.compile( + r"(/\*\s*XCRemoteSwiftPackageReference\s+\"(?P[^\"]+)\"\s*\*/\s*=\s*\{[^}]*?" + r"repositoryURL\s*=\s*\"(?P[^\"]+)\"[^}]*?" + r"requirement\s*=\s*\{(?P[^}]*)\})", + re.DOTALL, +) + +_KIND_RE = re.compile(r"kind\s*=\s*(\w+)\s*;") +_BRANCH_RE = re.compile(r"branch\s*=\s*(\w+)\s*;") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Check branch-pinned SPM dependencies for available tags." + ) + parser.add_argument( + "--project", + required=True, + help="Path to the .xcodeproj directory", + ) + parser.add_argument( + "--json", + action="store_true", + help="Output results as JSON", + ) + return parser.parse_args() + + +def find_branch_pins(pbxproj: str) -> List[Dict[str, str]]: + results: List[Dict[str, str]] = [] + for match in _PKG_REF_RE.finditer(pbxproj): + name = match.group("name") + url = match.group("url") + req = match.group("req") + kind_match = _KIND_RE.search(req) + if not kind_match: + continue + kind = kind_match.group(1) + if kind != "branch": + continue + branch_match = _BRANCH_RE.search(req) + branch = branch_match.group(1) if branch_match else "unknown" + results.append({"name": name, "url": url, "branch": branch}) + return results + + +def check_tags(url: str) -> List[str]: + try: + result = subprocess.run( + ["git", "ls-remote", "--tags", url], + capture_output=True, + text=True, + timeout=15, + ) + if result.returncode != 0: + return [] + tags: List[str] = [] + for line in result.stdout.strip().splitlines(): + ref = line.split("\t")[-1] if "\t" in line else "" + if ref.startswith("refs/tags/") and not ref.endswith("^{}"): + tags.append(ref.replace("refs/tags/", "")) + return tags + except (subprocess.TimeoutExpired, FileNotFoundError): + return [] + + +def main() -> int: + args = parse_args() + pbxproj_path = Path(args.project) / "project.pbxproj" + if not pbxproj_path.exists(): + sys.stderr.write(f"Not found: {pbxproj_path}\n") + return 1 + + pbxproj = pbxproj_path.read_text() + pins = find_branch_pins(pbxproj) + + if not pins: + print("No branch-pinned SPM dependencies found.") + return 0 + + results: List[Dict] = [] + for pin in pins: + tags = check_tags(pin["url"]) + entry = { + "name": pin["name"], + "url": pin["url"], + "branch": pin["branch"], + "tags_available": len(tags) > 0, + "latest_tags": tags[-5:] if tags else [], + } + results.append(entry) + + if args.json: + print(json.dumps(results, indent=2)) + else: + for r in results: + status = "tags available" if r["tags_available"] else "no tags (pin to revision)" + latest = f" (latest: {', '.join(r['latest_tags'])})" if r["latest_tags"] else "" + print(f" {r['name']}: branch={r['branch']} -> {status}{latest}") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/xcode-build-benchmark/SKILL.md b/skills/xcode-build-benchmark/SKILL.md index 08ae5e9..09b50bf 100644 --- a/skills/xcode-build-benchmark/SKILL.md +++ b/skills/xcode-build-benchmark/SKILL.md @@ -84,5 +84,5 @@ Stop after measurement if the user only asked for benchmarking. If they want opt ## Additional Resources - For the benchmark contract, see [references/benchmarking-workflow.md](references/benchmarking-workflow.md) -- For the shared artifact format, see [../../references/benchmark-artifacts.md](../../references/benchmark-artifacts.md) -- For the JSON schema, see [../../schemas/build-benchmark.schema.json](../../schemas/build-benchmark.schema.json) +- For the shared artifact format, see [references/benchmark-artifacts.md](references/benchmark-artifacts.md) +- For the JSON schema, see [schemas/build-benchmark.schema.json](schemas/build-benchmark.schema.json) diff --git a/skills/xcode-build-benchmark/references/benchmark-artifacts.md b/skills/xcode-build-benchmark/references/benchmark-artifacts.md new file mode 100644 index 0000000..2c35a79 --- /dev/null +++ b/skills/xcode-build-benchmark/references/benchmark-artifacts.md @@ -0,0 +1,94 @@ +# Benchmark Artifacts + +All skills in this repository should treat `.build-benchmark/` as the canonical location for measured build evidence. + +## Goals + +- Keep build measurements reproducible. +- Make clean and incremental build data easy to compare. +- Preserve enough context for later specialist analysis without rerunning the benchmark. + +## Wall-Clock vs Cumulative Task Time + +The `duration_seconds` field on each run and the `median_seconds` in the summary represent **wall-clock time** -- how long the developer actually waits. This is the primary success metric. + +The `timing_summary_categories` are **aggregated task times** parsed from Xcode's Build Timing Summary. Because Xcode runs many tasks in parallel across CPU cores, these totals typically exceed the wall-clock duration. A large cumulative `SwiftCompile` value is diagnostic evidence of compiler workload, not proof that compilation is blocking the build. Always compare category totals against the wall-clock median before concluding that a category is a bottleneck. + +## File Layout + +Recommended outputs: + +- `.build-benchmark/-.json` +- `.build-benchmark/--clean-1.log` +- `.build-benchmark/--clean-2.log` +- `.build-benchmark/--clean-3.log` +- `.build-benchmark/--cached-clean-1.log` (when COMPILATION_CACHING is enabled) +- `.build-benchmark/--cached-clean-2.log` +- `.build-benchmark/--cached-clean-3.log` +- `.build-benchmark/--incremental-1.log` +- `.build-benchmark/--incremental-2.log` +- `.build-benchmark/--incremental-3.log` + +Use an ISO-like UTC timestamp without spaces so the files sort naturally. + +## Artifact Requirements + +Each JSON artifact should include: + +- schema version +- creation timestamp +- project context +- environment details when available +- the normalized build command +- separate `clean` and `incremental` run arrays +- summary statistics for each build type +- parsed timing-summary categories +- free-form notes for caveats or noise + +## Clean, Cached Clean, And Incremental Separation + +Do not merge different build type measurements into a single list. They answer different questions: + +- **Clean builds** show full build-system, package, and module setup cost with a cold compilation cache. +- **Cached clean builds** show clean build cost when the compilation cache is warm. This is the realistic scenario for branch switching, pulling changes, or Clean Build Folder. Only present when `COMPILATION_CACHING = YES` is detected. +- **Incremental builds** show edit-loop productivity and script or cache invalidation problems. + +## Raw Logs + +Store raw `xcodebuild` output beside the JSON artifact whenever possible. That allows later skills to: + +- re-parse timing summaries +- inspect failed builds +- search for long type-check warnings +- correlate build-system phases with recommendations + +## Measurement Caveats + +### COMPILATION_CACHING + +`COMPILATION_CACHING = YES` stores compiled artifacts in a system-managed cache outside DerivedData so that repeated compilations of identical inputs are served from cache. The standard clean-build benchmark (`xcodebuild clean` between runs) may add overhead from cache population without showing the corresponding cache-hit benefit. + +The benchmark script automatically detects `COMPILATION_CACHING = YES` and runs a **cached clean** benchmark phase. This phase: + +1. Builds once to warm the compilation cache. +2. Deletes DerivedData (but not the compilation cache) before each measured run. +3. Rebuilds, measuring the cache-hit clean build time. + +The cached clean metric captures the realistic developer experience: branch switching, pulling changes, and Clean Build Folder. Use the cached clean median as the primary comparison metric when evaluating `COMPILATION_CACHING` impact. + +To skip this phase, pass `--no-cached-clean`. + +### First-Run Variance + +The first clean build after the warmup cycle often runs 20-40% slower than subsequent clean builds due to cold OS-level caches (disk I/O, dynamic linker cache, etc.). The benchmark script mitigates this by running a warmup clean+build cycle before measured runs. If variance between the first and later clean runs is still high, prefer the median or min over the mean, and note the variance in the artifact's `notes` field. + +## Shared Consumer Expectations + +Any skill reading a benchmark artifact should be able to identify: + +- what was measured +- how it was measured +- whether the run succeeded +- whether the results are stable enough to compare + +For the authoritative field-level schema, see [../schemas/build-benchmark.schema.json](../schemas/build-benchmark.schema.json). diff --git a/skills/xcode-build-benchmark/schemas/build-benchmark.schema.json b/skills/xcode-build-benchmark/schemas/build-benchmark.schema.json new file mode 100644 index 0000000..0ae8a9d --- /dev/null +++ b/skills/xcode-build-benchmark/schemas/build-benchmark.schema.json @@ -0,0 +1,230 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Xcode Build Benchmark Artifact", + "type": "object", + "required": [ + "schema_version", + "created_at", + "build", + "runs", + "summary" + ], + "properties": { + "schema_version": { + "type": "string", + "enum": ["1.0.0", "1.1.0", "1.2.0"] + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "build": { + "type": "object", + "required": [ + "entrypoint", + "scheme", + "configuration", + "destination", + "command" + ], + "properties": { + "entrypoint": { + "type": "string", + "enum": [ + "project", + "workspace" + ] + }, + "path": { + "type": "string" + }, + "scheme": { + "type": "string" + }, + "configuration": { + "type": "string" + }, + "destination": { + "type": "string" + }, + "derived_data_path": { + "type": "string" + }, + "command": { + "type": "string" + } + }, + "additionalProperties": true + }, + "environment": { + "type": "object", + "properties": { + "host": { + "type": "string" + }, + "xcode_version": { + "type": "string" + }, + "macos_version": { + "type": "string" + } + }, + "additionalProperties": true + }, + "runs": { + "type": "object", + "required": [ + "clean", + "incremental" + ], + "properties": { + "clean": { + "type": "array", + "items": { + "$ref": "#/definitions/run" + } + }, + "cached_clean": { + "type": "array", + "items": { + "$ref": "#/definitions/run" + } + }, + "incremental": { + "type": "array", + "items": { + "$ref": "#/definitions/run" + } + } + }, + "additionalProperties": false + }, + "summary": { + "type": "object", + "required": [ + "clean", + "incremental" + ], + "properties": { + "clean": { + "$ref": "#/definitions/stats" + }, + "cached_clean": { + "$ref": "#/definitions/stats" + }, + "incremental": { + "$ref": "#/definitions/stats" + } + }, + "additionalProperties": false + }, + "notes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "definitions": { + "run": { + "type": "object", + "required": [ + "id", + "build_type", + "duration_seconds", + "success", + "command" + ], + "properties": { + "id": { + "type": "string" + }, + "build_type": { + "type": "string", + "enum": [ + "clean", + "cached-clean", + "incremental" + ] + }, + "duration_seconds": { + "type": "number", + "minimum": 0 + }, + "success": { + "type": "boolean" + }, + "command": { + "type": "string" + }, + "exit_code": { + "type": "integer" + }, + "raw_log_path": { + "type": "string" + }, + "timing_summary_categories": { + "type": "array", + "items": { + "$ref": "#/definitions/category" + } + } + }, + "additionalProperties": true + }, + "category": { + "type": "object", + "required": [ + "name", + "seconds" + ], + "properties": { + "name": { + "type": "string" + }, + "seconds": { + "type": "number", + "minimum": 0 + }, + "task_count": { + "type": "integer", + "minimum": 0 + } + }, + "additionalProperties": true + }, + "stats": { + "type": "object", + "required": [ + "count", + "min_seconds", + "max_seconds", + "median_seconds", + "average_seconds" + ], + "properties": { + "count": { + "type": "integer", + "minimum": 0 + }, + "min_seconds": { + "type": "number", + "minimum": 0 + }, + "max_seconds": { + "type": "number", + "minimum": 0 + }, + "median_seconds": { + "type": "number", + "minimum": 0 + }, + "average_seconds": { + "type": "number", + "minimum": 0 + } + }, + "additionalProperties": true + } + } +} diff --git a/skills/xcode-build-benchmark/scripts/benchmark_builds.py b/skills/xcode-build-benchmark/scripts/benchmark_builds.py new file mode 100755 index 0000000..3e27788 --- /dev/null +++ b/skills/xcode-build-benchmark/scripts/benchmark_builds.py @@ -0,0 +1,308 @@ +#!/usr/bin/env python3 + +import argparse +import json +import os +import platform +import re +import shutil +import statistics +import subprocess +import sys +import tempfile +import time +from datetime import datetime, timezone +from pathlib import Path +from typing import Dict, List, Optional + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Benchmark Xcode clean and incremental builds.") + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument("--workspace", help="Path to the .xcworkspace file") + group.add_argument("--project", help="Path to the .xcodeproj file") + parser.add_argument("--scheme", required=True, help="Scheme to build") + parser.add_argument("--configuration", default="Debug", help="Build configuration") + parser.add_argument("--destination", help="xcodebuild destination string") + parser.add_argument("--derived-data-path", help="DerivedData path override") + parser.add_argument("--output-dir", default=".build-benchmark", help="Output directory for artifacts") + parser.add_argument("--repeats", type=int, default=3, help="Measured runs per build type") + parser.add_argument("--skip-warmup", action="store_true", help="Skip the validation build") + parser.add_argument( + "--touch-file", + help="Path to a source file to touch before each incremental build. " + "When provided, measures a real edit-rebuild loop instead of a zero-change build.", + ) + parser.add_argument( + "--no-cached-clean", + action="store_true", + help="Skip cached clean builds even when COMPILATION_CACHING is detected.", + ) + parser.add_argument( + "--extra-arg", + action="append", + default=[], + help="Additional xcodebuild argument to append. Can be passed multiple times.", + ) + return parser.parse_args() + + +def command_base(args: argparse.Namespace) -> List[str]: + command = ["xcodebuild"] + if args.workspace: + command.extend(["-workspace", args.workspace]) + if args.project: + command.extend(["-project", args.project]) + command.extend(["-scheme", args.scheme, "-configuration", args.configuration]) + if args.destination: + command.extend(["-destination", args.destination]) + if args.derived_data_path: + command.extend(["-derivedDataPath", args.derived_data_path]) + command.extend(args.extra_arg) + return command + + +def shell_join(parts: List[str]) -> str: + return " ".join(subprocess.list2cmdline([part]) for part in parts) + + +_TASK_COUNT_RE = re.compile(r"^(.+?)\s*\((\d+)\s+tasks?\)$") + + +def _extract_task_count(name: str) -> tuple[str, Optional[int]]: + """Split 'Category (N tasks)' into ('Category', N).""" + match = _TASK_COUNT_RE.match(name) + if match: + return match.group(1).strip(), int(match.group(2)) + return name, None + + +def parse_timing_summary(output: str) -> List[Dict]: + categories: Dict[str, float] = {} + task_counts: Dict[str, Optional[int]] = {} + for raw_line in output.splitlines(): + line = raw_line.strip() + if not line: + continue + for suffix in (" seconds", " second", " sec"): + if not line.endswith(suffix): + continue + trimmed = line[: -len(suffix)] + if "|" in trimmed: + name_part, _, seconds_text = trimmed.rpartition("|") + else: + name_part, _, seconds_text = trimmed.rpartition(" ") + try: + seconds = float(seconds_text.strip()) + except ValueError: + continue + cleaned_name = name_part.replace(" ", " ").strip(" -:") + if len(cleaned_name) < 3: + continue + base_name, count = _extract_task_count(cleaned_name) + categories[base_name] = categories.get(base_name, 0.0) + seconds + if count is not None: + task_counts[base_name] = (task_counts.get(base_name) or 0) + count + break + result: List[Dict] = [] + for name, seconds in sorted(categories.items(), key=lambda item: item[1], reverse=True): + entry: Dict = {"name": name, "seconds": round(seconds, 3)} + if name in task_counts: + entry["task_count"] = task_counts[name] + result.append(entry) + return result + + +def run_command(command: List[str]) -> subprocess.CompletedProcess: + return subprocess.run(command, capture_output=True, text=True) + + +def stats_for(runs: List[Dict[str, object]]) -> Dict[str, float]: + durations = [run["duration_seconds"] for run in runs if run.get("success")] + if not durations: + return { + "count": 0, + "min_seconds": 0.0, + "max_seconds": 0.0, + "median_seconds": 0.0, + "average_seconds": 0.0, + } + return { + "count": len(durations), + "min_seconds": round(min(durations), 3), + "max_seconds": round(max(durations), 3), + "median_seconds": round(statistics.median(durations), 3), + "average_seconds": round(statistics.fmean(durations), 3), + } + + +def xcode_version() -> str: + result = run_command(["xcodebuild", "-version"]) + return result.stdout.strip() if result.returncode == 0 else "unknown" + + +def detect_compilation_caching(base_command: List[str]) -> bool: + """Check whether COMPILATION_CACHING is enabled in the resolved build settings.""" + result = run_command([*base_command, "-showBuildSettings"]) + if result.returncode != 0: + return False + for line in result.stdout.splitlines(): + stripped = line.strip() + if stripped.startswith("COMPILATION_CACHING") and "=" in stripped: + value = stripped.split("=", 1)[1].strip() + return value == "YES" + return False + + +def measure_build( + base_command: List[str], + artifact_stem: str, + output_dir: Path, + build_type: str, + run_index: int, +) -> Dict[str, object]: + build_command = [*base_command, "build", "-showBuildTimingSummary"] + started = time.perf_counter() + result = run_command(build_command) + elapsed = round(time.perf_counter() - started, 3) + log_path = output_dir / f"{artifact_stem}-{build_type}-{run_index}.log" + log_path.write_text(result.stdout + result.stderr) + return { + "id": f"{build_type}-{run_index}", + "build_type": build_type, + "duration_seconds": elapsed, + "success": result.returncode == 0, + "exit_code": result.returncode, + "command": shell_join(build_command), + "raw_log_path": str(log_path), + "timing_summary_categories": parse_timing_summary(result.stdout + result.stderr), + } + + +def main() -> int: + args = parse_args() + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + artifact_stem = f"{timestamp}-{args.scheme.replace(' ', '-').lower()}" + base_command = command_base(args) + + if not args.skip_warmup: + warmup = run_command([*base_command, "build"]) + if warmup.returncode != 0: + sys.stderr.write(warmup.stdout + warmup.stderr) + return warmup.returncode + warmup_clean = run_command([*base_command, "clean"]) + if warmup_clean.returncode != 0: + sys.stderr.write(warmup_clean.stdout + warmup_clean.stderr) + return warmup_clean.returncode + warmup_rebuild = run_command([*base_command, "build"]) + if warmup_rebuild.returncode != 0: + sys.stderr.write(warmup_rebuild.stdout + warmup_rebuild.stderr) + return warmup_rebuild.returncode + + runs: Dict[str, list] = {"clean": [], "incremental": []} + + for index in range(1, args.repeats + 1): + clean_result = run_command([*base_command, "clean"]) + clean_log_path = output_dir / f"{artifact_stem}-clean-prep-{index}.log" + clean_log_path.write_text(clean_result.stdout + clean_result.stderr) + if clean_result.returncode != 0: + sys.stderr.write(clean_result.stdout + clean_result.stderr) + return clean_result.returncode + runs["clean"].append(measure_build(base_command, artifact_stem, output_dir, "clean", index)) + + # --- Cached clean builds --------------------------------------------------- + # When COMPILATION_CACHING is enabled, the compilation cache lives outside + # DerivedData and survives product deletion. We measure "cached clean" + # builds by pointing DerivedData at a temp directory, warming the cache with + # one build, then deleting the DerivedData directory (but not the cache) + # before each measured rebuild. This captures the realistic scenario: + # branch switching, pulling changes, or Clean Build Folder. + should_cached_clean = not args.no_cached_clean and detect_compilation_caching(base_command) + if should_cached_clean: + dd_path = Path(args.derived_data_path) if args.derived_data_path else Path( + tempfile.mkdtemp(prefix="xcode-bench-dd-") + ) + cached_cmd = list(base_command) + if not args.derived_data_path: + cached_cmd.extend(["-derivedDataPath", str(dd_path)]) + + cache_warmup = run_command([*cached_cmd, "build"]) + if cache_warmup.returncode != 0: + sys.stderr.write("Warning: cached clean warmup build failed, skipping cached clean benchmarks.\n") + sys.stderr.write(cache_warmup.stdout + cache_warmup.stderr) + should_cached_clean = False + + if should_cached_clean: + runs["cached_clean"] = [] + for index in range(1, args.repeats + 1): + shutil.rmtree(dd_path, ignore_errors=True) + runs["cached_clean"].append( + measure_build(cached_cmd, artifact_stem, output_dir, "cached-clean", index) + ) + shutil.rmtree(dd_path, ignore_errors=True) + + # --- Incremental / zero-change builds -------------------------------------- + incremental_label = "incremental" + if args.touch_file: + touch_path = Path(args.touch_file) + if not touch_path.exists(): + sys.stderr.write(f"--touch-file path does not exist: {touch_path}\n") + return 1 + incremental_label = "incremental" + else: + incremental_label = "zero-change" + + for index in range(1, args.repeats + 1): + if args.touch_file: + touch_path.touch() + runs["incremental"].append( + measure_build(base_command, artifact_stem, output_dir, incremental_label, index) + ) + + summary: Dict[str, object] = { + "clean": stats_for(runs["clean"]), + "incremental": stats_for(runs["incremental"]), + } + if "cached_clean" in runs: + summary["cached_clean"] = stats_for(runs["cached_clean"]) + + artifact = { + "schema_version": "1.2.0" if "cached_clean" in runs else "1.1.0", + "created_at": datetime.now(timezone.utc).isoformat(), + "build": { + "entrypoint": "workspace" if args.workspace else "project", + "path": args.workspace or args.project, + "scheme": args.scheme, + "configuration": args.configuration, + "destination": args.destination or "", + "derived_data_path": args.derived_data_path or "", + "command": shell_join(base_command), + }, + "environment": { + "host": platform.node(), + "macos_version": platform.platform(), + "xcode_version": xcode_version(), + "cwd": os.getcwd(), + }, + "runs": runs, + "summary": summary, + "notes": [f"touch-file: {args.touch_file}"] if args.touch_file else [], + } + + artifact_path = output_dir / f"{artifact_stem}.json" + artifact_path.write_text(json.dumps(artifact, indent=2) + "\n") + + print(f"Saved benchmark artifact: {artifact_path}") + print(f"Clean median: {artifact['summary']['clean']['median_seconds']}s") + if "cached_clean" in artifact["summary"]: + print(f"Cached clean median: {artifact['summary']['cached_clean']['median_seconds']}s") + inc_label = "Incremental" if args.touch_file else "Zero-change" + print(f"{inc_label} median: {artifact['summary']['incremental']['median_seconds']}s") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/xcode-build-fixer/SKILL.md b/skills/xcode-build-fixer/SKILL.md index 83bbcae..1ae341e 100644 --- a/skills/xcode-build-fixer/SKILL.md +++ b/skills/xcode-build-fixer/SKILL.md @@ -28,7 +28,7 @@ When working from an optimization plan, read the approval checklist and implemen ### Build Settings -Change `project.pbxproj` values to match the recommendations in [build-settings-best-practices.md](../../references/build-settings-best-practices.md). +Change `project.pbxproj` values to match the recommendations in [build-settings-best-practices.md](references/build-settings-best-practices.md). Typical fixes: @@ -214,5 +214,5 @@ If during implementation you discover issues outside this skill's scope: ## Additional Resources - For concrete before/after fix patterns, see [references/fix-patterns.md](references/fix-patterns.md) -- For build settings best practices, see [../../references/build-settings-best-practices.md](../../references/build-settings-best-practices.md) -- For the recommendation format, see [../../references/recommendation-format.md](../../references/recommendation-format.md) +- For build settings best practices, see [references/build-settings-best-practices.md](references/build-settings-best-practices.md) +- For the recommendation format, see [references/recommendation-format.md](references/recommendation-format.md) diff --git a/skills/xcode-build-fixer/references/build-settings-best-practices.md b/skills/xcode-build-fixer/references/build-settings-best-practices.md new file mode 100644 index 0000000..9dee131 --- /dev/null +++ b/skills/xcode-build-fixer/references/build-settings-best-practices.md @@ -0,0 +1,216 @@ +# Build Settings Best Practices + +This reference lists Xcode build settings that affect build performance. Use it to audit a project and produce a pass/fail checklist. + +The scope is strictly **build performance**. Do not flag language-migration settings like `SWIFT_STRICT_CONCURRENCY` or `SWIFT_UPCOMING_FEATURE_*` -- those are developer adoption choices unrelated to build speed. + +## How To Read This Reference + +Each setting includes: + +- **Setting name** and the Xcode build-settings key +- **Recommended value** for Debug and Release +- **Why it matters** for build time +- **Risk** of changing it + +Use checkmark and cross indicators when reporting: + +- `[x]` -- setting matches the recommended value +- `[ ]` -- setting does not match; include the actual value and the expected value + +## Debug Configuration + +These settings optimize for fast iteration during development. + +### Compilation Mode + +- **Key:** `SWIFT_COMPILATION_MODE` +- **Recommended:** `singlefile` (Xcode UI: "Incremental"; or unset -- Xcode defaults to singlefile for Debug) +- **Why:** Single-file mode recompiles only changed files. `wholemodule` recompiles the entire target on every change. +- **Risk:** Low + +### Swift Optimization Level + +- **Key:** `SWIFT_OPTIMIZATION_LEVEL` +- **Recommended:** `-Onone` +- **Why:** Optimization passes add significant compile time. Debug builds do not benefit from runtime speed improvements. +- **Risk:** Low + +### GCC Optimization Level + +- **Key:** `GCC_OPTIMIZATION_LEVEL` +- **Recommended:** `0` +- **Why:** Same rationale as Swift optimization level, but for C/C++/Objective-C sources. +- **Risk:** Low + +### Build Active Architecture Only + +- **Key:** `ONLY_ACTIVE_ARCH` (`BUILD_ACTIVE_ARCHITECTURE_ONLY`) +- **Recommended:** `YES` +- **Why:** Building all architectures doubles or triples compile and link time for no debug benefit. +- **Risk:** Low + +### Debug Information Format + +- **Key:** `DEBUG_INFORMATION_FORMAT` +- **Recommended:** `dwarf` +- **Why:** `dwarf-with-dsym` generates a separate dSYM bundle which adds overhead. Plain `dwarf` embeds debug info directly in the binary, which is sufficient for local debugging. +- **Risk:** Low + +### Enable Testability + +- **Key:** `ENABLE_TESTABILITY` +- **Recommended:** `YES` +- **Why:** Required for `@testable import`. Adds minor overhead by exporting internal symbols, but this is expected during development. +- **Risk:** Low + +### Active Compilation Conditions + +- **Key:** `SWIFT_ACTIVE_COMPILATION_CONDITIONS` +- **Recommended:** Should include `DEBUG` +- **Why:** Guards conditional compilation blocks (e.g., `#if DEBUG`) and ensures debug-only code paths are included. +- **Risk:** Low + +### Eager Linking + +- **Key:** `EAGER_LINKING` +- **Recommended:** `YES` +- **Why:** Allows the linker to start work before all compilation tasks finish, reducing wall-clock build time. Particularly effective for Debug builds where link time is a meaningful fraction of total build time. +- **Risk:** Low + +## Release Configuration + +These settings optimize for production builds. + +### Compilation Mode + +- **Key:** `SWIFT_COMPILATION_MODE` +- **Recommended:** `wholemodule` +- **Why:** Whole-module optimization produces faster runtime code. Build time is secondary for release. +- **Risk:** Low + +### Swift Optimization Level + +- **Key:** `SWIFT_OPTIMIZATION_LEVEL` +- **Recommended:** `-O` or `-Osize` +- **Why:** Produces optimized binaries. `-Osize` trades some speed for smaller binary size. +- **Risk:** Low + +### GCC Optimization Level + +- **Key:** `GCC_OPTIMIZATION_LEVEL` +- **Recommended:** `s` +- **Why:** Optimizes C/C++/Objective-C for size, matching the typical release expectation. +- **Risk:** Low + +### Build Active Architecture Only + +- **Key:** `ONLY_ACTIVE_ARCH` +- **Recommended:** `NO` +- **Why:** Release builds must include all supported architectures for distribution. +- **Risk:** Low + +### Debug Information Format + +- **Key:** `DEBUG_INFORMATION_FORMAT` +- **Recommended:** `dwarf-with-dsym` +- **Why:** dSYM bundles are required for crash symbolication in production. +- **Risk:** Low + +### Enable Testability + +- **Key:** `ENABLE_TESTABILITY` +- **Recommended:** `NO` +- **Why:** Removes internal-symbol export overhead from release builds. Testing should use Debug configuration. +- **Risk:** Low + +## General (All Configurations) + +### Compilation Caching + +- **Key:** `COMPILATION_CACHING` +- **Recommended:** `YES` +- **Why:** Caches compilation results for Swift and C-family sources so repeated compilations of the same inputs are served from cache. The biggest wins come from branch switching and clean builds where source files are recompiled unchanged. This is an opt-in feature. The umbrella setting controls both `SWIFT_ENABLE_COMPILE_CACHE` and `CLANG_ENABLE_COMPILE_CACHE` under the hood; those can be toggled independently if needed. +- **Measurement:** Measured 5-14% faster clean builds across tested projects (87 to 1,991 Swift files). The benefit compounds in real developer workflows where the cache persists between builds -- branch switching, pulling changes, and CI with persistent DerivedData -- though the exact savings depend on how many files change between builds. +- **Risk:** Low -- can also be enabled via per-user project settings so it does not need to be committed to the shared project file. + +### Integrated Swift Driver + +- **Key:** `SWIFT_USE_INTEGRATED_DRIVER` +- **Recommended:** `YES` +- **Why:** Uses the integrated Swift driver which runs inside the build system process, eliminating inter-process overhead for compilation scheduling. Enabled by default in modern Xcode but worth verifying in migrated projects. +- **Risk:** Low + +### Clang Module Compilation + +- **Key:** `CLANG_ENABLE_MODULES` +- **Recommended:** `YES` +- **Why:** Enables Clang module compilation for C/Objective-C sources, caching module maps on disk instead of reprocessing headers on every import. Eliminates redundant header parsing across translation units. +- **Risk:** Low + +### Explicit Module Builds + +- **Key:** `SWIFT_ENABLE_EXPLICIT_MODULES` (C/ObjC enabled by default in Xcode 16+; for Swift use `_EXPERIMENTAL_SWIFT_EXPLICIT_MODULES`) +- **Recommended:** Evaluate per-project +- **Why:** Makes module compilation visible to the build system as discrete tasks, improving parallelism and scheduling. Reduces redundant module rebuilds by making dependency edges explicit. Some projects see regressions due to the overhead of dependency scanning, so benchmark before and after enabling. +- **Risk:** Medium -- test thoroughly; currently experimental for Swift targets. + +## Cross-Target Consistency + +These checks find settings differences between targets that cause redundant build work. + +### Project-Level vs Target-Level Overrides + +Build-affecting settings should be set at the project level unless a target has a specific reason to override. Unnecessary per-target overrides cause confusion and can silently create module variants. + +Settings to check for project-level consistency: + +- `SWIFT_COMPILATION_MODE` +- `SWIFT_OPTIMIZATION_LEVEL` +- `ONLY_ACTIVE_ARCH` +- `DEBUG_INFORMATION_FORMAT` + +### Module Variant Duplication + +When multiple targets import the same SPM package but compile with different Swift compiler options, the build system produces separate module variants for each combination. This inflates `SwiftEmitModule` task counts. + +Check for drift in: + +- `SWIFT_OPTIMIZATION_LEVEL` +- `SWIFT_COMPILATION_MODE` +- `OTHER_SWIFT_FLAGS` +- Target-level build settings that override project defaults + +### Out of Scope + +Do **not** flag the following as build-performance issues: + +- `SWIFT_STRICT_CONCURRENCY` -- language migration choice +- `SWIFT_UPCOMING_FEATURE_*` -- language migration choice +- `SWIFT_APPROACHABLE_CONCURRENCY` -- language migration choice +- `SWIFT_ACTIVE_COMPILATION_CONDITIONS` values beyond `DEBUG` (e.g., `WIDGETS`, `APPCLIP`) -- intentional per-target customization + +## Checklist Output Format + +When reporting results, use this structure: + +```markdown +### Debug Configuration +- [x] `SWIFT_COMPILATION_MODE`: `singlefile` (recommended: `singlefile`) +- [ ] `DEBUG_INFORMATION_FORMAT`: `dwarf-with-dsym` (recommended: `dwarf`) +- [x] `SWIFT_OPTIMIZATION_LEVEL`: `-Onone` (recommended: `-Onone`) +... + +### Release Configuration +- [x] `SWIFT_COMPILATION_MODE`: `wholemodule` (recommended: `wholemodule`) +... + +### General (All Configurations) +- [ ] `COMPILATION_CACHING`: `NO` (recommended: `YES`) +... + +### Cross-Target Consistency +- [x] All targets inherit `SWIFT_OPTIMIZATION_LEVEL` from project level +- [ ] `OTHER_SWIFT_FLAGS` differs between Stock Analyzer and StockAnalyzerClip +... +``` diff --git a/skills/xcode-build-fixer/references/recommendation-format.md b/skills/xcode-build-fixer/references/recommendation-format.md new file mode 100644 index 0000000..46affd6 --- /dev/null +++ b/skills/xcode-build-fixer/references/recommendation-format.md @@ -0,0 +1,85 @@ +# Recommendation Format + +All optimization skills should report recommendations in a shared structure so the orchestrator can merge and prioritize them cleanly. + +## Required Fields + +Each recommendation should include: + +- `title` +- `wait_time_impact` -- plain-language statement of expected wall-clock impact, e.g. "Expected to reduce your clean build by ~3s", "Reduces parallel compile work but unlikely to reduce build wait time", or "Impact on wait time is uncertain -- re-benchmark to confirm" +- `actionability` -- classifies how fixable the issue is from the project (see values below) +- `category` +- `observed_evidence` +- `estimated_impact` +- `confidence` +- `approval_required` +- `benchmark_verification_status` + +### Actionability Values + +Every recommendation must include an `actionability` classification: + +- `repo-local` -- Fix lives entirely in project files, source code, or local configuration. The developer can apply it without side effects outside the repo. +- `package-manager` -- Requires CocoaPods or SPM configuration changes that may have broad side effects (e.g., linkage mode, dependency restructuring). These should be benchmarked before and after. +- `xcode-behavior` -- Observed cost is driven by Xcode internals and is not suppressible from the project. Report the finding for awareness but do not promise a fix. +- `upstream` -- Requires changes in a third-party dependency or external tool. The developer cannot fix it locally. + +## Suggested Optional Fields + +- `scope` +- `affected_files` +- `affected_targets` +- `affected_packages` +- `implementation_notes` +- `risk_level` + +## JSON Example + +```json +{ + "recommendations": [ + { + "title": "Guard a release-only symbol upload script", + "wait_time_impact": "Expected to reduce your incremental build by approximately 6 seconds.", + "actionability": "repo-local", + "category": "project", + "observed_evidence": [ + "Incremental builds spend 6.3 seconds in a run script phase.", + "The script runs for Debug builds even though the output is only needed in Release." + ], + "estimated_impact": "High incremental-build improvement", + "confidence": "High", + "approval_required": true, + "benchmark_verification_status": "Not yet verified", + "scope": "Target build phase", + "risk_level": "Low" + } + ] +} +``` + +## Markdown Rendering Guidance + +When rendering for human review, preserve the same field order: + +1. title +2. wait-time impact +3. actionability +4. observed evidence +5. estimated impact +6. confidence +7. approval required +8. benchmark verification status + +That makes it easier for the developer to approve or reject specific items quickly. + +## Verification Status Values + +Recommended values: + +- `Not yet verified` +- `Queued for verification` +- `Verified improvement` +- `No measurable improvement` +- `Inconclusive due to benchmark noise` diff --git a/skills/xcode-build-fixer/scripts/benchmark_builds.py b/skills/xcode-build-fixer/scripts/benchmark_builds.py new file mode 100755 index 0000000..3e27788 --- /dev/null +++ b/skills/xcode-build-fixer/scripts/benchmark_builds.py @@ -0,0 +1,308 @@ +#!/usr/bin/env python3 + +import argparse +import json +import os +import platform +import re +import shutil +import statistics +import subprocess +import sys +import tempfile +import time +from datetime import datetime, timezone +from pathlib import Path +from typing import Dict, List, Optional + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Benchmark Xcode clean and incremental builds.") + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument("--workspace", help="Path to the .xcworkspace file") + group.add_argument("--project", help="Path to the .xcodeproj file") + parser.add_argument("--scheme", required=True, help="Scheme to build") + parser.add_argument("--configuration", default="Debug", help="Build configuration") + parser.add_argument("--destination", help="xcodebuild destination string") + parser.add_argument("--derived-data-path", help="DerivedData path override") + parser.add_argument("--output-dir", default=".build-benchmark", help="Output directory for artifacts") + parser.add_argument("--repeats", type=int, default=3, help="Measured runs per build type") + parser.add_argument("--skip-warmup", action="store_true", help="Skip the validation build") + parser.add_argument( + "--touch-file", + help="Path to a source file to touch before each incremental build. " + "When provided, measures a real edit-rebuild loop instead of a zero-change build.", + ) + parser.add_argument( + "--no-cached-clean", + action="store_true", + help="Skip cached clean builds even when COMPILATION_CACHING is detected.", + ) + parser.add_argument( + "--extra-arg", + action="append", + default=[], + help="Additional xcodebuild argument to append. Can be passed multiple times.", + ) + return parser.parse_args() + + +def command_base(args: argparse.Namespace) -> List[str]: + command = ["xcodebuild"] + if args.workspace: + command.extend(["-workspace", args.workspace]) + if args.project: + command.extend(["-project", args.project]) + command.extend(["-scheme", args.scheme, "-configuration", args.configuration]) + if args.destination: + command.extend(["-destination", args.destination]) + if args.derived_data_path: + command.extend(["-derivedDataPath", args.derived_data_path]) + command.extend(args.extra_arg) + return command + + +def shell_join(parts: List[str]) -> str: + return " ".join(subprocess.list2cmdline([part]) for part in parts) + + +_TASK_COUNT_RE = re.compile(r"^(.+?)\s*\((\d+)\s+tasks?\)$") + + +def _extract_task_count(name: str) -> tuple[str, Optional[int]]: + """Split 'Category (N tasks)' into ('Category', N).""" + match = _TASK_COUNT_RE.match(name) + if match: + return match.group(1).strip(), int(match.group(2)) + return name, None + + +def parse_timing_summary(output: str) -> List[Dict]: + categories: Dict[str, float] = {} + task_counts: Dict[str, Optional[int]] = {} + for raw_line in output.splitlines(): + line = raw_line.strip() + if not line: + continue + for suffix in (" seconds", " second", " sec"): + if not line.endswith(suffix): + continue + trimmed = line[: -len(suffix)] + if "|" in trimmed: + name_part, _, seconds_text = trimmed.rpartition("|") + else: + name_part, _, seconds_text = trimmed.rpartition(" ") + try: + seconds = float(seconds_text.strip()) + except ValueError: + continue + cleaned_name = name_part.replace(" ", " ").strip(" -:") + if len(cleaned_name) < 3: + continue + base_name, count = _extract_task_count(cleaned_name) + categories[base_name] = categories.get(base_name, 0.0) + seconds + if count is not None: + task_counts[base_name] = (task_counts.get(base_name) or 0) + count + break + result: List[Dict] = [] + for name, seconds in sorted(categories.items(), key=lambda item: item[1], reverse=True): + entry: Dict = {"name": name, "seconds": round(seconds, 3)} + if name in task_counts: + entry["task_count"] = task_counts[name] + result.append(entry) + return result + + +def run_command(command: List[str]) -> subprocess.CompletedProcess: + return subprocess.run(command, capture_output=True, text=True) + + +def stats_for(runs: List[Dict[str, object]]) -> Dict[str, float]: + durations = [run["duration_seconds"] for run in runs if run.get("success")] + if not durations: + return { + "count": 0, + "min_seconds": 0.0, + "max_seconds": 0.0, + "median_seconds": 0.0, + "average_seconds": 0.0, + } + return { + "count": len(durations), + "min_seconds": round(min(durations), 3), + "max_seconds": round(max(durations), 3), + "median_seconds": round(statistics.median(durations), 3), + "average_seconds": round(statistics.fmean(durations), 3), + } + + +def xcode_version() -> str: + result = run_command(["xcodebuild", "-version"]) + return result.stdout.strip() if result.returncode == 0 else "unknown" + + +def detect_compilation_caching(base_command: List[str]) -> bool: + """Check whether COMPILATION_CACHING is enabled in the resolved build settings.""" + result = run_command([*base_command, "-showBuildSettings"]) + if result.returncode != 0: + return False + for line in result.stdout.splitlines(): + stripped = line.strip() + if stripped.startswith("COMPILATION_CACHING") and "=" in stripped: + value = stripped.split("=", 1)[1].strip() + return value == "YES" + return False + + +def measure_build( + base_command: List[str], + artifact_stem: str, + output_dir: Path, + build_type: str, + run_index: int, +) -> Dict[str, object]: + build_command = [*base_command, "build", "-showBuildTimingSummary"] + started = time.perf_counter() + result = run_command(build_command) + elapsed = round(time.perf_counter() - started, 3) + log_path = output_dir / f"{artifact_stem}-{build_type}-{run_index}.log" + log_path.write_text(result.stdout + result.stderr) + return { + "id": f"{build_type}-{run_index}", + "build_type": build_type, + "duration_seconds": elapsed, + "success": result.returncode == 0, + "exit_code": result.returncode, + "command": shell_join(build_command), + "raw_log_path": str(log_path), + "timing_summary_categories": parse_timing_summary(result.stdout + result.stderr), + } + + +def main() -> int: + args = parse_args() + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + artifact_stem = f"{timestamp}-{args.scheme.replace(' ', '-').lower()}" + base_command = command_base(args) + + if not args.skip_warmup: + warmup = run_command([*base_command, "build"]) + if warmup.returncode != 0: + sys.stderr.write(warmup.stdout + warmup.stderr) + return warmup.returncode + warmup_clean = run_command([*base_command, "clean"]) + if warmup_clean.returncode != 0: + sys.stderr.write(warmup_clean.stdout + warmup_clean.stderr) + return warmup_clean.returncode + warmup_rebuild = run_command([*base_command, "build"]) + if warmup_rebuild.returncode != 0: + sys.stderr.write(warmup_rebuild.stdout + warmup_rebuild.stderr) + return warmup_rebuild.returncode + + runs: Dict[str, list] = {"clean": [], "incremental": []} + + for index in range(1, args.repeats + 1): + clean_result = run_command([*base_command, "clean"]) + clean_log_path = output_dir / f"{artifact_stem}-clean-prep-{index}.log" + clean_log_path.write_text(clean_result.stdout + clean_result.stderr) + if clean_result.returncode != 0: + sys.stderr.write(clean_result.stdout + clean_result.stderr) + return clean_result.returncode + runs["clean"].append(measure_build(base_command, artifact_stem, output_dir, "clean", index)) + + # --- Cached clean builds --------------------------------------------------- + # When COMPILATION_CACHING is enabled, the compilation cache lives outside + # DerivedData and survives product deletion. We measure "cached clean" + # builds by pointing DerivedData at a temp directory, warming the cache with + # one build, then deleting the DerivedData directory (but not the cache) + # before each measured rebuild. This captures the realistic scenario: + # branch switching, pulling changes, or Clean Build Folder. + should_cached_clean = not args.no_cached_clean and detect_compilation_caching(base_command) + if should_cached_clean: + dd_path = Path(args.derived_data_path) if args.derived_data_path else Path( + tempfile.mkdtemp(prefix="xcode-bench-dd-") + ) + cached_cmd = list(base_command) + if not args.derived_data_path: + cached_cmd.extend(["-derivedDataPath", str(dd_path)]) + + cache_warmup = run_command([*cached_cmd, "build"]) + if cache_warmup.returncode != 0: + sys.stderr.write("Warning: cached clean warmup build failed, skipping cached clean benchmarks.\n") + sys.stderr.write(cache_warmup.stdout + cache_warmup.stderr) + should_cached_clean = False + + if should_cached_clean: + runs["cached_clean"] = [] + for index in range(1, args.repeats + 1): + shutil.rmtree(dd_path, ignore_errors=True) + runs["cached_clean"].append( + measure_build(cached_cmd, artifact_stem, output_dir, "cached-clean", index) + ) + shutil.rmtree(dd_path, ignore_errors=True) + + # --- Incremental / zero-change builds -------------------------------------- + incremental_label = "incremental" + if args.touch_file: + touch_path = Path(args.touch_file) + if not touch_path.exists(): + sys.stderr.write(f"--touch-file path does not exist: {touch_path}\n") + return 1 + incremental_label = "incremental" + else: + incremental_label = "zero-change" + + for index in range(1, args.repeats + 1): + if args.touch_file: + touch_path.touch() + runs["incremental"].append( + measure_build(base_command, artifact_stem, output_dir, incremental_label, index) + ) + + summary: Dict[str, object] = { + "clean": stats_for(runs["clean"]), + "incremental": stats_for(runs["incremental"]), + } + if "cached_clean" in runs: + summary["cached_clean"] = stats_for(runs["cached_clean"]) + + artifact = { + "schema_version": "1.2.0" if "cached_clean" in runs else "1.1.0", + "created_at": datetime.now(timezone.utc).isoformat(), + "build": { + "entrypoint": "workspace" if args.workspace else "project", + "path": args.workspace or args.project, + "scheme": args.scheme, + "configuration": args.configuration, + "destination": args.destination or "", + "derived_data_path": args.derived_data_path or "", + "command": shell_join(base_command), + }, + "environment": { + "host": platform.node(), + "macos_version": platform.platform(), + "xcode_version": xcode_version(), + "cwd": os.getcwd(), + }, + "runs": runs, + "summary": summary, + "notes": [f"touch-file: {args.touch_file}"] if args.touch_file else [], + } + + artifact_path = output_dir / f"{artifact_stem}.json" + artifact_path.write_text(json.dumps(artifact, indent=2) + "\n") + + print(f"Saved benchmark artifact: {artifact_path}") + print(f"Clean median: {artifact['summary']['clean']['median_seconds']}s") + if "cached_clean" in artifact["summary"]: + print(f"Cached clean median: {artifact['summary']['cached_clean']['median_seconds']}s") + inc_label = "Incremental" if args.touch_file else "Zero-change" + print(f"{inc_label} median: {artifact['summary']['incremental']['median_seconds']}s") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/xcode-build-orchestrator/SKILL.md b/skills/xcode-build-orchestrator/SKILL.md index 874d47e..f099017 100644 --- a/skills/xcode-build-orchestrator/SKILL.md +++ b/skills/xcode-build-orchestrator/SKILL.md @@ -151,6 +151,6 @@ python3 scripts/generate_optimization_report.py \ ## Additional Resources - For the report template, see [references/orchestration-report-template.md](references/orchestration-report-template.md) -- For benchmark artifact requirements, see [../../references/benchmark-artifacts.md](../../references/benchmark-artifacts.md) -- For the recommendation format, see [../../references/recommendation-format.md](../../references/recommendation-format.md) -- For build settings best practices, see [../../references/build-settings-best-practices.md](../../references/build-settings-best-practices.md) +- For benchmark artifact requirements, see [references/benchmark-artifacts.md](references/benchmark-artifacts.md) +- For the recommendation format, see [references/recommendation-format.md](references/recommendation-format.md) +- For build settings best practices, see [references/build-settings-best-practices.md](references/build-settings-best-practices.md) diff --git a/skills/xcode-build-orchestrator/references/benchmark-artifacts.md b/skills/xcode-build-orchestrator/references/benchmark-artifacts.md new file mode 100644 index 0000000..fbd5ebf --- /dev/null +++ b/skills/xcode-build-orchestrator/references/benchmark-artifacts.md @@ -0,0 +1,94 @@ +# Benchmark Artifacts + +All skills in this repository should treat `.build-benchmark/` as the canonical location for measured build evidence. + +## Goals + +- Keep build measurements reproducible. +- Make clean and incremental build data easy to compare. +- Preserve enough context for later specialist analysis without rerunning the benchmark. + +## Wall-Clock vs Cumulative Task Time + +The `duration_seconds` field on each run and the `median_seconds` in the summary represent **wall-clock time** -- how long the developer actually waits. This is the primary success metric. + +The `timing_summary_categories` are **aggregated task times** parsed from Xcode's Build Timing Summary. Because Xcode runs many tasks in parallel across CPU cores, these totals typically exceed the wall-clock duration. A large cumulative `SwiftCompile` value is diagnostic evidence of compiler workload, not proof that compilation is blocking the build. Always compare category totals against the wall-clock median before concluding that a category is a bottleneck. + +## File Layout + +Recommended outputs: + +- `.build-benchmark/-.json` +- `.build-benchmark/--clean-1.log` +- `.build-benchmark/--clean-2.log` +- `.build-benchmark/--clean-3.log` +- `.build-benchmark/--cached-clean-1.log` (when COMPILATION_CACHING is enabled) +- `.build-benchmark/--cached-clean-2.log` +- `.build-benchmark/--cached-clean-3.log` +- `.build-benchmark/--incremental-1.log` +- `.build-benchmark/--incremental-2.log` +- `.build-benchmark/--incremental-3.log` + +Use an ISO-like UTC timestamp without spaces so the files sort naturally. + +## Artifact Requirements + +Each JSON artifact should include: + +- schema version +- creation timestamp +- project context +- environment details when available +- the normalized build command +- separate `clean` and `incremental` run arrays +- summary statistics for each build type +- parsed timing-summary categories +- free-form notes for caveats or noise + +## Clean, Cached Clean, And Incremental Separation + +Do not merge different build type measurements into a single list. They answer different questions: + +- **Clean builds** show full build-system, package, and module setup cost with a cold compilation cache. +- **Cached clean builds** show clean build cost when the compilation cache is warm. This is the realistic scenario for branch switching, pulling changes, or Clean Build Folder. Only present when `COMPILATION_CACHING = YES` is detected. +- **Incremental builds** show edit-loop productivity and script or cache invalidation problems. + +## Raw Logs + +Store raw `xcodebuild` output beside the JSON artifact whenever possible. That allows later skills to: + +- re-parse timing summaries +- inspect failed builds +- search for long type-check warnings +- correlate build-system phases with recommendations + +## Measurement Caveats + +### COMPILATION_CACHING + +`COMPILATION_CACHING = YES` stores compiled artifacts in a system-managed cache outside DerivedData so that repeated compilations of identical inputs are served from cache. The standard clean-build benchmark (`xcodebuild clean` between runs) may add overhead from cache population without showing the corresponding cache-hit benefit. + +The benchmark script automatically detects `COMPILATION_CACHING = YES` and runs a **cached clean** benchmark phase. This phase: + +1. Builds once to warm the compilation cache. +2. Deletes DerivedData (but not the compilation cache) before each measured run. +3. Rebuilds, measuring the cache-hit clean build time. + +The cached clean metric captures the realistic developer experience: branch switching, pulling changes, and Clean Build Folder. Use the cached clean median as the primary comparison metric when evaluating `COMPILATION_CACHING` impact. + +To skip this phase, pass `--no-cached-clean`. + +### First-Run Variance + +The first clean build after the warmup cycle often runs 20-40% slower than subsequent clean builds due to cold OS-level caches (disk I/O, dynamic linker cache, etc.). The benchmark script mitigates this by running a warmup clean+build cycle before measured runs. If variance between the first and later clean runs is still high, prefer the median or min over the mean, and note the variance in the artifact's `notes` field. + +## Shared Consumer Expectations + +Any skill reading a benchmark artifact should be able to identify: + +- what was measured +- how it was measured +- whether the run succeeded +- whether the results are stable enough to compare + +For the authoritative field-level schema, see the `build-benchmark.schema.json` bundled with the xcode-build-benchmark skill. diff --git a/skills/xcode-build-orchestrator/references/build-settings-best-practices.md b/skills/xcode-build-orchestrator/references/build-settings-best-practices.md new file mode 100644 index 0000000..9dee131 --- /dev/null +++ b/skills/xcode-build-orchestrator/references/build-settings-best-practices.md @@ -0,0 +1,216 @@ +# Build Settings Best Practices + +This reference lists Xcode build settings that affect build performance. Use it to audit a project and produce a pass/fail checklist. + +The scope is strictly **build performance**. Do not flag language-migration settings like `SWIFT_STRICT_CONCURRENCY` or `SWIFT_UPCOMING_FEATURE_*` -- those are developer adoption choices unrelated to build speed. + +## How To Read This Reference + +Each setting includes: + +- **Setting name** and the Xcode build-settings key +- **Recommended value** for Debug and Release +- **Why it matters** for build time +- **Risk** of changing it + +Use checkmark and cross indicators when reporting: + +- `[x]` -- setting matches the recommended value +- `[ ]` -- setting does not match; include the actual value and the expected value + +## Debug Configuration + +These settings optimize for fast iteration during development. + +### Compilation Mode + +- **Key:** `SWIFT_COMPILATION_MODE` +- **Recommended:** `singlefile` (Xcode UI: "Incremental"; or unset -- Xcode defaults to singlefile for Debug) +- **Why:** Single-file mode recompiles only changed files. `wholemodule` recompiles the entire target on every change. +- **Risk:** Low + +### Swift Optimization Level + +- **Key:** `SWIFT_OPTIMIZATION_LEVEL` +- **Recommended:** `-Onone` +- **Why:** Optimization passes add significant compile time. Debug builds do not benefit from runtime speed improvements. +- **Risk:** Low + +### GCC Optimization Level + +- **Key:** `GCC_OPTIMIZATION_LEVEL` +- **Recommended:** `0` +- **Why:** Same rationale as Swift optimization level, but for C/C++/Objective-C sources. +- **Risk:** Low + +### Build Active Architecture Only + +- **Key:** `ONLY_ACTIVE_ARCH` (`BUILD_ACTIVE_ARCHITECTURE_ONLY`) +- **Recommended:** `YES` +- **Why:** Building all architectures doubles or triples compile and link time for no debug benefit. +- **Risk:** Low + +### Debug Information Format + +- **Key:** `DEBUG_INFORMATION_FORMAT` +- **Recommended:** `dwarf` +- **Why:** `dwarf-with-dsym` generates a separate dSYM bundle which adds overhead. Plain `dwarf` embeds debug info directly in the binary, which is sufficient for local debugging. +- **Risk:** Low + +### Enable Testability + +- **Key:** `ENABLE_TESTABILITY` +- **Recommended:** `YES` +- **Why:** Required for `@testable import`. Adds minor overhead by exporting internal symbols, but this is expected during development. +- **Risk:** Low + +### Active Compilation Conditions + +- **Key:** `SWIFT_ACTIVE_COMPILATION_CONDITIONS` +- **Recommended:** Should include `DEBUG` +- **Why:** Guards conditional compilation blocks (e.g., `#if DEBUG`) and ensures debug-only code paths are included. +- **Risk:** Low + +### Eager Linking + +- **Key:** `EAGER_LINKING` +- **Recommended:** `YES` +- **Why:** Allows the linker to start work before all compilation tasks finish, reducing wall-clock build time. Particularly effective for Debug builds where link time is a meaningful fraction of total build time. +- **Risk:** Low + +## Release Configuration + +These settings optimize for production builds. + +### Compilation Mode + +- **Key:** `SWIFT_COMPILATION_MODE` +- **Recommended:** `wholemodule` +- **Why:** Whole-module optimization produces faster runtime code. Build time is secondary for release. +- **Risk:** Low + +### Swift Optimization Level + +- **Key:** `SWIFT_OPTIMIZATION_LEVEL` +- **Recommended:** `-O` or `-Osize` +- **Why:** Produces optimized binaries. `-Osize` trades some speed for smaller binary size. +- **Risk:** Low + +### GCC Optimization Level + +- **Key:** `GCC_OPTIMIZATION_LEVEL` +- **Recommended:** `s` +- **Why:** Optimizes C/C++/Objective-C for size, matching the typical release expectation. +- **Risk:** Low + +### Build Active Architecture Only + +- **Key:** `ONLY_ACTIVE_ARCH` +- **Recommended:** `NO` +- **Why:** Release builds must include all supported architectures for distribution. +- **Risk:** Low + +### Debug Information Format + +- **Key:** `DEBUG_INFORMATION_FORMAT` +- **Recommended:** `dwarf-with-dsym` +- **Why:** dSYM bundles are required for crash symbolication in production. +- **Risk:** Low + +### Enable Testability + +- **Key:** `ENABLE_TESTABILITY` +- **Recommended:** `NO` +- **Why:** Removes internal-symbol export overhead from release builds. Testing should use Debug configuration. +- **Risk:** Low + +## General (All Configurations) + +### Compilation Caching + +- **Key:** `COMPILATION_CACHING` +- **Recommended:** `YES` +- **Why:** Caches compilation results for Swift and C-family sources so repeated compilations of the same inputs are served from cache. The biggest wins come from branch switching and clean builds where source files are recompiled unchanged. This is an opt-in feature. The umbrella setting controls both `SWIFT_ENABLE_COMPILE_CACHE` and `CLANG_ENABLE_COMPILE_CACHE` under the hood; those can be toggled independently if needed. +- **Measurement:** Measured 5-14% faster clean builds across tested projects (87 to 1,991 Swift files). The benefit compounds in real developer workflows where the cache persists between builds -- branch switching, pulling changes, and CI with persistent DerivedData -- though the exact savings depend on how many files change between builds. +- **Risk:** Low -- can also be enabled via per-user project settings so it does not need to be committed to the shared project file. + +### Integrated Swift Driver + +- **Key:** `SWIFT_USE_INTEGRATED_DRIVER` +- **Recommended:** `YES` +- **Why:** Uses the integrated Swift driver which runs inside the build system process, eliminating inter-process overhead for compilation scheduling. Enabled by default in modern Xcode but worth verifying in migrated projects. +- **Risk:** Low + +### Clang Module Compilation + +- **Key:** `CLANG_ENABLE_MODULES` +- **Recommended:** `YES` +- **Why:** Enables Clang module compilation for C/Objective-C sources, caching module maps on disk instead of reprocessing headers on every import. Eliminates redundant header parsing across translation units. +- **Risk:** Low + +### Explicit Module Builds + +- **Key:** `SWIFT_ENABLE_EXPLICIT_MODULES` (C/ObjC enabled by default in Xcode 16+; for Swift use `_EXPERIMENTAL_SWIFT_EXPLICIT_MODULES`) +- **Recommended:** Evaluate per-project +- **Why:** Makes module compilation visible to the build system as discrete tasks, improving parallelism and scheduling. Reduces redundant module rebuilds by making dependency edges explicit. Some projects see regressions due to the overhead of dependency scanning, so benchmark before and after enabling. +- **Risk:** Medium -- test thoroughly; currently experimental for Swift targets. + +## Cross-Target Consistency + +These checks find settings differences between targets that cause redundant build work. + +### Project-Level vs Target-Level Overrides + +Build-affecting settings should be set at the project level unless a target has a specific reason to override. Unnecessary per-target overrides cause confusion and can silently create module variants. + +Settings to check for project-level consistency: + +- `SWIFT_COMPILATION_MODE` +- `SWIFT_OPTIMIZATION_LEVEL` +- `ONLY_ACTIVE_ARCH` +- `DEBUG_INFORMATION_FORMAT` + +### Module Variant Duplication + +When multiple targets import the same SPM package but compile with different Swift compiler options, the build system produces separate module variants for each combination. This inflates `SwiftEmitModule` task counts. + +Check for drift in: + +- `SWIFT_OPTIMIZATION_LEVEL` +- `SWIFT_COMPILATION_MODE` +- `OTHER_SWIFT_FLAGS` +- Target-level build settings that override project defaults + +### Out of Scope + +Do **not** flag the following as build-performance issues: + +- `SWIFT_STRICT_CONCURRENCY` -- language migration choice +- `SWIFT_UPCOMING_FEATURE_*` -- language migration choice +- `SWIFT_APPROACHABLE_CONCURRENCY` -- language migration choice +- `SWIFT_ACTIVE_COMPILATION_CONDITIONS` values beyond `DEBUG` (e.g., `WIDGETS`, `APPCLIP`) -- intentional per-target customization + +## Checklist Output Format + +When reporting results, use this structure: + +```markdown +### Debug Configuration +- [x] `SWIFT_COMPILATION_MODE`: `singlefile` (recommended: `singlefile`) +- [ ] `DEBUG_INFORMATION_FORMAT`: `dwarf-with-dsym` (recommended: `dwarf`) +- [x] `SWIFT_OPTIMIZATION_LEVEL`: `-Onone` (recommended: `-Onone`) +... + +### Release Configuration +- [x] `SWIFT_COMPILATION_MODE`: `wholemodule` (recommended: `wholemodule`) +... + +### General (All Configurations) +- [ ] `COMPILATION_CACHING`: `NO` (recommended: `YES`) +... + +### Cross-Target Consistency +- [x] All targets inherit `SWIFT_OPTIMIZATION_LEVEL` from project level +- [ ] `OTHER_SWIFT_FLAGS` differs between Stock Analyzer and StockAnalyzerClip +... +``` diff --git a/skills/xcode-build-orchestrator/references/recommendation-format.md b/skills/xcode-build-orchestrator/references/recommendation-format.md new file mode 100644 index 0000000..46affd6 --- /dev/null +++ b/skills/xcode-build-orchestrator/references/recommendation-format.md @@ -0,0 +1,85 @@ +# Recommendation Format + +All optimization skills should report recommendations in a shared structure so the orchestrator can merge and prioritize them cleanly. + +## Required Fields + +Each recommendation should include: + +- `title` +- `wait_time_impact` -- plain-language statement of expected wall-clock impact, e.g. "Expected to reduce your clean build by ~3s", "Reduces parallel compile work but unlikely to reduce build wait time", or "Impact on wait time is uncertain -- re-benchmark to confirm" +- `actionability` -- classifies how fixable the issue is from the project (see values below) +- `category` +- `observed_evidence` +- `estimated_impact` +- `confidence` +- `approval_required` +- `benchmark_verification_status` + +### Actionability Values + +Every recommendation must include an `actionability` classification: + +- `repo-local` -- Fix lives entirely in project files, source code, or local configuration. The developer can apply it without side effects outside the repo. +- `package-manager` -- Requires CocoaPods or SPM configuration changes that may have broad side effects (e.g., linkage mode, dependency restructuring). These should be benchmarked before and after. +- `xcode-behavior` -- Observed cost is driven by Xcode internals and is not suppressible from the project. Report the finding for awareness but do not promise a fix. +- `upstream` -- Requires changes in a third-party dependency or external tool. The developer cannot fix it locally. + +## Suggested Optional Fields + +- `scope` +- `affected_files` +- `affected_targets` +- `affected_packages` +- `implementation_notes` +- `risk_level` + +## JSON Example + +```json +{ + "recommendations": [ + { + "title": "Guard a release-only symbol upload script", + "wait_time_impact": "Expected to reduce your incremental build by approximately 6 seconds.", + "actionability": "repo-local", + "category": "project", + "observed_evidence": [ + "Incremental builds spend 6.3 seconds in a run script phase.", + "The script runs for Debug builds even though the output is only needed in Release." + ], + "estimated_impact": "High incremental-build improvement", + "confidence": "High", + "approval_required": true, + "benchmark_verification_status": "Not yet verified", + "scope": "Target build phase", + "risk_level": "Low" + } + ] +} +``` + +## Markdown Rendering Guidance + +When rendering for human review, preserve the same field order: + +1. title +2. wait-time impact +3. actionability +4. observed evidence +5. estimated impact +6. confidence +7. approval required +8. benchmark verification status + +That makes it easier for the developer to approve or reject specific items quickly. + +## Verification Status Values + +Recommended values: + +- `Not yet verified` +- `Queued for verification` +- `Verified improvement` +- `No measurable improvement` +- `Inconclusive due to benchmark noise` diff --git a/skills/xcode-build-orchestrator/scripts/benchmark_builds.py b/skills/xcode-build-orchestrator/scripts/benchmark_builds.py new file mode 100755 index 0000000..3e27788 --- /dev/null +++ b/skills/xcode-build-orchestrator/scripts/benchmark_builds.py @@ -0,0 +1,308 @@ +#!/usr/bin/env python3 + +import argparse +import json +import os +import platform +import re +import shutil +import statistics +import subprocess +import sys +import tempfile +import time +from datetime import datetime, timezone +from pathlib import Path +from typing import Dict, List, Optional + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Benchmark Xcode clean and incremental builds.") + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument("--workspace", help="Path to the .xcworkspace file") + group.add_argument("--project", help="Path to the .xcodeproj file") + parser.add_argument("--scheme", required=True, help="Scheme to build") + parser.add_argument("--configuration", default="Debug", help="Build configuration") + parser.add_argument("--destination", help="xcodebuild destination string") + parser.add_argument("--derived-data-path", help="DerivedData path override") + parser.add_argument("--output-dir", default=".build-benchmark", help="Output directory for artifacts") + parser.add_argument("--repeats", type=int, default=3, help="Measured runs per build type") + parser.add_argument("--skip-warmup", action="store_true", help="Skip the validation build") + parser.add_argument( + "--touch-file", + help="Path to a source file to touch before each incremental build. " + "When provided, measures a real edit-rebuild loop instead of a zero-change build.", + ) + parser.add_argument( + "--no-cached-clean", + action="store_true", + help="Skip cached clean builds even when COMPILATION_CACHING is detected.", + ) + parser.add_argument( + "--extra-arg", + action="append", + default=[], + help="Additional xcodebuild argument to append. Can be passed multiple times.", + ) + return parser.parse_args() + + +def command_base(args: argparse.Namespace) -> List[str]: + command = ["xcodebuild"] + if args.workspace: + command.extend(["-workspace", args.workspace]) + if args.project: + command.extend(["-project", args.project]) + command.extend(["-scheme", args.scheme, "-configuration", args.configuration]) + if args.destination: + command.extend(["-destination", args.destination]) + if args.derived_data_path: + command.extend(["-derivedDataPath", args.derived_data_path]) + command.extend(args.extra_arg) + return command + + +def shell_join(parts: List[str]) -> str: + return " ".join(subprocess.list2cmdline([part]) for part in parts) + + +_TASK_COUNT_RE = re.compile(r"^(.+?)\s*\((\d+)\s+tasks?\)$") + + +def _extract_task_count(name: str) -> tuple[str, Optional[int]]: + """Split 'Category (N tasks)' into ('Category', N).""" + match = _TASK_COUNT_RE.match(name) + if match: + return match.group(1).strip(), int(match.group(2)) + return name, None + + +def parse_timing_summary(output: str) -> List[Dict]: + categories: Dict[str, float] = {} + task_counts: Dict[str, Optional[int]] = {} + for raw_line in output.splitlines(): + line = raw_line.strip() + if not line: + continue + for suffix in (" seconds", " second", " sec"): + if not line.endswith(suffix): + continue + trimmed = line[: -len(suffix)] + if "|" in trimmed: + name_part, _, seconds_text = trimmed.rpartition("|") + else: + name_part, _, seconds_text = trimmed.rpartition(" ") + try: + seconds = float(seconds_text.strip()) + except ValueError: + continue + cleaned_name = name_part.replace(" ", " ").strip(" -:") + if len(cleaned_name) < 3: + continue + base_name, count = _extract_task_count(cleaned_name) + categories[base_name] = categories.get(base_name, 0.0) + seconds + if count is not None: + task_counts[base_name] = (task_counts.get(base_name) or 0) + count + break + result: List[Dict] = [] + for name, seconds in sorted(categories.items(), key=lambda item: item[1], reverse=True): + entry: Dict = {"name": name, "seconds": round(seconds, 3)} + if name in task_counts: + entry["task_count"] = task_counts[name] + result.append(entry) + return result + + +def run_command(command: List[str]) -> subprocess.CompletedProcess: + return subprocess.run(command, capture_output=True, text=True) + + +def stats_for(runs: List[Dict[str, object]]) -> Dict[str, float]: + durations = [run["duration_seconds"] for run in runs if run.get("success")] + if not durations: + return { + "count": 0, + "min_seconds": 0.0, + "max_seconds": 0.0, + "median_seconds": 0.0, + "average_seconds": 0.0, + } + return { + "count": len(durations), + "min_seconds": round(min(durations), 3), + "max_seconds": round(max(durations), 3), + "median_seconds": round(statistics.median(durations), 3), + "average_seconds": round(statistics.fmean(durations), 3), + } + + +def xcode_version() -> str: + result = run_command(["xcodebuild", "-version"]) + return result.stdout.strip() if result.returncode == 0 else "unknown" + + +def detect_compilation_caching(base_command: List[str]) -> bool: + """Check whether COMPILATION_CACHING is enabled in the resolved build settings.""" + result = run_command([*base_command, "-showBuildSettings"]) + if result.returncode != 0: + return False + for line in result.stdout.splitlines(): + stripped = line.strip() + if stripped.startswith("COMPILATION_CACHING") and "=" in stripped: + value = stripped.split("=", 1)[1].strip() + return value == "YES" + return False + + +def measure_build( + base_command: List[str], + artifact_stem: str, + output_dir: Path, + build_type: str, + run_index: int, +) -> Dict[str, object]: + build_command = [*base_command, "build", "-showBuildTimingSummary"] + started = time.perf_counter() + result = run_command(build_command) + elapsed = round(time.perf_counter() - started, 3) + log_path = output_dir / f"{artifact_stem}-{build_type}-{run_index}.log" + log_path.write_text(result.stdout + result.stderr) + return { + "id": f"{build_type}-{run_index}", + "build_type": build_type, + "duration_seconds": elapsed, + "success": result.returncode == 0, + "exit_code": result.returncode, + "command": shell_join(build_command), + "raw_log_path": str(log_path), + "timing_summary_categories": parse_timing_summary(result.stdout + result.stderr), + } + + +def main() -> int: + args = parse_args() + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + artifact_stem = f"{timestamp}-{args.scheme.replace(' ', '-').lower()}" + base_command = command_base(args) + + if not args.skip_warmup: + warmup = run_command([*base_command, "build"]) + if warmup.returncode != 0: + sys.stderr.write(warmup.stdout + warmup.stderr) + return warmup.returncode + warmup_clean = run_command([*base_command, "clean"]) + if warmup_clean.returncode != 0: + sys.stderr.write(warmup_clean.stdout + warmup_clean.stderr) + return warmup_clean.returncode + warmup_rebuild = run_command([*base_command, "build"]) + if warmup_rebuild.returncode != 0: + sys.stderr.write(warmup_rebuild.stdout + warmup_rebuild.stderr) + return warmup_rebuild.returncode + + runs: Dict[str, list] = {"clean": [], "incremental": []} + + for index in range(1, args.repeats + 1): + clean_result = run_command([*base_command, "clean"]) + clean_log_path = output_dir / f"{artifact_stem}-clean-prep-{index}.log" + clean_log_path.write_text(clean_result.stdout + clean_result.stderr) + if clean_result.returncode != 0: + sys.stderr.write(clean_result.stdout + clean_result.stderr) + return clean_result.returncode + runs["clean"].append(measure_build(base_command, artifact_stem, output_dir, "clean", index)) + + # --- Cached clean builds --------------------------------------------------- + # When COMPILATION_CACHING is enabled, the compilation cache lives outside + # DerivedData and survives product deletion. We measure "cached clean" + # builds by pointing DerivedData at a temp directory, warming the cache with + # one build, then deleting the DerivedData directory (but not the cache) + # before each measured rebuild. This captures the realistic scenario: + # branch switching, pulling changes, or Clean Build Folder. + should_cached_clean = not args.no_cached_clean and detect_compilation_caching(base_command) + if should_cached_clean: + dd_path = Path(args.derived_data_path) if args.derived_data_path else Path( + tempfile.mkdtemp(prefix="xcode-bench-dd-") + ) + cached_cmd = list(base_command) + if not args.derived_data_path: + cached_cmd.extend(["-derivedDataPath", str(dd_path)]) + + cache_warmup = run_command([*cached_cmd, "build"]) + if cache_warmup.returncode != 0: + sys.stderr.write("Warning: cached clean warmup build failed, skipping cached clean benchmarks.\n") + sys.stderr.write(cache_warmup.stdout + cache_warmup.stderr) + should_cached_clean = False + + if should_cached_clean: + runs["cached_clean"] = [] + for index in range(1, args.repeats + 1): + shutil.rmtree(dd_path, ignore_errors=True) + runs["cached_clean"].append( + measure_build(cached_cmd, artifact_stem, output_dir, "cached-clean", index) + ) + shutil.rmtree(dd_path, ignore_errors=True) + + # --- Incremental / zero-change builds -------------------------------------- + incremental_label = "incremental" + if args.touch_file: + touch_path = Path(args.touch_file) + if not touch_path.exists(): + sys.stderr.write(f"--touch-file path does not exist: {touch_path}\n") + return 1 + incremental_label = "incremental" + else: + incremental_label = "zero-change" + + for index in range(1, args.repeats + 1): + if args.touch_file: + touch_path.touch() + runs["incremental"].append( + measure_build(base_command, artifact_stem, output_dir, incremental_label, index) + ) + + summary: Dict[str, object] = { + "clean": stats_for(runs["clean"]), + "incremental": stats_for(runs["incremental"]), + } + if "cached_clean" in runs: + summary["cached_clean"] = stats_for(runs["cached_clean"]) + + artifact = { + "schema_version": "1.2.0" if "cached_clean" in runs else "1.1.0", + "created_at": datetime.now(timezone.utc).isoformat(), + "build": { + "entrypoint": "workspace" if args.workspace else "project", + "path": args.workspace or args.project, + "scheme": args.scheme, + "configuration": args.configuration, + "destination": args.destination or "", + "derived_data_path": args.derived_data_path or "", + "command": shell_join(base_command), + }, + "environment": { + "host": platform.node(), + "macos_version": platform.platform(), + "xcode_version": xcode_version(), + "cwd": os.getcwd(), + }, + "runs": runs, + "summary": summary, + "notes": [f"touch-file: {args.touch_file}"] if args.touch_file else [], + } + + artifact_path = output_dir / f"{artifact_stem}.json" + artifact_path.write_text(json.dumps(artifact, indent=2) + "\n") + + print(f"Saved benchmark artifact: {artifact_path}") + print(f"Clean median: {artifact['summary']['clean']['median_seconds']}s") + if "cached_clean" in artifact["summary"]: + print(f"Cached clean median: {artifact['summary']['cached_clean']['median_seconds']}s") + inc_label = "Incremental" if args.touch_file else "Zero-change" + print(f"{inc_label} median: {artifact['summary']['incremental']['median_seconds']}s") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/xcode-build-orchestrator/scripts/diagnose_compilation.py b/skills/xcode-build-orchestrator/scripts/diagnose_compilation.py new file mode 100755 index 0000000..bb97085 --- /dev/null +++ b/skills/xcode-build-orchestrator/scripts/diagnose_compilation.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python3 + +"""Run a single Xcode build with -Xfrontend diagnostics to find slow type-checking.""" + +import argparse +import json +import re +import subprocess +import sys +import time +from datetime import datetime, timezone +from pathlib import Path +from typing import Dict, List, Optional + +_TYPECHECK_RE = re.compile( + r"^(?P.+?):(?P\d+):(?P\d+): warning: " + r"(?Pinstance method|global function|getter|type-check|expression) " + r"'?(?P[^']*?)'?\s+took\s+(?P\d+)ms\s+to\s+type-check" +) + +_EXPRESSION_RE = re.compile( + r"^(?P.+?):(?P\d+):(?P\d+): warning: " + r"expression took\s+(?P\d+)ms\s+to\s+type-check" +) + +_FILE_TIME_RE = re.compile( + r"^\s*(?P\d+(?:\.\d+)?)\s+seconds\s+.*\s+compiling\s+(?P\S+)" +) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Run an Xcode build with -Xfrontend type-checking diagnostics." + ) + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument("--workspace", help="Path to the .xcworkspace file") + group.add_argument("--project", help="Path to the .xcodeproj file") + parser.add_argument("--scheme", required=True, help="Scheme to build") + parser.add_argument("--configuration", default="Debug", help="Build configuration") + parser.add_argument("--destination", help="xcodebuild destination string") + parser.add_argument("--derived-data-path", help="DerivedData path override") + parser.add_argument("--output-dir", default=".build-benchmark", help="Output directory") + parser.add_argument( + "--threshold", + type=int, + default=100, + help="Millisecond threshold for -warn-long-function-bodies and " + "-warn-long-expression-type-checking (default: 100)", + ) + parser.add_argument("--skip-clean", action="store_true", help="Skip clean before build") + parser.add_argument( + "--per-file-timing", + action="store_true", + help="Add -Xfrontend -debug-time-compilation to report per-file compile times.", + ) + parser.add_argument( + "--stats-output", + action="store_true", + help="Add -Xfrontend -stats-output-dir to collect detailed compiler statistics.", + ) + parser.add_argument( + "--extra-arg", + action="append", + default=[], + help="Additional xcodebuild argument. Can be passed multiple times.", + ) + return parser.parse_args() + + +def command_base(args: argparse.Namespace) -> List[str]: + command = ["xcodebuild"] + if args.workspace: + command.extend(["-workspace", args.workspace]) + if args.project: + command.extend(["-project", args.project]) + command.extend(["-scheme", args.scheme, "-configuration", args.configuration]) + if args.destination: + command.extend(["-destination", args.destination]) + if args.derived_data_path: + command.extend(["-derivedDataPath", args.derived_data_path]) + command.extend(args.extra_arg) + return command + + +def parse_diagnostics(output: str) -> List[Dict]: + """Extract type-checking warnings from xcodebuild output.""" + warnings: List[Dict] = [] + seen = set() + for raw_line in output.splitlines(): + line = raw_line.strip() + match = _TYPECHECK_RE.match(line) + if match: + key = (match.group("file"), match.group("line"), match.group("col"), "function-body") + if key in seen: + continue + seen.add(key) + warnings.append( + { + "file": match.group("file"), + "line": int(match.group("line")), + "column": int(match.group("col")), + "duration_ms": int(match.group("ms")), + "kind": "function-body", + "name": match.group("name"), + } + ) + continue + match = _EXPRESSION_RE.match(line) + if match: + key = (match.group("file"), match.group("line"), match.group("col"), "expression") + if key in seen: + continue + seen.add(key) + warnings.append( + { + "file": match.group("file"), + "line": int(match.group("line")), + "column": int(match.group("col")), + "duration_ms": int(match.group("ms")), + "kind": "expression", + "name": "", + } + ) + warnings.sort(key=lambda w: w["duration_ms"], reverse=True) + return warnings + + +def parse_file_timings(output: str) -> List[Dict]: + """Extract per-file compile times from -debug-time-compilation output.""" + timings: List[Dict] = [] + seen = set() + for raw_line in output.splitlines(): + match = _FILE_TIME_RE.match(raw_line.strip()) + if match: + filepath = match.group("file") + if filepath in seen: + continue + seen.add(filepath) + timings.append( + { + "file": filepath, + "duration_seconds": float(match.group("seconds")), + } + ) + timings.sort(key=lambda t: t["duration_seconds"], reverse=True) + return timings + + +def main() -> int: + args = parse_args() + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + scheme_slug = args.scheme.replace(" ", "-").lower() + artifact_stem = f"{timestamp}-{scheme_slug}" + base = command_base(args) + + if not args.skip_clean: + print("Cleaning build products...") + clean = subprocess.run([*base, "clean"], capture_output=True, text=True) + if clean.returncode != 0: + sys.stderr.write(clean.stdout + clean.stderr) + return clean.returncode + + threshold = str(args.threshold) + swift_flags = ( + f"$(inherited) -Xfrontend -warn-long-function-bodies={threshold} " + f"-Xfrontend -warn-long-expression-type-checking={threshold}" + ) + if args.per_file_timing: + swift_flags += " -Xfrontend -debug-time-compilation" + + stats_dir: Optional[Path] = None + if args.stats_output: + stats_dir = output_dir / f"{artifact_stem}-stats" + stats_dir.mkdir(parents=True, exist_ok=True) + swift_flags += f" -Xfrontend -stats-output-dir -Xfrontend {stats_dir}" + + build_command = [ + *base, + "build", + "-showBuildTimingSummary", + f"OTHER_SWIFT_FLAGS={swift_flags}", + ] + + extras = [] + if args.per_file_timing: + extras.append("per-file timing") + if args.stats_output: + extras.append("stats output") + extras_label = f" + {', '.join(extras)}" if extras else "" + print(f"Building with type-check threshold {threshold}ms{extras_label}...") + started = time.perf_counter() + result = subprocess.run(build_command, capture_output=True, text=True) + elapsed = round(time.perf_counter() - started, 3) + + combined_output = result.stdout + result.stderr + log_path = output_dir / f"{artifact_stem}-diagnostics.log" + log_path.write_text(combined_output) + + warnings = parse_diagnostics(combined_output) + + file_timings: Optional[List[Dict]] = None + if args.per_file_timing: + file_timings = parse_file_timings(combined_output) + + artifact = { + "schema_version": "1.0.0", + "created_at": datetime.now(timezone.utc).isoformat(), + "type": "compilation-diagnostics", + "build": { + "entrypoint": "workspace" if args.workspace else "project", + "path": args.workspace or args.project, + "scheme": args.scheme, + "configuration": args.configuration, + "destination": args.destination or "", + }, + "threshold_ms": args.threshold, + "build_duration_seconds": elapsed, + "build_success": result.returncode == 0, + "raw_log_path": str(log_path), + "warnings": warnings, + "summary": { + "total_warnings": len(warnings), + "function_body_warnings": sum(1 for w in warnings if w["kind"] == "function-body"), + "expression_warnings": sum(1 for w in warnings if w["kind"] == "expression"), + "slowest_ms": warnings[0]["duration_ms"] if warnings else 0, + }, + } + + if file_timings is not None: + artifact["per_file_timings"] = file_timings + if stats_dir is not None: + artifact["stats_dir"] = str(stats_dir) + + artifact_path = output_dir / f"{artifact_stem}-diagnostics.json" + artifact_path.write_text(json.dumps(artifact, indent=2) + "\n") + + print(f"\nSaved diagnostics artifact: {artifact_path}") + print(f"Build {'succeeded' if result.returncode == 0 else 'failed'} in {elapsed}s") + print(f"Found {len(warnings)} type-check warnings above {threshold}ms threshold\n") + + if warnings: + print(f"{'Duration':>10} {'Kind':<15} {'Location'}") + print(f"{'--------':>10} {'----':<15} {'--------'}") + for w in warnings[:20]: + loc = f"{w['file']}:{w['line']}:{w['column']}" + label = w["name"] if w["name"] else "(expression)" + print(f"{w['duration_ms']:>8}ms {w['kind']:<15} {loc} {label}") + if len(warnings) > 20: + print(f"\n ... and {len(warnings) - 20} more (see {artifact_path})") + else: + print("No type-checking hotspots found above threshold.") + + if file_timings: + print(f"\nPer-file compile times (top 20):\n") + print(f"{'Duration':>12} {'File'}") + print(f"{'--------':>12} {'----'}") + for t in file_timings[:20]: + print(f"{t['duration_seconds']:>10.3f}s {t['file']}") + if len(file_timings) > 20: + print(f"\n ... and {len(file_timings) - 20} more (see {artifact_path})") + + if stats_dir is not None: + stat_files = list(stats_dir.glob("*.json")) + print(f"\nCompiler statistics: {len(stat_files)} files written to {stats_dir}") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/xcode-build-orchestrator/scripts/generate_optimization_report.py b/skills/xcode-build-orchestrator/scripts/generate_optimization_report.py new file mode 100644 index 0000000..086ec22 --- /dev/null +++ b/skills/xcode-build-orchestrator/scripts/generate_optimization_report.py @@ -0,0 +1,533 @@ +#!/usr/bin/env python3 + +"""Generate a Markdown optimization report from benchmark and diagnostics artifacts.""" + +import argparse +import json +import re +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple + + +# --------------------------------------------------------------------------- +# pbxproj helpers +# --------------------------------------------------------------------------- + +_SETTING_RE = re.compile(r"^\s*([A-Z_][A-Z_0-9]*)\s*=\s*(.+?)\s*;", re.MULTILINE) + +_CONFIG_ID_RE = re.compile(r"([0-9A-F]{24})\s*/\*\s*(Debug|Release)\s*\*/") + +_CONFIG_LIST_RE = re.compile( + r"([0-9A-F]{24})\s*/\*\s*Build configuration list for " + r"(?PPBXProject|PBXNativeTarget)\s+\"(?P[^\"]+)\"\s*\*/" +) + + +def _parse_all_build_configs(pbxproj: str) -> Dict[str, Tuple[str, Dict[str, str]]]: + """Return {config_id: (config_name, {key: value})} for every XCBuildConfiguration.""" + configs: Dict[str, Tuple[str, Dict[str, str]]] = {} + for match in re.finditer( + r"([0-9A-F]{24})\s*/\*\s*(Debug|Release)\s*\*/\s*=\s*\{\s*" + r"isa\s*=\s*XCBuildConfiguration;\s*buildSettings\s*=\s*\{([^}]*)\}", + pbxproj, + re.DOTALL, + ): + config_id = match.group(1) + config_name = match.group(2) + body = match.group(3) + settings: Dict[str, str] = {} + for s in _SETTING_RE.finditer(body): + val = s.group(2).strip().strip('"') + settings[s.group(1)] = val + configs[config_id] = (config_name, settings) + return configs + + +def _resolve_config_list( + pbxproj: str, all_configs: Dict[str, Tuple[str, Dict[str, str]]], kind: str +) -> Dict[str, Dict[str, Dict[str, str]]]: + """Resolve configuration lists for a given kind (PBXProject or PBXNativeTarget).""" + results: Dict[str, Dict[str, Dict[str, str]]] = {} + for list_match in _CONFIG_LIST_RE.finditer(pbxproj): + if list_match.group("kind") != kind: + continue + entity_name = list_match.group("name") + list_id = list_match.group(1) + block_start = pbxproj.find(f"{list_id} /*", list_match.end()) + if block_start == -1: + block_start = list_match.start() + block = pbxproj[block_start : block_start + 500] + configs: Dict[str, Dict[str, str]] = {} + for cid_match in _CONFIG_ID_RE.finditer(block): + cid = cid_match.group(1) + if cid in all_configs: + cname, settings = all_configs[cid] + configs[cname] = settings + if configs: + results[entity_name] = configs + return results + + +def _parse_project_level_configs(pbxproj: str) -> Dict[str, Dict[str, str]]: + """Extract project-level Debug and Release build settings.""" + all_configs = _parse_all_build_configs(pbxproj) + resolved = _resolve_config_list(pbxproj, all_configs, "PBXProject") + if resolved: + return next(iter(resolved.values())) + return {} + + +def _parse_target_configs(pbxproj: str) -> Dict[str, Dict[str, Dict[str, str]]]: + """Extract per-target Debug and Release build settings.""" + all_configs = _parse_all_build_configs(pbxproj) + return _resolve_config_list(pbxproj, all_configs, "PBXNativeTarget") + + +# --------------------------------------------------------------------------- +# Best-practices audit +# --------------------------------------------------------------------------- + +_DEBUG_EXPECTATIONS: List[Tuple[str, str, str]] = [ + ("SWIFT_COMPILATION_MODE", "incremental", "Incremental recompiles only changed files"), + ("SWIFT_OPTIMIZATION_LEVEL", "-Onone", "Optimization passes add compile time without debug benefit"), + ("GCC_OPTIMIZATION_LEVEL", "0", "C/ObjC optimization adds compile time without debug benefit"), + ("ONLY_ACTIVE_ARCH", "YES", "Building all architectures multiplies compile and link time"), + ("DEBUG_INFORMATION_FORMAT", "dwarf", "dwarf-with-dsym generates a separate dSYM, adding overhead"), + ("ENABLE_TESTABILITY", "YES", "Required for @testable import during development"), + ("EAGER_LINKING", "YES", "Allows linker to start before all compilation finishes, reducing wall-clock time"), +] + +_GENERAL_EXPECTATIONS: List[Tuple[str, str, str]] = [ + ("COMPILATION_CACHING", "YES", "Caches compilation results so repeat builds of unchanged inputs are served from cache. Measured 5-14% faster clean builds across tested projects; benefit compounds during branch switching and pulling changes"), +] + +_RELEASE_EXPECTATIONS: List[Tuple[str, str, str]] = [ + ("SWIFT_COMPILATION_MODE", "wholemodule", "Whole-module optimization produces faster runtime code"), + ("SWIFT_OPTIMIZATION_LEVEL", "-O", "Optimized binaries for production (-Osize also acceptable)"), + ("GCC_OPTIMIZATION_LEVEL", "s", "Optimizes C/ObjC for size in release"), + ("ONLY_ACTIVE_ARCH", "NO", "Release builds must include all architectures for distribution"), + ("DEBUG_INFORMATION_FORMAT", "dwarf-with-dsym", "dSYM bundles are needed for crash symbolication"), + ("ENABLE_TESTABILITY", "NO", "Removes internal-symbol export overhead from release builds"), +] + +_CONSISTENCY_KEYS = [ + "SWIFT_COMPILATION_MODE", + "SWIFT_OPTIMIZATION_LEVEL", + "ONLY_ACTIVE_ARCH", + "DEBUG_INFORMATION_FORMAT", +] + + +def _effective_value( + project: Dict[str, str], target: Dict[str, str], key: str +) -> Optional[str]: + return target.get(key, project.get(key)) + + +def _check(actual: Optional[str], expected: str) -> bool: + if actual is None: + if expected in ("incremental",): + return True + return False + if expected == "-O" and actual in ("-O", '"-O"', '"-Osize"', "-Osize"): + return True + return actual.strip('"') == expected + + +def _merged_project_settings( + project_configs: Dict[str, Dict[str, str]], +) -> Dict[str, str]: + """Return a flat dict of all settings across Debug and Release for general checks.""" + merged: Dict[str, str] = {} + for config in project_configs.values(): + merged.update(config) + return merged + + +def _audit_config( + project_settings: Dict[str, str], + expectations: List[Tuple[str, str, str]], + config_name: str, +) -> List[str]: + lines: List[str] = [] + for key, expected, _reason in expectations: + actual = project_settings.get(key) + display_actual = actual if actual else "(unset)" + passed = _check(actual, expected) + mark = "[x]" if passed else "[ ]" + lines.append(f"- {mark} `{key}`: `{display_actual}` (recommended: `{expected}`)") + return lines + + +def _audit_consistency( + project_configs: Dict[str, Dict[str, str]], + target_configs: Dict[str, Dict[str, Dict[str, str]]], +) -> List[str]: + lines: List[str] = [] + for key in _CONSISTENCY_KEYS: + overrides = [] + for target_name, configs in target_configs.items(): + for config_name in ("Debug", "Release"): + target_settings = configs.get(config_name, {}) + if key in target_settings: + proj_val = project_configs.get(config_name, {}).get(key, "(unset)") + tgt_val = target_settings[key] + if tgt_val != proj_val: + overrides.append( + f"{target_name} ({config_name}): `{tgt_val}` vs project `{proj_val}`" + ) + if overrides: + lines.append(f"- [ ] `{key}` has target-level overrides:") + for o in overrides: + lines.append(f" - {o}") + else: + lines.append(f"- [x] `{key}` is consistent across all targets") + return lines + + +# --------------------------------------------------------------------------- +# Auto-generated recommendations from audit +# --------------------------------------------------------------------------- + + +def _auto_recommendations_from_audit( + project_configs: Dict[str, Dict[str, str]], +) -> Dict[str, Any]: + """Generate basic recommendations from failing build settings audit checks.""" + items: List[Dict[str, str]] = [] + + debug_settings = project_configs.get("Debug", {}) + for key, expected, reason in _DEBUG_EXPECTATIONS: + if not _check(debug_settings.get(key), expected): + actual = debug_settings.get(key, "(unset)") + items.append({ + "title": f"Set `{key}` to `{expected}` for Debug", + "category": "build-settings", + "observed_evidence": f"Current value: `{actual}`. {reason}.", + "estimated_impact": "Medium", + "confidence": "High", + "risk_level": "Low", + }) + + merged = {} + for config in project_configs.values(): + merged.update(config) + for key, expected, reason in _GENERAL_EXPECTATIONS: + if not _check(merged.get(key), expected): + actual = merged.get(key, "(unset)") + items.append({ + "title": f"Enable `{key} = {expected}`", + "category": "build-settings", + "observed_evidence": f"Current value: `{actual}`. {reason}.", + "estimated_impact": "High", + "confidence": "High", + "risk_level": "Low", + }) + + release_settings = project_configs.get("Release", {}) + for key, expected, reason in _RELEASE_EXPECTATIONS: + if not _check(release_settings.get(key), expected): + actual = release_settings.get(key, "(unset)") + items.append({ + "title": f"Set `{key}` to `{expected}` for Release", + "category": "build-settings", + "observed_evidence": f"Current value: `{actual}`. {reason}.", + "estimated_impact": "Medium", + "confidence": "High", + "risk_level": "Low", + }) + + if not items: + return {"recommendations": []} + return {"recommendations": items} + + +# --------------------------------------------------------------------------- +# Report generation +# --------------------------------------------------------------------------- + + +def _section_context(benchmark: Dict[str, Any]) -> str: + build = benchmark.get("build", {}) + env = benchmark.get("environment", {}) + lines = [ + "## Project Context\n", + f"- **Project:** `{build.get('path', 'unknown')}`", + f"- **Scheme:** `{build.get('scheme', 'unknown')}`", + f"- **Configuration:** `{build.get('configuration', 'unknown')}`", + f"- **Destination:** `{build.get('destination', 'unknown')}`", + f"- **Xcode:** {env.get('xcode_version', 'unknown').replace(chr(10), ' ')}", + f"- **macOS:** {env.get('macos_version', 'unknown')}", + f"- **Date:** {benchmark.get('created_at', 'unknown')}", + f"- **Benchmark artifact:** `{benchmark.get('_artifact_path', 'unknown')}`", + ] + return "\n".join(lines) + + +def _section_baseline(benchmark: Dict[str, Any]) -> str: + summary = benchmark.get("summary", {}) + clean = summary.get("clean", {}) + cached_clean = summary.get("cached_clean", {}) + incremental = summary.get("incremental", {}) + has_cached = bool(cached_clean and cached_clean.get("count", 0) > 0) + + if has_cached: + lines = [ + "## Baseline Benchmarks\n", + "| Metric | Clean | Cached Clean | Incremental |", + "|--------|-------|-------------|-------------|", + f"| Median | {clean.get('median_seconds', 0):.3f}s | {cached_clean.get('median_seconds', 0):.3f}s | {incremental.get('median_seconds', 0):.3f}s |", + f"| Min | {clean.get('min_seconds', 0):.3f}s | {cached_clean.get('min_seconds', 0):.3f}s | {incremental.get('min_seconds', 0):.3f}s |", + f"| Max | {clean.get('max_seconds', 0):.3f}s | {cached_clean.get('max_seconds', 0):.3f}s | {incremental.get('max_seconds', 0):.3f}s |", + f"| Runs | {clean.get('count', 0)} | {cached_clean.get('count', 0)} | {incremental.get('count', 0)} |", + ] + lines.append( + "\n> **Cached Clean** = clean build with a warm compilation cache. " + "This is the realistic scenario for branch switching, pulling changes, or " + "Clean Build Folder. The compilation cache lives outside DerivedData and " + "survives product deletion.\n" + ) + else: + lines = [ + "## Baseline Benchmarks\n", + "| Metric | Clean | Incremental |", + "|--------|-------|-------------|", + f"| Median | {clean.get('median_seconds', 0):.3f}s | {incremental.get('median_seconds', 0):.3f}s |", + f"| Min | {clean.get('min_seconds', 0):.3f}s | {incremental.get('min_seconds', 0):.3f}s |", + f"| Max | {clean.get('max_seconds', 0):.3f}s | {incremental.get('max_seconds', 0):.3f}s |", + f"| Runs | {clean.get('count', 0)} | {incremental.get('count', 0)} |", + ] + + build_types = ["clean", "cached_clean", "incremental"] if has_cached else ["clean", "incremental"] + label_map = {"clean": "Clean", "cached_clean": "Cached Clean", "incremental": "Incremental"} + for build_type in build_types: + runs = benchmark.get("runs", {}).get(build_type, []) + all_cats: Dict[str, Dict] = {} + for run in runs: + for cat in run.get("timing_summary_categories", []): + name = cat["name"] + if name not in all_cats: + all_cats[name] = {"seconds": 0.0, "task_count": 0} + all_cats[name]["seconds"] += cat["seconds"] + all_cats[name]["task_count"] += cat.get("task_count", 0) + if all_cats: + count = len(runs) or 1 + ranked = sorted(all_cats.items(), key=lambda x: x[1]["seconds"], reverse=True) + label = label_map.get(build_type, build_type.title()) + lines.append(f"\n### {label} Build Timing Summary\n") + lines.append( + "> **Note:** These are aggregated task times across all CPU cores. " + "Because Xcode runs many tasks in parallel, these totals typically exceed " + "the actual build wait time shown above. A large number here does not mean " + "it is blocking your build.\n" + ) + lines.append("| Category | Tasks | Seconds |") + lines.append("|----------|------:|--------:|") + for name, data in ranked: + avg_sec = data["seconds"] / count + tasks = data["task_count"] // count if data["task_count"] else "" + lines.append(f"| {name} | {tasks} | {avg_sec:.3f}s |") + + return "\n".join(lines) + + +def _section_settings_audit( + project_configs: Dict[str, Dict[str, str]], + target_configs: Dict[str, Dict[str, Dict[str, str]]], +) -> str: + lines = ["## Build Settings Audit\n"] + + lines.append("### Debug Configuration\n") + lines.extend(_audit_config(project_configs.get("Debug", {}), _DEBUG_EXPECTATIONS, "Debug")) + + lines.append("\n### General (All Configurations)\n") + merged = _merged_project_settings(project_configs) + lines.extend(_audit_config(merged, _GENERAL_EXPECTATIONS, "General")) + + lines.append("\n### Release Configuration\n") + lines.extend(_audit_config(project_configs.get("Release", {}), _RELEASE_EXPECTATIONS, "Release")) + + lines.append("\n### Cross-Target Consistency\n") + lines.extend(_audit_consistency(project_configs, target_configs)) + + return "\n".join(lines) + + +def _section_diagnostics(diagnostics: Optional[Dict[str, Any]]) -> str: + if diagnostics is None: + return "## Compilation Diagnostics\n\nNo diagnostics artifact provided. Run `diagnose_compilation.py` to identify type-checking hotspots." + warnings = diagnostics.get("warnings", []) + summary = diagnostics.get("summary", {}) + threshold = diagnostics.get("threshold_ms", 100) + lines = [ + "## Compilation Diagnostics\n", + f"Threshold: {threshold}ms | " + f"Total warnings: {summary.get('total_warnings', 0)} | " + f"Function bodies: {summary.get('function_body_warnings', 0)} | " + f"Expressions: {summary.get('expression_warnings', 0)}\n", + ] + if warnings: + lines.append("| Duration | Kind | File | Line | Name |") + lines.append("|---------:|------|------|-----:|------|") + for w in warnings[:30]: + short_file = Path(w["file"]).name + name = w.get("name", "") or "(expression)" + lines.append( + f"| {w['duration_ms']}ms | {w['kind']} | {short_file} | {w['line']} | {name} |" + ) + if len(warnings) > 30: + lines.append(f"\n*... and {len(warnings) - 30} more warnings (see full artifact)*") + else: + lines.append("No type-checking hotspots found above threshold.") + return "\n".join(lines) + + +def _section_recommendations(recommendations: Optional[Dict[str, Any]]) -> str: + if recommendations is None: + return "## Prioritized Recommendations\n\nNo recommendations artifact provided." + items = recommendations.get("recommendations", []) + if not items: + return "## Prioritized Recommendations\n\nNo recommendations found." + lines = ["## Prioritized Recommendations\n"] + for i, item in enumerate(items, 1): + title = item.get("title", "Untitled") + lines.append(f"### {i}. {title}\n") + for field, label in [ + ("wait_time_impact", "Wait-Time Impact"), + ("actionability", "Actionability"), + ("category", "Category"), + ("observed_evidence", "Evidence"), + ("estimated_impact", "Impact"), + ("confidence", "Confidence"), + ("risk_level", "Risk"), + ("scope", "Scope"), + ]: + val = item.get(field) + if val is None: + continue + if isinstance(val, list): + lines.append(f"**{label}:**") + for entry in val: + lines.append(f"- {entry}") + else: + lines.append(f"**{label}:** {val}") + lines.append("") + return "\n".join(lines) + + +def _section_approval(recommendations: Optional[Dict[str, Any]]) -> str: + if recommendations is None: + return "## Approval Checklist\n\nNo recommendations to approve." + items = recommendations.get("recommendations", []) + if not items: + return "## Approval Checklist\n\nNo recommendations to approve." + lines = ["## Approval Checklist\n"] + for i, item in enumerate(items, 1): + title = item.get("title", "Untitled") + wait_impact = item.get("wait_time_impact", "") + impact = item.get("estimated_impact", "") + risk = item.get("risk_level", "") + actionability = item.get("actionability", "") + impact_str = wait_impact if wait_impact else impact + actionability_str = f" | Actionability: {actionability}" if actionability else "" + lines.append(f"- [ ] **{i}. {title}** -- Impact: {impact_str}{actionability_str} | Risk: {risk}") + return "\n".join(lines) + + +def _section_next_steps(benchmark: Dict[str, Any]) -> str: + build = benchmark.get("build", {}) + command = build.get("command", "xcodebuild build") + lines = [ + "## Next Steps\n", + "After implementing approved changes, re-benchmark with the same inputs:\n", + "```bash", + f"python3 scripts/benchmark_builds.py \\", + ] + if build.get("entrypoint") == "workspace": + lines.append(f" --workspace {build.get('path', 'App.xcworkspace')} \\") + else: + lines.append(f" --project {build.get('path', 'App.xcodeproj')} \\") + lines.extend([ + f" --scheme {build.get('scheme', 'App')} \\", + f" --configuration {build.get('configuration', 'Debug')} \\", + ]) + if build.get("destination"): + lines.append(f' --destination "{build["destination"]}" \\') + lines.append(" --output-dir .build-benchmark") + lines.append("```\n") + lines.append("Compare the new wall-clock medians against the baseline. Report results as:") + lines.append('"Your [clean/incremental] build now takes X.Xs (was Y.Ys) -- Z.Zs faster/slower."') + return "\n".join(lines) + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Generate a Markdown build optimization report.") + parser.add_argument("--benchmark", required=True, help="Path to benchmark JSON artifact") + parser.add_argument("--recommendations", help="Path to recommendations JSON") + parser.add_argument("--diagnostics", help="Path to diagnostics JSON") + parser.add_argument("--project-path", help="Path to .xcodeproj for build settings audit") + parser.add_argument("--output", help="Output Markdown path (default: stdout)") + return parser.parse_args() + + +def main() -> int: + args = parse_args() + + benchmark = json.loads(Path(args.benchmark).read_text()) + benchmark["_artifact_path"] = args.benchmark + + recommendations = None + if args.recommendations: + recommendations = json.loads(Path(args.recommendations).read_text()) + + diagnostics = None + if args.diagnostics: + diagnostics = json.loads(Path(args.diagnostics).read_text()) + + project_configs: Dict[str, Dict[str, str]] = {} + target_configs: Dict[str, Dict[str, Dict[str, str]]] = {} + if args.project_path: + pbxproj_path = Path(args.project_path) / "project.pbxproj" + if pbxproj_path.exists(): + pbxproj = pbxproj_path.read_text() + project_configs = _parse_project_level_configs(pbxproj) + target_configs = _parse_target_configs(pbxproj) + + if recommendations is None and project_configs: + auto = _auto_recommendations_from_audit(project_configs) + if auto["recommendations"]: + recommendations = auto + + sections = [ + "# Xcode Build Optimization Plan\n", + _section_context(benchmark), + _section_baseline(benchmark), + ] + + if project_configs: + sections.append(_section_settings_audit(project_configs, target_configs)) + + sections.append(_section_diagnostics(diagnostics)) + sections.append(_section_recommendations(recommendations)) + sections.append(_section_approval(recommendations)) + sections.append(_section_next_steps(benchmark)) + + report = "\n\n".join(sections) + "\n" + + if args.output: + Path(args.output).write_text(report) + print(f"Saved optimization report: {args.output}") + else: + print(report, end="") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/xcode-compilation-analyzer/SKILL.md b/skills/xcode-compilation-analyzer/SKILL.md index f82b550..0ecf5d0 100644 --- a/skills/xcode-compilation-analyzer/SKILL.md +++ b/skills/xcode-compilation-analyzer/SKILL.md @@ -85,5 +85,5 @@ If the evidence points to project configuration instead of source, hand off to [ ## Additional Resources - For the detailed audit checklist, see [references/code-compilation-checks.md](references/code-compilation-checks.md) -- For the shared recommendation structure, see [../../references/recommendation-format.md](../../references/recommendation-format.md) -- For source citations, see [../../references/build-optimization-sources.md](../../references/build-optimization-sources.md) +- For the shared recommendation structure, see [references/recommendation-format.md](references/recommendation-format.md) +- For source citations, see [references/build-optimization-sources.md](references/build-optimization-sources.md) diff --git a/skills/xcode-compilation-analyzer/references/build-optimization-sources.md b/skills/xcode-compilation-analyzer/references/build-optimization-sources.md new file mode 100644 index 0000000..dc29040 --- /dev/null +++ b/skills/xcode-compilation-analyzer/references/build-optimization-sources.md @@ -0,0 +1,159 @@ +# Build Optimization Sources + +This file stores the external sources that the README and skill docs should cite consistently. + +## Apple: Improving the speed of incremental builds + +Source: + +- + +Key takeaways: + +- Measure first with `Build With Timing Summary` or `xcodebuild -showBuildTimingSummary`. +- Accurate target dependencies improve correctness and parallelism. +- Run scripts should declare inputs and outputs so Xcode can skip unnecessary work. +- `.xcfilelist` files are appropriate when scripts have many inputs or outputs. +- Custom frameworks and libraries benefit from module maps, typically by enabling `DEFINES_MODULE`. +- Module reuse is strongest when related sources compile with consistent options. +- Breaking monolithic targets into better-scoped modules can reduce unnecessary rebuilds. + +## Apple: Improving build efficiency with good coding practices + +Source: + +- + +Key takeaways: + +- Use framework-qualified imports when module maps are available. +- Keep Objective-C bridging surfaces narrow. +- Prefer explicit type information when inference becomes expensive. +- Use explicit delegate protocols instead of overly generic delegate types. +- Simplify complex expressions that are hard for the compiler to type-check. + +## Apple: Building your project with explicit module dependencies + +Source: + +- + +Key takeaways: + +- Explicit module builds make module work visible in the build log and improve scheduling. +- Repeated builds of the same module often point to avoidable module variants. +- Inconsistent build options across targets can force duplicate module builds. +- Timing summaries can reveal option drift that prevents module reuse. + +## SwiftLee: Build performance analysis for speeding up Xcode builds + +Source: + +- + +Key takeaways: + +- Clean and incremental builds should both be measured because they reveal different problems. +- Build Timeline and Build Timing Summary are practical starting points for build optimization. +- Build scripts often produce large incremental-build wins when guarded correctly. +- `-warn-long-function-bodies` and `-warn-long-expression-type-checking` help surface compile hotspots. +- Typical debug and release build setting mismatches are worth auditing, especially in older projects. + +## Apple: Xcode Release Notes -- Compilation Caching + +Source: + +- Xcode Release Notes (149700201) + +Key takeaways: + +- Compilation caching is an opt-in feature for Swift and C-family languages. +- It caches prior compilation results and reuses them when the same source inputs are recompiled. +- Branch switching and clean builds benefit the most. +- Can be enabled via the "Enable Compilation Caching" build setting or per-user project settings. + +## Apple: Demystify explicitly built modules (WWDC24) + +Source: + +- + +Key takeaways: + +- Explains how explicitly built modules divide compilation into scan, module build, and source compile stages. +- Unrelated modules build in parallel, improving CPU utilization. +- Module variant duplication is a key bottleneck -- uniform compiler options across targets prevent it. +- The build log shows each module as a discrete task, making it easier to diagnose scheduling issues. + +## Stackademic: Improving Swift Compile-Time Performance -- 14 Tips + +Source: + +- + +Key takeaways: + +- Mark classes `final` to eliminate virtual dispatch overhead and help the compiler optimize. +- Use `private`/`fileprivate` for symbols not used outside their scope. +- Prefer `struct` and `enum` over `class` when reference semantics are not needed. +- Avoid long method chains without intermediate type annotations -- even simple-looking chains can take seconds to compile. +- Add explicit return types to closures passed to generic functions. +- Break large SwiftUI view bodies into smaller composed subviews. + +## Bitrise: Demystifying Explicitly Built Modules for Xcode + +Source: + +- + +Key takeaways: + +- Explicit module builds give `xcodebuild` visibility into smaller compilation tasks for better parallelism. +- Enabled by default for C/Objective-C in Xcode 16+; experimental for Swift. +- Minimizing module variants by aligning build options is the primary optimization lever. +- Some projects see regressions from dependency scanning overhead -- benchmark before and after. + +## Bitrise: Xcode Compilation Cache FAQ + +Source: + +- + +Key takeaways: + +- Granular caching is controlled by `SWIFT_ENABLE_COMPILE_CACHE` and `CLANG_ENABLE_COMPILE_CACHE`, under the umbrella `COMPILATION_CACHING` setting. +- Non-cacheable tasks include `CompileStoryboard`, `CompileXIB`, `CompileAssetCatalogVariant`, `PhaseScriptExecution`, `DataModelCompile`, `CopyPNGFile`, `GenerateDSYMFile`, and `Ld`. +- SPM dependencies are not yet cacheable as of Xcode 26 beta. + +## RocketSim Docs: Build Insights + +Sources: + +- +- + +Key takeaways: + +- RocketSim automatically tracks clean vs incremental builds over time without build scripts. +- It reports build counts, duration trends, and percentile-based metrics such as p75 and p95. +- Team Build Insights adds machine, Xcode, and macOS comparisons for cross-team visibility. +- This repository is best positioned as the point-in-time analyze-and-improve toolkit, while RocketSim is the monitor-over-time companion. + +## Swift Forums: Slow incremental builds because of planning swift module + +Source: + +- + +Key takeaways: + +- "Planning Swift module" can dominate incremental builds (up to 30s per module), sometimes exceeding clean build time. +- Replanning every module without scheduling compiles is a sign that build inputs are being modified unexpectedly (e.g., a misconfigured linter touching file timestamps). +- Enable **Task Backtraces** (Xcode 16.4+: Scheme Editor > Build > Build Debugging) to see why each task re-ran in an incremental build. +- Heavy Swift macro usage (e.g., TCA / swift-syntax) can cause trivial changes to cascade into near-full rebuilds. +- `swift-syntax` builds universally (all architectures) when no prebuilt binary is available, adding significant overhead. +- `SwiftEmitModule` can take 60s+ after a single-line change in large modules. +- Asset catalog compilation is single-threaded per target; splitting assets into separate bundles across targets enables parallel compilation. +- Multi-platform targets (e.g., adding watchOS) can cause SPM packages to build 3x (iOS arm64, iOS x86_64, watchOS arm64). +- Zero-change incremental builds still incur ~10s of fixed overhead: compute dependencies, send project description, create build description, script phases, codesigning, and validation. +- Codesigning and validation run even when output has not changed. diff --git a/skills/xcode-compilation-analyzer/references/recommendation-format.md b/skills/xcode-compilation-analyzer/references/recommendation-format.md new file mode 100644 index 0000000..46affd6 --- /dev/null +++ b/skills/xcode-compilation-analyzer/references/recommendation-format.md @@ -0,0 +1,85 @@ +# Recommendation Format + +All optimization skills should report recommendations in a shared structure so the orchestrator can merge and prioritize them cleanly. + +## Required Fields + +Each recommendation should include: + +- `title` +- `wait_time_impact` -- plain-language statement of expected wall-clock impact, e.g. "Expected to reduce your clean build by ~3s", "Reduces parallel compile work but unlikely to reduce build wait time", or "Impact on wait time is uncertain -- re-benchmark to confirm" +- `actionability` -- classifies how fixable the issue is from the project (see values below) +- `category` +- `observed_evidence` +- `estimated_impact` +- `confidence` +- `approval_required` +- `benchmark_verification_status` + +### Actionability Values + +Every recommendation must include an `actionability` classification: + +- `repo-local` -- Fix lives entirely in project files, source code, or local configuration. The developer can apply it without side effects outside the repo. +- `package-manager` -- Requires CocoaPods or SPM configuration changes that may have broad side effects (e.g., linkage mode, dependency restructuring). These should be benchmarked before and after. +- `xcode-behavior` -- Observed cost is driven by Xcode internals and is not suppressible from the project. Report the finding for awareness but do not promise a fix. +- `upstream` -- Requires changes in a third-party dependency or external tool. The developer cannot fix it locally. + +## Suggested Optional Fields + +- `scope` +- `affected_files` +- `affected_targets` +- `affected_packages` +- `implementation_notes` +- `risk_level` + +## JSON Example + +```json +{ + "recommendations": [ + { + "title": "Guard a release-only symbol upload script", + "wait_time_impact": "Expected to reduce your incremental build by approximately 6 seconds.", + "actionability": "repo-local", + "category": "project", + "observed_evidence": [ + "Incremental builds spend 6.3 seconds in a run script phase.", + "The script runs for Debug builds even though the output is only needed in Release." + ], + "estimated_impact": "High incremental-build improvement", + "confidence": "High", + "approval_required": true, + "benchmark_verification_status": "Not yet verified", + "scope": "Target build phase", + "risk_level": "Low" + } + ] +} +``` + +## Markdown Rendering Guidance + +When rendering for human review, preserve the same field order: + +1. title +2. wait-time impact +3. actionability +4. observed evidence +5. estimated impact +6. confidence +7. approval required +8. benchmark verification status + +That makes it easier for the developer to approve or reject specific items quickly. + +## Verification Status Values + +Recommended values: + +- `Not yet verified` +- `Queued for verification` +- `Verified improvement` +- `No measurable improvement` +- `Inconclusive due to benchmark noise` diff --git a/skills/xcode-compilation-analyzer/scripts/diagnose_compilation.py b/skills/xcode-compilation-analyzer/scripts/diagnose_compilation.py new file mode 100755 index 0000000..bb97085 --- /dev/null +++ b/skills/xcode-compilation-analyzer/scripts/diagnose_compilation.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python3 + +"""Run a single Xcode build with -Xfrontend diagnostics to find slow type-checking.""" + +import argparse +import json +import re +import subprocess +import sys +import time +from datetime import datetime, timezone +from pathlib import Path +from typing import Dict, List, Optional + +_TYPECHECK_RE = re.compile( + r"^(?P.+?):(?P\d+):(?P\d+): warning: " + r"(?Pinstance method|global function|getter|type-check|expression) " + r"'?(?P[^']*?)'?\s+took\s+(?P\d+)ms\s+to\s+type-check" +) + +_EXPRESSION_RE = re.compile( + r"^(?P.+?):(?P\d+):(?P\d+): warning: " + r"expression took\s+(?P\d+)ms\s+to\s+type-check" +) + +_FILE_TIME_RE = re.compile( + r"^\s*(?P\d+(?:\.\d+)?)\s+seconds\s+.*\s+compiling\s+(?P\S+)" +) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Run an Xcode build with -Xfrontend type-checking diagnostics." + ) + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument("--workspace", help="Path to the .xcworkspace file") + group.add_argument("--project", help="Path to the .xcodeproj file") + parser.add_argument("--scheme", required=True, help="Scheme to build") + parser.add_argument("--configuration", default="Debug", help="Build configuration") + parser.add_argument("--destination", help="xcodebuild destination string") + parser.add_argument("--derived-data-path", help="DerivedData path override") + parser.add_argument("--output-dir", default=".build-benchmark", help="Output directory") + parser.add_argument( + "--threshold", + type=int, + default=100, + help="Millisecond threshold for -warn-long-function-bodies and " + "-warn-long-expression-type-checking (default: 100)", + ) + parser.add_argument("--skip-clean", action="store_true", help="Skip clean before build") + parser.add_argument( + "--per-file-timing", + action="store_true", + help="Add -Xfrontend -debug-time-compilation to report per-file compile times.", + ) + parser.add_argument( + "--stats-output", + action="store_true", + help="Add -Xfrontend -stats-output-dir to collect detailed compiler statistics.", + ) + parser.add_argument( + "--extra-arg", + action="append", + default=[], + help="Additional xcodebuild argument. Can be passed multiple times.", + ) + return parser.parse_args() + + +def command_base(args: argparse.Namespace) -> List[str]: + command = ["xcodebuild"] + if args.workspace: + command.extend(["-workspace", args.workspace]) + if args.project: + command.extend(["-project", args.project]) + command.extend(["-scheme", args.scheme, "-configuration", args.configuration]) + if args.destination: + command.extend(["-destination", args.destination]) + if args.derived_data_path: + command.extend(["-derivedDataPath", args.derived_data_path]) + command.extend(args.extra_arg) + return command + + +def parse_diagnostics(output: str) -> List[Dict]: + """Extract type-checking warnings from xcodebuild output.""" + warnings: List[Dict] = [] + seen = set() + for raw_line in output.splitlines(): + line = raw_line.strip() + match = _TYPECHECK_RE.match(line) + if match: + key = (match.group("file"), match.group("line"), match.group("col"), "function-body") + if key in seen: + continue + seen.add(key) + warnings.append( + { + "file": match.group("file"), + "line": int(match.group("line")), + "column": int(match.group("col")), + "duration_ms": int(match.group("ms")), + "kind": "function-body", + "name": match.group("name"), + } + ) + continue + match = _EXPRESSION_RE.match(line) + if match: + key = (match.group("file"), match.group("line"), match.group("col"), "expression") + if key in seen: + continue + seen.add(key) + warnings.append( + { + "file": match.group("file"), + "line": int(match.group("line")), + "column": int(match.group("col")), + "duration_ms": int(match.group("ms")), + "kind": "expression", + "name": "", + } + ) + warnings.sort(key=lambda w: w["duration_ms"], reverse=True) + return warnings + + +def parse_file_timings(output: str) -> List[Dict]: + """Extract per-file compile times from -debug-time-compilation output.""" + timings: List[Dict] = [] + seen = set() + for raw_line in output.splitlines(): + match = _FILE_TIME_RE.match(raw_line.strip()) + if match: + filepath = match.group("file") + if filepath in seen: + continue + seen.add(filepath) + timings.append( + { + "file": filepath, + "duration_seconds": float(match.group("seconds")), + } + ) + timings.sort(key=lambda t: t["duration_seconds"], reverse=True) + return timings + + +def main() -> int: + args = parse_args() + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + scheme_slug = args.scheme.replace(" ", "-").lower() + artifact_stem = f"{timestamp}-{scheme_slug}" + base = command_base(args) + + if not args.skip_clean: + print("Cleaning build products...") + clean = subprocess.run([*base, "clean"], capture_output=True, text=True) + if clean.returncode != 0: + sys.stderr.write(clean.stdout + clean.stderr) + return clean.returncode + + threshold = str(args.threshold) + swift_flags = ( + f"$(inherited) -Xfrontend -warn-long-function-bodies={threshold} " + f"-Xfrontend -warn-long-expression-type-checking={threshold}" + ) + if args.per_file_timing: + swift_flags += " -Xfrontend -debug-time-compilation" + + stats_dir: Optional[Path] = None + if args.stats_output: + stats_dir = output_dir / f"{artifact_stem}-stats" + stats_dir.mkdir(parents=True, exist_ok=True) + swift_flags += f" -Xfrontend -stats-output-dir -Xfrontend {stats_dir}" + + build_command = [ + *base, + "build", + "-showBuildTimingSummary", + f"OTHER_SWIFT_FLAGS={swift_flags}", + ] + + extras = [] + if args.per_file_timing: + extras.append("per-file timing") + if args.stats_output: + extras.append("stats output") + extras_label = f" + {', '.join(extras)}" if extras else "" + print(f"Building with type-check threshold {threshold}ms{extras_label}...") + started = time.perf_counter() + result = subprocess.run(build_command, capture_output=True, text=True) + elapsed = round(time.perf_counter() - started, 3) + + combined_output = result.stdout + result.stderr + log_path = output_dir / f"{artifact_stem}-diagnostics.log" + log_path.write_text(combined_output) + + warnings = parse_diagnostics(combined_output) + + file_timings: Optional[List[Dict]] = None + if args.per_file_timing: + file_timings = parse_file_timings(combined_output) + + artifact = { + "schema_version": "1.0.0", + "created_at": datetime.now(timezone.utc).isoformat(), + "type": "compilation-diagnostics", + "build": { + "entrypoint": "workspace" if args.workspace else "project", + "path": args.workspace or args.project, + "scheme": args.scheme, + "configuration": args.configuration, + "destination": args.destination or "", + }, + "threshold_ms": args.threshold, + "build_duration_seconds": elapsed, + "build_success": result.returncode == 0, + "raw_log_path": str(log_path), + "warnings": warnings, + "summary": { + "total_warnings": len(warnings), + "function_body_warnings": sum(1 for w in warnings if w["kind"] == "function-body"), + "expression_warnings": sum(1 for w in warnings if w["kind"] == "expression"), + "slowest_ms": warnings[0]["duration_ms"] if warnings else 0, + }, + } + + if file_timings is not None: + artifact["per_file_timings"] = file_timings + if stats_dir is not None: + artifact["stats_dir"] = str(stats_dir) + + artifact_path = output_dir / f"{artifact_stem}-diagnostics.json" + artifact_path.write_text(json.dumps(artifact, indent=2) + "\n") + + print(f"\nSaved diagnostics artifact: {artifact_path}") + print(f"Build {'succeeded' if result.returncode == 0 else 'failed'} in {elapsed}s") + print(f"Found {len(warnings)} type-check warnings above {threshold}ms threshold\n") + + if warnings: + print(f"{'Duration':>10} {'Kind':<15} {'Location'}") + print(f"{'--------':>10} {'----':<15} {'--------'}") + for w in warnings[:20]: + loc = f"{w['file']}:{w['line']}:{w['column']}" + label = w["name"] if w["name"] else "(expression)" + print(f"{w['duration_ms']:>8}ms {w['kind']:<15} {loc} {label}") + if len(warnings) > 20: + print(f"\n ... and {len(warnings) - 20} more (see {artifact_path})") + else: + print("No type-checking hotspots found above threshold.") + + if file_timings: + print(f"\nPer-file compile times (top 20):\n") + print(f"{'Duration':>12} {'File'}") + print(f"{'--------':>12} {'----'}") + for t in file_timings[:20]: + print(f"{t['duration_seconds']:>10.3f}s {t['file']}") + if len(file_timings) > 20: + print(f"\n ... and {len(file_timings) - 20} more (see {artifact_path})") + + if stats_dir is not None: + stat_files = list(stats_dir.glob("*.json")) + print(f"\nCompiler statistics: {len(stat_files)} files written to {stats_dir}") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/xcode-project-analyzer/SKILL.md b/skills/xcode-project-analyzer/SKILL.md index 74382e9..3290454 100644 --- a/skills/xcode-project-analyzer/SKILL.md +++ b/skills/xcode-project-analyzer/SKILL.md @@ -17,7 +17,7 @@ Use this skill for project- and target-level build inefficiencies that are unlik ## What To Review - scheme build order and target dependencies -- debug vs release build settings against the [build settings best practices](../../references/build-settings-best-practices.md) +- debug vs release build settings against the [build settings best practices](references/build-settings-best-practices.md) - run script phases and dependency-analysis settings - derived-data churn or obviously invalidating custom steps - opportunities for parallelization @@ -31,7 +31,7 @@ Use this skill for project- and target-level build inefficiencies that are unlik ## Build Settings Best Practices Audit -Every project audit should include a build settings checklist comparing the project's Debug and Release configurations against the recommended values in [build-settings-best-practices.md](../../references/build-settings-best-practices.md). Present results using checkmark/cross indicators (`[x]`/`[ ]`). The scope is strictly build performance -- do not flag language-migration settings like `SWIFT_STRICT_CONCURRENCY` or `SWIFT_UPCOMING_FEATURE_*`. +Every project audit should include a build settings checklist comparing the project's Debug and Release configurations against the recommended values in [build-settings-best-practices.md](references/build-settings-best-practices.md). Present results using checkmark/cross indicators (`[x]`/`[ ]`). The scope is strictly build performance -- do not flag language-migration settings like `SWIFT_STRICT_CONCURRENCY` or `SWIFT_UPCOMING_FEATURE_*`. ## Apple-Derived Checks @@ -71,6 +71,6 @@ If the evidence points to package graph or build plugins, hand off to [`spm-buil ## Additional Resources - For the detailed audit checklist, see [references/project-audit-checks.md](references/project-audit-checks.md) -- For build settings best practices, see [../../references/build-settings-best-practices.md](../../references/build-settings-best-practices.md) -- For the shared recommendation structure, see [../../references/recommendation-format.md](../../references/recommendation-format.md) -- For Apple-aligned source summaries, see [../../references/build-optimization-sources.md](../../references/build-optimization-sources.md) +- For build settings best practices, see [references/build-settings-best-practices.md](references/build-settings-best-practices.md) +- For the shared recommendation structure, see [references/recommendation-format.md](references/recommendation-format.md) +- For Apple-aligned source summaries, see [references/build-optimization-sources.md](references/build-optimization-sources.md) diff --git a/skills/xcode-project-analyzer/references/build-optimization-sources.md b/skills/xcode-project-analyzer/references/build-optimization-sources.md new file mode 100644 index 0000000..dc29040 --- /dev/null +++ b/skills/xcode-project-analyzer/references/build-optimization-sources.md @@ -0,0 +1,159 @@ +# Build Optimization Sources + +This file stores the external sources that the README and skill docs should cite consistently. + +## Apple: Improving the speed of incremental builds + +Source: + +- + +Key takeaways: + +- Measure first with `Build With Timing Summary` or `xcodebuild -showBuildTimingSummary`. +- Accurate target dependencies improve correctness and parallelism. +- Run scripts should declare inputs and outputs so Xcode can skip unnecessary work. +- `.xcfilelist` files are appropriate when scripts have many inputs or outputs. +- Custom frameworks and libraries benefit from module maps, typically by enabling `DEFINES_MODULE`. +- Module reuse is strongest when related sources compile with consistent options. +- Breaking monolithic targets into better-scoped modules can reduce unnecessary rebuilds. + +## Apple: Improving build efficiency with good coding practices + +Source: + +- + +Key takeaways: + +- Use framework-qualified imports when module maps are available. +- Keep Objective-C bridging surfaces narrow. +- Prefer explicit type information when inference becomes expensive. +- Use explicit delegate protocols instead of overly generic delegate types. +- Simplify complex expressions that are hard for the compiler to type-check. + +## Apple: Building your project with explicit module dependencies + +Source: + +- + +Key takeaways: + +- Explicit module builds make module work visible in the build log and improve scheduling. +- Repeated builds of the same module often point to avoidable module variants. +- Inconsistent build options across targets can force duplicate module builds. +- Timing summaries can reveal option drift that prevents module reuse. + +## SwiftLee: Build performance analysis for speeding up Xcode builds + +Source: + +- + +Key takeaways: + +- Clean and incremental builds should both be measured because they reveal different problems. +- Build Timeline and Build Timing Summary are practical starting points for build optimization. +- Build scripts often produce large incremental-build wins when guarded correctly. +- `-warn-long-function-bodies` and `-warn-long-expression-type-checking` help surface compile hotspots. +- Typical debug and release build setting mismatches are worth auditing, especially in older projects. + +## Apple: Xcode Release Notes -- Compilation Caching + +Source: + +- Xcode Release Notes (149700201) + +Key takeaways: + +- Compilation caching is an opt-in feature for Swift and C-family languages. +- It caches prior compilation results and reuses them when the same source inputs are recompiled. +- Branch switching and clean builds benefit the most. +- Can be enabled via the "Enable Compilation Caching" build setting or per-user project settings. + +## Apple: Demystify explicitly built modules (WWDC24) + +Source: + +- + +Key takeaways: + +- Explains how explicitly built modules divide compilation into scan, module build, and source compile stages. +- Unrelated modules build in parallel, improving CPU utilization. +- Module variant duplication is a key bottleneck -- uniform compiler options across targets prevent it. +- The build log shows each module as a discrete task, making it easier to diagnose scheduling issues. + +## Stackademic: Improving Swift Compile-Time Performance -- 14 Tips + +Source: + +- + +Key takeaways: + +- Mark classes `final` to eliminate virtual dispatch overhead and help the compiler optimize. +- Use `private`/`fileprivate` for symbols not used outside their scope. +- Prefer `struct` and `enum` over `class` when reference semantics are not needed. +- Avoid long method chains without intermediate type annotations -- even simple-looking chains can take seconds to compile. +- Add explicit return types to closures passed to generic functions. +- Break large SwiftUI view bodies into smaller composed subviews. + +## Bitrise: Demystifying Explicitly Built Modules for Xcode + +Source: + +- + +Key takeaways: + +- Explicit module builds give `xcodebuild` visibility into smaller compilation tasks for better parallelism. +- Enabled by default for C/Objective-C in Xcode 16+; experimental for Swift. +- Minimizing module variants by aligning build options is the primary optimization lever. +- Some projects see regressions from dependency scanning overhead -- benchmark before and after. + +## Bitrise: Xcode Compilation Cache FAQ + +Source: + +- + +Key takeaways: + +- Granular caching is controlled by `SWIFT_ENABLE_COMPILE_CACHE` and `CLANG_ENABLE_COMPILE_CACHE`, under the umbrella `COMPILATION_CACHING` setting. +- Non-cacheable tasks include `CompileStoryboard`, `CompileXIB`, `CompileAssetCatalogVariant`, `PhaseScriptExecution`, `DataModelCompile`, `CopyPNGFile`, `GenerateDSYMFile`, and `Ld`. +- SPM dependencies are not yet cacheable as of Xcode 26 beta. + +## RocketSim Docs: Build Insights + +Sources: + +- +- + +Key takeaways: + +- RocketSim automatically tracks clean vs incremental builds over time without build scripts. +- It reports build counts, duration trends, and percentile-based metrics such as p75 and p95. +- Team Build Insights adds machine, Xcode, and macOS comparisons for cross-team visibility. +- This repository is best positioned as the point-in-time analyze-and-improve toolkit, while RocketSim is the monitor-over-time companion. + +## Swift Forums: Slow incremental builds because of planning swift module + +Source: + +- + +Key takeaways: + +- "Planning Swift module" can dominate incremental builds (up to 30s per module), sometimes exceeding clean build time. +- Replanning every module without scheduling compiles is a sign that build inputs are being modified unexpectedly (e.g., a misconfigured linter touching file timestamps). +- Enable **Task Backtraces** (Xcode 16.4+: Scheme Editor > Build > Build Debugging) to see why each task re-ran in an incremental build. +- Heavy Swift macro usage (e.g., TCA / swift-syntax) can cause trivial changes to cascade into near-full rebuilds. +- `swift-syntax` builds universally (all architectures) when no prebuilt binary is available, adding significant overhead. +- `SwiftEmitModule` can take 60s+ after a single-line change in large modules. +- Asset catalog compilation is single-threaded per target; splitting assets into separate bundles across targets enables parallel compilation. +- Multi-platform targets (e.g., adding watchOS) can cause SPM packages to build 3x (iOS arm64, iOS x86_64, watchOS arm64). +- Zero-change incremental builds still incur ~10s of fixed overhead: compute dependencies, send project description, create build description, script phases, codesigning, and validation. +- Codesigning and validation run even when output has not changed. diff --git a/skills/xcode-project-analyzer/references/build-settings-best-practices.md b/skills/xcode-project-analyzer/references/build-settings-best-practices.md new file mode 100644 index 0000000..9dee131 --- /dev/null +++ b/skills/xcode-project-analyzer/references/build-settings-best-practices.md @@ -0,0 +1,216 @@ +# Build Settings Best Practices + +This reference lists Xcode build settings that affect build performance. Use it to audit a project and produce a pass/fail checklist. + +The scope is strictly **build performance**. Do not flag language-migration settings like `SWIFT_STRICT_CONCURRENCY` or `SWIFT_UPCOMING_FEATURE_*` -- those are developer adoption choices unrelated to build speed. + +## How To Read This Reference + +Each setting includes: + +- **Setting name** and the Xcode build-settings key +- **Recommended value** for Debug and Release +- **Why it matters** for build time +- **Risk** of changing it + +Use checkmark and cross indicators when reporting: + +- `[x]` -- setting matches the recommended value +- `[ ]` -- setting does not match; include the actual value and the expected value + +## Debug Configuration + +These settings optimize for fast iteration during development. + +### Compilation Mode + +- **Key:** `SWIFT_COMPILATION_MODE` +- **Recommended:** `singlefile` (Xcode UI: "Incremental"; or unset -- Xcode defaults to singlefile for Debug) +- **Why:** Single-file mode recompiles only changed files. `wholemodule` recompiles the entire target on every change. +- **Risk:** Low + +### Swift Optimization Level + +- **Key:** `SWIFT_OPTIMIZATION_LEVEL` +- **Recommended:** `-Onone` +- **Why:** Optimization passes add significant compile time. Debug builds do not benefit from runtime speed improvements. +- **Risk:** Low + +### GCC Optimization Level + +- **Key:** `GCC_OPTIMIZATION_LEVEL` +- **Recommended:** `0` +- **Why:** Same rationale as Swift optimization level, but for C/C++/Objective-C sources. +- **Risk:** Low + +### Build Active Architecture Only + +- **Key:** `ONLY_ACTIVE_ARCH` (`BUILD_ACTIVE_ARCHITECTURE_ONLY`) +- **Recommended:** `YES` +- **Why:** Building all architectures doubles or triples compile and link time for no debug benefit. +- **Risk:** Low + +### Debug Information Format + +- **Key:** `DEBUG_INFORMATION_FORMAT` +- **Recommended:** `dwarf` +- **Why:** `dwarf-with-dsym` generates a separate dSYM bundle which adds overhead. Plain `dwarf` embeds debug info directly in the binary, which is sufficient for local debugging. +- **Risk:** Low + +### Enable Testability + +- **Key:** `ENABLE_TESTABILITY` +- **Recommended:** `YES` +- **Why:** Required for `@testable import`. Adds minor overhead by exporting internal symbols, but this is expected during development. +- **Risk:** Low + +### Active Compilation Conditions + +- **Key:** `SWIFT_ACTIVE_COMPILATION_CONDITIONS` +- **Recommended:** Should include `DEBUG` +- **Why:** Guards conditional compilation blocks (e.g., `#if DEBUG`) and ensures debug-only code paths are included. +- **Risk:** Low + +### Eager Linking + +- **Key:** `EAGER_LINKING` +- **Recommended:** `YES` +- **Why:** Allows the linker to start work before all compilation tasks finish, reducing wall-clock build time. Particularly effective for Debug builds where link time is a meaningful fraction of total build time. +- **Risk:** Low + +## Release Configuration + +These settings optimize for production builds. + +### Compilation Mode + +- **Key:** `SWIFT_COMPILATION_MODE` +- **Recommended:** `wholemodule` +- **Why:** Whole-module optimization produces faster runtime code. Build time is secondary for release. +- **Risk:** Low + +### Swift Optimization Level + +- **Key:** `SWIFT_OPTIMIZATION_LEVEL` +- **Recommended:** `-O` or `-Osize` +- **Why:** Produces optimized binaries. `-Osize` trades some speed for smaller binary size. +- **Risk:** Low + +### GCC Optimization Level + +- **Key:** `GCC_OPTIMIZATION_LEVEL` +- **Recommended:** `s` +- **Why:** Optimizes C/C++/Objective-C for size, matching the typical release expectation. +- **Risk:** Low + +### Build Active Architecture Only + +- **Key:** `ONLY_ACTIVE_ARCH` +- **Recommended:** `NO` +- **Why:** Release builds must include all supported architectures for distribution. +- **Risk:** Low + +### Debug Information Format + +- **Key:** `DEBUG_INFORMATION_FORMAT` +- **Recommended:** `dwarf-with-dsym` +- **Why:** dSYM bundles are required for crash symbolication in production. +- **Risk:** Low + +### Enable Testability + +- **Key:** `ENABLE_TESTABILITY` +- **Recommended:** `NO` +- **Why:** Removes internal-symbol export overhead from release builds. Testing should use Debug configuration. +- **Risk:** Low + +## General (All Configurations) + +### Compilation Caching + +- **Key:** `COMPILATION_CACHING` +- **Recommended:** `YES` +- **Why:** Caches compilation results for Swift and C-family sources so repeated compilations of the same inputs are served from cache. The biggest wins come from branch switching and clean builds where source files are recompiled unchanged. This is an opt-in feature. The umbrella setting controls both `SWIFT_ENABLE_COMPILE_CACHE` and `CLANG_ENABLE_COMPILE_CACHE` under the hood; those can be toggled independently if needed. +- **Measurement:** Measured 5-14% faster clean builds across tested projects (87 to 1,991 Swift files). The benefit compounds in real developer workflows where the cache persists between builds -- branch switching, pulling changes, and CI with persistent DerivedData -- though the exact savings depend on how many files change between builds. +- **Risk:** Low -- can also be enabled via per-user project settings so it does not need to be committed to the shared project file. + +### Integrated Swift Driver + +- **Key:** `SWIFT_USE_INTEGRATED_DRIVER` +- **Recommended:** `YES` +- **Why:** Uses the integrated Swift driver which runs inside the build system process, eliminating inter-process overhead for compilation scheduling. Enabled by default in modern Xcode but worth verifying in migrated projects. +- **Risk:** Low + +### Clang Module Compilation + +- **Key:** `CLANG_ENABLE_MODULES` +- **Recommended:** `YES` +- **Why:** Enables Clang module compilation for C/Objective-C sources, caching module maps on disk instead of reprocessing headers on every import. Eliminates redundant header parsing across translation units. +- **Risk:** Low + +### Explicit Module Builds + +- **Key:** `SWIFT_ENABLE_EXPLICIT_MODULES` (C/ObjC enabled by default in Xcode 16+; for Swift use `_EXPERIMENTAL_SWIFT_EXPLICIT_MODULES`) +- **Recommended:** Evaluate per-project +- **Why:** Makes module compilation visible to the build system as discrete tasks, improving parallelism and scheduling. Reduces redundant module rebuilds by making dependency edges explicit. Some projects see regressions due to the overhead of dependency scanning, so benchmark before and after enabling. +- **Risk:** Medium -- test thoroughly; currently experimental for Swift targets. + +## Cross-Target Consistency + +These checks find settings differences between targets that cause redundant build work. + +### Project-Level vs Target-Level Overrides + +Build-affecting settings should be set at the project level unless a target has a specific reason to override. Unnecessary per-target overrides cause confusion and can silently create module variants. + +Settings to check for project-level consistency: + +- `SWIFT_COMPILATION_MODE` +- `SWIFT_OPTIMIZATION_LEVEL` +- `ONLY_ACTIVE_ARCH` +- `DEBUG_INFORMATION_FORMAT` + +### Module Variant Duplication + +When multiple targets import the same SPM package but compile with different Swift compiler options, the build system produces separate module variants for each combination. This inflates `SwiftEmitModule` task counts. + +Check for drift in: + +- `SWIFT_OPTIMIZATION_LEVEL` +- `SWIFT_COMPILATION_MODE` +- `OTHER_SWIFT_FLAGS` +- Target-level build settings that override project defaults + +### Out of Scope + +Do **not** flag the following as build-performance issues: + +- `SWIFT_STRICT_CONCURRENCY` -- language migration choice +- `SWIFT_UPCOMING_FEATURE_*` -- language migration choice +- `SWIFT_APPROACHABLE_CONCURRENCY` -- language migration choice +- `SWIFT_ACTIVE_COMPILATION_CONDITIONS` values beyond `DEBUG` (e.g., `WIDGETS`, `APPCLIP`) -- intentional per-target customization + +## Checklist Output Format + +When reporting results, use this structure: + +```markdown +### Debug Configuration +- [x] `SWIFT_COMPILATION_MODE`: `singlefile` (recommended: `singlefile`) +- [ ] `DEBUG_INFORMATION_FORMAT`: `dwarf-with-dsym` (recommended: `dwarf`) +- [x] `SWIFT_OPTIMIZATION_LEVEL`: `-Onone` (recommended: `-Onone`) +... + +### Release Configuration +- [x] `SWIFT_COMPILATION_MODE`: `wholemodule` (recommended: `wholemodule`) +... + +### General (All Configurations) +- [ ] `COMPILATION_CACHING`: `NO` (recommended: `YES`) +... + +### Cross-Target Consistency +- [x] All targets inherit `SWIFT_OPTIMIZATION_LEVEL` from project level +- [ ] `OTHER_SWIFT_FLAGS` differs between Stock Analyzer and StockAnalyzerClip +... +``` diff --git a/skills/xcode-project-analyzer/references/project-audit-checks.md b/skills/xcode-project-analyzer/references/project-audit-checks.md index 3598ecc..0d32fca 100644 --- a/skills/xcode-project-analyzer/references/project-audit-checks.md +++ b/skills/xcode-project-analyzer/references/project-audit-checks.md @@ -49,7 +49,7 @@ A zero-change build above 5 seconds on Apple Silicon typically indicates script ## Build Setting Checks -Audit project-level and target-level settings against the [build settings best practices](../../../references/build-settings-best-practices.md). Present results as a checklist with `[x]`/`[ ]` indicators. +Audit project-level and target-level settings against the [build settings best practices](build-settings-best-practices.md). Present results as a checklist with `[x]`/`[ ]` indicators. Key settings to verify: diff --git a/skills/xcode-project-analyzer/references/recommendation-format.md b/skills/xcode-project-analyzer/references/recommendation-format.md new file mode 100644 index 0000000..46affd6 --- /dev/null +++ b/skills/xcode-project-analyzer/references/recommendation-format.md @@ -0,0 +1,85 @@ +# Recommendation Format + +All optimization skills should report recommendations in a shared structure so the orchestrator can merge and prioritize them cleanly. + +## Required Fields + +Each recommendation should include: + +- `title` +- `wait_time_impact` -- plain-language statement of expected wall-clock impact, e.g. "Expected to reduce your clean build by ~3s", "Reduces parallel compile work but unlikely to reduce build wait time", or "Impact on wait time is uncertain -- re-benchmark to confirm" +- `actionability` -- classifies how fixable the issue is from the project (see values below) +- `category` +- `observed_evidence` +- `estimated_impact` +- `confidence` +- `approval_required` +- `benchmark_verification_status` + +### Actionability Values + +Every recommendation must include an `actionability` classification: + +- `repo-local` -- Fix lives entirely in project files, source code, or local configuration. The developer can apply it without side effects outside the repo. +- `package-manager` -- Requires CocoaPods or SPM configuration changes that may have broad side effects (e.g., linkage mode, dependency restructuring). These should be benchmarked before and after. +- `xcode-behavior` -- Observed cost is driven by Xcode internals and is not suppressible from the project. Report the finding for awareness but do not promise a fix. +- `upstream` -- Requires changes in a third-party dependency or external tool. The developer cannot fix it locally. + +## Suggested Optional Fields + +- `scope` +- `affected_files` +- `affected_targets` +- `affected_packages` +- `implementation_notes` +- `risk_level` + +## JSON Example + +```json +{ + "recommendations": [ + { + "title": "Guard a release-only symbol upload script", + "wait_time_impact": "Expected to reduce your incremental build by approximately 6 seconds.", + "actionability": "repo-local", + "category": "project", + "observed_evidence": [ + "Incremental builds spend 6.3 seconds in a run script phase.", + "The script runs for Debug builds even though the output is only needed in Release." + ], + "estimated_impact": "High incremental-build improvement", + "confidence": "High", + "approval_required": true, + "benchmark_verification_status": "Not yet verified", + "scope": "Target build phase", + "risk_level": "Low" + } + ] +} +``` + +## Markdown Rendering Guidance + +When rendering for human review, preserve the same field order: + +1. title +2. wait-time impact +3. actionability +4. observed evidence +5. estimated impact +6. confidence +7. approval required +8. benchmark verification status + +That makes it easier for the developer to approve or reject specific items quickly. + +## Verification Status Values + +Recommended values: + +- `Not yet verified` +- `Queued for verification` +- `Verified improvement` +- `No measurable improvement` +- `Inconclusive due to benchmark noise` From 063218f3cb484f0c9831e7a3fd619f6009fc7bb5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 10:34:36 +0000 Subject: [PATCH 2/2] chore: sync README structure [skip ci] --- README.md | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 84e0de7..0b587ff 100644 --- a/README.md +++ b/README.md @@ -220,54 +220,40 @@ When a root-level file changes, the corresponding copies inside each skill that skills/ xcode-build-benchmark/ SKILL.md - scripts/ - benchmark_builds.py references/ - benchmarking-workflow.md benchmark-artifacts.md - schemas/ - build-benchmark.schema.json + benchmarking-workflow.md xcode-compilation-analyzer/ SKILL.md - scripts/ - diagnose_compilation.py references/ + build-optimization-sources.md code-compilation-checks.md recommendation-format.md - build-optimization-sources.md xcode-project-analyzer/ SKILL.md references/ - project-audit-checks.md + build-optimization-sources.md build-settings-best-practices.md + project-audit-checks.md recommendation-format.md - build-optimization-sources.md spm-build-analysis/ SKILL.md - scripts/ - check_spm_pins.py references/ - spm-analysis-checks.md - recommendation-format.md build-optimization-sources.md + recommendation-format.md + spm-analysis-checks.md xcode-build-orchestrator/ SKILL.md - scripts/ - benchmark_builds.py - diagnose_compilation.py - generate_optimization_report.py references/ - orchestration-report-template.md benchmark-artifacts.md - recommendation-format.md build-settings-best-practices.md + orchestration-report-template.md + recommendation-format.md xcode-build-fixer/ SKILL.md - scripts/ - benchmark_builds.py references/ - fix-patterns.md build-settings-best-practices.md + fix-patterns.md recommendation-format.md ```