mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-30 07:14:39 +08:00
Bound the compiler's cyclic garbage to twice its live heap
The cycle collector widens the growth fraction that schedules full passes up to 16 times the heap left by the last pass while passes find little. Lowering a large program leaves hundreds of megabytes of cyclic garbage, and whether a scheduled pass reclaims it before the build's peak depends on where the growth rule fires: small changes to the compiler moved the peak of a tsc-ts build between 1.66 and 2.05 GB. The native compiler now sets the growth cap to 2 at startup, as SCR_CYCLE_GROWTH_CAP=2 does, so a full pass runs once the heap has grown to three times its size after the last one. scr_cyc_set_growth_cap sets the cap for a program from its own code; the environment variable still takes precedence, and child processes are not affected.
This commit is contained in:
@@ -56,3 +56,18 @@ void scr_collect_cycles_phase(void);
|
||||
void scriptc_native_collect_cycles(void) {
|
||||
scr_collect_cycles_phase();
|
||||
}
|
||||
|
||||
/* The runtime's growth cap for full cycle passes (scr_rc.h). */
|
||||
void scr_cyc_set_growth_cap(long cap);
|
||||
|
||||
/* Bounds the cyclic garbage a build can hold to twice its live heap.
|
||||
* Lowering a large program leaves hundreds of megabytes of cyclic garbage,
|
||||
* and whether a scheduled full pass reclaims it before the build's peak
|
||||
* depends on where the growth rule fires: with the default cap of 16, small
|
||||
* changes to the compiler moved the peak of one build by 390 MB. With a cap
|
||||
* of 2 a full pass runs once the heap has grown to three times its size
|
||||
* after the last one; a cap of 1 bounds the garbage further but runs more
|
||||
* passes over the live heap. SCR_CYCLE_GROWTH_CAP still overrides it. */
|
||||
void scriptc_native_bound_cycle_growth(void) {
|
||||
scr_cyc_set_growth_cap(2);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"ffi_format": 6,
|
||||
"functions": [
|
||||
{ "name": "compilerNativePrivateDirectory", "symbol": "scriptc_native_private_directory", "params": ["string", "bool"], "returns": "bool" },
|
||||
{ "name": "compilerNativeCollectCycles", "symbol": "scriptc_native_collect_cycles", "params": [], "returns": "void" }
|
||||
{ "name": "compilerNativeCollectCycles", "symbol": "scriptc_native_collect_cycles", "params": [], "returns": "void" },
|
||||
{ "name": "compilerNativeBoundCycleGrowth", "symbol": "scriptc_native_bound_cycle_growth", "params": [], "returns": "void" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -23,8 +23,10 @@ import { runCompilerTask } from "./task.js";
|
||||
|
||||
declare function compilerNativePrivateDirectory(path: string, harden: boolean): boolean;
|
||||
declare function compilerNativeCollectCycles(): void;
|
||||
declare function compilerNativeBoundCycleGrowth(): void;
|
||||
|
||||
async function main(): Promise<number> {
|
||||
compilerNativeBoundCycleGrowth();
|
||||
setFrontendDisposedHook(() => compilerNativeCollectCycles());
|
||||
const manifestPath = process.env["SCRIPTC_TOOLCHAIN"] ?? process.execPath + ".json";
|
||||
let compiler: NativeCompiler | null = null;
|
||||
|
||||
@@ -529,7 +529,8 @@ static SCR_TL size_t scr_cyc_live_after[SCR_CYC_NGENS];
|
||||
* large program, say) those walks dominate collection time. The cap trades
|
||||
* that walking for memory only when passes find nothing: while they find
|
||||
* garbage, the yield keeps the fraction narrow. SCR_CYCLE_GROWTH_CAP lowers
|
||||
* the cap for memory-sensitive programs: a power of two from 1 to 16, with
|
||||
* the cap for memory-sensitive programs, as scr_cyc_set_growth_cap does for
|
||||
* a program that sets its own: a power of two from 1 to 16, with
|
||||
* other positive values rounded down to a power of two and larger ones read
|
||||
* as 16. At 1 the heap may at most double past its size after the last pass
|
||||
* at a level before the next. */
|
||||
@@ -539,20 +540,34 @@ static SCR_TL unsigned char scr_cyc_growth_shift[SCR_CYC_NGENS];
|
||||
static SCR_TL unsigned char scr_cyc_growth_streak[SCR_CYC_NGENS];
|
||||
#endif
|
||||
|
||||
/* The growth cap a program sets for itself (scr_cyc_set_growth_cap), 0 for
|
||||
* the default; SCR_CYCLE_GROWTH_CAP takes precedence over it. */
|
||||
static long scr_cyc_growth_cap_set = 0;
|
||||
static SCR_TL unsigned char scr_cyc_growth_max_shift_cached = 0;
|
||||
|
||||
/* Shift of the configured growth cap: 2 + log2(cap), since the base
|
||||
* fraction is 1/4. */
|
||||
static unsigned scr_cyc_growth_max_shift(void) {
|
||||
static SCR_TL unsigned char cached = 0;
|
||||
if (cached == 0) {
|
||||
if (scr_cyc_growth_max_shift_cached == 0) {
|
||||
const char *env = scr_getenv("SCR_CYCLE_GROWTH_CAP");
|
||||
long v = env ? strtol(env, NULL, 10) : 0;
|
||||
if (v <= 0) v = scr_cyc_growth_cap_set;
|
||||
unsigned shift = SCR_CYC_GROWTH_MAX_SHIFT;
|
||||
if (v > 0)
|
||||
for (shift = 2; shift < SCR_CYC_GROWTH_MAX_SHIFT && (2L << (shift - 2)) <= v;)
|
||||
shift++;
|
||||
cached = (unsigned char)shift;
|
||||
scr_cyc_growth_max_shift_cached = (unsigned char)shift;
|
||||
}
|
||||
return cached;
|
||||
return scr_cyc_growth_max_shift_cached;
|
||||
}
|
||||
|
||||
void scr_cyc_set_growth_cap(long cap) {
|
||||
scr_cyc_growth_cap_set = cap;
|
||||
scr_cyc_growth_max_shift_cached = 0;
|
||||
/* Fractions already widened past the new cap start at it. */
|
||||
unsigned max = scr_cyc_growth_max_shift();
|
||||
for (unsigned gen = 0; gen < SCR_CYC_NGENS; gen++)
|
||||
if (scr_cyc_growth_shift[gen] > max) scr_cyc_growth_shift[gen] = (unsigned char)max;
|
||||
}
|
||||
|
||||
/* Doublings of each generation's backlog threshold earned by consecutive
|
||||
|
||||
@@ -670,6 +670,10 @@ static inline void scr_cyc_dead(void *obj) {
|
||||
* live heap — do not put it on a per-turn path. */
|
||||
void scr_collect_cycles(void);
|
||||
void scr_collect_cycles_phase(void);
|
||||
/* Sets the growth cap SCR_CYCLE_GROWTH_CAP sets (scr_cycle.c), for a program
|
||||
* that knows its own memory profile; the environment variable takes
|
||||
* precedence. Call it before the program allocates much. */
|
||||
void scr_cyc_set_growth_cap(long cap);
|
||||
void scr_cyc_context_cleanup(void);
|
||||
|
||||
/* One pass on the normal generational schedule, for callers that reach a
|
||||
|
||||
@@ -536,6 +536,27 @@ static void check_mixed_dead_structure_narrows(void) {
|
||||
check(live <= narrowed, "a pass that freed a dead mature and old structure did not narrow the growth fraction");
|
||||
}
|
||||
|
||||
/* scr_cyc_set_growth_cap sets the cap SCR_CYCLE_GROWTH_CAP sets, and a
|
||||
* fraction already widened past it starts at it: after passes that found
|
||||
* nothing widened the old level's fraction to the configured cap, a cap of
|
||||
* 1 has the next dead cycle go once the heap has doubled past the floor.
|
||||
* Restores the configured cap. */
|
||||
static void check_program_growth_cap(void) {
|
||||
enum { FLOOR = 4096 };
|
||||
if (getenv("SCR_CYCLE_GROWTH_CAP") != NULL) return; /* it takes precedence */
|
||||
size_t slack = 2 * configured_nursery_threshold() + 2;
|
||||
size_t live = 0;
|
||||
for (int i = 0; i < 4; i++) live = grow_dead_cycle_until_full_pass(0, false);
|
||||
check(live > FLOOR + FLOOR * configured_growth_cap(),
|
||||
"passes that found nothing did not widen the growth fraction to the cap");
|
||||
scr_cyc_set_growth_cap(1);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
live = grow_dead_cycle_until_full_pass(0, false);
|
||||
check(live <= FLOOR + FLOOR + slack, "a program's growth cap of 1 did not bound the heap");
|
||||
}
|
||||
scr_cyc_set_growth_cap(0);
|
||||
}
|
||||
|
||||
/* The phase sweep (scr_collect_cycles_phase) is for a caller that knows a
|
||||
* phase's data has just died: it sweeps, and the growth schedule starts
|
||||
* over. After passes that find nothing have widened the old level's
|
||||
@@ -1500,6 +1521,7 @@ int main(void) {
|
||||
check_large_old_buffer_with_nursery_candidates();
|
||||
check_unjudged_backlog_follows_growth();
|
||||
check_acyclic_objects();
|
||||
check_program_growth_cap();
|
||||
printf("cycle collection checks passed: threshold=%zu growth_cap=%zu\n",
|
||||
configured_nursery_threshold(), configured_growth_cap());
|
||||
return 0;
|
||||
|
||||
Reference in New Issue
Block a user