mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-29 07:04:09 +08:00
Leave the main thread's heap to the operating system at exit
When the main thread's script ends, the executable released every module global and ran a full cycle collection before exiting, which takes time proportional to everything the program still holds (about 15 ms for 1.5 million live objects) and frees memory the operating system reclaims anyway. The main thread now skips those releases, the exit-time collection and the collector's buffer cleanup. Exit listeners and output flushing run as before. Worker threads still free their heaps when they exit, since later threads reuse their arenas, and builds that check for leaks (the RC audit, AddressSanitizer) free everything as before, as does any process run with SCRIPTC_EXIT_FREE=1 for external leak checkers. SCRIPTC_WORKER_STATS=1 reports, as each worker thread exits, how long it took to start, to run its script and to exit.
This commit is contained in:
@@ -76,7 +76,7 @@ Program objects define `main` and leave selected `scr_*` runtime functions undef
|
||||
|
||||
## Native runtime
|
||||
|
||||
- **Memory:** Values use reference counting. Acyclic values are freed when their last reference is released, and a cycle collector runs at defined collection points. Some cycles involving opaque native values can remain allocated.
|
||||
- **Memory:** Values use reference counting. Acyclic values are freed when their last reference is released, and a cycle collector runs at defined collection points. Some cycles involving opaque native values can remain allocated. When the main thread's script ends, the process exits without freeing what the program still holds, and the operating system reclaims it; worker threads free their heaps when they exit. Builds with `--sanitize`, and processes run with `SCRIPTC_EXIT_FREE=1` (for external leak checkers), free everything at exit.
|
||||
- **Concurrency:** `async`/`await` uses stackful fibers. The native event loop schedules microtasks, timers, and I/O, using kqueue on macOS and epoll on Linux.
|
||||
- **Networking:** Supported `net`, `http`, `https`, `tls`, `dgram`, and `dns` APIs use native implementations. TLS uses vendored mbedTLS.
|
||||
- **Numbers:** Values use JavaScript's double-precision representation. Number-to-string formatting uses shortest-roundtrip output and is tested against Node.js.
|
||||
|
||||
@@ -232,6 +232,8 @@ Without `@scriptc/threads`, a scriptc executable also shares a message of untype
|
||||
|
||||
## Cost
|
||||
|
||||
Starting a worker thread takes tens of microseconds plus the worker's own module initialization, since every worker initializes the program's modules again, as in Node.js. A worker that exits frees its heap first, which takes time proportional to what it still holds. `SCRIPTC_WORKER_STATS=1` prints, as each worker exits, how long it took to start, to run its script and to exit.
|
||||
|
||||
`publish` visits each object of the graph once and marks it immortal, so publication takes time proportional to the graph. Published objects then cost nothing on later sends, and reference counting and the cycle collector skip them. Stores into the types a program publishes carry one extra load and branch.
|
||||
|
||||
Threads that add to one shared collection of published values while they run can use an [`InternTable`](/docs/experimental#interntable) from `@scriptc/experimental`.
|
||||
|
||||
@@ -1897,7 +1897,24 @@ export class LlEmitter {
|
||||
` call void @scr_run_exit_listeners(double %${prefix}hd)`,
|
||||
];
|
||||
};
|
||||
const globalReleases = globalReleaseLines("g");
|
||||
// The entry's exits release the module globals only where the thread
|
||||
// frees its heap when its script ends (scr_exit_frees: workers, and
|
||||
// builds that check for leaks); the main thread of other builds leaves
|
||||
// it to the operating system.
|
||||
const exitReleaseLines = (prefix: string): string[] => {
|
||||
const releases = globalReleaseLines(prefix);
|
||||
if (releases.length === 0) return releases;
|
||||
this.declare(`declare zeroext i1 @scr_exit_frees()`);
|
||||
return [
|
||||
` %${prefix}.frees = call zeroext i1 @scr_exit_frees()`,
|
||||
` br i1 %${prefix}.frees, label %${prefix}.release, label %${prefix}.kept`,
|
||||
`${prefix}.release:`,
|
||||
...releases,
|
||||
` br label %${prefix}.kept`,
|
||||
`${prefix}.kept:`,
|
||||
];
|
||||
};
|
||||
const globalReleases = exitReleaseLines("g");
|
||||
const asyncEntry = this.fnByName.get(this.mod.entry)?.async === true;
|
||||
const entryMayThrow = this.mayThrow.has(this.mod.entry);
|
||||
// The event loop runs when timers appeared OR any async/generator
|
||||
@@ -1910,12 +1927,12 @@ export class LlEmitter {
|
||||
this.usesTimers ||
|
||||
this.ffiHasForeignCallback ||
|
||||
this.mod.functions.some((f) => f.async === true || f.generator !== undefined);
|
||||
const uncaughtReleases = entryMayThrow && !asyncEntry ? globalReleaseLines("gu") : [];
|
||||
const loopReleasesU = runsLoop ? globalReleaseLines("gl") : [];
|
||||
const loopReleasesR = runsLoop ? globalReleaseLines("gr") : [];
|
||||
const topRejectReleases = asyncEntry ? globalReleaseLines("gt") : [];
|
||||
const topPendingReleases = asyncEntry ? globalReleaseLines("gp") : [];
|
||||
const loopReportedReleases = runsLoop ? globalReleaseLines("gq") : [];
|
||||
const uncaughtReleases = entryMayThrow && !asyncEntry ? exitReleaseLines("gu") : [];
|
||||
const loopReleasesU = runsLoop ? exitReleaseLines("gl") : [];
|
||||
const loopReleasesR = runsLoop ? exitReleaseLines("gr") : [];
|
||||
const topRejectReleases = asyncEntry ? exitReleaseLines("gt") : [];
|
||||
const topPendingReleases = asyncEntry ? exitReleaseLines("gp") : [];
|
||||
const loopReportedReleases = runsLoop ? exitReleaseLines("gq") : [];
|
||||
// main's epilogues read the flag / the loop entry points — declared
|
||||
// HERE, before the extern block flushes. They run once, so they keep
|
||||
// the out-of-line scr_exc_pending call; per-call checks inline the
|
||||
|
||||
@@ -35,6 +35,25 @@
|
||||
static SCR_TL long scr_abandoned_fibers = 0;
|
||||
void scr_note_abandoned_fibers(long n) { scr_abandoned_fibers = n; }
|
||||
|
||||
bool scr_exit_frees(void) {
|
||||
#if defined(SCR_RC_AUDIT) || defined(SCR_LSAN_IMMORTAL)
|
||||
return true;
|
||||
#else
|
||||
if (!scr_context_is_main()) {
|
||||
#ifdef SCR_WORKERS
|
||||
scr_context_note_script_end();
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
static int forced = -1; /* the main thread reads it, once */
|
||||
if (forced < 0) {
|
||||
const char *value = getenv("SCRIPTC_EXIT_FREE");
|
||||
forced = value != NULL && value[0] == '1';
|
||||
}
|
||||
return forced != 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifndef SCR_LIB
|
||||
#ifdef SCR_RC_AUDIT
|
||||
extern long scr_str_live_count(void); /* scr_string.c */
|
||||
@@ -91,7 +110,9 @@ static void scr_rc_audit_at_exit(void) {
|
||||
|
||||
static void scr_flush_at_exit(void) { fflush(stdout); }
|
||||
|
||||
static void scr_collect_cycles_at_exit(void) { scr_collect_cycles(); }
|
||||
static void scr_collect_cycles_at_exit(void) {
|
||||
if (scr_exit_frees()) scr_collect_cycles();
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
static UINT scr_original_console_output_cp = 0;
|
||||
|
||||
@@ -115,6 +115,13 @@ ScrArr *scr_context_env_pairs(void) {
|
||||
|
||||
void scr_context_enter(uint64_t thread_id) { scr_context_id = thread_id; }
|
||||
bool scr_context_is_main(void) { return scr_context_id == 0; }
|
||||
|
||||
/* When the thread's script ended: its first scr_exit_frees call. */
|
||||
static SCR_TL uint64_t scr_context_script_end;
|
||||
void scr_context_note_script_end(void) {
|
||||
if (scr_context_script_end == 0) scr_context_script_end = scr_hrtime_ns();
|
||||
}
|
||||
uint64_t scr_context_script_end_ns(void) { return scr_context_script_end; }
|
||||
uint64_t scr_context_thread_id(void) { return scr_context_id; }
|
||||
double scr_context_thread_number(void) { return (double)scr_context_id; }
|
||||
|
||||
@@ -181,7 +188,7 @@ void scr_context_cleanup(void) {
|
||||
fn();
|
||||
}
|
||||
scr_exc_clear();
|
||||
scr_cyc_context_cleanup();
|
||||
if (scr_exit_frees()) scr_cyc_context_cleanup();
|
||||
}
|
||||
#else
|
||||
bool scr_context_is_main(void) { return true; }
|
||||
|
||||
@@ -347,6 +347,21 @@ struct ScrArr *scr_context_env_pairs(void);
|
||||
/* Also used by ordinary event-loop builds, where this always returns true. */
|
||||
bool scr_context_is_main(void);
|
||||
|
||||
/* Whether the calling thread frees what the program still holds when its
|
||||
* script ends: module globals, the final cycle collection, collector
|
||||
* buffers. Worker threads do (a later thread reuses their arena), and so
|
||||
* does every thread of a build that checks for leaks (the RC audit,
|
||||
* AddressSanitizer) or of a process run with SCRIPTC_EXIT_FREE=1. The main
|
||||
* thread of other builds leaves its heap to the operating system, which
|
||||
* reclaims it at exit. Exit listeners and output flushing run either way. */
|
||||
bool scr_exit_frees(void);
|
||||
#ifdef SCR_WORKERS
|
||||
/* The time the calling thread's script ended (scr_hrtime_ns; 0 until then),
|
||||
* recorded by scr_exit_frees for SCRIPTC_WORKER_STATS. */
|
||||
void scr_context_note_script_end(void);
|
||||
uint64_t scr_context_script_end_ns(void);
|
||||
#endif
|
||||
|
||||
/* Native stack guard. Compiled functions that can recurse compare their
|
||||
* stack pointer against scr_stack_guard on entry and throw Node's catchable
|
||||
* RangeError below it. The guard sits a reserve above the lowest usable
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#ifdef SCR_WORKERS
|
||||
#include <stdatomic.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <limits.h>
|
||||
#ifdef _WIN32
|
||||
@@ -63,6 +64,7 @@ struct ScrWorkerThread {
|
||||
int argc;
|
||||
char **argv;
|
||||
bool joined;
|
||||
uint64_t spawned_ns; /* SCRIPTC_WORKER_STATS only */
|
||||
#ifdef _WIN32
|
||||
HANDLE thread;
|
||||
#else
|
||||
@@ -74,6 +76,21 @@ static atomic_uint_fast64_t scr_worker_next_id = 1;
|
||||
static SCR_TL ScrWorkerThread *scr_worker_current;
|
||||
static SCR_TL ScrDyn *scr_worker_local_data;
|
||||
|
||||
/* SCRIPTC_WORKER_STATS=1 reports, as each worker thread exits, how long it
|
||||
* took to start, to run its script (runtime and module initialization
|
||||
* included) and to exit (freeing its heap). -1 until read. */
|
||||
static _Atomic int scr_worker_stats_mode = -1;
|
||||
|
||||
static bool scr_worker_stats(void) {
|
||||
int mode = atomic_load_explicit(&scr_worker_stats_mode, memory_order_relaxed);
|
||||
if (mode < 0) {
|
||||
const char *value = getenv("SCRIPTC_WORKER_STATS");
|
||||
mode = value != NULL && value[0] == '1';
|
||||
atomic_store_explicit(&scr_worker_stats_mode, mode, memory_order_relaxed);
|
||||
}
|
||||
return mode != 0;
|
||||
}
|
||||
|
||||
static ScrMailEvent *scr_worker_event(unsigned kind, uint64_t source,
|
||||
int code, ScrMessage *message) {
|
||||
ScrMailEvent *event = calloc(1, sizeof(*event));
|
||||
@@ -197,6 +214,8 @@ static void scr_worker_report_error(void) {
|
||||
}
|
||||
|
||||
static void scr_worker_thread_run(ScrWorkerThread *worker) {
|
||||
bool stats = scr_worker_stats();
|
||||
uint64_t started_ns = stats ? scr_hrtime_ns() : 0;
|
||||
scr_context_enter(worker->id);
|
||||
#ifdef SCR_EXC_FLAG
|
||||
pthread_mutex_lock(&worker->flag_lock);
|
||||
@@ -213,8 +232,15 @@ static void scr_worker_thread_run(ScrWorkerThread *worker) {
|
||||
scr_context_report_error = scr_worker_report_error;
|
||||
scr_mailbox_post(worker->parent, scr_worker_event(SCR_WORKER_ONLINE, worker->id, 0, NULL));
|
||||
int code = worker->entry(worker->root);
|
||||
scr_context_note_script_end(); /* when the program frees nothing at exit */
|
||||
scr_mailbox_close(worker->inbox);
|
||||
scr_context_cleanup();
|
||||
if (stats) {
|
||||
uint64_t now = scr_hrtime_ns();
|
||||
fprintf(stderr, "scriptc worker %llu: started %.2f ms after spawn, script %.2f ms, exit %.2f ms\n",
|
||||
(unsigned long long)worker->id, (double)(started_ns - worker->spawned_ns) / 1e6,
|
||||
(double)(scr_context_script_end_ns() - started_ns) / 1e6, (double)(now - scr_context_script_end_ns()) / 1e6);
|
||||
}
|
||||
scr_worker_current = NULL;
|
||||
scr_exc_alert_thread_exit();
|
||||
#ifdef SCR_EXC_FLAG
|
||||
@@ -281,6 +307,7 @@ ScrWorkerThread *scr_worker_thread_start(ScrWorkerEntry entry, uint32_t root,
|
||||
}
|
||||
worker->inbox = scr_mailbox_new();
|
||||
if (!worker->inbox) goto failed;
|
||||
if (scr_worker_stats()) worker->spawned_ns = scr_hrtime_ns();
|
||||
#ifdef _WIN32
|
||||
worker->thread = (HANDLE)_beginthreadex(NULL, 0, scr_worker_thread_main, worker, 0, NULL);
|
||||
if (!worker->thread) goto failed;
|
||||
|
||||
Reference in New Issue
Block a user