Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ systems like Buck and Bazel.
3. [Dependency matrix](docs/DEPENDENCY-MATRIX.md) — what may depend on what
4. [External deps catalogs](docs/DEPS-CATALOG.md) · [Compose](docs/COMPOSE.md) · [Environment](docs/ENV.md)
5. [Plugin publish path](docs/PLUGIN-PUBLISH.md) — Portal metadata DSL + release notes (F-016)
6. [Configuration performance](docs/CONFIGURATION-PERFORMANCE.md) — measure + hot-path guidance (F-017 / GH #106)

Configuration made easy:

Expand Down Expand Up @@ -191,6 +192,8 @@ Android getting-started tutorial: **F-015**
([`docs/GETTING-STARTED.md`](docs/GETTING-STARTED.md)).
Plugin Portal publish path: **F-016**
([`docs/PLUGIN-PUBLISH.md`](docs/PLUGIN-PUBLISH.md)).
Configuration-time performance: **F-017**
([`docs/CONFIGURATION-PERFORMANCE.md`](docs/CONFIGURATION-PERFORMANCE.md)).

Icons made by <a href="https://www.flaticon.com/authors/freepik" title="Freepik">Freepik</a>
from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a>
2 changes: 1 addition & 1 deletion TICKETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Update this file when picking or finishing work. Cron workers must pick the **hi
| F-014 | done | Sample app: gold-standard multi-feature structure | packageName + source-root alignment; `docs/SAMPLE-APP.md`; home/characters pattern |
| F-015 | done | Android project tutorial (getting started) | GH #53; `docs/GETTING-STARTED.md` + README entry |
| F-016 | done | Plugin publish path (Portal user + target publish config) | GH #132 done (`formaPluginConfiguration`/`formaPublishedPlugin`); GH #133 Portal org is human admin — see `docs/PLUGIN-PUBLISH.md` |
| F-017 | todo | Configuration-time performance pass | GH #106, #42 |
| F-017 | done | Configuration-time performance pass | GH #106, #42; validator/feature caches + lean deps; `docs/CONFIGURATION-PERFORMANCE.md` |

## P2 — forma-core extraction

Expand Down
3 changes: 2 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ Suggested extraction order (tickets F-020…F-024):
| ~~Missing Android getting-started tutorial~~ → [`docs/GETTING-STARTED.md`](GETTING-STARTED.md) | F-015 done |
| Shared `library` suffix for JVM vs Android library | F-020 |
| Plugin publish / Portal path | F-016 (`docs/PLUGIN-PUBLISH.md`; Portal org GH #133 is human) |
| Configuration-time cost (includer walk, stores) | F-017 |
| ~~Configuration-time cost (validators/deps/repos)~~ → [`docs/CONFIGURATION-PERFORMANCE.md`](CONFIGURATION-PERFORMANCE.md) | F-017 done |

---

Expand All @@ -344,6 +344,7 @@ Suggested extraction order (tickets F-020…F-024):
| Goal | Start here |
|------|------------|
| New user / first project | [`docs/GETTING-STARTED.md`](GETTING-STARTED.md) |
| Configuration-time performance | [`docs/CONFIGURATION-PERFORMANCE.md`](CONFIGURATION-PERFORMANCE.md) |
| New target type | `AndroidTargets.kt` + new DSL file under `plugins/android/src/main/java/` + validator list |
| Tighten dep rules | `validator(...)` in that DSL file; update this doc §2.2 |
| Global SDK/AGP defaults | `androidProjectConfiguration` + sample `application/build.gradle.kts` |
Expand Down
115 changes: 115 additions & 0 deletions docs/CONFIGURATION-PERFORMANCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Configuration-time performance (F-017)

Forma should keep **Gradle configuration** cheap so large multi-module Android
graphs stay interactive. This note captures what we measure, what we changed
for GH #106 / #42, and what to avoid when extending Forma.

## How to measure (sample `application/`)

From a clean daemon + no configuration cache:

```bash
source scripts/env-mac.sh
cd application
./gradlew --stop
rm -rf .gradle/configuration-cache
./gradlew help --no-configuration-cache --profile --offline
# open build/reports/profile/profile-*.html
# look at "Configuring Projects"
```

Warm / configuration-cache path:

```bash
./gradlew help --profile --offline
# second run should reuse configuration cache when inputs are unchanged
```

Optional: `./gradlew help --scan` when a Build Scan account is available
(GH #42). Local `--profile` is enough for regression checks on this host.

### Profile anchors on worker host (2026-07-12)

Absolute numbers move with machine load, daemon state, and composite plugin
recompilation. Prefer same-machine before/after pairs.

| Run | Configuring projects (before → after F-017) | Notes |
|-----|---------------------------------------------|-------|
| Daemon warm, `help --no-configuration-cache --offline` | ~4.8s → **~1.1s** (best pair) | first cold after heavy load can spike |
| `help` with configuration cache | ~0.7–1.3s | sample already enables CC |

Treat F-017 as **allocation / hot-path** work (validators, deps, feature
definitions, skip empty repo blocks), not as a claim of a fixed wall-clock %.

## What costs configuration time in Forma

1. **Per-target work** — every `api` / `impl` / `androidLibrary` / … call:
- suffix validation for self + each project dependency
- AGP / Kotlin plugin apply + extension configuration
- dependency registration (`applyDependencies`)
2. **Dependency list plumbing** — `map` / `filter` / `flatMap` on dep specs
allocates intermediate `ArrayList`s (GH #106).
3. **Repeated repository configuration** — historically each target invoked
`Forma.settings.repositories` on `project.repositories`. Prefer settings /
`dependencyResolutionManagement` (sample already does); Forma no longer
re-applies empty or default repo lambdas per module.
4. **Composite builds** — sample includes `plugins/`, `includer/`,
`build-settings/`, `build-dependencies/` (settings + classpath). That cost
is paid once per cold daemon, not per target, but dominates small samples.
5. **Includer** — uses flat module names (`path` with `-` separators) to avoid
Gradle intermediate projects (see `IncluderPlugin`).

## Changes shipped in F-017

| Area | Change |
|------|--------|
| `validator(...)` | Identity-cache single- and multi-suffix validators; hot path uses precomputed `endsWith` strings, no intermediate `map`/`contains` lists |
| `kotlinFeatureDefinition` / `kotlinAndroidFeatureDefinition` / kapt | Singleton `FeatureDefinition` instances; read live `Forma.settings` at apply time |
| `applyDependencies` | Skip `repositories {}` when config is the empty sentinel; skip entire `dependencies {}` block when all three dep args are `EmptyDependency`; skip plugin lookup when no plugin deps registered |
| `deps` / `FormaDependency.plus` | Prefer typed fields over `filterIsInstance`; pre-size / single-pass merges; indexed `forEach` over specs |
| Catalog generators | `filteredTokens` is a `Set` for O(1) membership |
| Android targets | Drop redundant `repositoriesConfiguration = Forma.settings.repositories` (default empty) |

## Guidance for contributors

**Do**

- Reuse shared validators / feature definitions instead of allocating per call.
- Prefer `FormaDependency.forEach` over materializing `.names` / `.targets` /
`.files` when applying deps.
- Put repositories in **settings** (`dependencyResolutionManagement` /
`pluginManagement`), not in every subproject.
- Keep content validators (`onlyAllowResources`, …) cheap — they touch the
filesystem once per target that needs them.
- Profile cold configuration after non-trivial plugin changes.

**Avoid**

- Configuration-time `println` / logging in hot generators (removed in F-012).
- Building new `List` pipelines on every dependency edge.
- Calling `project.repositories { … }` from each target with the same block.
- Nested Gradle projects named with `:` path separators (Includer already
flattens this).

## Configuration cache & build cache

Sample `application/gradle.properties` already enables:

- `org.gradle.caching=true`
- `org.gradle.parallel=true`
- `org.gradle.configureondemand=true`
- configuration-cache (unsafe flags for Gradle 8.4)

Forma still uses a process-wide `FormaSettingsStore` singleton. That is
configuration-cache friendly only when settings are written during settings /
root buildscript evaluation and read during project configuration of the same
build — which is the supported layout. Do not mutate the store from task
actions.

## Related

- GH #106 Optimize configuration time
- GH #42 Build performance impact
- `docs/ARCHITECTURE.md` — module map
- `docs/DEPS-CATALOG.md` — pure generators
- Ticket **F-017**
31 changes: 31 additions & 0 deletions docs/PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,37 @@

Newest entries first.

## 2026-07-12 — F-017 Configuration-time performance pass (GH #106, #42)

- **Ticket:** F-017 → `done`
- **Branch:** `forma/F-017-config-performance` (from `origin/v2`)
- **Code:**
- `validator(...)` identity-caches single/multi suffix validators; hot path uses
precomputed dash-suffixes (no per-call `map`/`contains` lists)
- `kotlinFeatureDefinition` / `kotlinAndroidFeatureDefinition` / kapt: singleton
`FeatureDefinition` instances; live `Forma.settings` read at apply time
- `applyDependencies`: empty-repo sentinel skips per-target `repositories {}`;
early-return when all dep bags are `EmptyDependency`; skip plugin lookup when
no plugin deps registered
- `deps` / `FormaDependency.plus` / `forEach`: typed merges + indexed loops
(less `filterIsInstance` / intermediate lists)
- Catalog `filteredTokens` → `Set`; Android targets drop redundant
`repositoriesConfiguration = Forma.settings.repositories`
- **Docs:** `docs/CONFIGURATION-PERFORMANCE.md` (measure + guidance); README /
ARCHITECTURE / SAMPLE-APP / TICKETS links
- **Verify (real tool output, OpenJDK 17 + SDK 34):**
- `plugins/`: `./gradlew build --offline` → **BUILD SUCCESSFUL** (63 tasks;
`:deps:test` green)
- `application/`: `./gradlew build --offline` → **BUILD SUCCESSFUL**
(2155 tasks)
- Profile `help --no-configuration-cache --offline` (daemon warm): Configuring
Projects ~4.8s → ~1.1s on best same-host pair; CC reuse `help` **880ms**
- Wall-clock is noisy under load (first cold after full app build spiked);
treat as allocation/hot-path win, not a fixed %
- **Commits/PRs:** this run — push + PR base `v2`
- **Blockers:** none for F-017; deeper remote Build Scans / GH #42 still optional
- **Next step:** F-020 Design forma-core public API (`docs/forma-core-api.md`)

## 2026-07-12 — F-016 Plugin publish path (Portal + target publish config)

- **Ticket:** F-016 → `done` (GH #132 code path; GH #133 Portal org remains human admin)
Expand Down
4 changes: 2 additions & 2 deletions docs/SAMPLE-APP.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,5 +119,5 @@ JDK **17**. See [ENV.md](ENV.md), [COMPOSE.md](COMPOSE.md), [ARCHITECTURE.md](AR
5. **Modern targets** represented: View widgets + Compose widget side by side.
6. **Documented** enough to copy without reading every `build.gradle.kts`.

Not in scope for F-014: full navigation redesign (GH #46), publish path (F-016),
or configuration-time performance (F-017).
Not in scope for F-014: full navigation redesign (GH #46), publish path (F-016).
Configuration-time performance: [CONFIGURATION-PERFORMANCE.md](CONFIGURATION-PERFORMANCE.md) (F-017).
1 change: 0 additions & 1 deletion plugins/android/src/main/java/androidApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ fun Project.androidApp(
UiLibraryTargetTemplate,
),
dependencies = dependencies,
repositoriesConfiguration = Forma.settings.repositories,
testDependencies = testDependencies,
androidTestDependencies = androidTestDependencies,
configurationFeatures = kaptConfigurationFeature()
Expand Down
3 changes: 1 addition & 2 deletions plugins/android/src/main/java/androidBinary.kt
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,7 @@ fun Project.androidBinary(
ComposeWidgetTargetTemplate,
UiLibraryTargetTemplate,
),
dependencies = dependencies,
repositoriesConfiguration = Forma.settings.repositories
dependencies = dependencies
)

return TargetBuilder(this)
Expand Down
1 change: 0 additions & 1 deletion plugins/android/src/main/java/androidLibrary.kt
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ fun Project.androidLibrary(
dependencies = dependencies,
testDependencies = testDependencies,
androidTestDependencies = androidTestDependencies,
repositoriesConfiguration = Forma.settings.repositories,
configurationFeatures = kaptConfigurationFeature()
)

Expand Down
3 changes: 1 addition & 2 deletions plugins/android/src/main/java/androidRes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ fun Project.androidRes(
WidgetTargetTemplate,
ComposeWidgetTargetTemplate,
),
dependencies = dependencies,
repositoriesConfiguration = Forma.settings.repositories
dependencies = dependencies
)
}
3 changes: 1 addition & 2 deletions plugins/android/src/main/java/androidTestUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ fun Project.androidTestUtil(

applyDependencies(
validator = validator(AndroidTestUtilTargetTemplate, TestUtilTargetTemplate),
dependencies = dependencies,
repositoriesConfiguration = Forma.settings.repositories
dependencies = dependencies
)
}
1 change: 0 additions & 1 deletion plugins/android/src/main/java/androidUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ fun Project.androidUtil(
),
dependencies = dependencies,
testDependencies = testDependencies,
repositoriesConfiguration = Forma.settings.repositories,
configurationFeatures = kaptConfigurationFeature()
)
}
3 changes: 1 addition & 2 deletions plugins/android/src/main/java/api.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ fun Project.api(
)
applyDependencies(
validator = validator(ApiTargetTemplate, LibraryTargetTemplate),
dependencies = dependencies,
repositoriesConfiguration = Forma.settings.repositories
dependencies = dependencies
)
}
3 changes: 1 addition & 2 deletions plugins/android/src/main/java/composeWidget.kt
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ fun Project.composeWidget(
),
dependencies = dependencies,
testDependencies = testDependencies,
androidTestDependencies = androidTestDependencies,
repositoriesConfiguration = Forma.settings.repositories
androidTestDependencies = androidTestDependencies
)
}
1 change: 0 additions & 1 deletion plugins/android/src/main/java/impl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ fun Project.impl(
dependencies = dependencies,
testDependencies = testDependencies,
androidTestDependencies = androidTestDependencies,
repositoriesConfiguration = Forma.settings.repositories,
configurationFeatures = kaptConfigurationFeature()
)
}
1 change: 0 additions & 1 deletion plugins/android/src/main/java/library.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ fun Project.library(
validator = validator(UtilTargetTemplate, TestUtilTargetTemplate),
dependencies = dependencies,
testDependencies = testDependencies,
repositoriesConfiguration = Forma.settings.repositories,
configurationFeatures = kaptConfigurationFeature()
)
}
3 changes: 1 addition & 2 deletions plugins/android/src/main/java/testUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ fun Project.testUtil(

applyDependencies(
validator = validator(TestUtilTargetTemplate, UtilTargetTemplate),
dependencies = dependencies,
repositoriesConfiguration = Forma.settings.repositories
dependencies = dependencies
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,19 @@ data class FeatureDefinition<Extension : Any, FeatureConfiguration : Any>(
val pluginExtension: KClass<Extension>,
val featureConfiguration: FeatureConfiguration,
val defaultDependencies: NamedDependency = emptyDependency(),
val androidProjectSettings: AndroidProjectSettings = Forma.settings,
/**
* Optional settings override. When null (default), [applyConfiguration] reads the live
* [Forma.settings] so cached [FeatureDefinition] instances stay correct after
* `androidProjectConfiguration { … }` stores settings (F-017).
*/
val androidProjectSettings: AndroidProjectSettings? = null,
val configuration: (Extension, FeatureConfiguration, Project, AndroidProjectSettings) -> Unit
) {
fun applyConfiguration(project: Project) = configuration(
project.the(pluginExtension),
featureConfiguration,
project,
androidProjectSettings
androidProjectSettings ?: Forma.settings
)
}

Expand All @@ -31,7 +36,10 @@ fun Project.applyFeatures(
) = features.forEach { definition ->
apply(plugin = definition.pluginName)
definition.applyConfiguration(this)
definition.defaultDependencies.names.forEach {
dependencies.addDependencyTo(it.config.name, it.name) { isTransitive = it.transitive }
val defaults = definition.defaultDependencies.names
if (defaults.isNotEmpty()) {
defaults.forEach {
dependencies.addDependencyTo(it.config.name, it.name) { isTransitive = it.transitive }
}
}
}
Loading
Loading