Conversation
Collaborator
Author
|
How I reproduced it:
This PR: #764 |
…ineOwnProperty() override of the class Three paths decide a store from the Structure and write the slot of the property. They have no PutPropertySlot to ask, so they never learn that the class of the object overrides put() or defineOwnProperty(): - PutByStatus::computeFor(StructureSet). The DFG folds the store into a PutByOffset. The abstract interpreter reads the same status and treats a PutById that it does not fold as free of side effects. - op_enumerator_put_by_val in OwnStructureMode, in its five copies. - The JSON.parse reviver walk. Structure gets one bit, classInterceptsOwnPropertyStores. The constructor sets it when the class overrides put() or defineOwnProperty() and does not override getOwnPropertySlot(). PutByStatus returns LikelyTakesSlowPath for such a structure. The five copies of the for-in store test one named mask that includes the bit. The reviver walk calls createDataProperty(). A class that also overrides getOwnPropertySlot() does not get the bit. PutByStatus already tests for that flag, and the global object stays exempt. $vm gets ObjectDoingSideEffectPutWithCorrectSlotStatus as a test subject. Its put() and defineOwnProperty() convert the value to a string.
robobun
force-pushed
the
robobun/8ddf1804/dfg-put-fold-overrides-put
branch
from
October 3, 2026 03:41
6ee4f72 to
2d86805
Compare
|
Preview build of 2d86805: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Structurealone and skip aput()ordefineOwnProperty()override: the DFG'sPutByStatus::computeFor(StructureSet)(bytecode/PutByStatus.cpp:364), the for-in store (op_enumerator_put_by_val) and theJSON.parsereviver walk.process.env.TZ = zonefrom a hot function stops changing the time zone (call 108 stored Asia/Tokyo offset 0).PutByIdthat it keeps counts as free of side effects, so a later load returns another property's value.Fix
Structuregets one bit,classInterceptsOwnPropertyStores: the class overridesput()ordefineOwnProperty(), and notgetOwnPropertySlot().PutByStatusreturnsLikelyTakesSlowPathfor it (the gate from [JSC] A store to Error.stackTraceLimit from JIT code keeps updating the stack trace limit #640). The five copies of the for-in store test one named mask that includes it. The reviver walk callscreateDataProperty().JSTests/stresstests. Fork main fails 50 of 51 runs (17 modes each), this branch none. Each of the 7 hunks, reverted alone, fails one test.Background
PutPropertySlotif it can cache a store. These paths have no slot.ProhibitsPropertyCachingon Bun'sprocess.envandrequire.extensions: it turns off their read caches and leaves two paths.Downsides
Structurecreation: +20 instructions in the creating constructor (149 to 169), +5 in the transition constructor (objdump, x64 release).JSON.parsewith a reviver: +6 instructions per revived property.process.envstore now callsput()each time.Notes
Repro in
jsc($vm.createObjectDoingSideEffectPutWithCorrectSlotStatus()is new in this PR:put()anddefineOwnProperty()convert the value to a string, andput()callsslot.disableCaching()):Fork main throws at iteration 99 with
--useConcurrentJIT=0. It passes with--useDFGJIT=0,--useAccessInlining=0or--useJIT=0.Repro in Bun (release 1.4.3-canary.1, Node v26.3.0 prints nothing):
How each path gets to the raw store
computeForthen returns a Replace variant andtryFoldAsPutByOffsetemitsPutByOffset.MultiPutByOffset. The DFG tier keeps thePutById, but the abstract interpreter already calleddidFoldClobberWorld().put()runstoString()on the value, that code changes another object, and a laterGetByOffseton that object has lost itsCheckStructure. The test gets"q"whereundefinedis correct.defineOwnProperty()when the class overrides it (CommonSlowPaths::canPutDirectFast). The fold skipped it.for (name in object) object[name] = valuewrites the slot when the structure matches the enumerator and has none of two bits. This happens in the LLInt already, so--useJIT=0fails too.putDirectOffset.The bit
OverridesGetOwnPropertySlot, and it setsOverridesPutor its method table has adefineOwnPropertyother thanJSObject::defineOwnProperty.getOwnPropertySlot()(JSArray, JSFunction, RegExpObject, ErrorInstance, arguments, the global object) is as before.PutByStatushas its own test for that flag, with the global object exempt. Theput()of those classes guards names that a for-in store does not reach.$vmclasses.sizeof(Structure)is 112 bytes before and after (x64). Bit 11 ofm_bitFieldwas free. [JSC] JSObject::makePropertiesImmutable(): an object's own properties and prototype stop changing, and its property attributes stay as they are #759 uses bit 10.Verification (Linux x64)
jscof the preview build passes the three new tests in 51 of 51 runs ofrun-jsc-stress-tests.it.todoscript of that PR stores 0 raw values of 300,000 (299,390 with the pinned WebKit) and the test passes as anit. TheTZloop above is correct for all 20,000 calls. A for-in store over a Worker'sprocess.envleaves 0 raw values of 3 (3 before). A for-in store overrequire.extensionscalls the hook (0 calls before). AJSON.parsereviver that putsprocess.envin the tree gets a string stored (a number before).NODE_TLS_REJECT_UNAUTHORIZEDset from a hot function changes whatfetchdoes again.test/js/node/process/process.test.js: 189 pass, 5 skip, and 1 failure becauseUSERis not set in my container.run-jsc-stress-testson the three new tests, release build: fork main plus the test class fails 50 of 51 runs (the DFG test passes only inlockdown, which has no JIT). This branch passes 51 of 51.enumerator-put-by-val-calls-put-override.jsfail, the reviver guard makesjson-parse-reviver-calls-define-own-property-override.jsfail, and thePutByStatusgate makesput-override-is-called-when-dfg-proves-structure.jsfail.JSTests/stresswhose name matchesput-by|for-in|forin|enumerator|json|class-field|define-field|object-assign|multi-put|reviver|incorrect-put|put-override, all modes, release build: 4,280 runs, 0 failures.$vm.createObjectDoingSideEffectPutWithoutCorrectSlotStatustests that run in a debug build, the enumerator and reviver tests, and the new ones. 249 runs, 0 failures.Cost measurements (release
jsc, fork main plus the test class against this branch)LLIntAssembly.hdiffers in the immediate at 3 sites (testl $262160, 16(%rdx)totestl $264208, 16(%rdx)). Baseline, DFG and FTL code forfunction f(o) { for (var k in o) o[k] = 1; }has the same size in both builds (1120, 1184 and 1472 bytes). The debug disassembly differs in that immediate only (testl $0x40010totestl $0x40810).PutByIdon a plain object with a cached load: same DFG graph, same code size (416 bytes), stillPutByOffset.--useJIT=0, gdb breakpoint onslow_path_enumerator_put_by_val): 12,000 hits before and after. A plain object: 0 hits before and after.size jsc(both built in the same directory): text 43952121 to 43951353, total 44732804 to 44732804.tstsequence.perf,valgrindandbloatyare not installed on the machine, so the instruction counts come fromobjdumpand from the JIT dumps.Not in this PR
WEBKIT_VERSION, callslot.disableCaching()inJSCommonJSExtensions::put, and add the Bun tests. process: let inline caches work on process.env and process.argv bun#44356 givesprocess.envits own slot input()and has anit.todofor the DFG fold.process.env[700] = 1) useputByIndexand indexed storage. That is a different hook.put()andgetOwnPropertySlot()keeps the for-in store and the reviver store as they are.JSCallbackObjectis such a class.PutByStatusgate. The named mask now holds its bit too.