Skip to content

unit-test builder: coverage phase became 20x+ slower since 22.1.5 (disableCodeSplitting forced for unit test builds) #33948

Description

@pascalmann

Command

test

Is this a regression?

  • Yes, this behavior used to work in the previous version

The previous version in which this bug was not present was

22.1.4

Description

Since @angular/build 22.1.5, running the unit-test builder (Vitest runner) with coverage enabled on a large workspace shows the following pattern on our biggest libraries:

  • all tests complete normally (e.g. 27 spec files / 400 tests in ~35 s),
  • then the process keeps running at 100–160 % CPU and ~3–4 GB RSS with no output for more than 15 minutes — it looks like a hang. It is in fact the V8 coverage collection/remap phase.

On 22.1.4 the exact same target completes in 57 s end to end.

We bisected it to 22.1.5's fix(@angular/build): disable code splitting for unit test builds (unit-test/runners/vitest/build-options.ts now forces disableCodeSplitting: true). Our understanding of the mechanism: every spec file being an entry point, the test build now produces a single monolithic chunk containing the whole transitive module graph of the project (in our workspace: hundreds of internal libraries), and @vitest/coverage-v8 has to remap the source map of that entire chunk for every project, even though the coverage thresholds / report only concern the project's own files. With code splitting, the remap work was proportional to what each spec actually loaded.

Confirmed by re-applying disableCodeSplitting: false locally (pnpm patch): same project, same 22.1.6, 39 s end to end, identical coverage numbers.

Things we tried that do not help:

  • coverage.include: ['<projectRoot>/**'] in the Vitest runner config → coverage drops to 0 %: the filter seems to be applied to the bundled chunk paths before source-map remapping, so real hits are discarded and only unloaded files are reported (with Failed to parse ... README.md/component.html. Excluding it from coverage. warnings for every non-JS file in the project).
  • Excluding **/*.html from coverage → no measurable effect.
  • 22.1.6 → same behaviour.

We understand the motivation of the change (live ESM bindings read across a lazily-initialised shared chunk), but as it stands the trade-off makes coverage unusable on large workspaces: with 186 projects our CI job went from ~15 min to jobs timing out on the largest libraries. Possible directions:

  • expose the behaviour as a builder option (e.g. codeSplitting: boolean on the unit-test builder, defaulting to the current false), so workspaces that never hit the live-binding issue can opt back in;
  • or keep the monolithic build but scope the V8 coverage remap to the files that match coverage.include / the project root after remapping (i.e. remap only the source-map segments that resolve to included sources);
  • or emit the shared modules as eagerly-initialised chunks instead of disabling splitting altogether.

Minimal Reproduction

We do not have a small public reproduction yet — the effect is proportional to the size of the transitive module graph. Characteristics of the workspace that reproduces it reliably:

  • Nx 23.1.1 monorepo, ~5 100 non-spec TypeScript files under libs/, 186 projects with a test target;
  • tests run through @nx/angular:unit-test (which delegates to the @angular/build unit-test builder), Vitest runner, coverage: true, provider v8, reporters lcovonly + cobertura, thresholds scoped to <projectRoot>/**;
  • the affected library has 27 spec files / 400 tests and its lcov report lists 462 source files (88 of which belong to the library itself, the rest being transitive internal libraries).

Timings on the same machine (14 CPU, container, Node 22.22.3), same project, --configuration=ci (coverage on):

@angular/build tests coverage phase total
22.1.4 ~35 s ~20 s 57 s
22.1.6 ~35 s > 15 min (killed)
22.1.6 + disableCodeSplitting: false (local patch) ~35 s ~5 s 39 s

Without coverage, 22.1.6 completes in ~30 s, so the tests themselves are unaffected. I am happy to run additional diagnostics (e.g. --dumpVirtualFiles, CPU profile of the coverage phase) or to try a patch.

Exception or Error

No exception — the process stays busy (100–160 % CPU, 3–4 GB RSS) with no output after the last test result. Killing it after 15+ minutes yields no report.

Your Environment

Angular CLI: 22.1.6
Node: 22.22.3
Package Manager: pnpm 11.24.0
OS: linux x64

@angular/build: 22.1.6 (regression introduced in 22.1.5)
@angular/core: 22.1.3
@angular/compiler-cli: 22.1.3
vitest: 4.1.11
@vitest/coverage-v8: 4.1.11
typescript: 6.0.3
jsdom: 30.0.1
nx / @nx/angular: 23.1.1

Anything else relevant?

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions