Skip to content

Improve performance - #1631

Open
ksh8281 wants to merge 7 commits into
Samsung:masterfrom
ksh8281:perf
Open

Improve performance#1631
ksh8281 wants to merge 7 commits into
Samsung:masterfrom
ksh8281:perf

Conversation

@ksh8281

@ksh8281 ksh8281 commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

No description provided.

ksh8281 added 3 commits August 8, 2026 10:31
…to 3 slots

The MRU front-entry check (added in 6c95698) only looks at index 0, on the
assumption that a callsite's cache tends to be re-hit at whatever shape was
inserted most recently. Fresh telemetry on Preact's VNode-construction
workload showed that assumption failing hard for this tier: of ~2.06M
front-entry misses, essentially all of them (~97.6%) resolved at scan
position 1 or 2, not deeper -- shape churn here commonly rotates through a
handful of distinct shapes per callsite rather than genuinely reusing the
last one, so a single-slot check almost never hits and nearly every access
was paying the NEVER_INLINE slow-path call anyway.

Signed-off-by: Seonghyun Kim <sh8281.kim@samsung.com>
…in Set IC

Set's inline cache only ever cached plain-data-and-writable own-property
writes; any found property that turned out to be an accessor, a native
getter/setter data property, or non-writable caused the callsite to
permanently disable its IC (GiveUp: clear the cache, never try again) and
fall back to the fully generic, fully-uncached property-set path forever
after. This was asymmetric with Get's Complex tier, which already caches
non-plain-data properties via getOwnNonPlainDataPropertyUtilForObject.

Signed-off-by: Seonghyun Kim <sh8281.kim@samsung.com>
setObjectPreComputedCaseOperationCacheMiss's "property not found, cache the
transition chain" branch walks the receiver's prototype chain checking, for
each prototype, whether it's safe to cache (inline-cacheable) and whether
the chain has already hit its depth cap. Commit 86525d1 added the depth
check but folded it into the existing `!UNLIKELY(obj->isInlineCacheable())`
condition as `!UNLIKELY(A || B)` instead of `UNLIKELY(!A || B)` -- by De
Morgan that's `!A && !B`, the opposite of the intended "bail if not
cacheable OR too deep": it only bails when the prototype is NOT
inline-cacheable AND the chain is still under the cap, and otherwise keeps
walking (and caching) past inlineCacheProtoTraverseMaxCount with no bound
other than the real prototype chain's own length.

Signed-off-by: Seonghyun Kim <sh8281.kim@samsung.com>
ksh8281 added 2 commits August 8, 2026 12:12
Both job-drain loops (evalScript's post-script drain and the 262-agent
worker loop) called waitEventFromAnotherThread(10) unconditionally on
every pass, even when a same-thread job was already pending. In a
single-threaded script that event never arrives, so every microtask
drained through these loops paid up to a 10ms wall-clock stall for
nothing.

A 200-link .then() chain took ~6s wall clock (0.04s CPU) before this
fix. Only block waiting for another thread's event when there is no
same-thread job ready to run right now.

Follow-up fix folded in here: skipping the cross-thread wait whenever a
same-thread job is pending starves an already-completed cross-thread
event (e.g. an expired Atomics.wait/waitAsync timeout) indefinitely if
the same-thread job queue keeps refilling itself -- a `setTimeout(fn, 0)`
polling loop, which is exactly the pattern test262's
Atomics/waitAsync/true-for-timeout.js and friends use. hasPendingJob()
never goes false in that case, so the cross-thread check never ran at
all. This surfaced as 6 test262 waitAsync failures that had been wrongly
written off as a pre-existing/known baseline flake in earlier
verification of this same change, instead of recognized as a regression
it introduced.

Fixed by adding VMInstance::hasCompletedJobFromAnotherThread() (plus its
EscargotPublic.h/.cpp wrapper), a non-blocking peek at whether a
cross-thread job has already completed. Both job-drain loops now use it
when a same-thread job is pending, instead of skipping the cross-thread
check entirely: zero added latency in the common case (no blocking call
happens), and an already-completed cross-thread event is picked up on
the very next loop iteration instead of being starved forever.

Verified: test262 Atomics/waitAsync/{true-for-timeout,
returns-result-object-value-is-promise-resolves-to-timed-out,
bigint/true-for-timeout} all pass now; promise-benchmark.js's 200-link
.then() chain still completes in ~0.06s wall clock (no stall
reintroduced).

Signed-off-by: Seonghyun Kim <sh8281.kim@samsung.com>
Reading arguments[i] or arguments.length currently always forces
materialization of an ArgumentsObject, even when the read could be
answered directly from the call frame. Add a LoadArgumentsElement
opcode (merged .length + indexed-element query, since both need the
same "is there already a materialized ArgumentsObject" check) that:

- for .length, returns argc directly when no ArgumentsObject exists
  yet, else defers to the real object (so length-redefinition etc.
  keeps working).
- for arguments[i], returns argv[i] directly without materializing
  when index is >= parameterCount and < argc -- this range can never
  alias a mapped-arguments parameter binding, so bypassing the object
  is observably identical to going through it, for both mapped and
  unmapped arguments objects. Anything outside that range (or once an
  ArgumentsObject already exists) falls back to the normal path.

Signed-off-by: Seonghyun Kim <sh8281.kim@samsung.com>
ksh8281 added 2 commits August 8, 2026 14:38
…ands

Both ADD_CUSTOM_COMMAND compile invocations for the N-API test addons
(js-native-api/node-api upstream TCs and the custom symbol-verify addon)
passed only -DNAPI_VERSION=10, but the additive Node.js-specific
declarations they use (node_api_create_object_with_properties,
node_api_set_prototype, node_api_post_finalizer,
node_api_is_sharedarraybuffer) are guarded by `#ifdef NAPI_EXPERIMENTAL`
in the vendored test/napi-tc/src/js_native_api.h, not by NAPI_VERSION.
Without the flag those addons fail to compile with implicit-declaration
errors.

Added -DNAPI_EXPERIMENTAL -DNODE_API_EXPERIMENTAL_NOGC_ENV_OPT_OUT to
both compile commands, matching what a known-working N-API checkout
already uses.

Pre-existing gap from the N-API merge, unrelated to any other change on
this branch -- found while trying to get a clean test.sh run.

Signed-off-by: Seonghyun Kim <sh8281.kim@samsung.com>
…ecrement

Adding fast-path opcodes grows interpret()'s dispatch table and hurts
icache; the counter-move is collapsing genuinely redundant opcodes
(near-duplicate handlers differing by one field) back down.

- JumpIfTrue + JumpIfFalse -> JumpIfBoolean(shouldNegate, registerIndex).
  Identical handlers but for one negation -- exactly the shape
  JumpIfUndefinedOrNull already collapsed into one opcode via its own
  m_shouldNegate flag. Same convention: result ^ shouldNegate. No
  bytecode size change (same two fields either way), ~15 callsites
  across src/parser/ast/*.h updated to construct/peek/lastCodePosition
  the merged type with the right shouldNegate value.

- Increment/Decrement absorb ToNumericIncrement/ToNumericDecrement.
  Increment(src,dst) (prefix ++i) and ToNumericIncrement(src,storeIndex,dst)
  (postfix i++) were the same op with one extra field. Increment/Decrement
  gain an m_storeIndex field (REGISTER_LIMIT sentinel for "no postfix
  store", the same convention already used for m_receiverIndex and
  LoadArgumentsElement's m_indexRegisterIndex) and absorb the two
  ToNumeric* opcodes outright -- 2 opcodes removed, not renamed. Handler
  merge is exactly the prior split logic gated on the sentinel check.
  Only 12 callsites, all in UpdateExpression{Increment,Decrement}
  {Prefix,Postfix}Node.h, none needing lastCodePosition/peekCode.

Signed-off-by: Seonghyun Kim <sh8281.kim@samsung.com>
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