|
| 1 | +# Angular + Bazel Build Research: Caching, Remote Build Execution, and islo/Incredibuild Feasibility |
| 2 | + |
| 3 | +> **This fork is a research artifact.** It is a fork of `angular/angular` created under an Incredibuild org solely to document a build-and-investigation session. It contains no product changes to Angular. **Only `//packages/core` was built**, and **no Incredibuild or islo endpoint was exercised** during this session — the RBE feasibility assessment below is analytical, not benchmarked against a live islo/Incredibuild backend. |
| 4 | +
|
| 5 | +## Abstract |
| 6 | + |
| 7 | +This document records a research session whose goal was to (1) understand the structure of the `angular/angular` monorepo, (2) build a slice of it locally on an Apple Silicon Mac, (3) deep-dive how Bazel caching and Remote Build Execution (RBE) work, and (4) honestly assess whether the Angular build could run on Incredibuild or islo as an RBE backend. |
| 8 | + |
| 9 | +The build half is fully empirical: every measurement below was independently confirmed on disk during the session. The feasibility half is analytical and modeled — it is explicitly labeled as such wherever a number is an estimate rather than a measurement. The short version of the verdict: a single laptop already wins from Bazel's local warm cache (~0.3s no-change rebuild), and neither Incredibuild nor islo appears to be a Bazel REAPI backend today, so the recommended first move is a cheap falsification test rather than an integration effort. |
| 10 | + |
| 11 | +## What we did |
| 12 | + |
| 13 | +| Step | Action | Outcome | |
| 14 | +|------|--------|---------| |
| 15 | +| 1 | Inspected the host machine and toolchain | Apple Silicon Mac, 10 cores, 32 GB RAM, macOS (darwin); git, Node v25.2.1, npm; host pnpm shims to the repo-pinned 11.7.0 (Homebrew binary is 10.25.0) | |
| 16 | +| 2 | Shallow-cloned `github.com/angular/angular` | ~196M working tree; repo uses `MODULE.bazel` (bzlmod) | |
| 17 | +| 3 | Installed `@bazel/bazelisk` globally | Bazelisk read `.bazelversion` (8.7.0) and fetched Bazel 8.7.0 | |
| 18 | +| 4 | Ran a cold build of `//packages/core:core` | Success ("Build completed successfully"); 304.142s elapsed, 2467 total actions | |
| 19 | +| 5 | Verified emitted artifacts on disk | Real compiler emit under `dist/bin/packages/core` (453 `.js`, 456 `.d.ts`, 453 `.js.map`) | |
| 20 | +| 6 | Ran a warm no-change rebuild | ~0.3s, "2 action cache hit, 1 internal" (1 total action) | |
| 21 | +| 7 | Confirmed the build ran 100% locally | Plain `bazel build` with no `--config=remote`; local `darwin-sandbox` + `worker` strategies only | |
| 22 | +| 8 | Studied Bazel caching + REAPI; assessed islo/Incredibuild as an RBE target | Findings and verdict below | |
| 23 | + |
| 24 | +## Build measurements |
| 25 | + |
| 26 | +### Environment |
| 27 | + |
| 28 | +- **Host:** Apple Silicon Mac, 10 cores, 32 GB RAM, macOS (darwin). |
| 29 | +- **Host tooling:** git, Node v25.2.1, npm. The host's Homebrew pnpm binary is 10.25.0, but pnpm self-manages to the repo's `packageManager: "pnpm@11.7.0"` pin, so the effective host pnpm is 11.7.0 — it matches the repo. |
| 30 | +- **Bazel:** 8.7.0, fetched by `@bazel/bazelisk` reading `.bazelversion`. |
| 31 | + |
| 32 | +**Hermeticity note (important):** the Angular build is hermetic. `rules_nodejs` 6.7.4 supplies Node 22.22.3 and a pinned pnpm 11.7.0 *inside the sandbox*, so the host toolchain was **not** used to compile. `aspect_rules_js` 3.2.1 and `aspect_rules_ts` 3.8.10 consume `pnpm-lock.yaml` directly. The only genuine host/sandbox version gap is Node (host v25.2.1 vs sandbox 22.22.3); the effective host pnpm (11.7.0, via self-management) already matches the sandbox. This is why the host toolchain version did not matter. |
| 33 | + |
| 34 | +### Cold build |
| 35 | + |
| 36 | +Command: |
| 37 | + |
| 38 | +``` |
| 39 | +bazel build //packages/core:core |
| 40 | +``` |
| 41 | + |
| 42 | +Result (from the build log): |
| 43 | + |
| 44 | +``` |
| 45 | +Elapsed time: 304.142s, Critical Path: 29.21s |
| 46 | +2467 processes: 695 internal, 1694 darwin-sandbox, 78 worker |
| 47 | +Build completed successfully, 2467 total actions |
| 48 | +``` |
| 49 | + |
| 50 | +The captured log reported "Build completed successfully" (its EXIT marker did not print a numeric exit code). |
| 51 | + |
| 52 | +What the action breakdown actually means: |
| 53 | + |
| 54 | +| Action class | Count | Meaning | |
| 55 | +|--------------|-------|---------| |
| 56 | +| `internal` | 695 | In-process Bazel actions — no subprocess spawned | |
| 57 | +| `darwin-sandbox` | 1694 | Subprocess actions under macOS `sandbox-exec`. **Not all "compiles"** — also includes bundling, `.d.ts` / source-map generation, and codegen | |
| 58 | +| `worker` | 78 | Persistent, reused worker processes (long-lived `tsc` / `ngtsc`) | |
| 59 | +| **Total** | **2467** | 695 + 1694 + 78 = 2467 | |
| 60 | + |
| 61 | +The 304s wall-clock vs. the 29.21s critical path is the key signal: the build is highly parallel, and on this 10-core machine wall-clock is dominated by throughput across many actions, while 29.21s is the longest dependency chain (the theoretical floor for this target on infinite cores). |
| 62 | + |
| 63 | +### Artifacts (proof of real emit) |
| 64 | + |
| 65 | +Angular's `.bazelrc` sets `--symlink_prefix=dist/`, so outputs land under `dist/bin/packages/core`: |
| 66 | + |
| 67 | +- 453 `.js` |
| 68 | +- 456 `.d.ts` |
| 69 | +- 453 `.js.map` |
| 70 | + |
| 71 | +Spot-checks confirming this is genuine compiler emit, not a copy of sources: |
| 72 | + |
| 73 | +- `src/version.js` has TypeScript types stripped and a `sourceMappingURL` footer. |
| 74 | +- `index.js` carries the license header and `export * from './public_api'`. |
| 75 | + |
| 76 | +**Scope:** only `//packages/core` was built. `dist/bin/packages` contains exactly one package directory (`core/`) — **not** `common/`, `compiler/`, `router/`, etc. (only loose toolchain files like `tsconfig-build.json` sit beside it). |
| 77 | + |
| 78 | +### Warm re-run |
| 79 | + |
| 80 | +An immediate no-change rebuild of the same target: |
| 81 | + |
| 82 | +- Measured ~0.3s (0.375s, then 0.335s). |
| 83 | +- Reported `2 action cache hit, 1 internal` (1 total action). |
| 84 | + |
| 85 | +Why it's that fast: |
| 86 | + |
| 87 | +- The **in-memory Skyframe incremental graph** lives on the persistent Bazel server. |
| 88 | +- The **on-disk local action cache** covers what the server doesn't hold in memory. |
| 89 | +- **External dependencies are already fetched.** |
| 90 | + |
| 91 | +Storage footprint observed: Bazel output base `/var/tmp/_bazel_yossi.eliaz` was ~2.0G, of which `external/` was ~1.3G. **Note:** no separate `--disk_cache` or `--repository_cache` was configured — dependencies live in the output base's `external/` directory. |
| 92 | + |
| 93 | +### Local vs. RBE — what actually ran |
| 94 | + |
| 95 | +The build ran **100% locally.** Evidence: |
| 96 | + |
| 97 | +- The command was plain `bazel build //packages/core:core` with **no** `--config=remote`. |
| 98 | +- The strategy names in the log — `darwin-sandbox` and `worker` — are **local** strategies. |
| 99 | +- Platform was `darwin_arm64-fastbuild`. |
| 100 | +- No `/tmp/rbe-grpc.log` was produced. |
| 101 | + |
| 102 | +For completeness: Angular's `.bazelrc` *does* define a Google RBE config — `build:remote` points `remote_executor` / `remote_cache` at `remotebuildexecution.googleapis.com` with `remote_instance_name=projects/internal-200822/instances/primary_instance`, and `build:remote-cache` references an `angular-team-cache` GCS bucket. But that config is gated behind `--google_default_credentials` and is inert unless `--config=remote` is passed. **We never passed it.** |
| 103 | + |
| 104 | +## How Bazel caching and RBE work |
| 105 | + |
| 106 | +### From targets to actions |
| 107 | + |
| 108 | +Bazel parses `BUILD` files into a **target graph**, then lowers it to an **action graph**. The **analysis phase** builds the full action graph and is pure — no commands run. The **execution phase** then runs (or cache-serves) each action. |
| 109 | + |
| 110 | +### Hermeticity and sandboxing make caching sound |
| 111 | + |
| 112 | +Each action sees only its **declared inputs** (enforced by the sandbox). This isolation is precisely what makes caching *sound*: if the declared inputs are identical, the output is guaranteed identical, so a cached result can be trusted. |
| 113 | + |
| 114 | +### The action key |
| 115 | + |
| 116 | +An action's cache key is a hash over: |
| 117 | + |
| 118 | +- the `argv`, |
| 119 | +- the **content digests** of every declared input, |
| 120 | +- the toolchain binary, |
| 121 | +- the declared environment, and |
| 122 | +- the platform. |
| 123 | + |
| 124 | +It hashes **content, not mtimes** — touching a file without changing its bytes does not bust the cache. |
| 125 | + |
| 126 | +### Caching layers |
| 127 | + |
| 128 | +1. **Skyframe in-memory** — per-server, volatile; cleared when the Bazel server dies. |
| 129 | +2. **Local action cache / `--disk_cache`** — content-addressable on local disk. |
| 130 | +3. **Repository cache** — network downloads keyed by sha256. |
| 131 | +4. **Remote cache** — a **CAS** (blobs addressed by `Digest`) plus an **ActionCache** (`Action` digest → `ActionResult`), spoken over the gRPC **Remote Execution API (REAPI v2)**. |
| 132 | + |
| 133 | +### Remote Build Execution |
| 134 | + |
| 135 | +RBE is remote *execution* over the same REAPI surface (`ContentAddressableStorage`, `ActionCache`, `Execution`, `Capabilities`): |
| 136 | + |
| 137 | +- Inputs are uploaded as a **Merkle tree** into CAS. |
| 138 | +- The action is **scheduled on a remote worker** and run hermetically. |
| 139 | +- Outputs are written **back to CAS**. |
| 140 | + |
| 141 | +Because the remote cache and remote executor **share the same CAS**, one engineer's (or CI's) previous execution becomes everyone's cache hit. |
| 142 | + |
| 143 | +## Can Angular run on islo / Incredibuild RBE? |
| 144 | + |
| 145 | +**Honest verdict up front: almost certainly not as a Bazel REAPI backend today.** The two products are not REAPI servers, so the interesting question is whether a cheap test can confirm or kill the idea quickly. The detail: |
| 146 | + |
| 147 | +### Why neither is a REAPI backend today |
| 148 | + |
| 149 | +- **Incredibuild** is *process virtualization*: an Initiator farms spawned child processes out to Helpers. It is **not REAPI**. The only known Bazel bridge — Bazel PR #18113 — **never shipped**. |
| 150 | +- **islo.dev** is a newer **microVM agent-sandbox** product with a **REST/JSON + SSE** API. It has **no gRPC and no REAPI**. Running `bazel` *inside* an islo sandbox makes islo a Bazel **client**, not an RBE **server**. |
| 151 | +- Neither product appears on Bazel's official **Remote Execution Services** vendor list (Buildbarn, Buildfarm, BuildGrid, NativeLink, BuildBuddy, EngFlow, ...). |
| 152 | + |
| 153 | +### Speedup model (MODELED — not benchmarked) |
| 154 | + |
| 155 | +These figures are analytical estimates from the observed build shape. They were **not** measured against any remote backend. |
| 156 | + |
| 157 | +| Scenario | Modeled speedup | Why | |
| 158 | +|----------|-----------------|-----| |
| 159 | +| Remote **execution**, fully cold build, **one laptop** | ~5–8x | The Mac is already CPU-saturated across 10 cores; the 29s critical path is the floor; you also **lose** the warm persistent workers | |
| 160 | +| Shared **cache**, fresh/clean checkout | ~20–100x | A teammate or CI populated the CAS; you download results instead of recomputing | |
| 161 | +| Local warm rebuild vs. remote cache hit | **Local wins** | Local warm rebuild ~0.3s already beats a remote cache hit (~3–15s, network-bound) | |
| 162 | + |
| 163 | +The real beneficiary of RBE/shared caching is a **CI fleet and a many-developer team**, not a single laptop that already has a hot local cache. |
| 164 | + |
| 165 | +### The hard part: darwin → linux |
| 166 | + |
| 167 | +Angular's `--config=remote` forces **linux/x86_64** (`--cpu=k8` plus linux platforms). To run remotely you would need: |
| 168 | + |
| 169 | +- a custom `platform()` definition, |
| 170 | +- a linux container image with a glibc userland, and |
| 171 | +- linux toolchains. |
| 172 | + |
| 173 | +And **the tests are the trap**: the Karma/Chrome test setup is the part most likely to break under a remote linux executor. |
| 174 | + |
| 175 | +### Verdict |
| 176 | + |
| 177 | +Treat islo/Incredibuild-as-Bazel-RBE as **unproven and probably unsupported** until a handshake test says otherwise. For a single laptop, local caching already wins; the value of RBE is a team/CI story and would require a real REAPI backend and a darwin→linux toolchain port. |
| 178 | + |
| 179 | +## Recommendations / next steps |
| 180 | + |
| 181 | +1. **Run a cheap falsification test first.** Add a `--config=islo` block that is **remote-cache-only** (no remote execution). The decisive check is whether the REAPI **`GetCapabilities` gRPC handshake** succeeds. |
| 182 | +2. **If the handshake fails** (likely, since islo is REST) the premise is dead. The right paths are then: |
| 183 | + - **(a) One laptop →** do nothing; local caching already wins. |
| 184 | + - **(b) Linux build/test →** run Bazel on a **Linux CI runner or VM** (solves the darwin→linux problem directly, no RBE needed). |
| 185 | + - **(c) Team / CI scale →** adopt a **real REAPI vendor** — BuildBuddy, EngFlow, NativeLink, or Buildbarn. |
| 186 | +3. **Do not invest in a darwin→linux remote toolchain port** until step 1 proves a working REAPI endpoint exists. |
| 187 | + |
| 188 | +## How to reproduce |
| 189 | + |
| 190 | +On an Apple Silicon Mac with git and Node available: |
| 191 | + |
| 192 | +```bash |
| 193 | +# 1. Install the Bazel launcher (reads .bazelversion -> Bazel 8.7.0) |
| 194 | +npm install -g @bazel/bazelisk |
| 195 | + |
| 196 | +# 2. Shallow-clone the repo |
| 197 | +git clone --depth=1 https://github.com/angular/angular.git |
| 198 | +cd angular |
| 199 | + |
| 200 | +# 3. Cold build of the core package only |
| 201 | +bazel build //packages/core:core |
| 202 | +# expect: ~304s elapsed, 2467 total actions, "Build completed successfully" |
| 203 | + |
| 204 | +# 4. Warm re-run (no changes) — same command |
| 205 | +bazel build //packages/core:core |
| 206 | +# expect: ~0.3s, "2 action cache hit, 1 internal" (1 total action) |
| 207 | + |
| 208 | +# 5. Inspect emitted artifacts |
| 209 | +ls dist/bin/packages/core |
| 210 | +# expect: real .js / .d.ts / .js.map emit (453 / 456 / 453) |
| 211 | +``` |
| 212 | + |
| 213 | +Notes: |
| 214 | + |
| 215 | +- Do **not** pass `--config=remote` unless you intend to use Google's RBE and have `--google_default_credentials` configured. This session never did. |
| 216 | +- The host's Node/pnpm versions do not affect the compile because the build is hermetic (toolchains are pinned inside the sandbox). |
| 217 | +- Numbers will vary with core count, disk, and cache state; the ~304s cold / ~0.3s warm split is the qualitative result to expect. |
| 218 | + |
| 219 | +## Appendix: caveats, unknowns, and a security note |
| 220 | + |
| 221 | +### Caveats and scope limits |
| 222 | + |
| 223 | +- **Only `//packages/core` was built.** No other Angular package (`common`, `compiler`, `router`, ...) was compiled, so whole-repo build cost is not measured. |
| 224 | +- **No Incredibuild or islo endpoint was exercised.** The feasibility section is analysis, not a benchmark. |
| 225 | +- All speedup figures are **modeled**, not measured. |
| 226 | +- Measurements are from a single machine and a single run pair (cold + warm); they are illustrative, not a statistical sample. |
| 227 | +- The Google RBE config in `.bazelrc` exists but was never invoked; its current validity for external users is unknown. |
| 228 | + |
| 229 | +### Open questions to resolve |
| 230 | + |
| 231 | +- Does any islo/Incredibuild endpoint answer a REAPI `GetCapabilities` call at all? (The proposed falsification test answers this.) |
| 232 | +- What is the real cost of a full-repo cold build, and the full-repo warm rebuild? |
| 233 | +- How does the Karma/Chrome test path behave under a remote linux executor? |
| 234 | + |
| 235 | +### Security note |
| 236 | + |
| 237 | +During the session, AWS access keys were observed sitting in **plaintext** in the user's global `~/.claude/CLAUDE.md`. These should be **rotated** and moved into a proper credential store (e.g., a secrets manager or the AWS CLI credential provider chain). No secret values are reproduced in this document. |
0 commit comments