Skip to content

[JSC] Improve the performance of maps in VariableEnvironments#60797

Merged
webkit-commit-queue merged 1 commit intoWebKit:mainfrom
ast-hugger:perf-inlinemap-squashed
Mar 18, 2026
Merged

[JSC] Improve the performance of maps in VariableEnvironments#60797
webkit-commit-queue merged 1 commit intoWebKit:mainfrom
ast-hugger:perf-inlinemap-squashed

Conversation

@ast-hugger
Copy link
Contributor

@ast-hugger ast-hugger commented Mar 17, 2026

577296d

[JSC] Improve the performance of maps in VariableEnvironments
https://bugs.webkit.org/show_bug.cgi?id=310108
rdar://172755753

Reviewed by Yusuke Suzuki.

This patch improves the performance of JavaScript parser.
Key changes:

    * Source/WTF/wtf/InlineMap.h: Added.
    * Tools/TestWebKitAPI/Tests/WTF/InlineMap.cpp: Added.

Introduces a new map implementation, largely compatible with the existing
UncheckedKeyHashMap. The map stores entries inline using a flat array with linear lookup
when the number of entries is at or below the InlineCapacity parameter value.
This is implemented as a new class rather than a change to the existing
UncheckedKeyHashMap / HashMap / HashTable class group to keep the change localized.
Changing the base HashTable to allow linear inline storage might be an interesting
exercise, but it would be touching many use cases and is better done as a separate
project.

    * Source/JavaScriptCore/parser/VariableEnvironment.h:
    * Source/JavaScriptCore/runtime/CachedTypes.cpp:

Class VariableEnvironment is changed to use the new map for its bindings.

Testing: The new class comes with unit tests. The parser is covered by existing tests.
The patch changes 3 of them:

    * JSTests/stress/can-declare-global-var-invoked-before-any-binding-is-created-eval.js:
    * JSTests/stress/can-declare-global-var-invoked-before-any-binding-is-created-global.js:
    * JSTests/stress/eval-func-decl-in-global-of-eval.js:

These tests were relying on the specific iteration order of the original hash map by
assuming which of the two problematic bindings was visited and detected first. The purpose
of the tests is to verify that a problematic binding prevents valid bindings in the same
eval unit from taking effect.

In this context, it actually makes sense to only have one problematic binding in the test.
This both avoids the non-determinism of which binding is detected first, and reduces the
chances of a false negative (having the valid bindings not take effect simply because the
invalid one was processed first). The patch removes the second problematic definition in
each test.

Canonical link: https://commits.webkit.org/309506@main

092cd7b

Misc iOS, visionOS, tvOS & watchOS macOS Linux Windows Apple Internal
✅ 🧪 style ✅ 🛠 ios ✅ 🛠 mac ✅ 🛠 wpe ✅ 🛠 win loading 🛠 ios-apple
✅ 🧪 bindings ✅ 🛠 ios-sim ✅ 🛠 mac-AS-debug ✅ 🧪 wpe-wk2 ✅ 🧪 win-tests loading 🛠 mac-apple
✅ 🧪 webkitperl ✅ 🧪 ios-wk2 ✅ 🧪 api-mac ✅ 🧪 api-wpe loading 🛠 vision-apple
✅ 🧪 ios-wk2-wpt ✅ 🧪 api-mac-debug ✅ 🛠 gtk3-libwebrtc
✅ 🛠 🧪 jsc ✅ 🧪 api-ios ✅ 🧪 mac-wk1 ✅ 🛠 gtk
✅ 🛠 🧪 jsc-debug-arm64 ✅ 🛠 ios-safer-cpp ✅ 🧪 mac-wk2 ✅ 🧪 gtk-wk2
✅ 🛠 vision ✅ 🧪 mac-AS-debug-wk2 ✅ 🧪 api-gtk
✅ 🛠 🧪 merge ✅ 🛠 vision-sim ✅ 🧪 mac-wk2-stress ✅ 🛠 playstation
✅ 🧪 vision-wk2 ✅ 🧪 mac-intel-wk2 ✅ 🛠 jsc-armv7
✅ 🛠 tv ✅ 🛠 mac-safer-cpp ✅ 🧪 jsc-armv7-tests
✅ 🛠 tv-sim
✅ 🛠 watch
✅ 🛠 watch-sim

@ast-hugger ast-hugger requested a review from a team as a code owner March 17, 2026 18:48
@ast-hugger ast-hugger self-assigned this Mar 17, 2026
@ast-hugger ast-hugger added the JavaScriptCore For bugs in JavaScriptCore, the JS engine used by WebKit, other than kxmlcore issues. label Mar 17, 2026
@webkit-ews-buildbot webkit-ews-buildbot added the merging-blocked Applied to prevent a change from being merged label Mar 17, 2026
@ast-hugger ast-hugger removed the merging-blocked Applied to prevent a change from being merged label Mar 17, 2026
@ast-hugger ast-hugger force-pushed the perf-inlinemap-squashed branch from b47e68f to a34ae33 Compare March 17, 2026 20:07
--m_size;
if (i != m_size) {
// Move last entry to fill the gap
new (&entryStorage[i]) Entry(WTF::move(entryStorage[m_size]));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using std::construct_at if we use std::destroy_at?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, this one and the "ditto"s

Comment on lines +329 to +337
// Hashed mode - mark as deleted
Entry* slot = findKeyOrEmpty(key);
if (isEmptyOrDeletedEntry(*slot))
return false;

std::destroy_at(slot);
constructDeletedEntry(*slot);
--m_size;
return true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove needs to do some rehashing based on load status because we may have a hashtable which is filled with Deleted entries.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Let's address this as a follow-up polish PR.

if (wasInline || !isEmptyOrDeletedEntry(oldEntries[i])) {
Entry* slot = findKeyOrEmptyInStorage(oldEntries[i].key, newEntries, newCapacity);
ASSERT(isEmptyEntry(*slot));
new (slot) Entry(WTF::move(oldEntries[i]));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

Comment on lines +74 to +75
for (unsigned i = 0; i < m_size; ++i)
new (&inlineStorage()[i]) Entry(WTF::move(other.inlineStorage()[i]));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.


if (size < m_capacity) {
Entry* slot = &entryStorage[size];
new (slot) Entry(std::forward<K>(key), std::forward<V>(value));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

if (isDeletedEntry(srcEntries[i]))
constructDeletedEntry(destEntries[i]);
else
new (&destEntries[i]) Entry(srcEntries[i]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

{
if (this != &other) {
std::destroy_at(this);
new (this) InlineMap(WTF::move(other));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

return { iterator { slot, end }, false };

// Slot is either empty or deleted - we can insert here
new (slot) Entry(std::forward<K>(key), std::forward<V>(value));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.


// Construct the Entry directly with the deleted key and empty value
// This avoids constructing an empty Entry first and then overwriting it
new (NotNull, std::addressof(entry)) Entry(WTF::move(deletedKey), MappedTraits::emptyValue());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

if constexpr (EntryTraits::emptyValueIsZero)
zeroBytes(entry);
else
new (NotNull, std::addressof(entry)) Entry(KeyTraits::emptyValue(), MappedTraits::emptyValue());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

@webkit-ews-buildbot webkit-ews-buildbot added the merging-blocked Applied to prevent a change from being merged label Mar 17, 2026
@ast-hugger ast-hugger removed the merging-blocked Applied to prevent a change from being merged label Mar 17, 2026
@ast-hugger ast-hugger force-pushed the perf-inlinemap-squashed branch from a34ae33 to 03a722d Compare March 17, 2026 20:22
@webkit-ews-buildbot webkit-ews-buildbot added the merging-blocked Applied to prevent a change from being merged label Mar 17, 2026
@ast-hugger ast-hugger removed the merging-blocked Applied to prevent a change from being merged label Mar 17, 2026
@webkit-ews-buildbot webkit-ews-buildbot added the merging-blocked Applied to prevent a change from being merged label Mar 17, 2026
@ast-hugger ast-hugger removed the merging-blocked Applied to prevent a change from being merged label Mar 17, 2026
@ast-hugger ast-hugger force-pushed the perf-inlinemap-squashed branch from 03a722d to 092cd7b Compare March 17, 2026 20:53
@ast-hugger ast-hugger added the merge-queue Applied to send a pull request to merge-queue label Mar 18, 2026
https://bugs.webkit.org/show_bug.cgi?id=310108
rdar://172755753

Reviewed by Yusuke Suzuki.

This patch improves the performance of JavaScript parser.
Key changes:

    * Source/WTF/wtf/InlineMap.h: Added.
    * Tools/TestWebKitAPI/Tests/WTF/InlineMap.cpp: Added.

Introduces a new map implementation, largely compatible with the existing
UncheckedKeyHashMap. The map stores entries inline using a flat array with linear lookup
when the number of entries is at or below the InlineCapacity parameter value.
This is implemented as a new class rather than a change to the existing
UncheckedKeyHashMap / HashMap / HashTable class group to keep the change localized.
Changing the base HashTable to allow linear inline storage might be an interesting
exercise, but it would be touching many use cases and is better done as a separate
project.

    * Source/JavaScriptCore/parser/VariableEnvironment.h:
    * Source/JavaScriptCore/runtime/CachedTypes.cpp:

Class VariableEnvironment is changed to use the new map for its bindings.

Testing: The new class comes with unit tests. The parser is covered by existing tests.
The patch changes 3 of them:

    * JSTests/stress/can-declare-global-var-invoked-before-any-binding-is-created-eval.js:
    * JSTests/stress/can-declare-global-var-invoked-before-any-binding-is-created-global.js:
    * JSTests/stress/eval-func-decl-in-global-of-eval.js:

These tests were relying on the specific iteration order of the original hash map by
assuming which of the two problematic bindings was visited and detected first. The purpose
of the tests is to verify that a problematic binding prevents valid bindings in the same
eval unit from taking effect.

In this context, it actually makes sense to only have one problematic binding in the test.
This both avoids the non-determinism of which binding is detected first, and reduces the
chances of a false negative (having the valid bindings not take effect simply because the
invalid one was processed first). The patch removes the second problematic definition in
each test.

Canonical link: https://commits.webkit.org/309506@main
@webkit-commit-queue
Copy link
Collaborator

Committed 309506@main (577296d): https://commits.webkit.org/309506@main

Reviewed commits have been landed. Closing PR #60797 and removing active labels.

@webkit-commit-queue webkit-commit-queue merged commit 577296d into WebKit:main Mar 18, 2026
@webkit-commit-queue webkit-commit-queue removed the merge-queue Applied to send a pull request to merge-queue label Mar 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

JavaScriptCore For bugs in JavaScriptCore, the JS engine used by WebKit, other than kxmlcore issues.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants