@leejungkiin/awkit 1.3.8 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/awk.js +630 -52
- package/bin/claude-generators.js +122 -0
- package/core/AGENTS.md +54 -0
- package/core/CLAUDE.md +155 -0
- package/core/GEMINI.md +44 -9
- package/core/GEMINI.md.bak +126 -199
- package/package.json +1 -1
- package/skills/ai-sprite-maker/SKILL.md +81 -0
- package/skills/ai-sprite-maker/scripts/animate_sprite.py +102 -0
- package/skills/ai-sprite-maker/scripts/process_sprites.py +140 -0
- package/skills/awf-session-restore/SKILL.md +12 -2
- package/skills/brainstorm-agent/SKILL.md +11 -8
- package/skills/code-review/SKILL.md +21 -33
- package/skills/gitnexus/gitnexus-cli/SKILL.md +82 -0
- package/skills/gitnexus/gitnexus-debugging/SKILL.md +89 -0
- package/skills/gitnexus/gitnexus-exploring/SKILL.md +78 -0
- package/skills/gitnexus/gitnexus-guide/SKILL.md +64 -0
- package/skills/gitnexus/gitnexus-impact-analysis/SKILL.md +97 -0
- package/skills/gitnexus/gitnexus-refactoring/SKILL.md +121 -0
- package/skills/lucylab-tts/SKILL.md +64 -0
- package/skills/lucylab-tts/resources/voices_library.json +908 -0
- package/skills/lucylab-tts/scripts/.env +1 -0
- package/skills/lucylab-tts/scripts/lucylab_tts.py +506 -0
- package/skills/nm-memory-sync/SKILL.md +14 -1
- package/skills/orchestrator/SKILL.md +5 -38
- package/skills/ship-to-code/SKILL.md +115 -0
- package/skills/short-maker/SKILL.md +150 -0
- package/skills/short-maker/_backup/storyboard.html +106 -0
- package/skills/short-maker/_backup/video_mixer.py +296 -0
- package/skills/short-maker/outputs/fitbite-promo/background.jpg +0 -0
- package/skills/short-maker/outputs/fitbite-promo/final/promo-final.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/script.md +19 -0
- package/skills/short-maker/outputs/fitbite-promo/segments/scene-01.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/segments/scene-02.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/segments/scene-03.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/segments/scene-04.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-01.png +0 -0
- package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-02.png +0 -0
- package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-03.png +0 -0
- package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-04.png +0 -0
- package/skills/short-maker/outputs/fitbite-promo/storyboard.html +133 -0
- package/skills/short-maker/outputs/fitbite-promo/storyboard.json +38 -0
- package/skills/short-maker/outputs/fitbite-promo/temp/merged_chroma.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/temp/merged_crossfaded.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/temp/ready_00.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/temp/ready_01.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/temp/ready_02.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/temp/ready_03.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/tts/manifest.json +31 -0
- package/skills/short-maker/outputs/fitbite-promo/tts/scene-01.wav +0 -0
- package/skills/short-maker/outputs/fitbite-promo/tts/scene-02.wav +0 -0
- package/skills/short-maker/outputs/fitbite-promo/tts/scene-03.wav +0 -0
- package/skills/short-maker/outputs/fitbite-promo/tts/scene-04.wav +0 -0
- package/skills/short-maker/outputs/fitbite-promo/tts_script.txt +11 -0
- package/skills/short-maker/scripts/google-flow-cli/.project-identity +41 -0
- package/skills/short-maker/scripts/google-flow-cli/.trae/rules/project_rules.md +52 -0
- package/skills/short-maker/scripts/google-flow-cli/CODEBASE.md +67 -0
- package/skills/short-maker/scripts/google-flow-cli/GoogleFlowCli.code-workspace +29 -0
- package/skills/short-maker/scripts/google-flow-cli/README.md +168 -0
- package/skills/short-maker/scripts/google-flow-cli/docs/specs/PROJECT.md +12 -0
- package/skills/short-maker/scripts/google-flow-cli/docs/specs/REQUIREMENTS.md +22 -0
- package/skills/short-maker/scripts/google-flow-cli/docs/specs/ROADMAP.md +16 -0
- package/skills/short-maker/scripts/google-flow-cli/docs/specs/TECH-SPEC.md +13 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/__init__.py +3 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/api/__init__.py +19 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/api/client.py +1921 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/api/models.py +64 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/api/rpc_ids.py +98 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/auth/__init__.py +15 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/auth/browser_auth.py +692 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/auth/humanizer.py +417 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/auth/proxy_ext.py +120 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/auth/recaptcha.py +482 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/batchexecute/__init__.py +5 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/batchexecute/client.py +414 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/cli/__init__.py +1 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/cli/main.py +1075 -0
- package/skills/short-maker/scripts/google-flow-cli/pyproject.toml +36 -0
- package/skills/short-maker/scripts/google-flow-cli/script.txt +22 -0
- package/skills/short-maker/scripts/google-flow-cli/tests/__init__.py +0 -0
- package/skills/short-maker/scripts/google-flow-cli/tests/test_batchexecute.py +113 -0
- package/skills/short-maker/scripts/google-flow-cli/tests/test_client.py +190 -0
- package/skills/short-maker/templates/aida_script.md +40 -0
- package/skills/short-maker/templates/mimic_analyzer.md +29 -0
- package/skills/single-flow-task-execution/SKILL.md +412 -0
- package/skills/single-flow-task-execution/code-quality-reviewer-prompt.md +20 -0
- package/skills/single-flow-task-execution/implementer-prompt.md +78 -0
- package/skills/single-flow-task-execution/spec-reviewer-prompt.md +61 -0
- package/skills/skill-creator/SKILL.md +44 -0
- package/skills/spm-build-analysis/SKILL.md +92 -0
- package/skills/spm-build-analysis/references/build-optimization-sources.md +155 -0
- package/skills/spm-build-analysis/references/recommendation-format.md +85 -0
- package/skills/spm-build-analysis/references/spm-analysis-checks.md +105 -0
- package/skills/spm-build-analysis/scripts/check_spm_pins.py +118 -0
- package/skills/symphony-enforcer/SKILL.md +83 -97
- package/skills/symphony-orchestrator/SKILL.md +1 -1
- package/skills/trello-sync/SKILL.md +52 -45
- package/skills/verification-gate/SKILL.md +13 -2
- package/skills/xcode-build-benchmark/SKILL.md +88 -0
- package/skills/xcode-build-benchmark/references/benchmark-artifacts.md +94 -0
- package/skills/xcode-build-benchmark/references/benchmarking-workflow.md +67 -0
- package/skills/xcode-build-benchmark/schemas/build-benchmark.schema.json +230 -0
- package/skills/xcode-build-benchmark/scripts/benchmark_builds.py +308 -0
- package/skills/xcode-build-fixer/SKILL.md +218 -0
- package/skills/xcode-build-fixer/references/build-settings-best-practices.md +216 -0
- package/skills/xcode-build-fixer/references/fix-patterns.md +290 -0
- package/skills/xcode-build-fixer/references/recommendation-format.md +85 -0
- package/skills/xcode-build-fixer/scripts/benchmark_builds.py +308 -0
- package/skills/xcode-build-orchestrator/SKILL.md +156 -0
- package/skills/xcode-build-orchestrator/references/benchmark-artifacts.md +94 -0
- package/skills/xcode-build-orchestrator/references/build-settings-best-practices.md +216 -0
- package/skills/xcode-build-orchestrator/references/orchestration-report-template.md +143 -0
- package/skills/xcode-build-orchestrator/references/recommendation-format.md +85 -0
- package/skills/xcode-build-orchestrator/scripts/benchmark_builds.py +308 -0
- package/skills/xcode-build-orchestrator/scripts/diagnose_compilation.py +273 -0
- package/skills/xcode-build-orchestrator/scripts/generate_optimization_report.py +533 -0
- package/skills/xcode-compilation-analyzer/SKILL.md +89 -0
- package/skills/xcode-compilation-analyzer/references/build-optimization-sources.md +155 -0
- package/skills/xcode-compilation-analyzer/references/code-compilation-checks.md +106 -0
- package/skills/xcode-compilation-analyzer/references/recommendation-format.md +85 -0
- package/skills/xcode-compilation-analyzer/scripts/diagnose_compilation.py +273 -0
- package/skills/xcode-project-analyzer/SKILL.md +76 -0
- package/skills/xcode-project-analyzer/references/build-optimization-sources.md +155 -0
- package/skills/xcode-project-analyzer/references/build-settings-best-practices.md +216 -0
- package/skills/xcode-project-analyzer/references/project-audit-checks.md +101 -0
- package/skills/xcode-project-analyzer/references/recommendation-format.md +85 -0
- package/templates/CODEBASE.md +26 -42
- package/templates/configs/trello-config.json +2 -2
- package/templates/workflow_dual_mode_template.md +5 -5
- package/workflows/_uncategorized/conductor-codex.md +125 -0
- package/workflows/_uncategorized/conductor.md +97 -0
- package/workflows/_uncategorized/ship-to-code.md +85 -0
- package/workflows/_uncategorized/trello-sync.md +52 -0
- package/workflows/context/codebase-sync.md +10 -87
- package/workflows/quality/visual-debug.md +66 -12
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Benchmark Artifacts
|
|
2
|
+
|
|
3
|
+
All skills in this repository should treat `.build-benchmark/` as the canonical location for measured build evidence.
|
|
4
|
+
|
|
5
|
+
## Goals
|
|
6
|
+
|
|
7
|
+
- Keep build measurements reproducible.
|
|
8
|
+
- Make clean and incremental build data easy to compare.
|
|
9
|
+
- Preserve enough context for later specialist analysis without rerunning the benchmark.
|
|
10
|
+
|
|
11
|
+
## Wall-Clock vs Cumulative Task Time
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
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.
|
|
16
|
+
|
|
17
|
+
## File Layout
|
|
18
|
+
|
|
19
|
+
Recommended outputs:
|
|
20
|
+
|
|
21
|
+
- `.build-benchmark/<timestamp>-<scheme>.json`
|
|
22
|
+
- `.build-benchmark/<timestamp>-<scheme>-clean-1.log`
|
|
23
|
+
- `.build-benchmark/<timestamp>-<scheme>-clean-2.log`
|
|
24
|
+
- `.build-benchmark/<timestamp>-<scheme>-clean-3.log`
|
|
25
|
+
- `.build-benchmark/<timestamp>-<scheme>-cached-clean-1.log` (when COMPILATION_CACHING is enabled)
|
|
26
|
+
- `.build-benchmark/<timestamp>-<scheme>-cached-clean-2.log`
|
|
27
|
+
- `.build-benchmark/<timestamp>-<scheme>-cached-clean-3.log`
|
|
28
|
+
- `.build-benchmark/<timestamp>-<scheme>-incremental-1.log`
|
|
29
|
+
- `.build-benchmark/<timestamp>-<scheme>-incremental-2.log`
|
|
30
|
+
- `.build-benchmark/<timestamp>-<scheme>-incremental-3.log`
|
|
31
|
+
|
|
32
|
+
Use an ISO-like UTC timestamp without spaces so the files sort naturally.
|
|
33
|
+
|
|
34
|
+
## Artifact Requirements
|
|
35
|
+
|
|
36
|
+
Each JSON artifact should include:
|
|
37
|
+
|
|
38
|
+
- schema version
|
|
39
|
+
- creation timestamp
|
|
40
|
+
- project context
|
|
41
|
+
- environment details when available
|
|
42
|
+
- the normalized build command
|
|
43
|
+
- separate `clean` and `incremental` run arrays
|
|
44
|
+
- summary statistics for each build type
|
|
45
|
+
- parsed timing-summary categories
|
|
46
|
+
- free-form notes for caveats or noise
|
|
47
|
+
|
|
48
|
+
## Clean, Cached Clean, And Incremental Separation
|
|
49
|
+
|
|
50
|
+
Do not merge different build type measurements into a single list. They answer different questions:
|
|
51
|
+
|
|
52
|
+
- **Clean builds** show full build-system, package, and module setup cost with a cold compilation cache.
|
|
53
|
+
- **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.
|
|
54
|
+
- **Incremental builds** show edit-loop productivity and script or cache invalidation problems.
|
|
55
|
+
|
|
56
|
+
## Raw Logs
|
|
57
|
+
|
|
58
|
+
Store raw `xcodebuild` output beside the JSON artifact whenever possible. That allows later skills to:
|
|
59
|
+
|
|
60
|
+
- re-parse timing summaries
|
|
61
|
+
- inspect failed builds
|
|
62
|
+
- search for long type-check warnings
|
|
63
|
+
- correlate build-system phases with recommendations
|
|
64
|
+
|
|
65
|
+
## Measurement Caveats
|
|
66
|
+
|
|
67
|
+
### COMPILATION_CACHING
|
|
68
|
+
|
|
69
|
+
`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.
|
|
70
|
+
|
|
71
|
+
The benchmark script automatically detects `COMPILATION_CACHING = YES` and runs a **cached clean** benchmark phase. This phase:
|
|
72
|
+
|
|
73
|
+
1. Builds once to warm the compilation cache.
|
|
74
|
+
2. Deletes DerivedData (but not the compilation cache) before each measured run.
|
|
75
|
+
3. Rebuilds, measuring the cache-hit clean build time.
|
|
76
|
+
|
|
77
|
+
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.
|
|
78
|
+
|
|
79
|
+
To skip this phase, pass `--no-cached-clean`.
|
|
80
|
+
|
|
81
|
+
### First-Run Variance
|
|
82
|
+
|
|
83
|
+
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.
|
|
84
|
+
|
|
85
|
+
## Shared Consumer Expectations
|
|
86
|
+
|
|
87
|
+
Any skill reading a benchmark artifact should be able to identify:
|
|
88
|
+
|
|
89
|
+
- what was measured
|
|
90
|
+
- how it was measured
|
|
91
|
+
- whether the run succeeded
|
|
92
|
+
- whether the results are stable enough to compare
|
|
93
|
+
|
|
94
|
+
For the authoritative field-level schema, see [../schemas/build-benchmark.schema.json](../schemas/build-benchmark.schema.json).
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Benchmarking Workflow
|
|
2
|
+
|
|
3
|
+
Use this reference when you need the full operational contract for collecting Xcode build measurements.
|
|
4
|
+
|
|
5
|
+
## Goal
|
|
6
|
+
|
|
7
|
+
Produce a benchmark artifact that another skill can trust without rerunning the same setup discovery.
|
|
8
|
+
|
|
9
|
+
## Benchmark Contract
|
|
10
|
+
|
|
11
|
+
- Measure both clean and incremental builds unless the user narrows the scope.
|
|
12
|
+
- Use the same scheme, configuration, destination, and command flags for all measured runs.
|
|
13
|
+
- Record the exact command and any environment overrides.
|
|
14
|
+
- Keep clean and incremental runs in separate arrays in the artifact.
|
|
15
|
+
- Save wall-clock timing plus any parsed timing-summary categories.
|
|
16
|
+
|
|
17
|
+
## Suggested Run Counts
|
|
18
|
+
|
|
19
|
+
- Clean builds: 3 measured runs
|
|
20
|
+
- Incremental builds: 3 measured runs
|
|
21
|
+
- Warm-up: 0 to 1 validation run, excluded from the summary unless the user explicitly wants it included
|
|
22
|
+
|
|
23
|
+
## Clean Build Rules
|
|
24
|
+
|
|
25
|
+
- Clear build products with `xcodebuild clean` or an equivalent clean-build-folder step before each measured clean run.
|
|
26
|
+
- Do not change scheme, destination, or configuration between runs.
|
|
27
|
+
- If the command fails, store the failure and stop rather than mixing failed and successful runs.
|
|
28
|
+
|
|
29
|
+
## Incremental Build Rules
|
|
30
|
+
|
|
31
|
+
- Use the same build command after a successful baseline build.
|
|
32
|
+
- Do not clean between incremental runs.
|
|
33
|
+
- If the user wants edit-loop benchmarking, note the file change strategy explicitly in the artifact.
|
|
34
|
+
- If there are no source edits between runs, label the result as no-edit incremental timing.
|
|
35
|
+
|
|
36
|
+
## What To Capture
|
|
37
|
+
|
|
38
|
+
At minimum, keep:
|
|
39
|
+
|
|
40
|
+
- timestamp
|
|
41
|
+
- host machine info if available
|
|
42
|
+
- Xcode version if available
|
|
43
|
+
- workspace or project path
|
|
44
|
+
- scheme, configuration, destination
|
|
45
|
+
- exact `xcodebuild` command
|
|
46
|
+
- duration per run
|
|
47
|
+
- success or failure
|
|
48
|
+
- parsed timing-summary categories
|
|
49
|
+
- notes on warm-up behavior or unusual noise
|
|
50
|
+
|
|
51
|
+
## Reporting Guidance
|
|
52
|
+
|
|
53
|
+
Use medians for the headline number. Also include:
|
|
54
|
+
|
|
55
|
+
- min and max
|
|
56
|
+
- range
|
|
57
|
+
- category totals from the timing summary
|
|
58
|
+
- obvious outliers or instability
|
|
59
|
+
|
|
60
|
+
## Handoff Expectations
|
|
61
|
+
|
|
62
|
+
The next optimization skill should be able to answer:
|
|
63
|
+
|
|
64
|
+
- Is the main problem clean, incremental, or both?
|
|
65
|
+
- Which build categories dominate time?
|
|
66
|
+
- Which command produced the evidence?
|
|
67
|
+
- Is the baseline trustworthy enough to compare before and after changes?
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Xcode Build Benchmark Artifact",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": [
|
|
6
|
+
"schema_version",
|
|
7
|
+
"created_at",
|
|
8
|
+
"build",
|
|
9
|
+
"runs",
|
|
10
|
+
"summary"
|
|
11
|
+
],
|
|
12
|
+
"properties": {
|
|
13
|
+
"schema_version": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"enum": ["1.0.0", "1.1.0", "1.2.0"]
|
|
16
|
+
},
|
|
17
|
+
"created_at": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"format": "date-time"
|
|
20
|
+
},
|
|
21
|
+
"build": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"required": [
|
|
24
|
+
"entrypoint",
|
|
25
|
+
"scheme",
|
|
26
|
+
"configuration",
|
|
27
|
+
"destination",
|
|
28
|
+
"command"
|
|
29
|
+
],
|
|
30
|
+
"properties": {
|
|
31
|
+
"entrypoint": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"enum": [
|
|
34
|
+
"project",
|
|
35
|
+
"workspace"
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
"path": {
|
|
39
|
+
"type": "string"
|
|
40
|
+
},
|
|
41
|
+
"scheme": {
|
|
42
|
+
"type": "string"
|
|
43
|
+
},
|
|
44
|
+
"configuration": {
|
|
45
|
+
"type": "string"
|
|
46
|
+
},
|
|
47
|
+
"destination": {
|
|
48
|
+
"type": "string"
|
|
49
|
+
},
|
|
50
|
+
"derived_data_path": {
|
|
51
|
+
"type": "string"
|
|
52
|
+
},
|
|
53
|
+
"command": {
|
|
54
|
+
"type": "string"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"additionalProperties": true
|
|
58
|
+
},
|
|
59
|
+
"environment": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"properties": {
|
|
62
|
+
"host": {
|
|
63
|
+
"type": "string"
|
|
64
|
+
},
|
|
65
|
+
"xcode_version": {
|
|
66
|
+
"type": "string"
|
|
67
|
+
},
|
|
68
|
+
"macos_version": {
|
|
69
|
+
"type": "string"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"additionalProperties": true
|
|
73
|
+
},
|
|
74
|
+
"runs": {
|
|
75
|
+
"type": "object",
|
|
76
|
+
"required": [
|
|
77
|
+
"clean",
|
|
78
|
+
"incremental"
|
|
79
|
+
],
|
|
80
|
+
"properties": {
|
|
81
|
+
"clean": {
|
|
82
|
+
"type": "array",
|
|
83
|
+
"items": {
|
|
84
|
+
"$ref": "#/definitions/run"
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"cached_clean": {
|
|
88
|
+
"type": "array",
|
|
89
|
+
"items": {
|
|
90
|
+
"$ref": "#/definitions/run"
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"incremental": {
|
|
94
|
+
"type": "array",
|
|
95
|
+
"items": {
|
|
96
|
+
"$ref": "#/definitions/run"
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"additionalProperties": false
|
|
101
|
+
},
|
|
102
|
+
"summary": {
|
|
103
|
+
"type": "object",
|
|
104
|
+
"required": [
|
|
105
|
+
"clean",
|
|
106
|
+
"incremental"
|
|
107
|
+
],
|
|
108
|
+
"properties": {
|
|
109
|
+
"clean": {
|
|
110
|
+
"$ref": "#/definitions/stats"
|
|
111
|
+
},
|
|
112
|
+
"cached_clean": {
|
|
113
|
+
"$ref": "#/definitions/stats"
|
|
114
|
+
},
|
|
115
|
+
"incremental": {
|
|
116
|
+
"$ref": "#/definitions/stats"
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
"additionalProperties": false
|
|
120
|
+
},
|
|
121
|
+
"notes": {
|
|
122
|
+
"type": "array",
|
|
123
|
+
"items": {
|
|
124
|
+
"type": "string"
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"definitions": {
|
|
129
|
+
"run": {
|
|
130
|
+
"type": "object",
|
|
131
|
+
"required": [
|
|
132
|
+
"id",
|
|
133
|
+
"build_type",
|
|
134
|
+
"duration_seconds",
|
|
135
|
+
"success",
|
|
136
|
+
"command"
|
|
137
|
+
],
|
|
138
|
+
"properties": {
|
|
139
|
+
"id": {
|
|
140
|
+
"type": "string"
|
|
141
|
+
},
|
|
142
|
+
"build_type": {
|
|
143
|
+
"type": "string",
|
|
144
|
+
"enum": [
|
|
145
|
+
"clean",
|
|
146
|
+
"cached-clean",
|
|
147
|
+
"incremental"
|
|
148
|
+
]
|
|
149
|
+
},
|
|
150
|
+
"duration_seconds": {
|
|
151
|
+
"type": "number",
|
|
152
|
+
"minimum": 0
|
|
153
|
+
},
|
|
154
|
+
"success": {
|
|
155
|
+
"type": "boolean"
|
|
156
|
+
},
|
|
157
|
+
"command": {
|
|
158
|
+
"type": "string"
|
|
159
|
+
},
|
|
160
|
+
"exit_code": {
|
|
161
|
+
"type": "integer"
|
|
162
|
+
},
|
|
163
|
+
"raw_log_path": {
|
|
164
|
+
"type": "string"
|
|
165
|
+
},
|
|
166
|
+
"timing_summary_categories": {
|
|
167
|
+
"type": "array",
|
|
168
|
+
"items": {
|
|
169
|
+
"$ref": "#/definitions/category"
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
"additionalProperties": true
|
|
174
|
+
},
|
|
175
|
+
"category": {
|
|
176
|
+
"type": "object",
|
|
177
|
+
"required": [
|
|
178
|
+
"name",
|
|
179
|
+
"seconds"
|
|
180
|
+
],
|
|
181
|
+
"properties": {
|
|
182
|
+
"name": {
|
|
183
|
+
"type": "string"
|
|
184
|
+
},
|
|
185
|
+
"seconds": {
|
|
186
|
+
"type": "number",
|
|
187
|
+
"minimum": 0
|
|
188
|
+
},
|
|
189
|
+
"task_count": {
|
|
190
|
+
"type": "integer",
|
|
191
|
+
"minimum": 0
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
"additionalProperties": true
|
|
195
|
+
},
|
|
196
|
+
"stats": {
|
|
197
|
+
"type": "object",
|
|
198
|
+
"required": [
|
|
199
|
+
"count",
|
|
200
|
+
"min_seconds",
|
|
201
|
+
"max_seconds",
|
|
202
|
+
"median_seconds",
|
|
203
|
+
"average_seconds"
|
|
204
|
+
],
|
|
205
|
+
"properties": {
|
|
206
|
+
"count": {
|
|
207
|
+
"type": "integer",
|
|
208
|
+
"minimum": 0
|
|
209
|
+
},
|
|
210
|
+
"min_seconds": {
|
|
211
|
+
"type": "number",
|
|
212
|
+
"minimum": 0
|
|
213
|
+
},
|
|
214
|
+
"max_seconds": {
|
|
215
|
+
"type": "number",
|
|
216
|
+
"minimum": 0
|
|
217
|
+
},
|
|
218
|
+
"median_seconds": {
|
|
219
|
+
"type": "number",
|
|
220
|
+
"minimum": 0
|
|
221
|
+
},
|
|
222
|
+
"average_seconds": {
|
|
223
|
+
"type": "number",
|
|
224
|
+
"minimum": 0
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
"additionalProperties": true
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
import re
|
|
8
|
+
import shutil
|
|
9
|
+
import statistics
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
import time
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def parse_args() -> argparse.Namespace:
|
|
20
|
+
parser = argparse.ArgumentParser(description="Benchmark Xcode clean and incremental builds.")
|
|
21
|
+
group = parser.add_mutually_exclusive_group(required=True)
|
|
22
|
+
group.add_argument("--workspace", help="Path to the .xcworkspace file")
|
|
23
|
+
group.add_argument("--project", help="Path to the .xcodeproj file")
|
|
24
|
+
parser.add_argument("--scheme", required=True, help="Scheme to build")
|
|
25
|
+
parser.add_argument("--configuration", default="Debug", help="Build configuration")
|
|
26
|
+
parser.add_argument("--destination", help="xcodebuild destination string")
|
|
27
|
+
parser.add_argument("--derived-data-path", help="DerivedData path override")
|
|
28
|
+
parser.add_argument("--output-dir", default=".build-benchmark", help="Output directory for artifacts")
|
|
29
|
+
parser.add_argument("--repeats", type=int, default=3, help="Measured runs per build type")
|
|
30
|
+
parser.add_argument("--skip-warmup", action="store_true", help="Skip the validation build")
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--touch-file",
|
|
33
|
+
help="Path to a source file to touch before each incremental build. "
|
|
34
|
+
"When provided, measures a real edit-rebuild loop instead of a zero-change build.",
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"--no-cached-clean",
|
|
38
|
+
action="store_true",
|
|
39
|
+
help="Skip cached clean builds even when COMPILATION_CACHING is detected.",
|
|
40
|
+
)
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"--extra-arg",
|
|
43
|
+
action="append",
|
|
44
|
+
default=[],
|
|
45
|
+
help="Additional xcodebuild argument to append. Can be passed multiple times.",
|
|
46
|
+
)
|
|
47
|
+
return parser.parse_args()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def command_base(args: argparse.Namespace) -> List[str]:
|
|
51
|
+
command = ["xcodebuild"]
|
|
52
|
+
if args.workspace:
|
|
53
|
+
command.extend(["-workspace", args.workspace])
|
|
54
|
+
if args.project:
|
|
55
|
+
command.extend(["-project", args.project])
|
|
56
|
+
command.extend(["-scheme", args.scheme, "-configuration", args.configuration])
|
|
57
|
+
if args.destination:
|
|
58
|
+
command.extend(["-destination", args.destination])
|
|
59
|
+
if args.derived_data_path:
|
|
60
|
+
command.extend(["-derivedDataPath", args.derived_data_path])
|
|
61
|
+
command.extend(args.extra_arg)
|
|
62
|
+
return command
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def shell_join(parts: List[str]) -> str:
|
|
66
|
+
return " ".join(subprocess.list2cmdline([part]) for part in parts)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
_TASK_COUNT_RE = re.compile(r"^(.+?)\s*\((\d+)\s+tasks?\)$")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _extract_task_count(name: str) -> tuple[str, Optional[int]]:
|
|
73
|
+
"""Split 'Category (N tasks)' into ('Category', N)."""
|
|
74
|
+
match = _TASK_COUNT_RE.match(name)
|
|
75
|
+
if match:
|
|
76
|
+
return match.group(1).strip(), int(match.group(2))
|
|
77
|
+
return name, None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def parse_timing_summary(output: str) -> List[Dict]:
|
|
81
|
+
categories: Dict[str, float] = {}
|
|
82
|
+
task_counts: Dict[str, Optional[int]] = {}
|
|
83
|
+
for raw_line in output.splitlines():
|
|
84
|
+
line = raw_line.strip()
|
|
85
|
+
if not line:
|
|
86
|
+
continue
|
|
87
|
+
for suffix in (" seconds", " second", " sec"):
|
|
88
|
+
if not line.endswith(suffix):
|
|
89
|
+
continue
|
|
90
|
+
trimmed = line[: -len(suffix)]
|
|
91
|
+
if "|" in trimmed:
|
|
92
|
+
name_part, _, seconds_text = trimmed.rpartition("|")
|
|
93
|
+
else:
|
|
94
|
+
name_part, _, seconds_text = trimmed.rpartition(" ")
|
|
95
|
+
try:
|
|
96
|
+
seconds = float(seconds_text.strip())
|
|
97
|
+
except ValueError:
|
|
98
|
+
continue
|
|
99
|
+
cleaned_name = name_part.replace(" ", " ").strip(" -:")
|
|
100
|
+
if len(cleaned_name) < 3:
|
|
101
|
+
continue
|
|
102
|
+
base_name, count = _extract_task_count(cleaned_name)
|
|
103
|
+
categories[base_name] = categories.get(base_name, 0.0) + seconds
|
|
104
|
+
if count is not None:
|
|
105
|
+
task_counts[base_name] = (task_counts.get(base_name) or 0) + count
|
|
106
|
+
break
|
|
107
|
+
result: List[Dict] = []
|
|
108
|
+
for name, seconds in sorted(categories.items(), key=lambda item: item[1], reverse=True):
|
|
109
|
+
entry: Dict = {"name": name, "seconds": round(seconds, 3)}
|
|
110
|
+
if name in task_counts:
|
|
111
|
+
entry["task_count"] = task_counts[name]
|
|
112
|
+
result.append(entry)
|
|
113
|
+
return result
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def run_command(command: List[str]) -> subprocess.CompletedProcess:
|
|
117
|
+
return subprocess.run(command, capture_output=True, text=True)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def stats_for(runs: List[Dict[str, object]]) -> Dict[str, float]:
|
|
121
|
+
durations = [run["duration_seconds"] for run in runs if run.get("success")]
|
|
122
|
+
if not durations:
|
|
123
|
+
return {
|
|
124
|
+
"count": 0,
|
|
125
|
+
"min_seconds": 0.0,
|
|
126
|
+
"max_seconds": 0.0,
|
|
127
|
+
"median_seconds": 0.0,
|
|
128
|
+
"average_seconds": 0.0,
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
"count": len(durations),
|
|
132
|
+
"min_seconds": round(min(durations), 3),
|
|
133
|
+
"max_seconds": round(max(durations), 3),
|
|
134
|
+
"median_seconds": round(statistics.median(durations), 3),
|
|
135
|
+
"average_seconds": round(statistics.fmean(durations), 3),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def xcode_version() -> str:
|
|
140
|
+
result = run_command(["xcodebuild", "-version"])
|
|
141
|
+
return result.stdout.strip() if result.returncode == 0 else "unknown"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def detect_compilation_caching(base_command: List[str]) -> bool:
|
|
145
|
+
"""Check whether COMPILATION_CACHING is enabled in the resolved build settings."""
|
|
146
|
+
result = run_command([*base_command, "-showBuildSettings"])
|
|
147
|
+
if result.returncode != 0:
|
|
148
|
+
return False
|
|
149
|
+
for line in result.stdout.splitlines():
|
|
150
|
+
stripped = line.strip()
|
|
151
|
+
if stripped.startswith("COMPILATION_CACHING") and "=" in stripped:
|
|
152
|
+
value = stripped.split("=", 1)[1].strip()
|
|
153
|
+
return value == "YES"
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def measure_build(
|
|
158
|
+
base_command: List[str],
|
|
159
|
+
artifact_stem: str,
|
|
160
|
+
output_dir: Path,
|
|
161
|
+
build_type: str,
|
|
162
|
+
run_index: int,
|
|
163
|
+
) -> Dict[str, object]:
|
|
164
|
+
build_command = [*base_command, "build", "-showBuildTimingSummary"]
|
|
165
|
+
started = time.perf_counter()
|
|
166
|
+
result = run_command(build_command)
|
|
167
|
+
elapsed = round(time.perf_counter() - started, 3)
|
|
168
|
+
log_path = output_dir / f"{artifact_stem}-{build_type}-{run_index}.log"
|
|
169
|
+
log_path.write_text(result.stdout + result.stderr)
|
|
170
|
+
return {
|
|
171
|
+
"id": f"{build_type}-{run_index}",
|
|
172
|
+
"build_type": build_type,
|
|
173
|
+
"duration_seconds": elapsed,
|
|
174
|
+
"success": result.returncode == 0,
|
|
175
|
+
"exit_code": result.returncode,
|
|
176
|
+
"command": shell_join(build_command),
|
|
177
|
+
"raw_log_path": str(log_path),
|
|
178
|
+
"timing_summary_categories": parse_timing_summary(result.stdout + result.stderr),
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def main() -> int:
|
|
183
|
+
args = parse_args()
|
|
184
|
+
output_dir = Path(args.output_dir)
|
|
185
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
186
|
+
|
|
187
|
+
timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
188
|
+
artifact_stem = f"{timestamp}-{args.scheme.replace(' ', '-').lower()}"
|
|
189
|
+
base_command = command_base(args)
|
|
190
|
+
|
|
191
|
+
if not args.skip_warmup:
|
|
192
|
+
warmup = run_command([*base_command, "build"])
|
|
193
|
+
if warmup.returncode != 0:
|
|
194
|
+
sys.stderr.write(warmup.stdout + warmup.stderr)
|
|
195
|
+
return warmup.returncode
|
|
196
|
+
warmup_clean = run_command([*base_command, "clean"])
|
|
197
|
+
if warmup_clean.returncode != 0:
|
|
198
|
+
sys.stderr.write(warmup_clean.stdout + warmup_clean.stderr)
|
|
199
|
+
return warmup_clean.returncode
|
|
200
|
+
warmup_rebuild = run_command([*base_command, "build"])
|
|
201
|
+
if warmup_rebuild.returncode != 0:
|
|
202
|
+
sys.stderr.write(warmup_rebuild.stdout + warmup_rebuild.stderr)
|
|
203
|
+
return warmup_rebuild.returncode
|
|
204
|
+
|
|
205
|
+
runs: Dict[str, list] = {"clean": [], "incremental": []}
|
|
206
|
+
|
|
207
|
+
for index in range(1, args.repeats + 1):
|
|
208
|
+
clean_result = run_command([*base_command, "clean"])
|
|
209
|
+
clean_log_path = output_dir / f"{artifact_stem}-clean-prep-{index}.log"
|
|
210
|
+
clean_log_path.write_text(clean_result.stdout + clean_result.stderr)
|
|
211
|
+
if clean_result.returncode != 0:
|
|
212
|
+
sys.stderr.write(clean_result.stdout + clean_result.stderr)
|
|
213
|
+
return clean_result.returncode
|
|
214
|
+
runs["clean"].append(measure_build(base_command, artifact_stem, output_dir, "clean", index))
|
|
215
|
+
|
|
216
|
+
# --- Cached clean builds ---------------------------------------------------
|
|
217
|
+
# When COMPILATION_CACHING is enabled, the compilation cache lives outside
|
|
218
|
+
# DerivedData and survives product deletion. We measure "cached clean"
|
|
219
|
+
# builds by pointing DerivedData at a temp directory, warming the cache with
|
|
220
|
+
# one build, then deleting the DerivedData directory (but not the cache)
|
|
221
|
+
# before each measured rebuild. This captures the realistic scenario:
|
|
222
|
+
# branch switching, pulling changes, or Clean Build Folder.
|
|
223
|
+
should_cached_clean = not args.no_cached_clean and detect_compilation_caching(base_command)
|
|
224
|
+
if should_cached_clean:
|
|
225
|
+
dd_path = Path(args.derived_data_path) if args.derived_data_path else Path(
|
|
226
|
+
tempfile.mkdtemp(prefix="xcode-bench-dd-")
|
|
227
|
+
)
|
|
228
|
+
cached_cmd = list(base_command)
|
|
229
|
+
if not args.derived_data_path:
|
|
230
|
+
cached_cmd.extend(["-derivedDataPath", str(dd_path)])
|
|
231
|
+
|
|
232
|
+
cache_warmup = run_command([*cached_cmd, "build"])
|
|
233
|
+
if cache_warmup.returncode != 0:
|
|
234
|
+
sys.stderr.write("Warning: cached clean warmup build failed, skipping cached clean benchmarks.\n")
|
|
235
|
+
sys.stderr.write(cache_warmup.stdout + cache_warmup.stderr)
|
|
236
|
+
should_cached_clean = False
|
|
237
|
+
|
|
238
|
+
if should_cached_clean:
|
|
239
|
+
runs["cached_clean"] = []
|
|
240
|
+
for index in range(1, args.repeats + 1):
|
|
241
|
+
shutil.rmtree(dd_path, ignore_errors=True)
|
|
242
|
+
runs["cached_clean"].append(
|
|
243
|
+
measure_build(cached_cmd, artifact_stem, output_dir, "cached-clean", index)
|
|
244
|
+
)
|
|
245
|
+
shutil.rmtree(dd_path, ignore_errors=True)
|
|
246
|
+
|
|
247
|
+
# --- Incremental / zero-change builds --------------------------------------
|
|
248
|
+
incremental_label = "incremental"
|
|
249
|
+
if args.touch_file:
|
|
250
|
+
touch_path = Path(args.touch_file)
|
|
251
|
+
if not touch_path.exists():
|
|
252
|
+
sys.stderr.write(f"--touch-file path does not exist: {touch_path}\n")
|
|
253
|
+
return 1
|
|
254
|
+
incremental_label = "incremental"
|
|
255
|
+
else:
|
|
256
|
+
incremental_label = "zero-change"
|
|
257
|
+
|
|
258
|
+
for index in range(1, args.repeats + 1):
|
|
259
|
+
if args.touch_file:
|
|
260
|
+
touch_path.touch()
|
|
261
|
+
runs["incremental"].append(
|
|
262
|
+
measure_build(base_command, artifact_stem, output_dir, incremental_label, index)
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
summary: Dict[str, object] = {
|
|
266
|
+
"clean": stats_for(runs["clean"]),
|
|
267
|
+
"incremental": stats_for(runs["incremental"]),
|
|
268
|
+
}
|
|
269
|
+
if "cached_clean" in runs:
|
|
270
|
+
summary["cached_clean"] = stats_for(runs["cached_clean"])
|
|
271
|
+
|
|
272
|
+
artifact = {
|
|
273
|
+
"schema_version": "1.2.0" if "cached_clean" in runs else "1.1.0",
|
|
274
|
+
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
275
|
+
"build": {
|
|
276
|
+
"entrypoint": "workspace" if args.workspace else "project",
|
|
277
|
+
"path": args.workspace or args.project,
|
|
278
|
+
"scheme": args.scheme,
|
|
279
|
+
"configuration": args.configuration,
|
|
280
|
+
"destination": args.destination or "",
|
|
281
|
+
"derived_data_path": args.derived_data_path or "",
|
|
282
|
+
"command": shell_join(base_command),
|
|
283
|
+
},
|
|
284
|
+
"environment": {
|
|
285
|
+
"host": platform.node(),
|
|
286
|
+
"macos_version": platform.platform(),
|
|
287
|
+
"xcode_version": xcode_version(),
|
|
288
|
+
"cwd": os.getcwd(),
|
|
289
|
+
},
|
|
290
|
+
"runs": runs,
|
|
291
|
+
"summary": summary,
|
|
292
|
+
"notes": [f"touch-file: {args.touch_file}"] if args.touch_file else [],
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
artifact_path = output_dir / f"{artifact_stem}.json"
|
|
296
|
+
artifact_path.write_text(json.dumps(artifact, indent=2) + "\n")
|
|
297
|
+
|
|
298
|
+
print(f"Saved benchmark artifact: {artifact_path}")
|
|
299
|
+
print(f"Clean median: {artifact['summary']['clean']['median_seconds']}s")
|
|
300
|
+
if "cached_clean" in artifact["summary"]:
|
|
301
|
+
print(f"Cached clean median: {artifact['summary']['cached_clean']['median_seconds']}s")
|
|
302
|
+
inc_label = "Incremental" if args.touch_file else "Zero-change"
|
|
303
|
+
print(f"{inc_label} median: {artifact['summary']['incremental']['median_seconds']}s")
|
|
304
|
+
return 0
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
if __name__ == "__main__":
|
|
308
|
+
raise SystemExit(main())
|