fix(elc): add el_mem_check() memory guard — abort before OS OOM-kill
Add el_mem_check() to el_runtime.c: reads ELC_MAX_MEM_MB (default 512), checks RSS via getrusage (macOS bytes / Linux KB normalised to MB), prints a clear diagnostic to stderr and exits(1) if exceeded. Wire it into two places: - compiler.el: upfront check at --emit-header entry point - codegen.el: per-function check in the streaming loop after each el_arena_pop, so runaway growth is caught at the earliest function boundary rather than after the machine is already dying.
This commit is contained in:
@@ -42,6 +42,7 @@
|
|||||||
#include <dirent.h>
|
#include <dirent.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
|
#include <sys/resource.h> /* getrusage — memory guard */
|
||||||
#ifdef HAVE_CURL
|
#ifdef HAVE_CURL
|
||||||
#include <curl/curl.h>
|
#include <curl/curl.h>
|
||||||
#endif
|
#endif
|
||||||
@@ -5674,6 +5675,50 @@ el_val_t getpid_now(void) {
|
|||||||
return (el_val_t)getpid();
|
return (el_val_t)getpid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* el_mem_check — self-terminating memory guard for long-running compiler runs.
|
||||||
|
*
|
||||||
|
* Call this periodically (e.g. after each function compiled) to detect runaway
|
||||||
|
* memory growth before the OS OOM-killer fires. Reads the limit from the env
|
||||||
|
* var ELC_MAX_MEM_MB (default 512 MB). If resident set size exceeds the limit,
|
||||||
|
* prints a diagnostic to stderr and exits with code 1 so the caller (elb or a
|
||||||
|
* CI script) can handle the failure gracefully instead of having the whole
|
||||||
|
* machine go down.
|
||||||
|
*
|
||||||
|
* Platform notes:
|
||||||
|
* macOS — ru_maxrss is in bytes.
|
||||||
|
* Linux — ru_maxrss is in kilobytes.
|
||||||
|
* We normalise to MB before comparing.
|
||||||
|
*
|
||||||
|
* Returns 0 always (the only non-return path is the exit() branch).
|
||||||
|
*/
|
||||||
|
el_val_t el_mem_check(void) {
|
||||||
|
/* Read limit from env; default 512 MB. */
|
||||||
|
long limit_mb = 512;
|
||||||
|
const char *env_val = getenv("ELC_MAX_MEM_MB");
|
||||||
|
if (env_val && *env_val) {
|
||||||
|
long v = atol(env_val);
|
||||||
|
if (v > 0) limit_mb = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct rusage ru;
|
||||||
|
if (getrusage(RUSAGE_SELF, &ru) != 0) return 0; /* can't read — skip check */
|
||||||
|
|
||||||
|
long rss_mb;
|
||||||
|
#if defined(__APPLE__) || defined(__MACH__)
|
||||||
|
/* macOS: ru_maxrss is bytes */
|
||||||
|
rss_mb = (long)(ru.ru_maxrss / (1024L * 1024L));
|
||||||
|
#else
|
||||||
|
/* Linux: ru_maxrss is kilobytes */
|
||||||
|
rss_mb = (long)(ru.ru_maxrss / 1024L);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (rss_mb >= limit_mb) {
|
||||||
|
fprintf(stderr, "elc: memory limit exceeded (%ldMB), aborting\n", limit_mb);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── args() — command-line argument access ──────────────────────────────────
|
/* ── args() — command-line argument access ──────────────────────────────────
|
||||||
* Compiled El programs call args() to get a list of CLI arguments.
|
* Compiled El programs call args() to get a list of CLI arguments.
|
||||||
* Call el_runtime_init_args(argc, argv) at the start of C main() to populate.
|
* Call el_runtime_init_args(argc, argv) at the start of C main() to populate.
|
||||||
|
|||||||
@@ -531,6 +531,12 @@ el_val_t parse_int(el_val_t s, el_val_t default_val);
|
|||||||
el_val_t exit_program(el_val_t code);
|
el_val_t exit_program(el_val_t code);
|
||||||
el_val_t getpid_now(void);
|
el_val_t getpid_now(void);
|
||||||
|
|
||||||
|
/* Self-terminating memory guard. Reads ELC_MAX_MEM_MB (default 512) and
|
||||||
|
* exits with code 1 if resident memory exceeds the limit. Call periodically
|
||||||
|
* during long compilation loops (e.g. after each function is compiled).
|
||||||
|
* Returns 0 when memory is within bounds. */
|
||||||
|
el_val_t el_mem_check(void);
|
||||||
|
|
||||||
/* ── CGI identity ─────────────────────────────────────────────────────────────
|
/* ── CGI identity ─────────────────────────────────────────────────────────────
|
||||||
* Called at the start of main() in CGI programs (those with a `cgi {}` block).
|
* Called at the start of main() in CGI programs (those with a `cgi {}` block).
|
||||||
* Records the program's DHARMA identity before any other code executes. */
|
* Records the program's DHARMA identity before any other code executes. */
|
||||||
|
|||||||
@@ -3730,6 +3730,7 @@ fn codegen_streaming(tokens: [Any], sigs: [Map<String, Any>], source: String) ->
|
|||||||
cg_fn(stmt)
|
cg_fn(stmt)
|
||||||
el_release(stmt)
|
el_release(stmt)
|
||||||
el_arena_pop(fn_arena_mark)
|
el_arena_pop(fn_arena_mark)
|
||||||
|
el_mem_check()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if is_top_level_decl(stmt) {
|
if is_top_level_decl(stmt) {
|
||||||
|
|||||||
@@ -571,6 +571,7 @@ fn main() -> Void {
|
|||||||
// BinOp chains (e.g. checkout.el) — parse() builds O(whole-program AST)
|
// BinOp chains (e.g. checkout.el) — parse() builds O(whole-program AST)
|
||||||
// while scan_fn_sigs_el keeps peak memory at O(tokens).
|
// while scan_fn_sigs_el keeps peak memory at O(tokens).
|
||||||
if do_emit_header {
|
if do_emit_header {
|
||||||
|
el_mem_check()
|
||||||
let raw_source: String = fs_read(src_path)
|
let raw_source: String = fs_read(src_path)
|
||||||
let hdr_tokens: [Any] = lex(raw_source)
|
let hdr_tokens: [Any] = lex(raw_source)
|
||||||
let hdr_sigs: [Map<String, Any>] = scan_fn_sigs_el(hdr_tokens)
|
let hdr_sigs: [Map<String, Any>] = scan_fn_sigs_el(hdr_tokens)
|
||||||
|
|||||||
Reference in New Issue
Block a user