Skip to content

Parallelize CodeSplitter's not-exclusive CFA computation - #10396

Open
rhuanhianc wants to merge 1 commit into
gwtproject:mainfrom
rhuanhianc:perf/codesplitter-parallel-cfa
Open

rhuanhianc wants to merge 1 commit into
gwtproject:mainfrom
rhuanhianc:perf/codesplitter-parallel-cfa

Conversation

@rhuanhianc

@rhuanhianc rhuanhianc commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

computeNotExclusiveCfaForFragments is quadratic in the number of exclusive fragments: for each fragment it traverses the run-asyncs of every other fragment. Applications with many split points pay heavily — with 231 split points this loop alone is 25.6% of the permutation's CPU, and CodeSplitter overall is 181s of an 11-minute compile.

The iterations are independent. ControlFlowAnalyzer's copy constructor deep-copies every mutable set, each traversal writes only to its own analyzer, and ControlFlowAnalyzer is purely analytical — it contains no setters on AST nodes. This runs them on a fixed thread pool.

Two constraints are respected:

  • Dependency-graph recording is stateful and order sensitive, so the parallel path is only taken when no recorder is installed. With -compileReport the original serial loop runs unchanged.
  • JProgram.getTypeArray lazily creates array types in a plain HashMap and is reachable from the traversal, so it is now synchronized. It is the only shared mutable state on this path; getAllArrayTypes, which already sorts to avoid nondeterminism, is not reachable from traverseFromRunAsync.

Happy to derive the pool size from -localWorkers instead of availableProcessors() if you'd prefer to avoid oversubscription when permutations are already compiled in parallel.

CodeSplitter drops from 38.4s to 17.0s with 122 split points and from 181.4s to 71.5s with 231. Generated JavaScript is byte-for-byte identical across 874 output files from four applications. ant -Dtarget=test dev passes: 1936 tests, 0 failures.

Fixes #10395

computeNotExclusiveCfaForFragments traverses the run-asyncs of every other
fragment for each fragment, so it is quadratic in fragment count. The
iterations are independent, so run them on a thread pool.

The serial loop is kept for when a dependency recorder is installed, since
recording is order sensitive. JProgram.getTypeArray is now synchronized, being
the only shared state the traversals create on demand.

CodeSplitter goes from 181.4s to 71.5s on an application with 231 split
points, with byte-identical output.

@niloc132 niloc132 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about this - at the very least, tying it to localWorkers will take the sting off of it a bit, but we still risk actually running localWorkers^2 concurrent threads (and running out of memory by doing so much at once, or other issues from too much work scheduled).

For threaded workers, we could at least share a single ExecService with extra localWorker count (e.g. localWorkers - permutationCount threads haven't been used yet, let them be scheduled for this work, plus or minus how many permutations are paused waiting for their exclusive fragments). But when we use external processes for workers (e.g. ExternalPermutationWorkerFactory), we don't have as nice of a way to communicate about which process should get the extra threads...

Mechanically I don't much to complain about though, just want to consider this approach carefully before risking breaking anyone's CI.

Comment on lines +1026 to +1027
// Synchronized because CodeSplitter reaches this from concurrent CFA traversals.
public synchronized JArrayType getTypeArray(JType elementType) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead, perhaps just call program.getTypeArray(JPrimitiveType.INT) once before branching so we guarantee the type exists. I think we could even drop that call entirely, since it only exists for an assert (and once it matches a == check, we just use the same instance).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CodeSplitter's not-exclusive CFA computation is quadratic in fragment count

2 participants