refactor(page): 删除旧版页面文件

This commit is contained in:
2026-08-16 05:31:45 +08:00
parent ca76678afc
commit 1396487af9
88 changed files with 12633 additions and 1081 deletions

View File

@@ -0,0 +1,154 @@
---
name: kotlin-tooling-native-build-performance
description: >
Diagnoses and fixes slow Kotlin/Native compilation and linking in Kotlin
Multiplatform projects that target iOS. Use when the user reports slow iOS or
shared-framework builds, long linkDebug*/linkRelease* or XCFramework tasks,
cold CI builds that re-download the Kotlin/Native toolchain, KSP or other
generated code on the native path, transitiveExport usage, or asks for a
local-development versus CI build performance plan.
license: Apache-2.0
metadata:
author: JetBrains
version: "1.0.0"
tested_models: "openai/gpt-5.5, openai/gpt-5.4-mini"
last_eval: "2026-07-06"
---
# Kotlin/Native Build Performance
Turn "the iOS build is slow" into a measured diagnosis and a small set of safe
fixes. Two rules apply throughout:
1. Never trade away required release behavior. A faster local loop must not
change what CI publishes.
2. Measure before and after with the same command and the same build state.
An unmeasured fix is a guess.
## Step 0: Classify the Slow Scenario
Establish four facts before editing anything: **where** (local or CI),
**what** (debug feedback loop or release/distribution artifact), **state**
(first build, clean, warm, or no-op), and **phase** (which tasks dominate the
log). Then match the dominant symptom:
| Symptom in the build log | Likely cause | Read |
|---|---|---|
| `linkRelease*` or `*ReleaseXCFramework` tasks in a local development loop | Building distribution artifacts for development | [artifacts-and-targets](references/artifacts-and-targets.md) |
| Kotlin/Native compiler distribution downloaded on every CI run | `~/.konan` not preserved between runs | [caching-and-gradle](references/caching-and-gradle.md) |
| Long pause before the first task starts | Configuration phase, no configuration cache | [caching-and-gradle](references/caching-and-gradle.md) |
| All iOS targets build when only one simulator is needed | Broad task (`build`, `assemble`, `assemble*XCFramework`) or unused targets | [artifacts-and-targets](references/artifacts-and-targets.md) |
| `ksp*` tasks ahead of `compileKotlinIos*` | Generated-code work on the native path | [exports-and-generated-code](references/exports-and-generated-code.md) |
| Small source edit recompiles and relinks everything | Compiler caches disabled, or missing incrementality | [caching-and-gradle](references/caching-and-gradle.md), [experimental](references/experimental.md) |
| Machine overloaded while several `link*` tasks run at once | Parallel native linking | [caching-and-gradle](references/caching-and-gradle.md), worker-limit caveat |
## Step 1: Audit and Measure
1. Run the static audit from the project root:
```bash
scripts/audit-native-build.sh /path/to/project
```
It is read-only and prints `file:line` findings (disabled caches, broad
local tasks, `transitiveExport`, broad KSP configuration, missing CI
`.konan` cache), each pointing at the reference file with the fix.
Findings are leads, not verdicts — confirm each against project policy.
2. Find the command the user actually waits for: a script, a CI step, or the
Gradle invocation inside an Xcode build phase. Optimize that command, not
a task you picked yourself.
3. Run it twice when practical. The first build downloads Kotlin/Native
components and fills caches; only the second and later runs are
representative. Attribute time per task before blaming the compiler:
```properties
kotlin.build.report.output=file # writes build/reports/kotlin-build/
```
Gradle's `--scan` or `--profile` work too.
4. If you cannot run the build (no macOS host, no Xcode), analyze logs, build
scans, or checked-in metrics instead — and state explicitly that the
conclusion is static.
## Step 2: Fix in Safe Order
Apply fixes one at a time, re-measuring as you go:
1. **Restore healthy defaults** — remove cache/daemon workarounds, enable
Gradle build and configuration caches, keep `~/.konan` warm in CI, update
Kotlin: [references/caching-and-gradle.md](references/caching-and-gradle.md)
2. **Build only what the feedback loop needs** — one specific task per loop,
correct integration method, justified target matrix:
[references/artifacts-and-targets.md](references/artifacts-and-targets.md)
3. **Cut export and generated-code cost** — drop `transitiveExport`, narrow
`export(...)`, scope KSP work to the native compilations that need it:
[references/exports-and-generated-code.md](references/exports-and-generated-code.md)
4. **Experimental switches last, with the user's agreement**:
[references/experimental.md](references/experimental.md)
## Worked Example
A developer on an Apple Silicon Mac complains that "every shared-module
change costs 12 minutes". Their loop runs `./gradlew :shared:assembleXCFramework`.
A build scan of the second (warm) run shows:
```
:shared:linkReleaseFrameworkIosArm64 348s
:shared:linkReleaseFrameworkIosX64 341s
:shared:compileKotlinIosX64 96s
:shared:linkDebugFrameworkIosSimulatorArm64 41s
:shared:compileKotlinIosSimulatorArm64 38s
configuration phase 64s
```
Reasoning chain:
- The loop is **local + debug + warm**, but ~690s goes to `linkRelease*` —
release linking is an order of magnitude slower than debug and only CI
needs it. Replace the local command with
`:shared:linkDebugFrameworkIosSimulatorArm64` (or the Xcode embed task if
Xcode drives the build). *(artifacts-and-targets)*
- All `iosX64` work serves Intel simulators; ask whether the team still
supports them before removing the target. *(artifacts-and-targets)*
- 64s of configuration on every run disappears behind
`org.gradle.configuration-cache=true` once trialed. *(caching-and-gradle)*
- Expected loop after the change: ~40s compile + ~40s link on warm builds —
confirm by re-running the new command twice and comparing.
- CI keeps `assembleXCFramework` untouched; note that explicitly in the
report.
## Verify
- [ ] Re-run the exact baseline command; compare warm build against warm
build, not warm against cold.
- [ ] Second run with the configuration cache reports it is being reused.
- [ ] The local development log no longer contains `linkRelease*`,
`*ReleaseXCFramework`, or removed generator tasks.
- [ ] CI still produces every required release artifact, unchanged.
- [ ] Tests pass and the app still runs from Xcode.
- [ ] `scripts/audit-native-build.sh` reports no findings you have not
consciously accepted and documented.
## Report Your Changes
Close with a short performance note:
- The slow scenario (local/CI, debug/release, cold/warm) and the measured
evidence — or a statement that the analysis was static.
- Each change, and why it is safe for release behavior.
- The before/after commands the user can run to confirm the win.
- Remaining tradeoffs: experimental flags enabled, targets removed under a
policy assumption, worker limits, or generated-code work deferred.
- Links to the relevant official documentation below.
## Official Documentation
| Topic | Link |
|---|---|
| Improving Kotlin/Native compilation time | https://kotlinlang.org/docs/native-improving-compilation-time.html |
| Kotlin Gradle plugin compilation and caches | https://kotlinlang.org/docs/gradle-compilation-and-caches.html |
| iOS integration methods | https://kotlinlang.org/docs/multiplatform-ios-integration-overview.html |
| Direct integration with Xcode | https://kotlinlang.org/docs/multiplatform/multiplatform-direct-integration.html |
| Building final native binaries and XCFrameworks | https://kotlinlang.org/docs/multiplatform/multiplatform-build-native-binaries.html |
| Kotlin/Native binary options | https://kotlinlang.org/docs/native-binary-options.html |
| KSP with Kotlin Multiplatform | https://kotlinlang.org/docs/ksp-multiplatform.html |

View File

@@ -0,0 +1,32 @@
# Testing
This skill is A/B evaluated with the JetBrains [`skills-ab-eval`](https://github.com/JetBrains/skills-ab-eval-cookbook) tool.
The suite lives at
[`kotlin-tooling-native-build-performance`](https://github.com/JetBrains/skills-ab-eval-cookbook/tree/main/kotlin-tooling-native-build-performance)
and contains two tasks:
- **`native-build-performance-audit-task`** — a synthetic KMP iOS fixture
seeded with common Kotlin/Native build-performance mistakes.
- **`kotlinproject-native-build-performance-task`** — a KotlinProject template
copy with intentional cache, target, local-build, CI, and export regressions.
Each task runs the agent with and without the skill and scores the result on a
weighted rubric (reward 01), requiring a `BUILD_PERFORMANCE_REPORT.md` that
preserves production release behavior.
## Latest results (2026-07-06)
Run via `skills-ab-eval` on the `codex` agent, `openai/gpt-5.5` at low reasoning
effort, n = 6 pairs per task:
| Task | Without skill | With skill | Δ | Significance |
|---|---:|---:|---:|---|
| Synthetic native build audit | 0.74 ± 0.05 | 0.99 ± 0.02 | +0.25 | p = 0.031 |
| KotlinProject native build audit | 0.59 ± 0.05 | 0.90 ± 0.02 | +0.31 | p = 0.031 |
The with-skill arms are near-deterministic (σ ≤ 0.02): the diagnostic procedure
lives in the skill, not in the model's reasoning budget. Additional
`openai/gpt-5.4-mini` runs (high and low reasoning) are recorded per task.
See each task's `EVALUATION.md` in the cookbook for full per-trial reward
breakdowns and token/cost metrics.

View File

@@ -0,0 +1,44 @@
# Build the Right Artifact for the Feedback Loop
A release binary takes roughly an order of magnitude longer to build than a
debug binary, and umbrella tasks such as `build` and `assemble` compile the
same code several times. Map each feedback loop to one specific task.
## Task table
| Feedback loop | Correct task |
|---|---|
| Xcode builds and runs the app (direct integration) | `:shared:embedAndSignAppleFrameworkForXcode` |
| Gradle-only check of the Apple Silicon simulator framework | `:shared:linkDebugFrameworkIosSimulatorArm64` |
| CocoaPods integration | `:shared:linkPodDebugFrameworkIosSimulatorArm64` |
| Distribution or App Store validation | `assemble<Name>ReleaseXCFramework` — in CI, not in the local loop |
| Debug XCFramework genuinely required | `assemble<Name>DebugXCFramework` |
Per-target link tasks follow the pattern
`link<BuildType>Framework<Target>`; find the exact names with
`./gradlew :shared:tasks` or in the build log.
## Rules
- `embedAndSignAppleFrameworkForXcode` builds only the slice Xcode asked for
and must run from an Xcode build phase, not standalone. Direct integration
uses the documented run-script phase; keep its
`OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED` guard so the IDE does not trigger a
second Gradle invocation.
- Do not mix integration methods: a project either uses direct integration or
the CocoaPods integration, and the local task must match the one in use.
- Do not replace CI release artifacts with debug artifacts. If CI release
builds are slow, fix caching, the target matrix, and exports instead.
## Target matrix
`*XCFramework` tasks build every declared target. Remove a target only when
project policy confirms it is unused — the common case is dropping
`iosX64()` when the team no longer supports Intel-based simulators. State the
policy assumption in your report; if policy is unclear, ask instead of
deleting.
Docs:
https://kotlinlang.org/docs/multiplatform-ios-integration-overview.html,
https://kotlinlang.org/docs/multiplatform/multiplatform-direct-integration.html,
https://kotlinlang.org/docs/multiplatform/multiplatform-build-native-binaries.html

View File

@@ -0,0 +1,68 @@
# Caching and Gradle Configuration
Safe for every scenario; apply these before anything else.
## Update Kotlin
The latest Kotlin version is the first official recommendation for
Kotlin/Native compilation time — each release improves compiler performance.
Check `gradle/libs.versions.toml` or the plugin block and propose an upgrade
if the project is behind. Read the compatibility guide for the target release
before upgrading; for example, use the
[Kotlin 2.4 compatibility guide](https://kotlinlang.org/docs/compatibility-guide-24.html)
when moving to Kotlin 2.4.x. Each target release has a corresponding
compatibility guide. See the
[Kotlin 2.3.20 release notes](https://kotlinlang.org/docs/whatsnew2320.html#new-dsl-for-disabling-compilation-cache)
for the related cache change.
## Remove stale workarounds
Projects accumulate workarounds for long-fixed compiler issues. Upgrade Kotlin
first, then inspect:
- `kotlin.native.disableCompilerDaemon=true`
- `org.gradle.daemon=false`
Each disables a performance feature. Remove stale workarounds and check
whether the build completes successfully.
## Enable Gradle caching
```properties
# gradle.properties
org.gradle.caching=true
org.gradle.configuration-cache=true
```
- Trial the configuration cache with the user's real task before committing
it. If Gradle reports configuration-cache problems, fix the blockers listed
in the HTML report instead of abandoning the cache.
- The configuration cache implicitly enables parallel task execution, which
can run several `link*` tasks at once and overload the machine (KT-70915).
If that happens, bound it with `org.gradle.workers.max` in
`gradle.properties` or `--max-workers` on the command line — do not turn
the cache off for this reason alone.
- Delete `org.gradle.configureondemand=true`. Kotlin Multiplatform does not
support Configuration on Demand, and it is not the same feature as the
configuration cache.
- For CI, a remote Gradle build cache extends `org.gradle.caching` across
machines.
## Keep `~/.konan` warm in CI
Kotlin/Native stores its compiler distribution and caches in `$HOME/.konan`.
Ephemeral CI machines and containers that lose it pay the cold-start cost on
every build. On GitHub Actions:
```yaml
- uses: actions/cache@v4
with:
path: ~/.konan
key: konan-${{ runner.os }}-${{ hashFiles('**/libs.versions.toml') }}
```
Use the `konan.data.dir` Gradle property only when the project intentionally
relocates that directory (for example, to a cacheable path on a CI runner).
Docs: https://kotlinlang.org/docs/native-improving-compilation-time.html and
https://kotlinlang.org/docs/gradle-compilation-and-caches.html

View File

@@ -0,0 +1,32 @@
# Experimental Switches
Offer these last, label them experimental in the report, and keep them out of
the default recommendation set. Get the user's agreement before enabling any
of them.
## Incremental compilation of klib artifacts
```properties
# gradle.properties
kotlin.incremental.native=true
```
Recompiles only the changed part of a klib into the final binary, which helps
warm rebuilds after small edits. If it causes broken or inconsistent builds,
revert it and file a YouTrack issue with a minimized reproducer.
## smallBinary
The `smallBinary` binary option sets `-Oz` as the default LLVM optimization
level to shrink release binaries and their link time. It can cost runtime
performance, so verify hot paths before keeping it. It applies to release
binaries — it is not a fix for slow debug loops.
## LLVM backend customization
Customizing the LLVM backend is a last resort when nothing else helps, and is
out of scope for a routine performance pass — point the user at the official
documentation instead of improvising compiler flags.
Docs: https://kotlinlang.org/docs/native-binary-options.html and
https://kotlinlang.org/docs/native-improving-compilation-time.html

View File

@@ -0,0 +1,50 @@
# Framework Exports and Generated Code
## Framework exports
Every exported module grows the API surface the compiler and linker must
keep.
- Remove `transitiveExport = true`. It exports the entire transitive closure
and disables dead code elimination in many cases — it is almost never what
the project actually needs.
- Keep an explicit `export(...)` only for modules whose API Swift or
Objective-C code calls directly.
```kotlin
// Before: exports everything analytics depends on, defeats DCE
binaries.framework {
export(project(":analytics"))
transitiveExport = true
}
// After: exports exactly the Swift-facing API
binaries.framework {
export(project(":analytics"))
}
```
If Swift code stops compiling after narrowing exports, add back only the
specific modules it references — that is the export list the project really
needs.
## Generated code
If `ksp*` tasks dominate the measured time, report the bottleneck as
generated-code work — do not present a Kotlin/Native tweak as the fix.
- Scope KSP to the targets or source sets that need generated code instead of
a broad `ksp(...)` dependency:
```kotlin
dependencies {
add("kspCommonMainMetadata", libs.myprocessor)
add("kspIosSimulatorArm64", libs.myprocessor)
}
```
- Confirm the generated sources are consumed by the corresponding native
compilation before adding another target-specific KSP configuration.
Docs: https://kotlinlang.org/docs/ksp-multiplatform.html and
https://kotlinlang.org/docs/native-improving-compilation-time.html

View File

@@ -0,0 +1,162 @@
#!/usr/bin/env bash
# Static audit for Kotlin/Native build performance in a KMP project.
#
# Read-only: scans Gradle properties, build scripts, shell scripts, and CI
# workflows for configurations known to slow Kotlin/Native builds, and prints
# findings as `[SEVERITY] file:line message -> reference`.
#
# Usage: audit-native-build.sh [project-root] (default: current directory)
#
# Exit code: 0 always (findings are advice, not errors), unless the root is
# not a Gradle project at all.
set -uo pipefail
ROOT="${1:-.}"
if [ ! -e "$ROOT/settings.gradle.kts" ] && [ ! -e "$ROOT/settings.gradle" ] \
&& [ ! -e "$ROOT/build.gradle.kts" ] && [ ! -e "$ROOT/build.gradle" ]; then
echo "error: $ROOT does not look like a Gradle project root" >&2
exit 1
fi
FINDINGS=0
EXCLUDES=(--exclude-dir=.git --exclude-dir=build --exclude-dir=.gradle --exclude-dir=.kotlin)
# scan <severity> <message> <reference> <grep -E pattern> <include glob>...
scan() {
local severity="$1" message="$2" reference="$3" pattern="$4"
shift 4
local includes=()
for glob in "$@"; do includes+=(--include="$glob"); done
local hits
hits=$(grep -RInE "${EXCLUDES[@]}" "${includes[@]}" -e "$pattern" "$ROOT" 2>/dev/null \
| grep -vE '^[^:]+:[0-9]+:[[:space:]]*(#|//)' || true)
[ -z "$hits" ] && return 0
while IFS= read -r hit; do
FINDINGS=$((FINDINGS + 1))
printf '[%s] %s\n %s\n -> %s\n' \
"$severity" "${hit%%:*}:$(echo "$hit" | cut -d: -f2)" "$message" "$reference"
done <<< "$hits"
}
# require_property <message> <reference> <key> <value>
# Reports when no gradle.properties sets key=value (commented lines ignored).
require_property() {
local message="$1" reference="$2" key="$3" value="$4"
if ! grep -RInE "${EXCLUDES[@]}" --include='gradle.properties' \
-e "^[[:space:]]*${key}[[:space:]]*=[[:space:]]*${value}[[:space:]]*$" \
"$ROOT" >/dev/null 2>&1; then
FINDINGS=$((FINDINGS + 1))
printf '[%s] %s\n %s\n -> %s\n' \
"MEDIUM" "gradle.properties" "$message" "$reference"
fi
}
echo "== Kotlin/Native build performance audit: $ROOT =="
echo
## 1. Disabled performance defaults (highest impact, safest to fix)
scan HIGH \
"Kotlin/Native compiler daemon disabled" \
"references/caching-and-gradle.md: remove stale workarounds" \
'^[[:space:]]*kotlin\.native\.disableCompilerDaemon[[:space:]]*=[[:space:]]*true' \
'gradle.properties'
scan HIGH \
"Gradle daemon disabled" \
"references/caching-and-gradle.md: remove stale workarounds" \
'^[[:space:]]*org\.gradle\.daemon[[:space:]]*=[[:space:]]*false' \
'gradle.properties'
scan MEDIUM \
"Configuration on Demand is unsupported by KMP and is not the configuration cache" \
"references/caching-and-gradle.md: enable Gradle caching" \
'^[[:space:]]*org\.gradle\.configureondemand[[:space:]]*=[[:space:]]*true' \
'gradle.properties'
scan MEDIUM \
"Gradle build cache explicitly disabled" \
"references/caching-and-gradle.md: enable Gradle caching" \
'^[[:space:]]*org\.gradle\.caching[[:space:]]*=[[:space:]]*false' \
'gradle.properties'
require_property \
"org.gradle.caching=true is not set" \
"references/caching-and-gradle.md: enable Gradle caching" \
'org\.gradle\.caching' 'true'
require_property \
"org.gradle.configuration-cache=true is not set (trial it with the real task first)" \
"references/caching-and-gradle.md: enable Gradle caching" \
'org\.gradle\.configuration-cache' 'true'
## 2. Framework exports
scan HIGH \
"transitiveExport = true disables dead code elimination in many cases" \
"references/exports-and-generated-code.md: framework exports" \
'transitiveExport[[:space:]]*=[[:space:]]*true' \
'*.gradle.kts' '*.gradle'
## 3. Generated code on the native path
scan INFO \
"broad ksp(...) dependency; prefer per-target add(\"ksp<Target>\", ...) in KMP" \
"references/exports-and-generated-code.md: generated code" \
'^[[:space:]]*ksp\(' \
'*.gradle.kts' '*.gradle'
## 4. Targets and local build scope
scan INFO \
"iosX64 target declared; confirm Intel-based simulators are still supported" \
"references/artifacts-and-targets.md: target matrix" \
'iosX64[[:space:]]*\(' \
'*.gradle.kts' '*.gradle'
scan MEDIUM \
"broad or release Gradle task in a shell script; if this is the local loop, narrow it" \
"references/artifacts-and-targets.md: task table" \
'gradlew?[^#]*([[:space:]](clean|build|assemble)([[:space:]]|$)|XCFramework|linkRelease)' \
'*.sh'
## 5. CI cold starts
WORKFLOW_HITS=$(grep -RIlE "${EXCLUDES[@]}" --include='*.yml' --include='*.yaml' \
-e 'gradlew|gradle/actions|setup-gradle' "$ROOT/.github" 2>/dev/null || true)
for wf in $WORKFLOW_HITS; do
if ! grep -qE '\.konan' "$wf"; then
FINDINGS=$((FINDINGS + 1))
printf '[%s] %s\n %s\n -> %s\n' \
"MEDIUM" "$wf" \
"workflow runs Gradle but does not cache ~/.konan (cold Kotlin/Native toolchain every run)" \
"references/caching-and-gradle.md: keep .konan warm in CI"
fi
done
## 6. Informational
scan INFO \
"konan.data.dir relocates the Kotlin/Native cache; confirm the new location is preserved" \
"references/caching-and-gradle.md: keep .konan warm in CI" \
'^[[:space:]]*konan\.data\.dir[[:space:]]*=' \
'gradle.properties'
scan INFO \
"experimental kotlin.incremental.native is enabled; keep it labeled experimental in reports" \
"references/experimental.md" \
'^[[:space:]]*kotlin\.incremental\.native[[:space:]]*=[[:space:]]*true' \
'gradle.properties'
echo
if [ "$FINDINGS" -eq 0 ]; then
echo "No static findings. Measure before concluding the build is healthy:"
echo "run the user's real command twice and check per-task time with"
echo "kotlin.build.report.output=file or --scan."
else
echo "$FINDINGS finding(s). Confirm each against the project's policy before fixing;"
echo "measure before and after with the same command (see SKILL.md, Step 1)."
fi
exit 0