Repository navigation
Event and PerformanceMark: the user-timing and DOM defects the corpus found - #3244
Merged
Merged
Conversation
… object
`new PerformanceMark("m", 123)` built a mark. So did `performance.mark("m", 123)`
— which the wpt corpus could not see, because `user-timing/mark-errors.any.js`
calls the method **unbound**, as `testInfo.testFunction(self.performance.mark)`,
so the `TypeError` its `[performance.mark]` half observed came from the brand
check on a `this` of `undefined` rather than from any conversion. The two halves
were never disagreeing about the dictionary; only one of them was ever asking.
Both run one conversion — `UserTiming.ReadMarkOptions`, reached through
`PerformanceMarkConstructor.ReadArguments`, which `performance.mark`'s step 1
runs — and it treated every non-object as the empty dictionary. Step 1 of
https://webidl.spec.whatwg.org/#es-dictionary is the whole rule: "If jsDict is
not an Object and jsDict is neither undefined nor null, then throw a TypeError."
`EventConstructor.ReadEventInit` already had it; this is the same three lines in
the sibling conversion, so the fix is in the shared path and not in either
caller.
`undefined` and `null` stay the empty dictionary, so `performance.mark('x').detail`
is still `null`, and a boxed `new Number(5)` is still an Object and so a
dictionary with no members.
The four `NeedsTriage` rows of `mark-errors.any.js` go with it, and
`PerformanceTimelineTests` pins the *bound* call the corpus cannot make.
Item 1 of sebastienros#3212.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JmK1CzJSgMd1LyTEMX3yiE
…e] means `isTrusted` sat on `Event.prototype` beside `type` and `bubbles`, an ordinary configurable accessor a script could shadow. The DOM Standard declares it `[LegacyUnforgeable]` (https://dom.spec.whatwg.org/#dom-event-istrusted), which WebIDL defines as "non-configurable and … exist[ing] as an own property on the object itself rather than on its prototype" (https://webidl.spec.whatwg.org/#LegacyUnforgeable). §3.7.6 says both halves of that: "Remove from attributes all the attributes that are unforgeable" before defining the prototype's, and `configurable` is false for an unforgeable one — so the property is `{ [[Get]]: getter, [[Set]]: undefined, [[Enumerable]]: true, [[Configurable]]: false }` on every instance and absent from the prototype. The getter is the *interface's*, one per realm, which is what `dom/events/Event-isTrusted.any.js` asserts: it reads the descriptor off two separate events and requires the same function object in both. It costs nothing. `JsEvent.GetOwnProperty` answers the name from the realm's one shared descriptor and `GetInitialOwnStringPropertyKeys` lists it ahead of anything script adds — the shape `Function` already uses for `length`/`name`/`prototype` and `JsError` for `message`. Measured with `GC.GetAllocatedBytesForCurrentThread` over 20,000 iterations, before and after: `new Event('x')` is 112 bytes either way, and reading `.isTrusted` is 112 either way. The obvious implementation is not free — one *stored* own property on an event measures +184 bytes, more than doubling it, on an object the engine creates per `dispatchEvent` and throughout the abort, message, worker and fetch paths. Sharing one descriptor across every event of a realm is safe for the reason the engine already shares `AllForbiddenDescriptor` process-wide for every function's `length`: a non-configurable descriptor with no setter is out of reach of every mutating branch of ValidateAndApplyPropertyDescriptor. The one redefinition the spec permits — an identical `{ get: theSameGetter, set: undefined }` — is applied to a copy, which the ordinary machinery stores; both lookups prefer that copy so the key is never reported twice. Because the property is non-configurable and setter-less it is a constant of the type — always present, never removable — which is what keeps the interpreter's version-gated prototype cache sound for an event receiver. What changed for every event is what enumeration reports, and it is the point: `Object.keys(new Event('x'))` is `["isTrusted"]` and `JSON.stringify` gives `{"isTrusted":false}`, as a browser answers. Item 2 of sebastienros#3212. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmK1CzJSgMd1LyTEMX3yiE
…ce declares Both are in https://dom.spec.whatwg.org/#interface-event — marked `// legacy`, normative all the same — and neither existed here. `srcElement` is the easy half: a plain `readonly attribute EventTarget?` whose "getter steps are to return this's target", so it is an ordinary configurable prototype accessor like the rest. It carries no extended attribute, unlike `isTrusted`, so nothing about where it lives is special. `returnValue` is not a property over a field, and implementing it as one would have been wrong. Its getter is "false if this's canceled flag is set; otherwise true", and its setter is "set the canceled flag with this if the given value is false; otherwise do nothing" — the *algorithm* (https://dom.spec.whatwg.org/#set-the-canceled-flag), which is gated on `cancelable` and on the in-passive-listener flag. So it is `preventDefault()` under another name: `e.returnValue = false` inside a passive listener is ignored exactly as `e.preventDefault()` is, and assigning `true` can never clear a flag already set. The IDL type is `boolean`, so the assigned value goes through ToBoolean rather than being refused — `e.returnValue = 0` cancels, `= 1` does not. `AddEventListenerOptions-passive.any.js`'s "returnValue should be ignored if-and-only-if the passive option is true" was the only one of that file's five rows to fail, and its exclusion goes with the fix. `dom/events/Event-constructors.any.js` stays out of the corpus, and the reason recorded for it was incomplete: each of its two unnamed failures also asserts `"initEvent" in ev`, a third legacy member of the same interface. The row now says so. Item 3 of sebastienros#3212. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmK1CzJSgMd1LyTEMX3yiE
…rs.any.js
`dom/events/Event-constructors.any.js` sat in `_notVendored` because all
fourteen of its tests are registered without a name, so the driver reports every
one of them under the same one and no per-test exclusion can single out the two
that fail. The reason recorded for those two was `srcElement` and `returnValue`,
and that reading was incomplete: each of them asserts the whole initial state of
a new event, `assert_true("initEvent" in ev)` included. Implementing the previous
commit's two members left the file exactly as red as it was, failing one
assertion later — which is why the file is the check and the prose was not.
So the third legacy member of the same interface:
https://dom.spec.whatwg.org/#dom-event-initevent
https://dom.spec.whatwg.org/#dom-customevent-initcustomevent
Both are "If this's dispatch flag is set, then return. Initialize this with
type, bubbles, and cancelable", with `initCustomEvent` adding "set this's detail
attribute to detail". `JsEvent.InitializeEvent` is *initialize an event*
(https://dom.spec.whatwg.org/#concept-event-initialize) step for step: unset the
stop-propagation, stop-immediate-propagation and canceled flags, clear
`isTrusted`, clear `target`, then set type, bubbles and cancelable. It
deliberately leaves the composed flag alone, which is the limitation the
specification notes ("incapable of setting composed").
Step 1's "set event's initialized flag" has nothing to set. The flag is unset by
exactly one algorithm in the standard, `document.createEvent()`, and read by
exactly one, `dispatchEvent`'s `InvalidStateError` guard. There is no `document`
here, so every event Jint can build has it set from birth and no observation can
tell a stored flag from an assumed one.
The dispatch-flag guard is what stops a listener re-initializing the very event
it is handed: a silent no-op there, not an error. `EventType`, `TypeName`,
`Bubbles`, `Cancelable` and `CustomEvent`'s `Detail` become settable for this
and nothing else — private setters, one caller.
The file is vendored byte-identical to upstream at the pin: `git hash-object`
gives faa623e, the blob id GitHub reports for
that path at 6c7127bdd9. All fourteen of its tests pass, and it gets a
minimum-test floor of 14 like every other vendored file.
Beyond the three items of sebastienros#3212 — it is what made the file vendorable, which
that issue's item 3 asked for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JmK1CzJSgMd1LyTEMX3yiE
This was referenced Aug 22, 2026
lahma
added a commit
that referenced
this pull request
Aug 22, 2026
… record order (#3247) * Fetch/FormData/URLSearchParams: give every iterator prototype's `next` WebIDL's attributes https://webidl.spec.whatwg.org/#es-iterator-prototype-object: "an iterator prototype object must have a `next` data property with attributes `{ [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true }`". Jint's three were `enumerable: false`, which is ECMA-262's rule for a built-in function property (https://tc39.es/ecma262/#sec-ecmascript-standard-built-in-objects) and what `[JsFunction]` defaults to. web-platform-tests' `fetch/api/headers/headers-basic.any.js` reads the descriptor through its own `checkIteratorProperties` and asserts all three attributes, which is what the three excluded rows ("Check keys method", "Check values method", "Check entries method") were. Writable and configurable were already right; enumerable was the whole of it. This is the family, not the row. `HeadersIteratorPrototype`, `FormDataIteratorPrototype` and `UrlSearchParamsIteratorPrototype` each declared a bare `[JsFunction(Name = "next")]`, and no vendored corpus row reaches the latter two — so the two siblings were the same defect with nothing to find it. `ReadableStreamAsyncIteratorPrototype` already carries the flags from #3238, which is where `JsFunctionAttribute.Flags` came from. Nothing else on those objects moves: `@@toStringTag` is a class string, which WebIDL also gives `{ [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: true }`, and that is what `PropertyFlag.Configurable` already produced. `next.length` is 0 and `next.name` is "next" as before; the declarations now say `Length = 0` explicitly rather than deriving it. The three exclusion rows are deleted, so the driver enforces the fix. While re-reading the `NeedsTriage` doc comment: three of its bullets described the streams defects #3195/#3238 had already fixed and whose exclusion rows had already gone. Rewritten to say so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmK1CzJSgMd1LyTEMX3yiE * Fetch: a consumed bytes-source body hands back a locked stream, not merely a disturbed one `new Response('hi').text()` left `response.body.getReader()` working, where web-platform-tests' `response-stream-disturbed-5.any.js` asserts a `TypeError` — eight of its twelve rows, the four whose body source is a `ReadableStream` passing. The recorded analysis said the stream `.body` exposes for a byte-source body "is not the one the consume path locked". Half right, and the wrong half is the interesting one: the consume path never locked anything, and *disturbed* is not what `getReader()` refuses. `getReader()` refuses a **locked** stream and only a locked one — https://streams.spec.whatwg.org/#set-up-readable-stream-default-reader step 1 — so a disturbed-but-unlocked stream is handed a reader quite correctly. What makes a browser refuse it is that consuming a body runs *fully read* (https://fetch.spec.whatwg.org/#concept-body-fully-read), whose step 3 is "let reader be the result of getting a reader for body's stream" (https://streams.spec.whatwg.org/#readablestream-get-a-reader, which locks it) and whose read-all-bytes step never releases that reader. The stream a consumed body exposes is therefore disturbed, closed *and locked for good*. That is exactly why the `ReadableStream`-source rows already passed: those go through `FetchBody.FullyRead`, which acquires the reader and holds it. `GetOrCreateStream` defers building the stream for a bytes-source body, so the state it has to reproduce on first ask is the state fully read would have left — and it was reproducing only the `disturbed` half. It now cancels *and* acquires a default reader. The stream is built empty in that case: nothing can read a locked stream, so filling it from the source would copy a body that has already been handed over only to throw the copy away. Neighbours checked, and none of them moves: `bodyUsed` was already true before this (the source-disturbed flag stands in until a stream exists, and `Cancel` sets `disturbed` afterwards); `clone()` after a consume still throws, because "body is unusable" is disturbed **or** locked and it was already disturbed; reading `body` *before* consuming still leaves the body usable and a released reader still leaves it usable; and the mixin is shared, so a `Request` behaves the same way — pinned in `RequestTests`. All six consumers are covered by a theory: `text`, `json`, `arrayBuffer`, `blob`, `bytes`, `formData`. The two exclusion globs are deleted, so the driver enforces the fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmK1CzJSgMd1LyTEMX3yiE * Fetch: convert a `record<ByteString, ByteString>` in WebIDL's order, keys included `new Headers(proxy)` performed one operation more than the specification's order allows, on the two rows of `fetch/api/headers/headers-record.any.js` that count them: "Correct operation ordering with two properties one of which has an invalid name" (6 where 5 are allowed) and "Basic operation with Symbol keys" (8 where 7 are). Measured through the file's own logging proxy, the extra operation is the same one in both: a `[[Get]]` of the key that ends the conversion. https://webidl.spec.whatwg.org/#es-record step 4.2 is, per key: 4.2.1 "let typedKey be key converted to an IDL value of type K", *then* 4.2.2 "let value be ? Get(O, key)". `FillFromRecord` did the `Get` first and converted the key inside `append`, so a key that cannot be a `ByteString` — a Symbol, or a string carrying a code unit above 255 — was read before it was refused. Two things follow from writing step 3 as `? O.[[OwnPropertyKeys]]()` rather than as Jint's string-filtered overload. It is what the specification says: a Symbol key is a member of the walk, its descriptor is fetched, and if it is enumerable the conversion of the key is where the `TypeError` comes from. And it removes an inconsistency that was there before: `JsProxy.GetOwnPropertyKeys` honours the `types` filter only when there is no `ownKeys` trap — correctly, since the spec's internal method has no filter — so a Symbol key already reached the walk through a proxy while `new Headers({ [Symbol.toStringTag]: 'x' })` on a plain object was silently accepted. Both now throw, as a browser does. The record is also converted *in full* before the fill appends anything, which is what the two algorithms actually say: the conversion produces a `record<ByteString, ByteString>`, and only then does https://fetch.spec.whatwg.org/#concept-headers-fill run `append` over it. The observable difference is that a record whose second entry has an invalid header *name* no longer leaves the first entry appended. `AppendChecked`/`Validate` keep the `ByteString` conversions for `Headers.prototype.append` and `.set`, where they are WebIDL's argument conversions; the record path calls the converted-string half directly. The two exclusion rows are deleted, so the driver enforces the order. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmK1CzJSgMd1LyTEMX3yiE * Wpt: the empty-FormData row is not a defect, and the corpus table had gone stale Item 10 of #3212 asked that HTML's multipart/form-data encoding algorithm be read before anything was changed. Reading it settles the question the other way, so nothing is changed and the exclusion stays — with the reason rewritten from "wants a specification read" to what the reading found. HTML's algorithm normalizes the newlines in each entry and then says to "return the byte sequence resulting from encoding the entry list using the rules described by RFC 7578": it defines the *escaping* and delegates the *framing*. RFC 7578 defers to RFC 2046, whose §5.1.1 grammar ends every `multipart-body` with a `close-delimiter` that is not optional. An empty entry list therefore has no shorter conforming encoding than the 50 bytes `MultipartFormData` writes, and nothing licenses the empty body `response-consume-empty.any.js` asks for. Nor does any implementation produce one. Measured on wpt.fyi against the four aligned stable runs (chrome 152, edge 151, firefox 154, safari 26.6), "Consume empty FormData response body as text" is 0/1 in all four while the file's thirteen other rows are 1/1 in all four. So the row keeps its exclusion but `NeedsTriage` is the wrong category for it — that category means "a bug or a specification detail to chase". Giving it one of its own is left to a follow-up rather than done here. Also re-measures the corpus-wide table in `Vendor/README.md`, which five rows had drifted from: Fetch is this change's own (88 -> 75), and Streams (16 -> 11), Compression (84 -> 22), User Timing (9 -> 5) and DOM (2 -> 0, 12 -> 13 files, 62 -> 76 assertions) had gone stale against #3195/#3238, #3243 and #3244, which removed their exclusions without revisiting the table. Every per-standard section above it was already right; only the summary and the header count (269/40,617/2,980 -> 270/40,631/2,894) were behind. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmK1CzJSgMd1LyTEMX3yiE --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 23, 2026
Closed
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.
Fixes items 1, 2 and 3 of #3212 — the user-timing and DOM
Eventdefects. With #3243 (items 4–6) that leaves four of the eleven; #3212 stays open.Two of the three recorded analyses were wrong, and both corrections are in the docs rather than quietly worked around.
Item 1 —
PerformanceMarkoptionsThe issue says
performance.mark(name, 123)"refuses it correctly", so the two halves ofmark-errors.any.jsdisagree. They did not disagree.mark-errors.any.jscalls the method unbound (testInfo.testFunction(self.performance.mark)), so theTypeErrorits passing half observed came from the brand check on athisofundefined— not from any conversion.performance.mark(name, 123)did not refuse a non-object either.Both halves already shared one conversion,
UserTiming.ReadMarkOptions(reached throughPerformanceMarkConstructor.ReadArguments, whichperformance.markruns as its own step 1), and it treated every non-object as the empty dictionary. Fixed in that shared path per WebIDL §3.2.19 step 1 — "If jsDict is not an Object and jsDict is neither undefined nor null, then throw a TypeError" — which is exactly whatEventConstructor.ReadEventInitalready did.Item 2 —
isTrusted, and its allocation costDOM declares it
[LegacyUnforgeable]; WebIDL §3.4.10 makes that "non-configurable and … an own property on the object itself rather than on its prototype", and §3.7.6 both gives it{[[Set]]: undefined, [[Enumerable]]: true, [[Configurable]]: false}and removes it from the prototype. Implemented as a virtual own property — the shapeFunctionalready uses forlength/nameandJsErrorformessage.The per-instance cost is zero, which is not the obvious outcome and was measured rather than asserted (
GC.GetAllocatedBytesForCurrentThread, 20,000 iterations, best of 7, net10.0, engine reverted tomainfor the before column):new Event('x')new CustomEvent('x')new Event('x'); e.isTrustedt.dispatchEvent(new Event('x'))Object.getOwnPropertyDescriptor(e,'isTrusted')Nothing per instance and nothing per read. The only row that moved is the descriptor query, which previously returned
undefined; the 504 B is ECMAScript's ownFromPropertyDescriptorresult — what any accessor costs.Worth stating because the naive implementation is not free: one stored own property on an event measures 296 B against 112 B — +184 B, +164%. Two things buy the zero: the descriptor is one shared instance per realm (an accessor is unwrapped by calling its getter with the receiver), and it is never stored. Sharing is safe by the argument the engine already makes process-wide for
AllForbiddenDescriptor— a non-configurable, setter-less descriptor is unreachable by every mutating branch ofValidateAndApplyPropertyDescriptor.Behaviour that changed for every event, and is the point:
Object.keys(new Event('x'))is now["isTrusted"]andJSON.stringifygives{"isTrusted":false}, as a browser answers. One existing test asserted the old count and was updated.Item 3 —
srcElementandreturnValueThe recorded analysis called
srcElement[LegacyUnforgeable]. Re-deriving from the interface IDL shows it carries no extended attribute — onlyisTrusteddoes — so it is an ordinary prototype accessor returningthis's target.returnValueis implemented as the algorithm, not a field: the getter is!canceledFlagand the setter runs set the canceled flag, so it ispreventDefault()under another name — ignored by a non-cancelable event and inside a passive listener, and= truenever clears a flag already set. Its IDL type isboolean, so= 0cancels and= 1does not.Event-constructors.any.js— vendored, but it needed a third memberItems 2 and 3 did not make it vendorable, and the recorded reason was incomplete: each of its two unnamed failures asserts the whole initial state of a new event,
assert_true("initEvent" in ev)included, so fixingsrcElement/returnValueleft it failing one assertion later.initEvent()andinitCustomEvent()are implemented — initialize an event step for step behind the dispatch-flag guard, deliberately not touching the composed flag — and the file is vendored. Its "set the initialized flag" step has nothing to set here: that flag is unset only bydocument.createEvent()and read only bydispatchEvent'sInvalidStateErrorguard, and there is nodocument.Byte-verified:
git hash-objectof the vendored copy isfaa623ea92991b72742477a18471449f5382f1a8, the blob id GitHub reports for that path at the pin. Minimum-test floor 14 — the file has 14 tests, not the 15 the old prose claimed. All 14 pass.This is beyond the three items and is its own commit, so it can be dropped without touching the rest.
Exclusion accounting
NeedsTriage29 → 23 rows: 4 ×mark-errors.any.js,Event-isTrusted.any.js,AddEventListenerOptions-passive.any.js._notVendoredloses itsEvent-constructors.any.jsrow. TheNeedsTriagedoc comment and threeVendor/README.mdsections were rewritten to match.Verification
Solution builds Release, 0 errors. All production changes are under
Jint/WebApi/.Jint.TestsJint.Tests+JINT_HOST_CONTRACT_VERIFICATION=1Jint.Tests.PublicInterfaceJint.Tests.PublicInterface+ verificationWPT 417/417 files. Per-row before/after with only the engine reverted — exactly 8 rows moved, all FAIL→PASS, all intended:
dom/events37/4 → 41/0,user-timing69/9 → 73/5, and every other corpus (dom/abort,hr-time,html/*,fetch/api/*,streams,workers,WebCryptoAPI,encoding,url,urlpattern,FileAPI,compression) byte-identical.test262 not run: nothing reaches non-web-API engine code — the
ObjectInstancevirtuals are overridden only onJsEvent, which test262 never creates.Left undone
cancelBubbleis still absent — the last legacyEventmember, not in the issue and needed by no vendored file.🤖 Generated with Claude Code