mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Shrink the publish log and keep it between publishes (#1104)
This commit is contained in:
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5704-publish-refused-restores-counts.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -28,14 +28,24 @@
|
||||
* whose emitted walker refuses) restores every logged count, so a publish
|
||||
* that throws leaves the graph unchanged, exactly like the Node.js
|
||||
* implementation, which validates before it freezes. */
|
||||
/* One logged mark, 16 bytes on 64-bit targets: the log is written once per
|
||||
* object, so its size is most of the walk's memory traffic. `obj` carries
|
||||
* SCR_PUB_DYN for a dyn container frozen on success. `rc` is the count
|
||||
* before the mark, with two escapes on 64-bit targets: SCR_PUB_RC_LIFETIME
|
||||
* for the program-lifetime count (2^63), SCR_PUB_RC_BIG for any other
|
||||
* count of 32 bits or more, kept in order in ScrPub.big. */
|
||||
typedef struct {
|
||||
void *obj;
|
||||
size_t rc;
|
||||
uintptr_t obj;
|
||||
uint32_t rc;
|
||||
uint32_t buf; /* a candidate taken out of its buffer: its index + 1, else 0 */
|
||||
bool dyn; /* a dyn container frozen on success */
|
||||
uint8_t kind; /* SCR_PUB_KIND_*: leaves the RC audit's live count on success */
|
||||
} ScrPubLog;
|
||||
|
||||
#define SCR_PUB_DYN ((uintptr_t)1)
|
||||
#define SCR_PUB_RC_LIFETIME UINT32_C(0xfffffffe)
|
||||
#define SCR_PUB_RC_BIG UINT32_C(0xffffffff)
|
||||
|
||||
static inline void *scr_pub_log_obj(const ScrPubLog *e) { return (void *)(e->obj & ~SCR_PUB_DYN); }
|
||||
|
||||
typedef struct {
|
||||
void *v;
|
||||
ScrPubFn fn;
|
||||
@@ -50,6 +60,12 @@ struct ScrPub {
|
||||
size_t len, cap;
|
||||
ScrPubLog *log;
|
||||
size_t logged, log_cap;
|
||||
size_t *big; /* counts SCR_PUB_RC_BIG stands for, in log order */
|
||||
size_t nbig, big_cap;
|
||||
#ifdef SCR_RC_AUDIT
|
||||
uint8_t *kinds; /* per logged mark, SCR_PUB_KIND_*: leaves the audit's live count */
|
||||
size_t kinds_cap;
|
||||
#endif
|
||||
size_t taken; /* logged candidates taken out of their buffers */
|
||||
size_t dyns; /* logged dyn containers */
|
||||
const char *refusal; /* first refusal message; the walk stops */
|
||||
@@ -96,12 +112,31 @@ bool scr_pub_mark(ScrPub *ctx, void *obj, bool cyc, int kind) {
|
||||
if (*rc == SIZE_MAX || ctx->refusal) return false;
|
||||
if (ctx->logged == ctx->log_cap)
|
||||
ctx->log = scr_pub_grow(ctx->log, &ctx->log_cap, sizeof *ctx->log, ctx->log_inline);
|
||||
#ifdef SCR_RC_AUDIT
|
||||
if (ctx->logged == ctx->kinds_cap) ctx->kinds = scr_pub_grow(ctx->kinds, &ctx->kinds_cap, 1, NULL);
|
||||
ctx->kinds[ctx->logged] = (uint8_t)kind;
|
||||
#else
|
||||
(void)kind;
|
||||
#endif
|
||||
ScrPubLog *e = &ctx->log[ctx->logged++];
|
||||
*e = (ScrPubLog){obj, *rc, 0, false, (uint8_t)kind};
|
||||
uint32_t buf = 0;
|
||||
if (cyc && scr_cyc_is_buffered(scr_cyc_hdr(obj))) {
|
||||
e->buf = scr_cyc_pub_take(obj) + 1;
|
||||
buf = scr_cyc_pub_take(obj) + 1;
|
||||
ctx->taken++;
|
||||
}
|
||||
size_t count = *rc;
|
||||
uint32_t logged = (uint32_t)count;
|
||||
#if SIZE_MAX > UINT32_MAX
|
||||
if (count >= SCR_PUB_RC_LIFETIME) {
|
||||
if (count == (size_t)1 << 63) logged = SCR_PUB_RC_LIFETIME;
|
||||
else {
|
||||
if (ctx->nbig == ctx->big_cap) ctx->big = scr_pub_grow(ctx->big, &ctx->big_cap, sizeof *ctx->big, NULL);
|
||||
ctx->big[ctx->nbig++] = count;
|
||||
logged = SCR_PUB_RC_BIG;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
*e = (ScrPubLog){(uintptr_t)obj, logged, buf};
|
||||
*rc = SIZE_MAX;
|
||||
return true;
|
||||
}
|
||||
@@ -268,7 +303,7 @@ void scr_pub_dyn_fn(void *v, ScrPub *ctx) {
|
||||
else {
|
||||
ScrDyn *d = p;
|
||||
if (scr_pub_mark(ctx, d, true, SCR_PUB_KIND_DYN) && (d->kind == SCR_DYN_ARR || d->kind == SCR_DYN_OBJ)) {
|
||||
ctx->log[ctx->logged - 1].dyn = true;
|
||||
ctx->log[ctx->logged - 1].obj |= SCR_PUB_DYN;
|
||||
ctx->dyns++;
|
||||
}
|
||||
}
|
||||
@@ -294,15 +329,64 @@ static void scr_pub_audit_forget(int kind) {
|
||||
/* SCRIPTC_PUBLISH_TRACE, read once: -1 until then. */
|
||||
static _Atomic int scr_pub_trace = -1;
|
||||
|
||||
/* The stack and log of this thread's last publish, kept while they hold at
|
||||
* most SCR_PUB_KEEP bytes: a thread that publishes many graphs (a file per
|
||||
* parse) reuses them instead of allocating and faulting in a fresh log each
|
||||
* time. Freed at thread (or process) exit. */
|
||||
#define SCR_PUB_KEEP ((size_t)16 << 20)
|
||||
typedef struct {
|
||||
ScrPubItem *stack;
|
||||
size_t cap;
|
||||
ScrPubLog *log;
|
||||
size_t log_cap;
|
||||
bool registered;
|
||||
} ScrPubKeep;
|
||||
static SCR_TL ScrPubKeep scr_pub_keep;
|
||||
|
||||
static void scr_pub_keep_free(void) {
|
||||
free(scr_pub_keep.stack);
|
||||
free(scr_pub_keep.log);
|
||||
scr_pub_keep = (ScrPubKeep){0};
|
||||
scr_pub_keep.registered = true; /* the cleanup ran; later publishes free their buffers */
|
||||
}
|
||||
|
||||
/* Keeps a heap buffer of `bytes` for the next publish, or frees it. */
|
||||
static bool scr_pub_keep_ok(size_t bytes) {
|
||||
if (bytes > SCR_PUB_KEEP) return false;
|
||||
if (!scr_pub_keep.registered) {
|
||||
scr_pub_keep.registered = true;
|
||||
scr_atexit(scr_pub_keep_free);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void scr_publish(void *root, ScrPubFn fn) {
|
||||
if (!root || *(size_t *)root == SIZE_MAX) return;
|
||||
ScrPub ctx;
|
||||
ctx.stack = ctx.stack_inline;
|
||||
if (scr_pub_keep.stack) {
|
||||
ctx.stack = scr_pub_keep.stack;
|
||||
ctx.cap = scr_pub_keep.cap;
|
||||
scr_pub_keep.stack = NULL;
|
||||
} else {
|
||||
ctx.stack = ctx.stack_inline;
|
||||
ctx.cap = SCR_PUB_INLINE;
|
||||
}
|
||||
ctx.len = 0;
|
||||
ctx.cap = SCR_PUB_INLINE;
|
||||
ctx.log = ctx.log_inline;
|
||||
if (scr_pub_keep.log) {
|
||||
ctx.log = scr_pub_keep.log;
|
||||
ctx.log_cap = scr_pub_keep.log_cap;
|
||||
scr_pub_keep.log = NULL;
|
||||
} else {
|
||||
ctx.log = ctx.log_inline;
|
||||
ctx.log_cap = SCR_PUB_INLINE;
|
||||
}
|
||||
ctx.logged = 0;
|
||||
ctx.log_cap = SCR_PUB_INLINE;
|
||||
ctx.big = NULL;
|
||||
ctx.nbig = ctx.big_cap = 0;
|
||||
#ifdef SCR_RC_AUDIT
|
||||
ctx.kinds = NULL;
|
||||
ctx.kinds_cap = 0;
|
||||
#endif
|
||||
ctx.taken = 0;
|
||||
ctx.dyns = 0;
|
||||
ctx.refusal = NULL;
|
||||
@@ -320,8 +404,15 @@ void scr_publish(void *root, ScrPubFn fn) {
|
||||
}
|
||||
if (ctx.refusal) {
|
||||
for (size_t i = ctx.logged; i-- > 0;) {
|
||||
*(size_t *)ctx.log[i].obj = ctx.log[i].rc;
|
||||
if (ctx.log[i].buf) scr_cyc_pub_restore(ctx.log[i].obj, ctx.log[i].buf - 1);
|
||||
const ScrPubLog *e = &ctx.log[i];
|
||||
void *obj = scr_pub_log_obj(e);
|
||||
size_t rc = e->rc;
|
||||
#if SIZE_MAX > UINT32_MAX
|
||||
if (e->rc == SCR_PUB_RC_LIFETIME) rc = (size_t)1 << 63;
|
||||
else if (e->rc == SCR_PUB_RC_BIG) rc = ctx.big[--ctx.nbig];
|
||||
#endif
|
||||
*(size_t *)obj = rc;
|
||||
if (e->buf) scr_cyc_pub_restore(obj, e->buf - 1);
|
||||
}
|
||||
if (ctx.taken) scr_cyc_pub_fill();
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, ctx.refusal, strlen(ctx.refusal));
|
||||
@@ -337,11 +428,12 @@ void scr_publish(void *root, ScrPubFn fn) {
|
||||
size_t each = ctx.dyns ? ctx.logged : 0;
|
||||
#endif
|
||||
for (size_t i = 0; i < each; i++) {
|
||||
if (ctx.log[i].dyn) scr_dyn_release(scr_dyn_freeze(ctx.log[i].obj));
|
||||
void *obj = scr_pub_log_obj(&ctx.log[i]);
|
||||
if (ctx.log[i].obj & SCR_PUB_DYN) scr_dyn_release(scr_dyn_freeze(obj));
|
||||
#ifdef SCR_RC_AUDIT
|
||||
scr_pub_audit_forget(ctx.log[i].kind);
|
||||
scr_pub_audit_forget(ctx.kinds[i]);
|
||||
#endif
|
||||
scr_lsan_immortal(ctx.log[i].obj);
|
||||
scr_lsan_immortal(obj);
|
||||
}
|
||||
if (trace) {
|
||||
uint64_t t2 = scr_hrtime_ns();
|
||||
@@ -349,8 +441,22 @@ void scr_publish(void *root, ScrPubFn fn) {
|
||||
fprintf(stderr, "scriptc: published %zu objects (walk %.1fms, commit %.1fms)\n", ctx.logged, walk, post);
|
||||
}
|
||||
}
|
||||
if (ctx.stack != ctx.stack_inline) free(ctx.stack);
|
||||
if (ctx.log != ctx.log_inline) free(ctx.log);
|
||||
if (ctx.stack != ctx.stack_inline) {
|
||||
if (!scr_pub_keep.stack && scr_pub_keep_ok(ctx.cap * sizeof *ctx.stack)) {
|
||||
scr_pub_keep.stack = ctx.stack;
|
||||
scr_pub_keep.cap = ctx.cap;
|
||||
} else free(ctx.stack);
|
||||
}
|
||||
if (ctx.log != ctx.log_inline) {
|
||||
if (!scr_pub_keep.log && scr_pub_keep_ok(ctx.log_cap * sizeof *ctx.log)) {
|
||||
scr_pub_keep.log = ctx.log;
|
||||
scr_pub_keep.log_cap = ctx.log_cap;
|
||||
} else free(ctx.log);
|
||||
}
|
||||
free(ctx.big);
|
||||
#ifdef SCR_RC_AUDIT
|
||||
free(ctx.kinds);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Writes into published objects (the emitted guards' cold paths): Node's
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { publish } from "@scriptc/threads";
|
||||
import type { ProgramLifetime } from "@scriptc/types";
|
||||
|
||||
// publish() logs each object's count before marking it immortal, and a
|
||||
// refused publish restores every logged count. The graphs here mix ordinary
|
||||
// objects shared by several holders (counts above 1), program-lifetime
|
||||
// instances (whose count is a large constant) and strings, and they are
|
||||
// published repeatedly on one thread, refused ones in between: each graph
|
||||
// must stay writable after a refusal and behave normally afterwards.
|
||||
|
||||
class Sym implements ProgramLifetime {
|
||||
readonly name: string;
|
||||
readonly decls: Decl[] = [];
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
class Decl {
|
||||
readonly sym: Sym;
|
||||
readonly pos: number;
|
||||
note: string;
|
||||
hook: (() => string) | undefined = undefined;
|
||||
constructor(sym: Sym, pos: number) {
|
||||
this.sym = sym;
|
||||
this.pos = pos;
|
||||
this.note = `d${pos}`;
|
||||
sym.decls.push(this);
|
||||
}
|
||||
}
|
||||
|
||||
class Unit {
|
||||
readonly syms: Sym[] = [];
|
||||
readonly decls: Decl[] = [];
|
||||
readonly shared: Decl[] = [];
|
||||
}
|
||||
|
||||
function build(n: number, round: number): Unit {
|
||||
const unit = new Unit();
|
||||
for (let i = 0; i < n; i++) {
|
||||
const sym = new Sym(`s${round}_${i}`);
|
||||
unit.syms.push(sym);
|
||||
for (let k = 0; k < 3; k++) unit.decls.push(new Decl(sym, i * 3 + k));
|
||||
}
|
||||
// Decls held by two arrays: counts of 2 and more before the publish.
|
||||
for (let i = 0; i < unit.decls.length; i += 4) unit.shared.push(unit.decls[i]!);
|
||||
return unit;
|
||||
}
|
||||
|
||||
function attempt(what: string, run: () => void): void {
|
||||
try {
|
||||
run();
|
||||
console.log(`${what}: ok`);
|
||||
} catch (e) {
|
||||
console.log(`${what}: ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const kept: Unit[] = [];
|
||||
for (let round = 0; round < 6; round++) {
|
||||
const unit = build(50 * (round + 1), round);
|
||||
const refused = round % 2 === 0;
|
||||
if (refused) unit.decls[unit.decls.length - 1]!.hook = () => "hook";
|
||||
attempt(`round ${round} publish`, () => {
|
||||
publish(unit);
|
||||
});
|
||||
attempt(`round ${round} write`, () => {
|
||||
unit.decls[1]!.note = "changed";
|
||||
});
|
||||
if (refused) {
|
||||
// Still ordinary: drop the hook and publish again.
|
||||
unit.decls[unit.decls.length - 1]!.hook = undefined;
|
||||
unit.shared.push(unit.decls[2]!);
|
||||
attempt(`round ${round} republish`, () => {
|
||||
publish(unit);
|
||||
});
|
||||
attempt(`round ${round} write after`, () => {
|
||||
unit.decls[1]!.note = "again";
|
||||
});
|
||||
}
|
||||
kept.push(unit);
|
||||
let decls = 0;
|
||||
for (const sym of unit.syms) decls += sym.decls.length;
|
||||
console.log(round, unit.syms.length, decls, unit.shared.length, unit.decls[1]!.note, unit.syms[0]!.name);
|
||||
}
|
||||
let total = 0;
|
||||
for (const unit of kept) for (const d of unit.shared) total += d.pos + d.sym.decls.length;
|
||||
console.log("total", total);
|
||||
Reference in New Issue
Block a user