Skip to content

perf(codegen): Coop's self-contained Next.js route takes 116 min to compile on 0.5.1519 (~8 min on 0.5.1512) — one LLVM unit thread in TailCallElim's alloca walk #8883

Description

@proggeramlug

Summary

Compiling Coop's Next.js fixture — the production next build --webpack App Route with server chunk splitting disabled, so the route is one self-contained bundle (~15–21 MB of IR per unit, per PerryTS/coop#12) — took 6,973,947 ms (116 min) wall on Perry 0.5.1519 (41e8479a5), with two earlier attempts killed by 1800 s and 5400 s budgets. PerryTS/coop#12 measured the same fixture at roughly 8 minutes on a quiet M1 on 0.5.1512 (fc6b3378).

Caveat up front: this host was not quiet (load average 25–56 on 10 cores, from unrelated Perry builds), and an M1 thread pushed onto efficiency cores burns more CPU-seconds per unit of work. So the wall-clock ratio overstates it. The reason I think it is still a regression rather than contention: the single hot thread accumulated >100 CPU-minutes inside one analysis, which no contention model turns into 8 wall-minutes of total work. It needs a quiet-machine confirmation, which I could not give it today.

Where the time goes

sample on the compiler process, twice 30 minutes apart, same picture both times. Four workers (perry-module-0/1, perry-llvm-unit-0/1); three parked on channels, one — perry-llvm-unit-1 — running:

llvm::SmallPtrSetImplBase::insert_imp_big(void const*)                       3507 / 2024 samples
(anonymous namespace)::AllocaDerivedValueTracker::walk(llvm::Value*)          108 /   47
llvm::CallBase::getCaptureInfo · llvm::CallBase::onlyReadsMemory · llvm::AttributeList::getMemoryEffects

called from the CGSCC pipeline (llvm::LazyCallGraph::SCC, CGSCCUpdateResult, AnalysisManager<LazyCallGraph::SCC> frames present). AllocaDerivedValueTracker is TailCallElim's alloca-escape walk (Transforms/Scalar/TailRecursionElimination.cpp): for every alloca it walks every derived use into a SmallPtrSet, so on a function with a very large alloca count × use count it is effectively quadratic. RSS stayed at 0.2–0.3 GB for the ~100 minutes this thread was alone, then jumped to 3.9 GB when the remaining units ran in parallel — i.e. one function held the whole build.

Perry does not use LLVM coroutines and names no coroutine or TRE pass itself; this is the default function-simplification pipeline meeting a function of that width. Per-unit stats (PERRY_CODEGEN_UNIT_TIMINGS) were not enabled on this run — they would name the function; #8586 added exactly that instrument for this situation.

What changed between the pins

crates/perry-codegen/src/native_emit.rs moved 646+/128− between fc6b3378 and 41e8479a5 (#8586, #8602, #8657, #8696, #8700 among them), and 72 codegen commits landed in the range, including the perf(codegen) series of 2026-08-24/25. I have not bisected. #8586's notes say #8421's contract — every function optimized at the requested level — stands, so nothing is demoted; if a change in that range made the widest function wider (more allocas, more uses), TRE's walk is where it would show first.

Reproduction

PerryTS/coop main at 3e7180e, then scripts/build-perry-libraries.sh, cargo build --release -p coop-daemon -p coop-worker, and

PERRY_CODEGEN_UNIT_TIMINGS=1 COOP_NEXT_COMPILE_TIMEOUT=10800 scripts/prepare-next-benchmark.sh

(the env override is in PerryTS/coop#17). The daemon writes the compiler's stderr only on failure, so for the unit stats run perry compile directly on the staged deployment with the same argv the daemon logs under compiling deployment … perry=….

Filed alongside the 0.5.1519 init regression on the same fixture; the two together are what currently keeps #8546 from being reproduced on main.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions