Size the translator heap from the machine so large apps build unmodified - #5521
Conversation
Every ByteCodeTranslator fork carried a hard-coded -Xmx tuned years ago against the sample apps: 512m for JavaScript, 1024m for iOS, 2g for the native Windows/Linux targets. A genuinely large app exhausts that partway through translation and dies with OutOfMemoryError, and the only way out was to discover CN1_TRANSLATOR_OPTS by reading this plugin's source (issue #5511). The defaults were tighter than they looked. Bisecting the heap on the in-repo hellocodenameone JS build, the translator OOMs at 384m and needs 448m to finish -- under 1.5x headroom on the sample app itself. TranslatorHeap now derives the ceiling from the machine: half the detected memory budget, capped at 4g, and floored at each target's historical constant so no target can end up with less heap than before. The budget reads the cgroup limit before falling back to physical RAM, so a build running inside a container sizes against the container's limit rather than the host's -- which is what keeps an over-large app failing as a Java OutOfMemoryError we can report instead of a kernel OOM-kill we cannot. Raising the ceiling is close to free for ordinary apps because -Xmx is a reservation, not a commitment. CN1_TRANSLATOR_MAX_HEAP_MB pins the value per box. It may sit above the 4g cap, which only bounds the value we pick automatically -- clamping the knob would achieve nothing, since an operator would just switch to CN1_TRANSLATOR_OPTS=-Xmx, which no ceiling applies to. It may not exceed the memory the machine actually has: that is the setting that OOM-kills a build box rather than failing one build, and it is the likely shape of a typo. Alongside that, two things the translator failure path was missing: * A translator that died for lack of memory previously returned false and the build reported nothing more useful than a failed step. Both the JS and iOS builders now recognise the out-of-memory signature in the captured output and say so, naming the heap in use and how to raise it. * The local iOS translator ran under a 420s timeout while the cloud builder runs the identical translator over identical input at 600s. Translation time grows with app size, so a large app could translate fine on the build server yet be killed mid-run on the developer's own machine, which is usually the slower of the two. Both are 600s now. JavascriptBundleWriter also held the whole bundle twice -- as StringBuilders and again as Strings -- at the point it materialises the chunks, and kept every chunk resident until the last one had been written. Chunks are now released as they materialise and as they reach disk. On a two-chunk bundle that drops the minimum viable heap from 416m to 384m, about one full UTF-16 copy of the emitted JS; the saving scales with bundle size, so it does nothing for a single-chunk app and progressively more for a large one. The emitted bundle is byte-identical, verified on both the single-chunk and multi-chunk paths. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
check-copyright-headers rejects any added source file without the complete Codename One GPLv2 + Classpath Exception header; the new TranslatorHeapTest went up without one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
✅ Continuous Quality ReportTest & Coverage
Static Analysis
Generated automatically by the PR CI workflow. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fac0ce6fe7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
This PR improves the reliability of large-app builds by replacing per-target hard-coded ByteCodeTranslator heaps with an automatically sized heap derived from the machine/container memory budget (with per-target floors and a configurable override). It also improves diagnostics for translator out-of-memory failures, adjusts the local iOS translator timeout to match cloud behavior, and reduces peak memory during JavaScript bundle emission.
Changes:
- Introduce
TranslatorHeapto auto-size-Xmxfrom cgroup/physical RAM with floors, a 4GB auto cap, andCN1_TRANSLATOR_MAX_HEAP_MBoverride. - Improve JS/iOS builder error reporting by detecting translator OOM signatures and emitting actionable advice; increase local iOS translator timeout to 600s.
- Reduce JavaScript bundle writer peak by releasing chunk builders/strings as soon as they materialize and are written.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java | Releases JS chunk builders/strings earlier to reduce peak memory during bundle emission. |
| maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/TranslatorHeap.java | New heap sizing policy: cgroup-aware detection, floors, cap, and environment override; includes OOM signature/advice helpers. |
| maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/TranslatorHeapTest.java | Adds unit tests to pin heap sizing and OOM-advice behavior. |
| maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuilder.java | Switches JS translator heap to auto-sized value and adds OOM failure recognition/advice. |
| maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java | Switches iOS translator heap to auto-sized value, increases timeout to 600s, and adds OOM recognition/advice. |
| maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/WindowsNativeBuilder.java | Replaces fixed -Xmx2g with TranslatorHeap sizing floored at 2GB. |
| maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/LinuxNativeBuilder.java | Replaces fixed -Xmx2g with TranslatorHeap sizing floored at 2GB. |
| maven/codenameone-maven-plugin/spotbugs-exclude.xml | Adds SpotBugs exclusion for fixed Linux cgroup limit probe paths in TranslatorHeap. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
Suppressed comments (4)
maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/TranslatorHeapTest.java:142
- This test hard-fails if
detectBudgetMB()returns -1 on the current runtime/container. Since the implementation is explicitly designed to gracefully fall back to historical constants when detection is unavailable, this assertion can make the test suite flaky across CI environments/JVMs without indicating a functional regression.
@Test
public void detectsSomeMemoryBudgetOnThisMachine() {
// The whole policy degrades to the old constants if this returns -1, so
// make sure the detection actually works on a normal build machine.
assertTrue(TranslatorHeap.detectBudgetMB() > 0);
maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/TranslatorHeap.java:213
cgroupLimitMB()reads fixed paths under/sys/fs/cgroup, which only reflect the root cgroup. If the Maven process runs in a nested cgroup with a memory limit (e.g. systemd slice scopes, some CI setups), these files can be unlimited while the process’ own cgroup directory is limited. That can cause the heap auto-sizing to overshoot the real limit and get OOM-killed, undermining the goal of sizing to the container budget.
private static long cgroupLimitMB() {
long v2 = readLimitFile(new File("/sys/fs/cgroup/memory.max"));
if (v2 > 0) {
return v2;
}
return readLimitFile(new File("/sys/fs/cgroup/memory/memory.limit_in_bytes"));
}
maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/TranslatorHeap.java:166
outOfMemoryAdvice()says the machine’s “memory budget” doesn’t allow a larger heap, but the OOM may also occur because of the policy ceiling (or an explicit floor) even when the machine has plenty of RAM. This wording can mislead users into thinking their hardware/container limit is the blocker when it may just be the default cap/config.
static String outOfMemoryAdvice(int heapMB, boolean canConfigureEnvironment) {
String head = "The ByteCodeTranslator ran out of memory (it was given -Xmx" + heapMB + "m). "
+ "This app needs a larger translation heap than the build machine's memory budget allows. ";
if (canConfigureEnvironment) {
vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java:237
- Nulling out the
chunkslist entries won’t actually release the last (often largest)StringBuilderearly because the localtailvariable still holds a strong reference to it until the method returns. This partially defeats the intended peak-memory reduction during the subsequent minify/hoist passes.
for (int i = 0; i < chunks.size(); i++) {
// Drop each builder as it materialises. Holding the whole bundle
// simultaneously as StringBuilders and as Strings doubles the
// translator's peak for no benefit, and on a large app that
// doubling is the difference between finishing and dying with
// OutOfMemoryError (issue #5511).
chunkStrings.add(chunks.get(i).toString());
chunks.set(i, null);
✅ ByteCodeTranslator Quality ReportTest & Coverage
Benchmark Results
Static Analysis
Generated automatically by the PR CI workflow. |
Cloudflare Preview
|
|
Compared 151 screenshots: 151 matched. Native Android coverage
✅ Native Android screenshot tests passed. Native Android coverage
Benchmark ResultsDetailed Performance Metrics
|
|
Compared 146 screenshots: 146 matched. Benchmark ResultsDetailed Performance Metrics
|
|
Compared 146 screenshots: 146 matched. Benchmark ResultsDetailed Performance Metrics
|
|
Compared 147 screenshots: 147 matched. |
|
Compared 147 screenshots: 147 matched. |
|
Compared 181 screenshots: 181 matched. |
|
Compared 146 screenshots: 146 matched. Benchmark ResultsDetailed Performance Metrics
|
|
Compared 148 screenshots: 148 matched. Benchmark Results
Detailed Performance Metrics
|
|
Compared 143 screenshots: 143 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
|
Compared 144 screenshots: 144 matched. |
|
Compared 217 screenshots: 217 matched. |
|
Compared 149 screenshots: 149 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
… the log reader Two defects found in review. cgroupLimitMB only read the mount-root files. That is correct when the process has its own cgroup namespace -- the usual container case -- but under a systemd unit with MemoryMax=, or a container sharing the host cgroup namespace, the root shows the host's unlimited value and the real limit lives at the path named in /proc/self/cgroup. In exactly those configurations the probe fell back to host physical RAM and could pick a 4g heap inside a much smaller container, producing the kernel OOM-kill this helper exists to prevent. It now resolves the process's own cgroup path and takes the smallest limit from the mount root down to that leaf, since a limit set anywhere on the path applies. The mount-root read remains as the fallback for the namespaced case. Executor.executeProcess started the output reader thread but never joined it, returning as soon as waitFor did. Anything inspecting the captured output afterwards raced a reader that might still be draining the pipe -- and the tail is precisely where a JVM prints its OutOfMemoryError, so the new translator diagnostic could miss the very text it looks for, while also reading a StringBuilder under concurrent mutation. The reader is now joined (bounded at 30s; the process has already exited, so EOF arrives promptly). The memory-detection test asserted unconditionally that this machine exposes a budget, which is environment-dependent: detection legitimately returns -1 on a JVM without the com.sun accessor. It now asserts that only when the accessor is actually present, which is the condition that makes detection obligatory, so it still catches a silent regression without being able to fail for the wrong reason. Adds cgroup tests covering the systemd/host-namespace layout, a tighter ancestor limit, v1, the unlimited sentinel, and cgroup paths containing ':' (legitimate in systemd scope names). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (1)
maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/TranslatorHeap.java:166
- The OOM advice currently says the app needs more heap than the build machine's memory budget allows, but the auto-sized heap can also be constrained by the 4GB ceiling (or other policy choices) even when the machine has plenty of RAM. This wording can mislead users into thinking their machine can't support the build when it may just need a higher configured limit.
static String outOfMemoryAdvice(int heapMB, boolean canConfigureEnvironment) {
String head = "The ByteCodeTranslator ran out of memory (it was given -Xmx" + heapMB + "m). "
+ "This app needs a larger translation heap than the build machine's memory budget allows. ";
if (canConfigureEnvironment) {
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: baacb5980b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…s is not failed Regression from the reader-thread join added in the previous commit. The watcher loop runs while running[0] is true, and that flag was only cleared after the join. A command that exited successfully inside its deadline could therefore have the watcher cross that deadline while the join was still draining the pipe, set destroyed[0], and make executeProcess return 1 for a run that had already succeeded. The steps that carry a timeout are the iOS and native translator runs and the CMake builds, so this would have surfaced as an intermittent "translator failed" on builds that were fine. running[0] is now cleared immediately after waitFor(), before the join, and the timeout verdict is captured at that point rather than read afterwards. The regression test needs the join to actually block, which means the pipe has to outlive the process: the shell exits at once while a backgrounded grandchild keeps the inherited stdout open. The first version of this test used a plain sleeping foreground command, so the join returned instantly, the watcher never got the chance to fire, and the test passed against the broken ordering as happily as against the fixed one. Verified the rewritten one fails on the old ordering (expected 0 but was 1) and passes on the new. Also covers the two properties either way: that the captured output is complete when exec returns (the tail is where a JVM prints its OutOfMemoryError, which is what the translator diagnostic looks for), and that a genuine overrun is still reported as a timeout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (5)
maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/ExecutorProcessTimeoutTest.java:92
- This test depends on
/bin/shand also onseqbeing available in the shell environment. That can make the test fail in otherwise-valid environments (e.g. non-POSIX hosts, or minimal shells without coreutils). Guard on/bin/shand use a POSIX-shell loop to avoid relying onseq.
TestExecutor e = new TestExecutor();
StringBuilder sb = new StringBuilder();
ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c",
"for i in $(seq 1 500); do echo line-$i; done; echo java.lang.OutOfMemoryError: Java heap space");
int rc = executeCapturing(e, pb, sb);
maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/ExecutorProcessTimeoutTest.java:112
- This test hard-requires
/bin/sh, which will fail on Windows/non-POSIX environments. Add an assumption so the test is skipped when/bin/shisn’t present.
TestExecutor e = new TestExecutor();
ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c", "sleep 30");
int rc = e.executeProcess(pb, 1000);
vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java:239
- The loop nulls out entries in
chunks, butcurrent(line 199) andtail(line 210) still hold references to the last (potentially largest)StringBuilderuntil the method returns. That keeps an extra full UTF-16 buffer alive duringminifyGeneratedIdentifiers()/aliasHotCn1Identifiers()and undermines the peak-memory reduction you’re aiming for.
chunkStrings.add(chunks.get(i).toString());
chunks.set(i, null);
}
maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/TranslatorHeap.java:165
outOfMemoryAdvice()says the app needs more heap than the machine’s memory budget allows, but the heap can be capped by policy (e.g.CEILING_MB) even when the machine/budget would allow more. This wording can mislead users on large machines/boxes where the budget isn’t the limiting factor.
String head = "The ByteCodeTranslator ran out of memory (it was given -Xmx" + heapMB + "m). "
+ "This app needs a larger translation heap than the build machine's memory budget allows. ";
maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/ExecutorProcessTimeoutTest.java:78
- This test hard-requires
/bin/sh, so it will fail on Windows (and any environment without that path) even though it’s exercising Java code. Consider guarding with a JUnit assumption so the suite can still run on non-POSIX dev machines/CI agents.
This issue also appears in the following locations of the same file:
- line 88
- line 110
TestExecutor e = new TestExecutor();
ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c", "sleep 3 & echo started; exit 0");
int rc = e.executeProcess(pb, 1000);
The Linux and Windows native builders never read CN1_TRANSLATOR_OPTS, so an operator setting -Xmx6g or a diagnostic flag saw it take effect on the JavaScript and iOS targets and silently do nothing on those two -- the escape hatch worked or not depending on which target you were building. The parsing now lives in TranslatorHeap.extraJvmOptions/specifiesHeap and all four builders call it, replacing three separately-maintained copies of the same loop with one. That is what let the two native targets drift in the first place. While these call sites were open, the two native builders also gained the out-of-memory diagnostic the JavaScript and iOS builders already had, so a translator that dies for lack of memory explains itself on every target rather than four fifths of them. specifiesHeap deliberately matches only -Xmx: -Xms is not a maximum and must not suppress the auto-sized ceiling. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (4)
maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/ExecutorProcessTimeoutTest.java:92
- This test hard-codes
/bin/sh, which will fail on Windows runners (and any environment without that shell). Please guard it with a JUnit assumption so the suite stays portable.
StringBuilder sb = new StringBuilder();
ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c",
"for i in $(seq 1 500); do echo line-$i; done; echo java.lang.OutOfMemoryError: Java heap space");
int rc = executeCapturing(e, pb, sb);
maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/ExecutorProcessTimeoutTest.java:113
- This test hard-codes
/bin/sh, which will fail on Windows runners (and any environment without that shell). Please guard it with a JUnit assumption so the suite stays portable.
void aProcessThatOverrunsItsDeadlineIsStillReportedAsTimedOut() throws Exception {
TestExecutor e = new TestExecutor();
ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c", "sleep 30");
int rc = e.executeProcess(pb, 1000);
assertEquals(1, rc, "a genuine timeout must still fail");
maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/Executor.java:2009
reader.join(30000)is unconditional, so a genuine timeout can now block for up to an extra 30s after the deadline, and if the reader hasn’t finished by then the method can still return with an incomplete/unstableoutputMessage. Consider joining for a shorter period whentimedOutis already known, and if the reader is still alive force-close the stream and re-join briefly so the output is finalized before returning.
// OutOfMemoryError. Bounded so a wedged reader cannot hang the build;
// the process has already exited, so the stream reaches EOF promptly.
reader.join(30000);
maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/ExecutorProcessTimeoutTest.java:77
- This test hard-codes
/bin/sh, which will fail on Windows runners (and any environment without that shell). Please guard it with a JUnit assumption so the suite stays portable.
This issue also appears in the following locations of the same file:
- line 89
- line 109
TestExecutor e = new TestExecutor();
ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c", "sleep 3 & echo started; exit 0");
int rc = e.executeProcess(pb, 1000);
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1d328009e8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The three tests added with the timeout fix hard-coded /bin/sh, so on Windows ProcessBuilder would throw before any of the behaviour under test ran and the module's suite stopped being runnable off Unix. The plugin is built and developed on Windows too, so that is a real hole rather than a theoretical one. Each child is now a JVM running a helper class from this test, launched from java.home with the current java.class.path, which behaves the same on every platform. Skipping the tests off Unix would have been the smaller change but would have left Windows with no coverage of a fix whose whole point is that timed steps stop failing spuriously. The helper that reproduces the regression starts a grandchild inheriting its stdout and exits immediately, so the pipe outlives the process and the reader join genuinely blocks -- the property the test depends on. Verified again after the rewrite that it still fails against the old ordering (expected 0 but was 1) and passes against the fixed one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (2)
maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/Executor.java:2010
- executeProcess() joins the reader thread for up to 30s, but if the pipe outlives the process for longer (e.g. a child process inherits stdout/stderr and keeps it open), join() can time out and the reader may continue appending to outputMessage after executeProcess() returns. That reintroduces the race this change is trying to eliminate and can leave non-daemon threads running unexpectedly. Consider closing the stream and waiting briefly if the reader is still alive after the bounded join.
reader.join(30000);
maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/TranslatorHeap.java:124
- maxHeapMB() can return a heap larger than the detected memory budget when the historical floor is higher than the budget (e.g. floorMB=2048 on a 1GB cgroup/host). That undermines the stated goal of sizing within the container/machine limit and can cause the forked JVM to fail to start (or be OOM-killed) due to an impossible -Xmx. Consider clamping the auto-sized result to budgetMB as well, even if that means going below the historical floor in undersized environments.
if (heap < floorMB) {
heap = floorMB;
}
return (int) heap;
Fixes #5511.
The problem
The reported
-Xmx512mis not an isolated lowball. Every ByteCodeTranslator fork carried a hard-coded heap, tuned years ago against the sample apps:A large app exhausts that partway through translation and dies with
OutOfMemoryError. The only way out was to discoverCN1_TRANSLATOR_OPTSby reading this plugin's source, which is exactly what the reporter had to do.The defaults were tighter than they looked. Bisecting the heap on the in-repo
hellocodenameoneJS build, the translator OOMs at 384m and needs 448m to finish. That is under 1.5x headroom on the sample app.The fix
TranslatorHeapderives the ceiling from the machine: half the detected memory budget, capped at 4g, floored at each target's historical constant so no target can end up with less heap than before. The budget reads the cgroup limit before falling back to physical RAM, so a build inside a container sizes against the container's limit rather than the host's. That is what keeps an over-large app failing as a JavaOutOfMemoryErrorwe can report, instead of a kernel OOM-kill we cannot.Raising the ceiling is close to free for ordinary apps:
-Xmxis a reservation, not a commitment.CN1_TRANSLATOR_MAX_HEAP_MBpins the value per box. It may sit above the 4g cap (that cap bounds only the automatic choice; clamping the knob would achieve nothing since an operator would just switch toCN1_TRANSLATOR_OPTS=-Xmx, which no ceiling applies to). It may not exceed the memory the machine actually has -- that is the setting that OOM-kills a build box rather than failing one build, and it is the likely shape of a typo.Also in scope
falseand the build reported nothing more useful than a failed step. The JS and iOS builders recognise the signature in the captured output and name the heap in use and how to raise it.JavascriptBundleWriterpeak reduction. It held the whole bundle twice (asStringBuilders and again asStrings) at the chunk handoff, and kept every chunk resident until the last was written. Chunks are now freed as they materialise and as they reach disk.Measured
rta.off)The 32MB reduction is about one full UTF-16 copy of the emitted JS. It scales with bundle size, so it does nothing for a single-chunk app and progressively more for a large one -- the emit change is a modest structural win; the heap sizing is what actually unblocks the reported app.
The emitted bundle is byte-identical, verified on both the single-chunk and multi-chunk paths (the first check alone would not have covered the lead-chunk write loop the change touches).
Verification
Companion change
The cloud side needs the matching fix and is a separate PR against BuildDaemon: the daemon carried the same 512m for JS and, on iOS, no
CN1_TRANSLATOR_OPTSpass-through at all -- so a cloud customer, who cannot set env vars on the build box, had no escape hatch whatsoever.Not addressed
Android remains a fixed cliff:
org.gradle.jvmargs=-Xmx2048manddexOptions javaMaxHeapSize "3g"are hard-coded with no build-hint override. That is a separate subsystem from the translator, but it is where a very large app hits the next wall.🤖 Generated with Claude Code