From 52e3a4e46103e047399f74422bd0dcc6ac306073 Mon Sep 17 00:00:00 2001 From: Rhuan de Souza Hianc Date: Wed, 19 Aug 2026 00:09:13 -0300 Subject: [PATCH] Parallelize CodeSplitter's not-exclusive CFA computation 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. --- .../com/google/gwt/dev/jjs/ast/JProgram.java | 3 +- .../jjs/impl/codesplitter/CodeSplitter.java | 64 +++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/dev/core/src/com/google/gwt/dev/jjs/ast/JProgram.java b/dev/core/src/com/google/gwt/dev/jjs/ast/JProgram.java index baf36e21a2..b088626a73 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/ast/JProgram.java +++ b/dev/core/src/com/google/gwt/dev/jjs/ast/JProgram.java @@ -1023,7 +1023,8 @@ public JMethod getStaticImpl(JMethod method) { return staticImpl; } - public JArrayType getTypeArray(JType elementType) { + // Synchronized because CodeSplitter reaches this from concurrent CFA traversals. + public synchronized JArrayType getTypeArray(JType elementType) { JArrayType arrayType = arrayTypes.get(elementType); if (arrayType == null) { arrayType = new JArrayType(elementType); diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/codesplitter/CodeSplitter.java b/dev/core/src/com/google/gwt/dev/jjs/impl/codesplitter/CodeSplitter.java index 540d6a58c7..fbf1d8df79 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/impl/codesplitter/CodeSplitter.java +++ b/dev/core/src/com/google/gwt/dev/jjs/impl/codesplitter/CodeSplitter.java @@ -43,6 +43,11 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; /** *

@@ -274,6 +279,12 @@ private List statementsForFragment(int fragmentId, */ private Map computeNotExclusiveCfaForFragments( Collection exclusiveFragments) { + // Recording a dependency graph is order sensitive, so it keeps the serial loop below. + if (dependencyRecorder == MultipleDependencyGraphRecorder.NULL_RECORDER + && exclusiveFragments.size() > 1) { + return computeNotExclusiveCfaForFragmentsInParallel(exclusiveFragments); + } + String dependencyGraphNameAfterInitialSequence = dependencyGraphNameAfterInitialSequence(); Map notExclusiveCfaByFragment = Maps.newHashMap(); @@ -304,6 +315,59 @@ private Map computeNotExclusiveCfaForFragments( return notExclusiveCfaByFragment; } + /** + * Runs {@link #computeNotExclusiveCfaForFragment} for every fragment in parallel. The iterations + * are independent: each builds its own ControlFlowAnalyzer, whose copy constructor duplicates + * every mutable set, and otherwise only reads the program. + */ + private Map computeNotExclusiveCfaForFragmentsInParallel( + final Collection exclusiveFragments) { + Map notExclusiveCfaByFragment = Maps.newLinkedHashMap(); + int threads = Math.min(Runtime.getRuntime().availableProcessors(), exclusiveFragments.size()); + ExecutorService executor = Executors.newFixedThreadPool(threads); + try { + Map> futures = Maps.newLinkedHashMap(); + for (final Fragment fragment : exclusiveFragments) { + assert fragment.isExclusive(); + futures.put(fragment, executor.submit(new Callable() { + @Override + public ControlFlowAnalyzer call() { + return computeNotExclusiveCfaForFragment(fragment, exclusiveFragments); + } + })); + } + for (Map.Entry> entry : futures.entrySet()) { + try { + notExclusiveCfaByFragment.put(entry.getKey(), entry.getValue().get()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } catch (ExecutionException e) { + throw new RuntimeException(e.getCause()); + } + } + } finally { + executor.shutdown(); + } + return notExclusiveCfaByFragment; + } + + private ControlFlowAnalyzer computeNotExclusiveCfaForFragment(Fragment fragment, + Collection exclusiveFragments) { + ControlFlowAnalyzer cfa = new ControlFlowAnalyzer(initialSequenceCfa); + for (Fragment otherFragment : exclusiveFragments) { + // don't trace the initial fragments as they have already been traced and their atoms are + // already in {@code initialSequenceCfa}. + if (otherFragment.isInitial() || otherFragment == fragment) { + continue; + } + for (JRunAsync otherRunAsync : otherFragment.getRunAsyncs()) { + cfa.traverseFromRunAsync(otherRunAsync); + } + } + return cfa; + } + /** * Compute a CFA that covers the entire live code of the program. */