Cache untyped property access in programs that declare classes (reland) (#2017)

This commit is contained in:
Chris Tate
2026-10-23 14:47:10 -05:00
committed by GitHub
15 changed files with 521 additions and 31 deletions
+1
View File
@@ -13,6 +13,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
| `cyclic-graphs` | cpu | object graphs with back references built and dropped: trees with parent and sibling links, rings of records sharing a long-lived registry, closures capturing their owner |
| `dictionary-walks` | cpu | `Record<string, number>` dictionaries walked with for-in and `Object.keys`/`values`/`entries` while entries are added, updated and deleted |
| `dynamic-methods` | cpu | untyped JavaScript calling builtin methods on values it does not type: string `slice`/`indexOf`/`toUpperCase`/`padStart`, array `push`/`pop`/`includes`, `Map` and `Set`, and an object's own methods |
| `dynamic-properties` | cpu | untyped JavaScript reading and writing properties of values it does not type: records built by object literals in two key orders, nested records, and instances of a class and its subclass |
| `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records |
| `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables |
| `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation |
@@ -0,0 +1,6 @@
{
"entry": "workloads/dynamic-properties.mjs",
"args": [
"1000000"
]
}
@@ -0,0 +1,56 @@
// Untyped JavaScript reading and writing properties of values it never
// declares types for: records built by object literals in two key orders,
// nested records, property updates, and instances of a class and its
// subclass read and written the same way. Argument: the number of
// iterations.
const rounds = Number(process.argv[2] ?? "1000000");
class Cursor {
constructor(x, y) {
this.x = x;
this.y = y;
}
step(dx, dy) {
this.x += dx;
this.y += dy;
return this;
}
}
class Marker extends Cursor {
constructor(x, y, label) {
super(x, y);
this.label = label;
}
}
function makeItem(i) {
if (i % 3 === 0)
return { id: i, price: i % 17, qty: i % 5, meta: { tag: i % 7, weight: i % 11 } };
return { id: i, qty: i % 5, price: i % 13, meta: { weight: i % 9, tag: i % 5 } };
}
function cost(item) {
return item.price * item.qty + item.meta.weight;
}
function restock(item, round) {
item.qty = (item.qty + round) % 7;
item.meta.tag = (item.meta.tag + 1) % 7;
}
function distance(a, b) {
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
}
const items = [];
for (let i = 0; i < 1024; i++) items.push(makeItem(i));
const cursors = [];
for (let i = 0; i < 64; i++)
cursors.push(i % 2 === 0 ? new Cursor(i - 32, 7 - i) : new Marker(i - 32, 7 - i, "m" + i));
const origin = { x: 0, y: 0 };
let checksum = 0;
for (let r = 0; r < rounds; r++) {
const item = items[r & 1023];
checksum += cost(item);
restock(item, r & 3);
checksum += item.meta.tag;
const cursor = cursors[r & 63];
checksum += distance(cursor.step((r % 3) - 1, (r % 5) - 2), origin);
if (cursor.x > 100 || cursor.x < -100) cursor.x = 0;
if (cursor.y > 100 || cursor.y < -100) cursor.y = 0;
}
console.log(checksum, items[5].qty, items[6].meta.tag, cursors[3].x, cursors[3].y, cursors[3].label);
+188
View File
@@ -126,6 +126,140 @@ test("property helpers whose class branches only test capsules are cache sites",
expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "set", key: "y" });
});
test("bags converted between dyn and jsval are still the receiver", () => {
// A program that declares a class returns its bags as jsvals, and the
// helpers read them through js.toDyn.
const toDyn = (value: IrExpr): IrExpr => ({
kind: "libCall",
fn: "js.toDyn",
args: [value],
type: DYN,
loc,
});
const fromDyn = (value: IrExpr): IrExpr => ({
kind: "libCall",
fn: "js.fromDyn",
args: [value],
type: { kind: "jsval" },
loc,
});
const jsvalBag = helper(
"%dyn.class.readPropertiesOrEmpty.js",
["v"],
[capsuleBranch("v"), ret(fromDyn(ref("v")))],
);
const staticBag = helper("%dyn.class.staticProperties", ["v"], [ret(ref("v"))]);
const getter = getHelper(
"%dyn.class.property.0",
"x",
[capsuleBranch("p.0")],
call(staticBag.name, toDyn(call(jsvalBag.name, ref("p.0")))),
);
const setter = setHelper("%dyn.class.property.1", "y");
const write = setter.body[0] as Extract<IrStmt, { kind: "exprStmt" }>;
const keySet = write.expr as Extract<IrExpr, { kind: "libCall" }>;
keySet.args[0] = toDyn(call(jsvalBag.name, ref("p.0")));
// A converted value that is not the receiver stays a helper call.
const foreign = getHelper(
"%dyn.class.property.2",
"x",
[],
toDyn(fromDyn({ kind: "dynObjLit", type: DYN, loc } as IrExpr)),
);
const sites = sitesOf(jsvalBag, staticBag, getter, setter, foreign);
expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "get", key: "x" });
expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "set", key: "y" });
expect(sites.site("%dyn.class.property.2")).toBeNull();
});
test("a bag helper may first rebind its parameter to a bag of it", () => {
// Programs with subclasses normalize the value before the class branches.
const normalize = helper("%dyn.class.normalize", ["v"], [capsuleBranch("v"), ret(ref("v"))]);
const rebind = (value: IrExpr): IrStmt => ({ kind: "assign", localId: "v", value, loc });
const normalized = helper(
"%dyn.class.readPropertiesOrEmpty.normalized",
["v"],
[rebind(call(normalize.name, ref("v"))), capsuleBranch("v"), ret(ref("v"))],
);
const replaced = helper(
"%dyn.class.readPropertiesOrEmpty.replaced",
["v"],
[rebind({ kind: "dynObjLit", type: DYN, loc } as IrExpr), ret(ref("v"))],
);
const sites = sitesOf(
normalize,
normalized,
replaced,
getHelper("%dyn.class.property.0", "x", [], call(normalized.name, ref("p.0"))),
getHelper("%dyn.class.property.1", "x", [], call(replaced.name, ref("p.0"))),
// The property helper itself normalizes its receiver first, too.
getHelper("%dyn.class.property.2", "x", [
{ kind: "assign", localId: "p.0", value: call(normalize.name, ref("p.0")), loc },
capsuleBranch("p.0"),
]),
);
expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "get", key: "x" });
expect(sites.site("%dyn.class.property.1")).toBeNull();
expect(sites.site("%dyn.class.property.2")).toEqual({ kind: "get", key: "x" });
});
test("keyed helpers may test capsules between the key's coercion and the read", () => {
const read = (object: IrExpr, key: string): IrExpr => ({
kind: "libCall",
fn: "js.fromDyn",
args: [{ kind: "dynKeyGet", value: object, key: ref(key), type: DYN, loc }],
type: { kind: "jsval" },
loc,
});
const guard = (test: "symbol" | "nullish", tested: string, object: IrExpr): IrStmt => ({
kind: "if",
cond: { kind: "dynTest", test, value: ref(tested), type: BOOL, loc },
then: [ret(read(object, "p.key"))],
else_: null,
loc,
});
const keyed = (name: string, between: IrStmt[]): IrFunction =>
helper(
name,
["p.0", "p.key"],
[
capsuleBranch("p.0"),
guard("symbol", "p.key", call(identityBag.name, ref("p.0"))),
guard("nullish", "p.0", ref("p.0")),
{
kind: "varDecl",
localId: "key.string",
init: {
kind: "libCall",
fn: "dyn.toStringCoerce",
args: [ref("p.key")],
type: STRING,
loc,
},
loc,
},
...between,
ret(read(call(identityBag.name, ref("p.0")), "key.string")),
],
["key.string"],
);
const errorBranch: IrStmt = {
kind: "if",
cond: { kind: "dynTest", test: "error", value: ref("p.0"), type: BOOL, loc },
then: [ret(ref("p.0"))],
else_: null,
loc,
};
const sites = sitesOf(
keyed("%dyn.class.computed.0", []),
keyed("%dyn.class.computed.1", [capsuleBranch("p.0"), capsuleBranch("p.0", "dyn.classIs")]),
keyed("%dyn.class.computed.2", [errorBranch]),
);
expect(sites.site("%dyn.class.computed.0")).toEqual({ kind: "keyed" });
expect(sites.site("%dyn.class.computed.1")).toEqual({ kind: "keyed" });
expect(sites.site("%dyn.class.computed.2")).toBeNull();
});
test("any other prefix or tail keeps the helper call", () => {
const errorBranch: IrStmt = {
kind: "if",
@@ -237,3 +371,57 @@ test("bag helpers that exit early for values no class branch matches are still t
expect(sites.site("%dyn.class.property.2")).toBeNull();
expect(sites.site("%dyn.class.property.3")).toBeNull();
});
test("helpers that answer plain values first with the same operation are cache sites", () => {
const negated = (test: "classCapsule" | "funcKind"): IrExpr => ({
kind: "dynTest",
test,
negated: true,
value: ref("p.0"),
type: BOOL,
loc,
});
const exit = (body: IrStmt[], test: IrExpr = negated("classCapsule")): IrStmt => ({
kind: "if",
cond: test,
then: [{ kind: "if", cond: negated("funcKind"), then: body, else_: null, loc }],
else_: null,
loc,
});
const store = (key: string): IrStmt[] => [
{
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "dyn.keySet",
args: [ref("p.0"), str(key), ref("p.1")],
type: VOID,
loc,
},
loc,
},
ret(null),
];
const load = (key: string): IrStmt[] => [
ret({ kind: "dynKeyGet", value: ref("p.0"), key: str(key), type: DYN, loc }),
];
const sites = sitesOf(
setHelper("%dyn.class.property.0", "x", [exit(store("x")), capsuleBranch("p.0")]),
getHelper("%dyn.class.property.1", "x", [exit(load("x")), capsuleBranch("p.0")]),
// An exit for another key, or one plain values do not take.
setHelper("%dyn.class.property.2", "x", [exit(store("y"))]),
setHelper("%dyn.class.property.3", "x", [
exit(store("x"), {
kind: "dynTest",
test: "classCapsule",
value: ref("p.0"),
type: BOOL,
loc,
}),
]),
);
expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "set", key: "x" });
expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "get", key: "x" });
expect(sites.site("%dyn.class.property.2")).toBeNull();
expect(sites.site("%dyn.class.property.3")).toBeNull();
});
+82 -10
View File
@@ -61,13 +61,68 @@ export class JsIcSites {
if (fn.params.some((p) => p.type.kind !== "dyn")) return null;
const receiver = fn.params[0]?.localId;
if (receiver === undefined) return null;
const second = fn.params[1]?.localId;
const exits: JsIcSite[] = [];
let i = 0;
while (i < fn.body.length && this.capsuleBranch(fn.body[i]!, receiver)) i++;
for (; i < fn.body.length; i++) {
const s = fn.body[i]!;
if (this.capsuleBranch(s, receiver) || this.rebinding(s, receiver)) continue;
const exit = this.plainExit(s, receiver, second);
if (exit === null) break;
exits.push(exit);
}
const tail = fn.body.slice(i);
if (fn.params.length === 1) return this.getTail(tail, receiver);
if (fn.params.length !== 2) return null;
const second = fn.params[1]!.localId;
return this.setTail(tail, receiver, second) ?? this.keyedTail(tail, receiver, second);
let site: JsIcSite | null;
if (fn.params.length === 1) site = this.getTail(tail, receiver);
else if (fn.params.length !== 2 || second === undefined) return null;
else site = this.setTail(tail, receiver, second) ?? this.keyedTail(tail, receiver, second);
if (site === null || site.kind === "keyed") return exits.length === 0 ? site : null;
// An exit answers plain values with the operation the site caches.
return exits.every((exit) => exit.kind === site.kind && exit.key === site.key) ? site : null;
}
/** `if (!classCapsule(receiver)) { [if (!funcKind(receiver))] OP }`, no
* else: an exit that runs the helper's generic operation directly on a
* value no class table matches. OP is `return [js.fromDyn](dynKeyGet(R,
* K))` or `dyn.keySet(R, K, value); return`, with R the receiver (or a
* bag of it). */
private plainExit(s: IrStmt, receiver: string, value: string | undefined): JsIcSite | null {
const negated = (e: IrExpr, test: "classCapsule" | "funcKind"): boolean =>
e.kind === "dynTest" && e.test === test && e.negated === true && isVar(e.value, receiver);
if (s.kind !== "if" || s.else_ !== null || !negated(s.cond, "classCapsule")) return null;
let body = s.then;
const inner = body[0];
if (
body.length === 1 &&
inner?.kind === "if" &&
inner.else_ === null &&
negated(inner.cond, "funcKind")
)
body = inner.then;
const [first, ret] = body;
if (body.length === 1 && first?.kind === "return") {
const read = unwrapFromDyn(first.value);
if (
read?.kind !== "dynKeyGet" ||
read.key.kind !== "strLit" ||
!this.bagOf(read.value, receiver)
)
return null;
return { kind: "get", key: read.key.value };
}
if (
body.length !== 2 ||
first?.kind !== "exprStmt" ||
ret?.kind !== "return" ||
ret.value !== null
)
return null;
const call = first.expr;
if (call.kind !== "libCall" || call.fn !== "dyn.keySet" || call.args.length !== 3) return null;
const [bag, key, stored] = call.args;
if (!bag || !this.bagOf(bag, receiver) || key?.kind !== "strLit") return null;
if (value === undefined || !isVar(stored, value)) return null;
return { kind: "set", key: key.value };
}
/** `varDecl bag = BAG(receiver); return [js.fromDyn](bag === receiver ?
@@ -109,10 +164,14 @@ export class JsIcSites {
/** `if (typeof key === "symbol") return dynKeyGet(BAG(receiver), key);
* if (receiver == null) return dynKeyGet(receiver, key);
* const s = String(key); return dynKeyGet(BAG(receiver), s)`, each read
* possibly under js.fromDyn. */
* possibly under js.fromDyn. Capsule branches may precede the final read:
* a program that declares classes looks the string key up in each class's
* key table there. */
private keyedTail(tail: IrStmt[], receiver: string, key: string): JsIcSite | null {
const [symbol, nullish, decl, ret] = tail;
if (tail.length !== 4) return null;
const [symbol, nullish, decl] = tail;
const ret = tail[tail.length - 1];
if (tail.length < 4) return null;
if (!tail.slice(3, -1).every((s) => this.capsuleBranch(s, receiver))) return null;
if (!this.guard(symbol, "symbol", key, (read) => this.bagOf(read, receiver), key)) return null;
if (!this.guard(nullish, "nullish", receiver, (read) => isVar(read, receiver), key))
return null;
@@ -159,9 +218,12 @@ export class JsIcSites {
}
/** Whether `e` is the receiver itself for every ordinary object or array:
* the receiver, or a chain of property-bag helpers applied to it. */
* the receiver, or a chain of property-bag helpers applied to it. A
* conversion between the dyn and jsval forms keeps the object it holds. */
private bagOf(e: IrExpr, receiver: string): boolean {
if (isVar(e, receiver)) return true;
if (e.kind === "libCall" && (e.fn === "js.toDyn" || e.fn === "js.fromDyn"))
return e.args.length === 1 && this.bagOf(e.args[0]!, receiver);
return (
e.kind === "call" &&
e.args.length === 1 &&
@@ -182,6 +244,13 @@ export class JsIcSites {
return ret.kind === "return" && ret.value !== null && this.bagOf(ret.value, param);
}
/** `param = <bag of param>`, such as the subclass normalization
* `value = %dyn.class.normalize(value)`, which leaves an ordinary object
* or array in `param`. */
private rebinding(s: IrStmt, param: string): boolean {
return s.kind === "assign" && s.localId === param && this.bagOf(s.value, param);
}
/** A property-bag helper that returns its argument unless a capsule
* branch is taken. */
private identityHelper(name: string): boolean {
@@ -204,7 +273,10 @@ export class JsIcSites {
identity =
fn.body
.slice(0, -1)
.every((s) => this.capsuleBranch(s, param) || this.kindExit(s, param)) &&
.every(
(s) =>
this.capsuleBranch(s, param) || this.kindExit(s, param) || this.rebinding(s, param),
) &&
last?.kind === "return" &&
last.value !== null &&
this.bagOf(last.value, param);
@@ -64,6 +64,50 @@ console.log(read(o), at(dyn([5, 6]), 1), call(o), o.n);
}
});
test("property sites keep their caches in programs that declare classes and subclasses", async () => {
const dir = await mkdtemp(join(tmpdir(), "scriptc-js-ic-class-"));
try {
const entry = join(dir, "main.js");
const outPath = join(dir, "main.ir.json");
// Declared classes give every property helper class branches and
// property-bag lookups, and a subclass adds a normalization step; plain
// objects still read through the caches.
await writeFile(
entry,
`
class Point {
constructor(x) { this.x = x; }
}
class Tagged extends Point {
constructor(x, tag) { super(x); this.tag = tag; }
}
function dyn(v) { return v; }
function read(o) { return o.x; }
function write(o, v) { o.x = v; }
function at(a, i) { return a[i]; }
var o = dyn({ x: 1 });
var p = dyn(new Point(4));
var t = dyn(new Tagged(6, "t"));
write(o, 3);
write(p, 5);
write(t, 7);
console.log(read(o), read(p), read(t), at(dyn([5, 6]), 1), at(p, "x"), at(t, "tag"));
`,
);
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
if (!result.ok)
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
const mod = deserializeModule(await readFile(outPath, "utf8"));
expect(validateModule(mod)).toEqual([]);
const llvm = emitLlvmModule(mod);
expect(body(llvm, "read")).toContain("@scr_js_load_probe(");
expect(body(llvm, "write")).toContain("@scr_js_store_probe(");
expect(body(llvm, "at")).toContain("@scr_js_keyed_probe(");
} finally {
await rm(dir, { recursive: true, force: true });
}
});
test("typed programs have no cache sites and keep the ordinary runtime", async () => {
const dir = await mkdtemp(join(tmpdir(), "scriptc-js-ic-typed-"));
try {
+7 -4
View File
@@ -195,10 +195,13 @@ bool scr_js_truthy(ScrJsVal value);
* index and which keys an object lacks.
*
* Each property site of compiled dynamic-tier code owns one zeroed,
* thread-local ScrJsIc. The code at the site is fixed: it tests entry 0
* inline and calls a probe for everything else. Probes never run user
* code: they answer from plain data properties or return a miss (NULL /
* false) with nothing done, and the site then runs its generic path.
* thread-local ScrJsIc with static storage: the reset that frees a
* thread's shapes (at thread exit, and when a library session ends) clears
* every record that holds entries. The code at the site is fixed: it tests
* entry 0 inline and calls a probe for everything else. Probes never run
* user code: they answer from plain data properties or return a miss
* (NULL / false) with nothing done, and the site then runs its generic
* path.
*
* load entry: shape = receiver shape; target = NULL for an own property
* (slot indexes the receiver's entries), else the shape of
+28 -4
View File
@@ -18,6 +18,7 @@
#include "scr_key.h"
#include <stdlib.h>
#include <string.h>
/* The compiler's inline cache tests read these in place (64-bit targets;
* backend/llvm/js-ic.ts). */
@@ -37,10 +38,10 @@ _Static_assert(sizeof(ScrJsIcEntry) == 40 && sizeof(ScrJsIc) == 168, "cache layo
* take the generic path and its hash index. */
#define SCR_JS_SHAPE_MAX 64
/* Shapes live until the thread exits, so a thread creates at most this
* many, holding at most this many key bytes. Once either budget is spent,
* objects whose key sequence has no shape yet stay unshaped; shapes that
* exist keep serving. */
/* Shapes live until the thread exits or a library session ends, so a thread
* creates at most this many, holding at most this many key bytes. Once
* either budget is spent, objects whose key sequence has no shape yet stay
* unshaped; shapes that exist keep serving. */
#define SCR_JS_SHAPE_BUDGET ((size_t)1 << 16)
#define SCR_JS_SHAPE_KEY_BYTES_BUDGET ((size_t)4 << 20)
@@ -63,12 +64,25 @@ static SCR_TL size_t scr_js_transition_count;
static SCR_TL size_t scr_js_shape_key_bytes;
static SCR_TL bool scr_js_shapes_registered;
/* The cache records that hold entries. Entries name shapes and prototype
* objects, so the reset that frees the shapes also clears these records: a
* library's next session runs the same sites with the same records. */
static SCR_TL ScrJsIc **scr_js_filled_ics;
static SCR_TL size_t scr_js_filled_count;
static SCR_TL size_t scr_js_filled_capacity;
/* The shape word of an object whose key sequence has no shape within the
* budgets. No cache entry names it, so its accesses take the probes,
* which answer with a miss until the key set changes. */
static ScrJsShape scr_js_unshaped;
static void scr_js_shapes_reset(void) {
for (size_t i = 0; i < scr_js_filled_count; i++)
memset(scr_js_filled_ics[i], 0, sizeof *scr_js_filled_ics[i]);
free(scr_js_filled_ics);
scr_js_filled_ics = NULL;
scr_js_filled_count = 0;
scr_js_filled_capacity = 0;
while (scr_js_all_shapes) {
ScrJsShape *shape = scr_js_all_shapes;
scr_js_all_shapes = shape->all_next;
@@ -192,6 +206,16 @@ static size_t scr_js_shape_find(const ScrJsShape *shape, const char *key, size_t
/* Fills a cache entry: `stale` (a matching entry whose hit failed) when
* there is one, else a free or replaceable one. */
static void scr_js_ic_add(ScrJsIc *ic, uint32_t stale, ScrJsIcEntry entry) {
if (ic->count == 0) {
if (scr_js_filled_count == scr_js_filled_capacity) {
size_t capacity = scr_js_filled_capacity ? scr_js_filled_capacity * 2 : 64;
ScrJsIc **grown = realloc(scr_js_filled_ics, capacity * sizeof *grown);
if (!grown) scr_trap("scriptc: out of memory\n");
scr_js_filled_ics = grown;
scr_js_filled_capacity = capacity;
}
scr_js_filled_ics[scr_js_filled_count++] = ic;
}
uint32_t index;
if (stale < ic->count) {
index = stale;
+15 -11
View File
@@ -24,11 +24,17 @@ static ScrDyn *object_with(const ScrStr *key, double n) {
return object;
}
/* Cache records have static storage, as the compiled sites' do: the reset
* at exit clears every record that holds entries. Each check starts from
* an empty record. */
static ScrJsIc load_ic;
static ScrJsIc store_ic;
/* Whether a load probe answers `object.key` and returns `n`. */
static bool probe_reads(ScrDyn *object, const ScrStr *key, double n) {
ScrJsIc ic;
memset(&ic, 0, sizeof ic);
ScrDyn *value = scr_js_load_probe(object, key, &ic);
ScrJsIc *ic = &load_ic;
memset(ic, 0, sizeof *ic);
ScrDyn *value = scr_js_load_probe(object, key, ic);
if (!value) return false;
assert(value->kind == SCR_DYN_NUM && value->v.num == n);
scr_dyn_release(value);
@@ -66,14 +72,13 @@ static void budget_bounds_shapes(size_t key_len, size_t budget) {
/* A store probe that would add a new shape leaves the add to the generic
* store; one that replaces through an existing shape still runs. */
ScrJsIc ic;
memset(&ic, 0, sizeof ic);
memset(&store_ic, 0, sizeof store_ic);
ScrStr *added = scr_str_new("added", 5);
ScrDyn *value = scr_dyn_new_num(8);
assert(!scr_js_store_probe(object, added, value, &ic));
assert(!scr_js_store_probe(object, added, value, &store_ic));
assert(object->v.obj.len == 1);
memset(&ic, 0, sizeof ic);
assert(scr_js_store_probe(object, first, value, &ic));
memset(&store_ic, 0, sizeof store_ic);
assert(scr_js_store_probe(object, first, value, &store_ic));
assert(probe_reads(object, first, 8));
scr_dyn_release(value);
scr_str_release(added);
@@ -90,10 +95,9 @@ static void immortal_objects_stay_unshaped(void) {
object->rc = SIZE_MAX;
assert(!probe_reads(object, key, 3));
assert(object->v.obj.shape == NULL);
ScrJsIc ic;
memset(&ic, 0, sizeof ic);
memset(&store_ic, 0, sizeof store_ic);
ScrDyn *value = scr_dyn_new_num(4);
assert(!scr_js_store_probe(object, key, value, &ic));
assert(!scr_js_store_probe(object, key, value, &store_ic));
assert(object->v.obj.shape == NULL);
scr_dyn_release(value);
scr_str_release(key);
+3 -2
View File
@@ -394,8 +394,8 @@ export function dynamicTierSymbols(names) {
* not yet traced to its typed root. bench/thread-slots,
* bench/channel-messages and bench/worker-messages, added later, link them
* through worker messages:
* node:worker_threads values are dyn objects. bench/dynamic-methods is
* untyped JavaScript. Every other bench workload is fully typed and must link none. The gate and
* node:worker_threads values are dyn objects. bench/dynamic-methods and
* bench/dynamic-properties are untyped JavaScript. Every other bench workload is fully typed and must link none. The gate and
* tests/harness/bench-dynamic-tier.test.ts enforce that, and the test also
* fails when a listed workload stops linking them, so remove a workload here
* when a leak fix makes its binary clean. */
@@ -410,6 +410,7 @@ export const DYNAMIC_WORKLOADS = new Set([
"bench/thread-slots",
"bench/channel-messages",
"bench/dynamic-methods",
"bench/dynamic-properties",
"bench/worker-messages",
"bench/typed-array-scan-workers",
"bench/log-summary",
+20
View File
@@ -363,6 +363,17 @@ globals: undefined fenced
expect(run.stdout).toBe(SESSION + SESSION + SESSION);
});
test("K4: untyped property caches start empty in every session", async () => {
// The shapes a session's cache entries name are freed when it ends;
// the next session's sites must not find those entries.
const { archive, outDir } = await buildLibrary("js-ic-sessions", emission);
const probe = buildProbe("js-ic-sessions", archive, outDir);
const run = runProbe(probe);
expect(run.signal).toBeNull();
expect(run.status).toBe(0);
expect(run.stdout).toBe("run: 20000\n".repeat(3));
});
/* ── K5/K6/K7: the trap channel ──────────────────────────────────────── */
test("K5: a trap delivers to the sink exactly once, host survives", async () => {
@@ -582,6 +593,15 @@ survived, sink_calls=1
expect(run.stdout).toBe(SESSION + SESSION + SESSION);
});
platformTest("K10: property caches across sessions under ASan", async () => {
const { archive, outDir } = await buildLibrary("js-ic-sessions", emission, { sanitize: true });
const probe = buildProbe("js-ic-sessions", archive, outDir, { sanitize: true });
const run = runProbe(probe);
expect(run.signal).toBeNull();
expect(run.status).toBe(0);
expect(run.stdout).toBe("run: 20000\n".repeat(3));
});
platformTest("K10: K5/K7 under ASan", async () => {
const { archive, outDir } = await buildLibrary("traps", emission, { sanitize: true });
const probe = buildProbe("traps", archive, outDir, { sanitize: true });
+6
View File
@@ -0,0 +1,6 @@
// The library's typed export over untyped code (work.mjs).
import { runAll } from "./work.mjs";
export function run(): number {
return runAll() as number;
}
+25
View File
@@ -0,0 +1,25 @@
/* Three sessions of a library whose untyped code reads and writes through
* property caches: each prints the same result, and under the sanitize
* flavor no session reads a cache entry the previous session freed. */
#include <stdint.h>
#include <stdio.h>
extern void kj_init(void);
extern void kj_set_panic_sink(void (*fn)(void *, const uint8_t *, size_t, uint64_t), void *ctx);
extern void kj_collect(void);
extern double kj_run(void);
static void sink(void *ctx, const uint8_t *msg, size_t len, uint64_t addr) {
(void)ctx; (void)addr;
printf("UNEXPECTED SINK: %.*s", (int)len, (const char *)msg);
}
int main(void) {
kj_set_panic_sink(sink, NULL);
for (int session = 0; session < 3; session++) {
kj_init();
printf("run: %.0f\n", kj_run());
kj_collect();
}
return 0;
}
@@ -0,0 +1,14 @@
{
"profile_format": 1,
"name": "conformance-js-ic-sessions",
"entry": "lib.ts",
"emission": "llvm",
"abi": {
"prefix": "kj_",
"init_symbol": "kj_init",
"sink_register_symbol": "kj_set_panic_sink",
"collect_symbol": "kj_collect",
"result_reset_symbol": null
},
"exports": [{ "export": "run", "symbol": "kj_run", "params": [], "returns": "f64" }]
}
@@ -0,0 +1,26 @@
// Dynamic-tier property caches across library sessions: objects built empty
// and filled through untyped stores, read back through untyped loads, and a
// class instance at the same kind of sites. Each session must start with
// empty caches, since the shapes their entries named are freed when the
// previous session ends.
class Box {
constructor(v) {
this.v = v;
}
}
function fill(o, n) {
o.a = n;
o.b = n + 1;
return o;
}
function sum(o) {
return o.a + o.b;
}
function unbox(b) {
return b.v;
}
export function runAll() {
let total = 0;
for (let i = 0; i < 100; i++) total += sum(fill({}, i));
return total + unbox(new Box(total));
}