| name | bytecode-impact |
| description | Measure and compare bytecode impact of macro/codegen changes on compiled Scala class files. Use this skill whenever investigating bytecode size, method count, class file overhead, lazy val machinery, LazyRef/lzyINIT patterns, or comparing "before vs after" bytecode output of a code change. Also triggers on "bytecode comparison", "prove bytecode improvement", "how much bytecode", "class file size", "method count", "bytecode regression", "lazy val overhead", or "does this change reduce bytecode". Use this skill alongside compile-time and runtime benchmarks for a complete picture of a change's impact. |
Bytecode Impact Analysis
Measure the bytecode footprint of compiled Scala code and compare before/after to prove (or disprove) that a change reduces generated bytecode.
When to Use
- After a macro or codegen change that should reduce bytecode (e.g., emitting
val instead of lazy val, removing wrapper classes, simplifying generated code)
- To quantify overhead from patterns like
LazyRef, lzyINIT, monitorenter (lazy val synchronization)
- To compare bytecode between two compilation variants (with/without a feature flag, two branches, etc.)
Quick Start
Single directory analysis
python3 .claude/skills/bytecode-impact/scripts/analyze_bytecode.py \
out/benchmark/sanely/compile.dest/classes
Before/after comparison
python3 .claude/skills/bytecode-impact/scripts/analyze_bytecode.py \
results/bytecode-impact/classes-after results/bytecode-impact/classes-before \
--labels "With Change" "Without Change"
Full Workflow
Step 1: Compile with the current code (the "after" state)
rm -rf out/benchmark/sanely
./mill benchmark.sanely.compile
Copy or note the classes directory:
mkdir -p results/bytecode-impact
cp -r out/benchmark/sanely/compile.dest/classes results/bytecode-impact/classes-after
Step 2: Temporarily revert the change and recompile (the "before" state)
Edit the source to revert (e.g., force lazy val for all types), recompile the library, then recompile the benchmark:
./mill sanely.jvm.compile
rm -rf out/benchmark/sanely
./mill benchmark.sanely.compile
cp -r out/benchmark/sanely/compile.dest/classes results/bytecode-impact/classes-before
Then restore the original code and recompile again.
Step 3: Compare
python3 .claude/skills/bytecode-impact/scripts/analyze_bytecode.py \
results/bytecode-impact/classes-after results/bytecode-impact/classes-before \
--labels "With Change" "Without Change"
Step 4: Inspect specific classes (optional)
For a detailed look at a specific class:
javap -c -p out/benchmark/sanely/compile.dest/classes/benchmark/SomeType$.class
Look for:
LazyRef / lzyINIT — lazy val initialization machinery
monitorenter / monitorexit — synchronization blocks
_self*$1 / _self*$lzyINIT1$1 — self-referencing methods for recursive types
- Anonymous class count (
$$anon$N.class files)
Script Details
The bundled scripts/analyze_bytecode.py script collects:
| Metric | What it measures |
|---|
| Class files | Number of .class files |
| Total bytecode | Sum of all .class file sizes in bytes |
| Total methods | Method declarations found by javap -p |
| Pattern counts | Occurrences of configurable patterns in disassembled bytecode |
Default patterns: LazyRef, lzyINIT, monitorenter, monitorexit — these indicate lazy val overhead.
Custom patterns
Override with --patterns:
python3 .claude/skills/bytecode-impact/scripts/analyze_bytecode.py \
out/benchmark/sanely/compile.dest/classes \
--patterns "LazyRef" "lzyINIT" "monitorenter" "invokedynamic" "anon"
Dump javap output
Save full disassembly for manual grep/inspection:
python3 .claude/skills/bytecode-impact/scripts/analyze_bytecode.py \
out/benchmark/sanely/compile.dest/classes \
--dump-javap results/bytecode-impact/javap-full.txt
Interpreting Results
- Bytecode size reduction — fewer bytes means faster classloading and less memory for class metadata
- Method count reduction — fewer methods means less JIT compilation work and smaller vtables
- LazyRef/lzyINIT elimination — removing unnecessary lazy val machinery avoids synchronization overhead on first access and reduces bytecode by ~3 methods per encoder/decoder pair
- No change in class file count — expected when the change affects code generation patterns but not the number of types
Applies To
This workflow works for any Mill module, not just the benchmark. Adjust the compile target:
./mill benchmark.sanely.compile
./mill benchmark-configured.sanely.compile
./mill sanely.jvm.compile