# FastBuild - Android Incremental Build Accelerator English | [简体中文](README.md) ## Background In large Android multi-module projects, every time you run `assembleDebug` or `assembleRelease`, Gradle executes a large number of compile-related tasks for **all** modules—even if you only changed code in a single module. You may have noticed that when you run an incremental build without modifying any code, these tasks show a status of `UP-TO-DATE`, seemingly skipped—but in reality, Gradle still performs **input/output cache comparison** inside each `UP-TO-DATE` task (checking file fingerprints, comparing snapshots, etc.). The overhead varies by task, and testing reveals that some tasks spend **hundreds of milliseconds** just on their `UP-TO-DATE` check. The more modules in a project, the more these "seemingly skipped" tasks accumulate into **minutes of wasted time**. **FastBuild** takes a different approach: if a module's code hasn't changed, there's no need to perform `UP-TO-DATE` checks at all. By setting tasks to `disabled`, their execution status becomes `SKIPPED`—**zero overhead**—completely eliminating this unnecessary cost. It works by **concurrently scanning** each module's file states (mtime + MD5) before compilation using a thread pool sized by CPU cores, performing an incremental comparison against the states saved from the last successful build, automatically identifying unchanged modules, and disabling their compile tasks at runtime. ## Test Results Results from a real production project (**100+ modules**): | Scenario | Without FastBuild | With FastBuild | Improvement | |----------|------------------|---------------|-------------| | One-line code change, incremental build | ~2 minutes | ~25 seconds | **80%+ faster** | > The more modules in your project, the higher the proportion of unchanged modules, and the greater the speedup. ## How It Works ### Overall Flow ``` gradlew assembleDebug │ ▼ ① Check if the command starts with "assemble" ├─ No → Do nothing, proceed normally └─ Yes ↓ ② Load previous build cache (.json files) │ ▼ ③ taskGraph.whenReady callback ├─ Extract modules that actually participate in the task graph ├─ Create thread pool sized by CPU cores ├─ Concurrently scan each module's files: record mtime + MD5 hash ├─ Compare with previous state → changedModules (modules with file changes) ├─ Dependency propagation: if B changed, all modules that depend on B (e.g. A) are added to changedModules, so A is recompiled when B’s API changes and runtime signature errors are avoided └─ Set tasks of unchangedModules to disabled (SKIPPED) │ ▼ ④ Gradle runs the build (unchanged module tasks are SKIPPED, zero overhead) │ ▼ ⑤ buildFinished callback └─ On success, concurrently write each module's state to JSON ``` ### The Key: UP-TO-DATE vs SKIPPED | Status | Meaning | Actual Overhead | |--------|---------|----------------| | `UP-TO-DATE` | Gradle ran cache comparison and confirmed no input/output changes | **Has overhead** (file fingerprinting, snapshot comparison; hundreds of ms for some tasks) | | `SKIPPED` | task.enabled = false; Gradle **does not execute at all** | **Zero overhead** | FastBuild essentially promotes unchanged module tasks from `UP-TO-DATE` to `SKIPPED`. ### File State Detection | File Type | Recorded Data | Comparison Strategy | |-----------|--------------|-------------------| | Source / resource files | `lastModifiedTime` + MD5 `hash` | Compare mtime first; if changed, compare hash; different hash → changed | | build directory | Fingerprint: file count + total size (configurable) | Fingerprint changed → changed; when disabled, only detects build dir deletion | | Excluded file suffixes | Not recorded | Not compared | ### Dependency propagation (avoid runtime errors from API changes) If **A depends on B** and B’s **public API changes** (e.g. Kotlin interface method changes from `fun get() = "B"` to `fun get() = true`, return type changes) while A’s source is unchanged, file-only comparison would mark A as unchanged. Then only B would be recompiled; A would still call B with the old method signature at runtime and hit **method signature mismatch** errors. After computing file-based changedModules, FastBuild performs **dependency propagation** using Gradle’s **compileClasspath** dependency graph: if B is in changedModules, every module that **directly or transitively depends on B** (e.g. A) is also added to changedModules, so A is recompiled and stays in sync with B’s new API. ## Quick Start ### 1. Add the File Place `fastbuild.gradle` in your project's **root directory**. ### 2. Apply in Root build.gradle ```groovy // Root build.gradle apply from: 'FastBuild.gradle' ``` ### 3. Build as Usual ```bash ./gradlew assembleDebug ``` On the first run, FastBuild performs a full scan and records each module's file states. From the second run onward, unchanged modules will automatically skip compile tasks. ## Configuration All configuration options are member variables in the `FastBuildPlugin` class inside `fastbuild.gradle`. Edit them directly: ### EXCLUDED_MODULE_NAMES ```groovy private static final List EXCLUDED_MODULE_NAMES = ['app'] ``` **Modules to completely exclude from processing.** Modules in this list will not be scanned, will not participate in change detection, and their tasks will not be disabled. Typical usage: The `app` module is usually the final packaging module and should always be excluded. ### EXCLUDED_TASK_NAMES ```groovy private static final List EXCLUDED_TASK_NAMES = ['lint'] ``` **Task names to never disable.** Even if the owning module is unchanged, tasks whose names contain any keyword in this list will not be disabled. Matching rule: `taskName.toLowerCase().contains(keyword.toLowerCase())`. ### INTEGRITY_CHECK ```groovy private static final boolean INTEGRITY_CHECK = true ``` | Value | Behavior | |-------|----------| | `true` | **Integrity mode**: Records build directory "file count + total size" fingerprint + dependency chain propagation (B changed → A that depends on B is also recompiled) | | `false` | **Fast mode**: Only triggers recompilation when the build directory is **deleted**; does not check dependency chains | - **CI/CD or Gerrit Code Check**: Recommended to set to `true` to ensure build artifact integrity and avoid runtime method signature errors from API changes. - **Daily development**: Can be set to `false` to skip build fingerprinting and dependency analysis for faster scanning; however, if a dependency module changes its public API (e.g. return type), upstream modules may not auto-recompile—manual clean may be needed. ### EXCLUDED_FILE ```groovy private static final List EXCLUDED_FILE = ['.log', '.tmp', '.bak', '.swp', '.puml', '.md'] ``` **File suffixes to exclude from recording.** Matching files are not recorded and do not participate in change detection. Modifying these files will not trigger module recompilation. ## Supported Scenarios ### Daily Incremental Development The most common scenario. After modifying code in a module, run `assembleDebug`. FastBuild automatically identifies changed modules for compilation while the rest are directly SKIPPED. ### Dependency API changes (e.g. Kotlin interface return type) Typical case: A depends on B; B exposes `fun get() = "B"` (implicit String return) and A only calls and prints it. If B is changed to `fun get() = true` (Boolean), file-only comparison would mark A as unchanged; compiling only B would cause runtime method signature errors. When `INTEGRITY_CHECK = true`, FastBuild **propagates dependencies**: when B is marked changed, every module that depends on B (including A) is added to changedModules, so A is recompiled and this class of runtime errors is avoided. ### Gerrit Code Check / CI Acceleration In Gerrit-triggered Code Check builds, typically only a few modules have changes. FastBuild can dramatically reduce CI build time. **It is recommended to enable `INTEGRITY_CHECK = true` in CI environments** to ensure build artifact integrity and avoid incorrect results caused by stale build directories from previous builds. ### Branch Switching When switching from branch A to branch B and back to A: - Cache is **not** fully invalidated; no full rebuild occurs - Only modules whose file states actually changed will be recompiled - With `INTEGRITY_CHECK` enabled, modules whose build directories were modified by another branch will also be detected, and dependency chains are transitively checked ### Build Failure / Interruption - On build failure or Ctrl+C interruption, **cache is not written back** - Next build uses the cache from the last **successful** build—no corrupted state ### Large Multi-Module Projects - File scanning and cache writing are **parallelized** by CPU core count; more modules = greater speedup - A single module scan failure does not affect others; that module is treated as changed and compiled normally ### First Build After Clean After running `clean`, the cache (located at `build/FastBuild/`) is cleared. The first build is equivalent to a full build that rebuilds the cache; subsequent builds resume incremental mode. ## Cache Directory Cache files are stored under `build/FastBuild/` in the project root, one `.json` file per module. They are automatically cleaned by `clean`. ``` build/FastBuild/ ├── moduleA.json ├── moduleB.json └── moduleC.json ``` JSON files are pretty-printed, making it easy to inspect which files are tracked and their states. ## Dependencies - **Gson**: Used for JSON serialization/deserialization (the `buildscript` dependency is declared within the script itself) - **Gradle**: Compatible with standard Android Gradle projects ## Notes 1. Only activates for `assemble*` commands; other commands (`lint`, `test`, `clean`, etc.) are unaffected 2. Do not run multiple `assemble` processes concurrently on the same project directory (concurrent cache writes may conflict) 3. The first build is always a full build (no cache); acceleration takes effect from the second build onward 4. If you suspect cache-related build issues, delete `build/FastBuild/` or run `clean` to reset ## License MIT License