-
-
Notifications
You must be signed in to change notification settings - Fork 159
perf(compile): reduce generated bundle bloat #8418
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
72f53f8
59ac610
12a29ae
aacafd5
d6d03ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| ### Improved | ||
|
|
||
| - Reduced native output for large dependency graphs by removing obsolete | ||
| program-wide class metadata augmentation and by keeping imported JSON | ||
| serialized until `JSON.parse` runs at startup. On the 4,743-module OpenCode | ||
| source build this cut the Windows executable from 1,685.4 MiB to 1,143.7 MiB | ||
| and reduced module-codegen time by 12.8%. | ||
| - Added opt-in hybrid size optimization with | ||
| `PERRY_LL_PREOPT_OPTNONE_INSTRS`: generated functions above the configured | ||
| instruction cap skip the LLVM middle-end while ordinary siblings in the same | ||
| codegen unit remain eligible for `-Os`. With an 8,192-instruction cap, the | ||
| same OpenCode executable shrank further to 797.0 MiB (52.7% below baseline); | ||
| module codegen took 88.8 minutes versus the 73.2-minute baseline. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,7 @@ | |
| //! | ||
| //! Decision parity by construction: this module does not re-derive optimization | ||
| //! or CPU tuning. It interprets the *same* argv `build_clang_compile_plan` | ||
| //! produces for clang (`-O3`, `-mcpu=native`, `-mllvm | ||
| //! produces for clang (`-O3`/explicit `-Os`, `-mcpu=native`, `-mllvm | ||
| //! -inlinehint-threshold=N`, `-target <triple>`), so the two backends cannot | ||
| //! drift on a decision without drifting on the plan — which the plan's own | ||
| //! tests pin. | ||
|
|
@@ -21,6 +21,7 @@ use std::ffi::CString; | |
| use std::sync::Once; | ||
|
|
||
| use anyhow::{anyhow, Result}; | ||
| use inkwell::attributes::{Attribute, AttributeLoc}; | ||
| use inkwell::context::Context; | ||
| use inkwell::memory_buffer::MemoryBuffer; | ||
| use inkwell::passes::PassBuilderOptions; | ||
|
|
@@ -327,6 +328,91 @@ pub(crate) fn optimize_and_emit_module( | |
| ) | ||
| } | ||
|
|
||
| /// Optional pre-optimization escape hatch for unusually large generated | ||
| /// functions. | ||
| /// | ||
| /// Dense generated bundles often contain one parser/table initializer that is | ||
| /// large enough to make the `-O1+` middle-end super-linear, alongside hundreds | ||
| /// of ordinary functions that benefit substantially from `-Os`. Routing the | ||
| /// whole codegen unit to `-O0` keeps compilation bounded but also bloats every | ||
| /// ordinary sibling. When this cap is non-zero, only functions above it are | ||
| /// stamped `optnone`+`noinline` before the module pipeline runs. This makes | ||
| /// `PERRY_LL_SIZE_OPT=1` a practical hybrid mode instead of an all-or-nothing | ||
| /// gamble on the largest function in each unit. | ||
| /// | ||
| /// Disabled by default while the threshold is calibrated across the bundle | ||
| /// corpus. `PERRY_LL_PREOPT_OPTNONE_INSTRS=N` enables it; `0` disables it. | ||
| const DEFAULT_PREOPT_OPTNONE_INSTRS: usize = 0; | ||
|
|
||
| fn preopt_optnone_instr_cap() -> usize { | ||
| std::env::var("PERRY_LL_PREOPT_OPTNONE_INSTRS") | ||
| .ok() | ||
| .and_then(|s| s.trim().parse::<usize>().ok()) | ||
| .unwrap_or(DEFAULT_PREOPT_OPTNONE_INSTRS) | ||
| } | ||
|
|
||
| fn stamp_function_optnone(function: inkwell::values::FunctionValue<'_>) { | ||
| let context = function.get_type().get_context(); | ||
| let optnone_kind = Attribute::get_named_enum_kind_id("optnone"); | ||
| let noinline_kind = Attribute::get_named_enum_kind_id("noinline"); | ||
| // `alwaysinline` and `noinline` are verifier-incompatible. Generated | ||
| // functions do not normally carry it, but the opt-in must remain safe for | ||
| // imported/generated IR that does. | ||
| function.remove_enum_attribute( | ||
| AttributeLoc::Function, | ||
| Attribute::get_named_enum_kind_id("alwaysinline"), | ||
| ); | ||
| function.remove_enum_attribute( | ||
| AttributeLoc::Function, | ||
| Attribute::get_named_enum_kind_id("inlinehint"), | ||
| ); | ||
| function.add_attribute( | ||
| AttributeLoc::Function, | ||
| context.create_enum_attribute(optnone_kind, 0), | ||
| ); | ||
| function.add_attribute( | ||
| AttributeLoc::Function, | ||
| context.create_enum_attribute(noinline_kind, 0), | ||
| ); | ||
|
Comment on lines
+354
to
+376
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- target symbols and nearby code ---'
rg -n -C 18 'stamp_function_optnone|optnone|minsize|optsize|optdebug|alwaysinline|inlinehint' crates/perry-codegen/src/inprocess.rs crates/perry-codegen tests 2>/dev/null || true
printf '%s\n' '--- call sites ---'
rg -n -C 8 'stamp_function_optnone|demot|optnone' crates/perry-codegen --glob '*.rs' --glob '*.ll' --glob '*.wat' 2>/dev/null || true
printf '%s\n' '--- candidate tests and fixtures ---'
git ls-files | rg '(^|/)(test|tests|fixtures|fixture|.*\.ll$)' | head -200Repository: PerryTS/perry Length of output: 50370 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- LLVM/Inkwell configuration ---'
rg -n -C 3 'inkwell|llvm[0-9]+|LLVM_SYS|LLVM_VERSION' Cargo.toml Cargo.lock crates --glob 'Cargo.toml' --glob 'Cargo.lock' --glob '*.rs' 2>/dev/null | head -240
printf '%s\n' '--- exact attribute references ---'
rg -n --glob '*.rs' --glob '*.ll' --glob '*.toml' '\b(minsize|optsize|optdebug|alwaysinline|inlinehint|optnone|noinline)\b' crates/perry-codegen crates/perry 2>/dev/null | head -240
printf '%s\n' '--- LLVM tools ---'
for tool in llvm-as opt clang clang++; do
if command -v "$tool" >/dev/null 2>&1; then
printf '%s: ' "$tool"
"$tool" --version 2>/dev/null | head -1
else
printf '%s: unavailable\n' "$tool"
fi
doneRepository: PerryTS/perry Length of output: 24681 🌐 Web query:
💡 Result: In LLVM, the optnone function attribute is designed to prevent optimizations on a function [1][2][3]. Due to its purpose, it is explicitly incompatible with several other function attributes that either mandate or influence specific optimization behaviors [1][2][4]. The optnone attribute is incompatible with the following: - alwaysinline: The optnone attribute cannot be used with alwaysinline [1][2][3]. LLVM verifiers explicitly flag these as incompatible because optnone forbids optimizations while alwaysinline mandates them [2][3]. - minsize: This is incompatible with optnone [1][2][3]. - optsize: This is incompatible with optnone [1][2][3]. - optdebug: This is also incompatible with optnone [4]. Additionally, the optnone attribute implicitly requires the noinline attribute to be present [1]. This ensures that the function is never inlined into any caller, which is consistent with its goal of preventing optimization-related transformations [1][2][5]. While inlinehint is not always explicitly listed in the same "incompatible" error blocks as alwaysinline in the verifier, it is generally considered conceptually incompatible with the "no optimization" goal of optnone [6]. Top results: [1], [2], [3], [4] Citations:
🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re
path = Path("crates/perry-codegen/src/inprocess.rs")
text = path.read_text()
body = re.search(
r"fn stamp_function_optnone\(.*?\n\}\n",
text,
re.S,
)
if not body:
raise SystemExit("stamp_function_optnone not found")
removed = set(re.findall(r'get_named_enum_kind_id\("([^"]+)"\)', body.group(0)))
required = {"alwaysinline", "minsize", "optsize", "optdebug"}
print("removed attributes:", sorted(removed))
print("required incompatible attributes:", sorted(required))
print("missing required removals:", sorted(required - removed))
print("extra removals:", sorted(removed - required))
print("demotion call present:", "stamp_function_optnone(f);" in text)
PY
printf '%s\n' '--- LLVM LangRef exact text ---'
curl -L --fail --silent --show-error \
https://raw.githubusercontent.com/llvm/llvm-project/main/llvm/docs/LangRef.rst |
rg -n -A18 -B5 'optnone.*incompatible|incompatible.*optnone|optnone attribute' | head -100
printf '%s\n' '--- llvm-sys version ---'
rg -n -A5 -B2 'name = "llvm-sys"' Cargo.lockRepository: PerryTS/perry Length of output: 822 🏁 Script executed: #!/bin/bash
set -eu
for url in \
https://releases.llvm.org/22.0.0/docs/LangRef.html \
https://releases.llvm.org/22.1.0/docs/LangRef.html \
https://llvm.org/docs/LangRef.html
do
printf '%s\n' "--- $url ---"
if curl -L --fail --silent --show-error "$url" |
rg -in -A12 -B5 'optnone|optdebug|optsize|minsize' | head -80
then
break
fi
doneRepository: PerryTS/perry Length of output: 259 Remove If a demoted function carries any of these attributes, LLVM verification rejects the module. Add a fixture with one conflicting attribute. 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| fn function_instruction_count(function: inkwell::values::FunctionValue<'_>, cap: usize) -> usize { | ||
| let mut instrs = 0usize; | ||
| 'body: for bb in function.get_basic_blocks() { | ||
| let mut inst = bb.get_first_instruction(); | ||
| while let Some(i) = inst { | ||
| instrs += 1; | ||
| if instrs > cap { | ||
| break 'body; | ||
| } | ||
| inst = i.get_next_instruction(); | ||
| } | ||
| } | ||
| instrs | ||
| } | ||
|
|
||
| /// Demote large functions before the ordinary optimization pipeline while | ||
| /// leaving every smaller sibling eligible for the unit's requested opt level. | ||
| fn demote_preoptimization_bloated_functions(module: &inkwell::module::Module<'_>, cap: usize) { | ||
| if cap == 0 { | ||
| return; | ||
| } | ||
| let mut function = module.get_first_function(); | ||
| while let Some(f) = function { | ||
| if function_instruction_count(f, cap) > cap { | ||
| stamp_function_optnone(f); | ||
| eprintln!( | ||
| "perry: `{}` exceeds {} pre-optimization instructions; compiling only this \ | ||
| function unoptimized (optnone) while its siblings keep the module's size \ | ||
| optimization. Override with PERRY_LL_PREOPT_OPTNONE_INSTRS.", | ||
| f.get_name().to_string_lossy(), | ||
| cap, | ||
| ); | ||
| } | ||
| function = f.get_next_function(); | ||
| } | ||
| } | ||
|
|
||
| fn optimize_and_emit( | ||
| module: &inkwell::module::Module<'_>, | ||
| effective_target: &str, | ||
|
|
@@ -387,6 +473,12 @@ fn optimize_and_emit( | |
| module.set_triple(&triple); | ||
| module.set_data_layout(&tm.get_target_data().get_data_layout()); | ||
|
|
||
| // Opt-in hybrid size optimization for generated bundles: protect only the | ||
| // pathological bodies before entering the requested module pipeline. | ||
| if opt != '0' { | ||
| demote_preoptimization_bloated_functions(module, preopt_optnone_instr_cap()); | ||
| } | ||
|
|
||
| // RS4GC must run BEFORE the optimization pipeline, and — critically — in | ||
| // this process, against this LLVM. | ||
| // | ||
|
|
@@ -515,6 +607,47 @@ mod tests { | |
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn preoptimization_bloated_function_is_demoted_without_demoting_its_sibling() { | ||
| global_init(&[]); | ||
| let context = Context::create(); | ||
| let ir = "define i64 @big(i64 %a) {\n\ | ||
| entry:\n\ | ||
| \x20 %x1 = add i64 %a, 1\n\ | ||
| \x20 %x2 = add i64 %x1, 1\n\ | ||
| \x20 %x3 = add i64 %x2, 1\n\ | ||
| \x20 %x4 = add i64 %x3, 1\n\ | ||
| \x20 %x5 = add i64 %x4, 1\n\ | ||
| \x20 ret i64 %x5\n\ | ||
| }\n\ | ||
| define i64 @small(i64 %a) {\n\ | ||
| entry:\n\ | ||
| \x20 %x1 = add i64 %a, 1\n\ | ||
| \x20 ret i64 %x1\n\ | ||
| }\n"; | ||
| let module = | ||
| parse_ir_text(&context, ir, "preopt_optnone_demotion").expect("fixture parses"); | ||
| demote_preoptimization_bloated_functions(&module, 4); | ||
|
|
||
| let optnone_kind = Attribute::get_named_enum_kind_id("optnone"); | ||
| let big = module.get_function("big").expect("big exists"); | ||
| let small = module.get_function("small").expect("small exists"); | ||
| assert!( | ||
| big.get_enum_attribute(AttributeLoc::Function, optnone_kind) | ||
| .is_some(), | ||
| "a function past the pre-optimization cap must be stamped optnone" | ||
| ); | ||
| assert!( | ||
| small | ||
| .get_enum_attribute(AttributeLoc::Function, optnone_kind) | ||
| .is_none(), | ||
| "an ordinary sibling must keep the module optimization pipeline" | ||
| ); | ||
| module | ||
| .verify() | ||
| .expect("optnone+noinline must remain verifier-valid"); | ||
| } | ||
|
|
||
| fn constant_fold_order_fixture(folded: bool) -> String { | ||
| let mut ir = String::from( | ||
| "declare i64 @may_collect()\n\ndefine i64 @f(i64 %d0, i64 %d1, i64 %d2, i64 %d3, i64 %d4, i64 %d5, i64 %d6, i64 %d7) gc \"statepoint-example\" {\nentry:\n", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| //! JSON-module source synthesis. | ||
| //! | ||
| //! Keep JSON as a serialized string until runtime. Lowering a large JSON object | ||
| //! literal emits an allocation and property-store sequence for every member; | ||
| //! data sets such as `mime-db` consequently produced megabytes of machine code. | ||
|
|
||
| use anyhow::{anyhow, Result}; | ||
| use std::path::Path; | ||
|
|
||
| pub(super) fn synthesize_json_module(raw: &str, canonical: &Path) -> Result<String> { | ||
| serde_json::from_str::<serde_json::Value>(raw).map_err(|error| { | ||
| anyhow!( | ||
| "Failed to parse JSON module {}: {}", | ||
| canonical.display(), | ||
| error | ||
| ) | ||
| })?; | ||
|
Comment on lines
+10
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect the serde_json dependency declaration and enabled feature wiring.
fd '^Cargo\.toml$' . -x rg -n -C 3 'serde_json|arbitrary_precision' {}
# Locate existing JSON-module tests and numeric-boundary coverage.
rg -n -C 5 'synthesize_json_module|json_module|1e400|JSON\.parse' cratesRepository: PerryTS/perry Length of output: 5553 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- workspace dependency configuration ---'
rg -n -C 8 '^serde_json\s*=|arbitrary_precision' Cargo.toml crates/perry/Cargo.toml
echo '--- target implementation ---'
target=$(fd -t f 'json_module\.rs$' . | head -n 1)
printf 'file: %s\n' "$target"
cat -n "$target"
echo '--- JSON module tests and generated-loader references ---'
rg -n -C 8 'synthesize_json_module|__perry_json_default|loaded: false|JSON\.parse|json module|json_module' crates/perry crates/perry-runtime test-files test-parity 2>/dev/null || true
echo '--- serde_json lock entry ---'
rg -n -A 12 -B 2 '^name = "serde_json"$' Cargo.lockRepository: PerryTS/perry Length of output: 50370 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- workspace serde_json declaration ---'
sed -n '340,370p' Cargo.toml
echo '--- target implementation and tests ---'
cat -n crates/perry/src/commands/compile/collect_modules/json_module.rs
echo '--- path-module registration and lookup ---'
rg -n -C 12 '__perry_register_path_module|__perry_require_path_module|__perry_has_path_module' crates/perry/src/commands/compile crates/perry-runtime
echo '--- CJS record loading ---'
rg -n -C 16 '(__perry_cjs_record|__perry_cjs_factory|loaded\s*[:=])' crates/perry/src/commands/compile crates/perry-runtimeRepository: PerryTS/perry Length of output: 50369 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- ECMAScript behavior ---'
node - <<'JS'
for (const text of ['1e400', '-1e400', '1e-400']) {
try {
const value = JSON.parse(text);
console.log(text, 'accepted', String(value), Number.isFinite(value));
} catch (error) {
console.log(text, 'rejected', error.name, error.message);
}
}
JS
echo '--- locked serde_json version ---'
rg -n -A 10 -B 2 '^name = "serde_json"$' Cargo.lock
echo '--- local serde_json source, if present ---'
find "${CARGO_HOME:-$HOME/.cargo}/registry/src" -maxdepth 3 -type d -name 'serde_json-*' 2>/dev/null | head -n 5Repository: PerryTS/perry Length of output: 676 🌐 Web query:
💡 Result: In serde_json, the behavior when parsing a number like "1e400" depends on whether the arbitrary_precision feature is enabled [1][2]. When the arbitrary_precision feature is enabled, serde_json preserves the string representation of the number within the serde_json::Number type [2][3]. Consequently, parsing "1e400" using from_str succeeds, and it is stored as a valid Value::Number [1][4]. However, because this value exceeds the range of a standard 64-bit float (f64), calling the.as_f64 method on this number will return None [1][4]. Conversely, without the arbitrary_precision feature, serde_json typically attempts to parse the number into a representation compatible with f64. Numbers that are out of range for f64 may be rejected or handled differently depending on the specific context of parsing [5]. It is important to note that the arbitrary_precision feature is not enabled by default [6]. To use it, you must explicitly enable it in your Cargo.toml dependencies: [dependencies] serde_json = { version = "1.0", features = ["arbitrary_precision"] } This feature ensures that JSON numbers of arbitrary size or precision can be read into a Number and written back to a JSON string without loss of precision, as opposed to the default behavior which prioritizes f64 compatibility [2][3]. Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
curl -fsSL \
https://raw.githubusercontent.com/serde-rs/json/v1.0.151/src/de.rs \
-o "$tmp"
echo '--- number parser references ---'
rg -n -C 10 'parse_number|from_str::<f64>|number out of range|arbitrary_precision' "$tmp"Repository: PerryTS/perry Length of output: 8781 🏁 Script executed: #!/bin/bash
set -euo pipefail
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
curl -fsSL \
https://raw.githubusercontent.com/serde-rs/json/v1.0.151/src/de.rs \
-o "$tmp"
sed -n '520,700p' "$tmp"Repository: PerryTS/perry Length of output: 6270 Match JSON-module number acceptance to
🤖 Prompt for AI Agents |
||
|
|
||
| // serde_json's string serializer produces a valid JavaScript literal and | ||
| // escapes control characters, quotes, backslashes, and line separators. | ||
| let serialized = serde_json::to_string(raw.trim()).map_err(|error| { | ||
| anyhow!( | ||
| "Failed to encode JSON module {}: {}", | ||
| canonical.display(), | ||
| error | ||
| ) | ||
| })?; | ||
| let path = canonical.to_string_lossy(); | ||
|
|
||
| Ok(format!( | ||
| "function __perry_json_factory() {{ return JSON.parse({serialized}); }}\n\ | ||
| const __perry_json_default = __perry_json_factory();\n\ | ||
| const __perry_json_module = {{ __perry_cjs_record: true, __perry_cjs_factory: __perry_json_factory, exports: __perry_json_default, loaded: false }};\n\ | ||
| __perry_register_path_module({path:?}, __perry_json_module);\n\ | ||
| export default __perry_json_default;\n" | ||
| )) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::synthesize_json_module; | ||
| use std::path::Path; | ||
|
|
||
| #[test] | ||
| fn keeps_json_serialized_instead_of_lowering_an_object_literal() { | ||
| let source = synthesize_json_module( | ||
| r#"{"items":[1,true,null],"nested":{"message":"hello"}}"#, | ||
| Path::new("data.json"), | ||
| ) | ||
| .expect("synthesize JSON module"); | ||
|
|
||
| assert!(source.contains("return JSON.parse(")); | ||
| assert!(!source.contains("return {\"items\"")); | ||
| assert!(source.contains("export default __perry_json_default")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn rejects_invalid_json_before_typescript_parsing() { | ||
| let error = synthesize_json_module("{broken", Path::new("broken.json")) | ||
| .expect_err("invalid JSON should fail"); | ||
| assert!(error.to_string().contains("Failed to parse JSON module")); | ||
| assert!(error.to_string().contains("broken.json")); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Document both hybrid-mode variables.
PERRY_LL_PREOPT_OPTNONE_INSTRSalone keeps ordinary functions at the default-O3.-Osrequires a truthyPERRY_LL_SIZE_OPT. State that the measured hybrid mode uses bothPERRY_LL_SIZE_OPT=1andPERRY_LL_PREOPT_OPTNONE_INSTRS=8192.🤖 Prompt for AI Agents