Skip to content

perf: add app.pack and record javac.pack from a realistic compile - #76

Merged
Project516 merged 1 commit into
masterfrom
p516/packs
Nov 8, 2026
Merged

Project516 merged 1 commit into
masterfrom
p516/packs

Conversation

@Project516

Copy link
Copy Markdown
Owner

What this changes

A cold session fetched most of its JDK bytes as scattered 64 KB Range requests. javac.pack was recorded from a one-line compile, so a real compile read 7 to 12 MB more from the image files, in 100 to 180 requests. A program that never compiles still fetched the whole of javac.pack once it touched one of the java.base classes recorded there.

  • New jdk/app.pack, fetched the first time a program reads one of its parts. It holds what a typical program reads beyond a bare start, recorded from scripts/packs/App.java run from jars: lambdas and streams, java.time, BigDecimal, threads, file I/O, resources from a jar and System.Logger. It also holds every java.base part javac reads.
  • jdk/javac.pack now holds only the compiler's own modules. It is recorded from a javax.tools compile with a jar classpath and an annotation processor (scripts/packs/), as well as the browser self test's compile.
  • boot.pack is unchanged. Its index can now list more than one lazy pack for a file.

Tests

  • Unit tests for the pack index, including two lazy packs for one file.

  • Headless Chromium against a static server that compresses like GitHub Pages, cold sessions (empty cache), JDK bytes only. Two rounds, medians; master is the build after feat: put each JDK module but java.base in its own image file #75:

    Session Master This PR
    Hello 2.3 MB, 0.47 s 2.3 MB, 0.48 s
    Template compile, no processors 11.2 MB, 3.7 s 6.9 MB, 4.2 s
    Template compile, two annotation processors 16.5 MB, 6.0 s 8.2 MB, 4.6 s
    TimedRobot for 3 s 9.1 MB, 5.3 s 5.4 MB, 5.1 s; no javac.pack
    XML parser 5.0 MB 4.1 MB

    At a tenth of one CPU, the TimedRobot's cold run took 39 s instead of 50 s, and the plain template compile took the same time.

  • Warm sessions (cached) fetch nothing on either.

Noticed, not fixed

  • In one of four cold runs, Hello also fetched app.pack (0.7 MB), and in an earlier run, javac.pack. Something read at a bare start depends on timing, and the boot recording does not always catch it.

@coderabbitai

coderabbitai Bot commented Nov 8, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 15 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: b2b5d283-03e4-4727-9bab-76f624c23e99
Commits

Reviewing files that changed from the base of the PR and between c34bf53 and 4b9bf35.

Files selected for processing: 15

  • AGENTS.md
  • docs/browser.md
  • docs/development.md
  • package.json
  • scripts/build-web.ts
  • scripts/packs/App.java
  • scripts/packs/Compile.java
  • scripts/packs/app.properties
  • scripts/packs/lib/Lib.java
  • scripts/packs/proc/Generate.java
  • scripts/packs/proc/GenerateProcessor.java
  • scripts/packs/src/Model.java
  • scripts/packs/src/Uses.java
  • src/browser/http.ts
  • test/image.test.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@project516-review-bot project516-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This PR adds a new jdk/app.pack for typical program byte ranges beyond a bare start, and re-records jdk/javac.pack to contain only the compiler's own modules. The build script now records from realistic workloads (App.java with lambdas, streams, time, I/O, threads, and a javax.tools compile with annotation processor). The boot pack index is updated to support multiple lazy packs per file, and the browser HTTP layer handles the new structure. Performance tests show reduced cold-start bytes for compile workloads.


review-bot, model nvidia/nemotron-3-ultra-550b-a55b:free, verdict approve

Comment thread scripts/build-web.ts
const beyondBoot = (rec: Recorder) => new Map([...rec.ranges].map(([p, r]) => [p, subtract(merge(r), bootRanges.get(p) ?? [])]));
const appRanges = beyondBoot(appRecorder), javacRanges = new Map<string, [number, number][]>();
for (const [p, r] of beyondBoot(javacRecorder)) {
const own = p === "/lib/modules" ? subtract(r, subtract(r, compiler)) : [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This computes the intersection of r and compiler via double subtraction. It works but is hard to read. Consider a helper intersect(a, b) or at least a comment explaining the intent: own is the part of r that belongs to compiler modules.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Agreed that it reads poorly. own is the part of r inside the compiler modules. Each push here reruns the full OpenJDK suite (about 25 minutes), so the intersect(a, b) helper and its comment go in with the next change to this script rather than as a push of their own.

Comment thread scripts/build-web.ts
...javacFiles.map((p): [string, Uint8Array] => [p, javacRecorder.read(p)!]),
...rangeEntries(javacRanges, javacRecorder),
const appPack = writePack(join(dist, "jdk", "app.pack"), [
...appFiles.map((p): [string, Uint8Array] => [p, jdkFiles.read(p)!]),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Non-null assertion on jdkFiles.read(p). If a file recorded during appRecorder/javacRecorder runs is missing from the JDK image (e.g., a race or build inconsistency), the build will crash with a cryptic error. Add a null check and throw a descriptive error.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This read cannot return null. appFiles holds only paths in the recorders' seen sets, and Recorder.read adds a path to seen only when the read returned data (if (d) this.seen.add(p);, line 102). jdkFiles reads the same build/jdk through the same NodeBackend, in the same build step.

Comment thread scripts/build-web.ts
// program reads one of its parts, holds what the typical program reads beyond
// that and every java.base part javac reads. javac.pack holds the rest of what
// javac reads: the compiler's own modules.
const linked = new Set(/^MODULES="(.*)"$/m.exec(readFileSync(join(jdk, "release"), "utf8"))![1].split(" "));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Parsing the release file with a regex is fragile if the file format changes. The JDK is pinned so this is low risk, but a comment noting the expected format would help future maintenance.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This is the MODULES="..." line that jlink writes into release. scripts/build-jdk.sh reads it with the same pattern (sed -n 's/^MODULES="\(.*\)"/\1/p'), and src/jvm.ts parses it at every start. If the format ever changed, the ! here would throw at build time, before anything is packed.

Comment thread scripts/build-web.ts
// that and every java.base part javac reads. javac.pack holds the rest of what
// javac reads: the compiler's own modules.
const linked = new Set(/^MODULES="(.*)"$/m.exec(readFileSync(join(jdk, "release"), "utf8"))![1].split(" "));
const compiler = merge(moduleRuns(readFileSync(join(jdk, "lib", "modules")))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reading and parsing /lib/modules (a jimage file) to extract compiler module ranges depends on the internal jimage layout. This is build-time only and the JDK is pinned, so acceptable, but worth a comment that it relies on the current jimage structure.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Agreed, it relies on the jimage layout. That dependence and its check came in #75: moduleRuns reads the jimage index format src/jimage.ts already depends on at run time, and build-web.ts throws (lib/modules has <module> in more than one place) if jlink ever stops laying a module out in one run. So a layout change fails the build instead of producing wrong packs.

@Project516
Project516 merged commit cb5c1d1 into master Nov 8, 2026
16 checks passed
@Project516
Project516 deleted the p516/packs branch November 8, 2026 22:12
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.

1 participant