Skip to content

Bump WebKit (oven-sh/WebKit#850 preview): a delete of a property with attributes reuses its Structure - #46419

Draft
robobun wants to merge 1 commit into
mainfrom
robobun/dcc60519/delete-transition-reuse
Draft

robobun wants to merge 1 commit into
mainfrom
robobun/dcc60519/delete-transition-reuse

Conversation

@robobun

@robobun robobun commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Draft until oven-sh/WebKit#850 merges (oven-sh/WebKit#697 is under it). WEBKIT_VERSION is the preview build of #850. I set it to the merged commit after that.

Problem

  • Each PostgresError that a program keeps costs 11 Structure cells, each MySQLError 7. In Bun 1.4.3 they share one. It started with sql: consolidate the open Bun.sql, bun:sqlite and node:sqlite pull requests #45304, which is in no release.
  • SQLError.cpp:68-69 and errors.ts:58-59 delete the non-enumerable line and column of the Error. JavaScriptCore never found the Structure that an earlier delete of such a property made, so each delete made a new one.
  • Every delete of a non-enumerable, read-only or accessor property did that, in Bun 1.4.3 too: 1000 objects make 1997 Structures.

Fix

  • Bump WebKit to [JSC] A delete of a property with attributes reuses its Structure transition WebKit#850, which has the cause and the change. Objects that lose the same property share a Structure, and such a delete gets an inline cache.
  • It also corrects two answers of a hot delete site: delete process.env.TZ resets the time zone, and delete x sees a var x that a later script declared.
  • Verified: test/js/bun/jsc/delete-property-transition.test.ts (new, 19 of 20 fail on main) and two new tests in test/js/sql/sql-errors.test.ts. Also bun-jsc.test.ts, capture-stack-trace.test.js and cron-local-time.test.ts.

Background

  • A Structure is JavaScriptCore's record of the shape of an object. Objects of one shape share it, and inline caches check it.
  • A delete stub is inline-cache code for one Structure. It does not call the deleteProperty of the class.
  • Considered a change in Bun alone: make line and column ordinary properties before the delete. It repairs 2 of the 14 kinds of such a delete.

Downsides

  • process.env, require.extensions and the global object (a bare delete x) get no delete stub: their deleteProperty does work that a stub skips. delete process.env.X for an unset X takes about 9 ns, was about 1 ns at a hot site.
  • delete process.env.TZ takes 12 us each time: it resets the time zone. A hot site took 5 ns and did not reset it.
  • The text of WebKit's jsc grows by 416 bytes, fix(examples/bun-bakery): Create gitignore #697 included.
Notes

Repro 1, any delete of a property with attributes:

import { heapStats } from "bun:jsc";
function structures() { Bun.gc(true); return heapStats().objectTypeCounts.Structure ?? 0; }
function make(enumerable) {
  const o = {};
  Object.defineProperty(o, "a", { value: 1, enumerable, configurable: true, writable: true });
  delete o.a;
  o.b = 1;
  return o;
}
for (const enumerable of [true, false]) {
  const kept = [];
  for (let i = 0; i < 10; i++) kept.push(make(enumerable));
  const before = structures();
  for (let i = 0; i < 1000; i++) kept.push(make(enumerable));
  console.log(`deleted property enumerable=${enumerable}: ${structures() - before} Structures for 1000 objects`);
}
enumerable=true enumerable=false
Bun 1.4.3 4 1997
main (debug build) 4 1997
this branch (debug build) 4 0

The 4 of the first case are made once, not per object.

Repro 2, 20 failed queries to warm up, then 300 that the program keeps. On main the 300 Postgres errors make 3304 Structures, on this branch 4 (SELECT * FROM no_such_table_zzz, heapStats().objectTypeCounts.Structure after Bun.gc(true), distinct StructureID of describe(error)):

Structure cells per error distinct Structures among 300 errors
PostgresError, Bun 1.4.3 0.03 1
PostgresError, main (debug build) 11.01 300
MySQLError, main (debug build) 7.00 300
PostgresError, this branch (debug build) 0.01 1
MySQLError, this branch (debug build) -0.01 1

Other shapes, distinct Structures among 1010 kept objects, Bun 1.4.3 and this branch: a read-only property, a getter, a symbol key, Reflect.deleteProperty, a Proxy without a trap, an attribute change then a delete, a delete then an add again, two deletes, delete error.stack, delete f.name, delete arguments.length, a second Error.captureStackTrace: 1010 and 1 for each. A class with the constructor statements of SQL.PostgresError: 1010 and 1.

Tests. No file under src/ changes. The tests run against the WebKit that scripts/build/deps/webkit.ts names. On main (e53cd3d) in a debug build: delete-property-transition.test.ts has 19 of 20 fail. The one that passes checks that a hot site does for such a property what the first delete did, and main has no cache there. Bun 1.4.3 fails the same 19. sql-errors.test.ts has its 2 new tests fail with 20 Structures for 20 errors. With this branch: 20 of 20 and 29 of 29 pass.

Also run on this branch in a debug build: test/js/bun/jsc/bun-jsc.test.ts (45 pass), test/js/node/v8/capture-stack-trace.test.js (71 pass), test/js/bun/cron/cron-local-time.test.ts (30 pass), test/js/node/vm/vm.test.ts (336 pass, 1 timeout) and test/js/node/process/process.test.js (195 pass, 2 timeouts). The timeouts are the speed of the debug build: the vm test builds a string of 2^27 characters and its child needs 7.9 s, and the two signal tests pass when they run alone. None of the three has a delete.

process.env. JSEnvironmentVariableMap::deleteProperty resets the time zone when the name is TZ. A delete stub does not call it. On main, delete process.env.TZ at a site that ran 1500 times leaves the zone that setTimeZone("Asia/Tokyo") set. The time per delete is from Bun 1.4.3 with and without inline caches (BUN_JSC_forceICFailure=1), less the time of an empty call in the same mode. With inline caches a call that only does the delete takes 2.9 ns and an empty call 4.2 ns. Without them the two take 33.1 ns and 24.4 ns. A pair of process.env.X = "1" and delete process.env.X takes 118 ns with inline caches. The set is 29 ns of that. delete process.env.TZ takes 5.1 ns with inline caches and 12017 ns without them.

Related. oven-sh/WebKit#803 with #45774, and #44583: an object that was a dictionary still gets a Structure per delete. This change does not touch that. oven-sh/WebKit#738 with #44097: a warm read of a global after a script declared it.


[policy-decision:webkit] gate passed · iteration 0 · 3 files touched

passes on PR (with fix)
Test-only change.

Debug/ASAN (expected pass):
$ bun bd test 'test/js/bun/jsc/delete-property-transition.test.ts' 'test/js/sql/sql-errors.test.ts'
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/js/bun/jsc/delete-property-transition.test.ts test/js/sql/sql-errors.test.ts
bun test v1.4.4 (367d939d9)

test/js/sql/sql-errors.test.ts:
Container ready via docker-compose: mysql_plain at 127.0.0.1:3306
(pass) mysql > an error that the server sends while the client connects is a MySQLError [366.95ms]
(pass) mysql > a native error has the layout of new SQL.MySQLError() [566.07ms]
(pass) mysql > each run of a statement that fails to prepare rejects with the same MySQLError [1043.72ms]
(pass) mysql > sql.reserve: a wrong number of parameters rejects with a MySQLError [1195.49ms]
(pass) mysql > sql.unsafe: a wrong number of parameters rejects with a MySQLError [1691.55ms]
(pass) mysql > a value that a parameter throws arrives unchanged [221.07ms]
(pass) mysql > sql.begin: a wrong number of parameters rejects with a MySQLError [1393.75ms]
(pass) mysql > errors with the same fields have one Structure [520.15ms]
Container ready via docker-compose: postgres_plain at 127.0.0.1:5432
(pass) postgres > an error that the server sends while the client connects is a PostgresError [335.39ms]
(pass) postgres > sql.reserve: a query with too many parameters rejects with a PostgresError [1010.46ms]
(pass) postgres > sql.unsafe: a query with too many parameters rejects with a PostgresError [1213.10ms]
(pass) postgres > sql.begin: a query with too many parameters rejects with a PostgresError [1169.57ms]
(pass) postgres > a value that a parameter throws arrives unchanged [196.77ms]
(pass) postgres > errors with the same fields have one Structure [183.73ms]
(pass) postgres > a native error has the layout of new SQL.PostgresError() [1449.19ms]
(pass) postgres > every field of the server's error is text, not only the message [1133.68ms]
Container ready via docker-compose: mysql_plain at 127.0.0.1:3306
(pass) mysql: the stack of a native error > made with no JavaScript on the stack: Error.prepareS
... (truncated)
Exit: 0
diff hotspot
scripts/build/deps/webkit.ts                       |   2 +-
 test/js/bun/jsc/delete-property-transition.test.ts | 339 +++++++++++++++++++++
 test/js/sql/sql-errors.test.ts                     |  35 +++
 3 files changed, 375 insertions(+), 1 deletion(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                                reads  edits  tests
scripts/build/deps/webkit.ts                            0      0     47
test/js/bun/jsc/delete-property-transition.test.ts      2      2     30
test/js/sql/sql-errors.test.ts                          0      0     31

… attributes reuses its Structure

A delete of a non-enumerable, read-only or accessor property made a new
Structure each time. JavaScriptCore stored the deletion transition without
the attributes of the property and looked it up with them, so the lookup
never found it. PostgresError and MySQLError delete the non-enumerable
`line` and `column` of their Error, so each such error had 11 or 7
Structures of its own.

oven-sh/WebKit#850 changes the lookup and the rule for what an inline cache
stub for a delete may skip. WEBKIT_VERSION is its preview build until it
merges.
@robobun

robobun commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

Status

Reproduced on main (e53cd3d) in a debug build: 300 failed Postgres queries that the program keeps make 11.01 Structure cells per error, and the 300 errors have 300 Structures. In Bun 1.4.3, 1000 objects that delete a non-enumerable property make 1997 Structures.

The change is in oven-sh/WebKit#850. This pull request pins its preview build and adds the tests. It stays a draft until that pull request merges.

@robobun

robobun commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 10:47 PM PT - Oct 21st, 2026

✅ @robobun, your commit 0109025cd548695e509bf2c5a95a4823535e07a8 passed in Build #130519! 🎉


🧪   To try this PR locally:

bunx bun-pr 46419

That installs a local version of the PR into your bun-46419 executable, so you can run:

bun-46419 --bun

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant