Make bulk built-ins interruptible by execution constraints (#2486)#2487
Merged
Conversation
…ros#2486) Several built-ins performed a full bulk operation in a single CLR call without per-step constraint checks, so TimeoutInterval / MaxStatements / LimitMemory could not interrupt them. For large inputs they either hung for seconds or threw an uncatchable System.OutOfMemoryException instead of one of Jint's catchable constraint exceptions. This continues the fix class of sebastienros#2349 and sebastienros#2465 across the remaining sites (the reporter and maintainer asked to also cover String.raw, Array.prototype.join/sort and any other affected methods): - String.prototype.padStart/padEnd: use ToLength instead of ToInt32 so a large length can't wrap past the size cap; resolve the fill string only after the length check (spec-correct side-effect ordering); throw a catchable RangeError when the result exceeds ClrLimits.MaxArrayLength; build incrementally with periodic checks instead of allocating ~3x the result up front. Net effect: the common small-pad case allocates ~60% less and is slightly faster. - Array.from, String.raw / fromCharCode / fromCodePoint: periodic checks in the element/char loops; size cap added to fromCharCode. - Array.prototype and %TypedArray%.prototype bulk loops (join, sort, map, filter, forEach, reduce/reduceRight, find*, slice, splice, unshift, shift, concat, toSpliced, toLocaleString, set, and the slow indexOf/lastIndexOf/ includes scans): periodic checks. Dense primitive fast-path scans are left unguarded. - RegExp.prototype @@replace / @@split / @@match outer match-collection loops. - JSON.stringify breadth (array/object/replacer) and JSON.parse array/object/ string-literal loops. Reuses the existing `% ConstraintCheckInterval (10_000)` idiom; the predicate is only added to loops whose bodies are already heavy (a JS call, property Get/Set, ToString, regex NextMatch), so the cost is within measurement noise and allocation counts are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… gaps Follow-up to the execution-constraints work, addressing code-review findings. Correctness: - Array.prototype.join/toLocaleString: wrap the element loop in try/finally so the entry is always popped from the long-lived join stack even when a periodic Constraints.Check() throws. Previously an interrupted join could leave the array on the stack, making a later join of the same array silently return "". - Return pooled resources on the Check()-throw path: 'using' the ValueStringBuilder in padStart/padEnd, repeat and toWellFormed; try/finally around the rented args array in ArrayInstance/ObjectInstance FindWithCallback (this also fixes a pre-existing leak on the return-on-match path) and in ConstructArrayFromIEnumerable. Close remaining gaps of the same class (bulk loops over JS-controlled sizes that the first pass missed) — these matter for native/host iterators and native callbacks that run no per-element JS statements and so cannot self-throttle: - IteratorProtocol.Execute + AddEntriesFromIterable, and the Set/WeakSet constructor loops: covers Array.from(iterable), [...iterable], new Map/Set/WeakMap/WeakSet and Object.fromEntries. - ObjectInstance.EnumerableOwnProperties (Object.keys/values/entries) and Object.assign. - ArrayOperations.GetAll: the array-like expansion behind Function.prototype.apply and Reflect.apply/construct (the existing size cap is kept; this adds interruptibility). - Map.prototype.forEach / Set.prototype.forEach. Cleanup: - Single-source the interval as Engine.ConstraintCheckInterval and alias the per-class consts to it, so the value is defined once. - Remove the unreachable size cap in String.fromCharCode (length is bounded by the already-materialized arguments array, unlike padStart/repeat). Tests: fixed ShouldLimitArraySizeForForEach (the previous version tripped fill() before forEach ever ran, so it gave no coverage of the forEach guard) and added coverage for join-stack recovery after interruption, the native-iterator Array.from path, Object.keys, and Map.forEach with a native callback. All green: Jint.Tests 3028 passed, Test262 99,015 passed, 0 regressions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jun 4, 2026
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 #2486.
Problem
Several built-ins perform a full bulk operation in a single CLR call without per-step constraint checks, so a configured
TimeoutInterval/MaxStatements/LimitMemorycannot interrupt them. For large inputs they either hang for seconds (no constraint can fire until the call returns) or the CLR throwsSystem.OutOfMemoryException— which is not one of Jint's catchable constraint exceptions (TimeoutException,MemoryLimitExceededException,StatementsCountOverflowException).The cases from the issue:
This is the same fix class as #2349 (BigInt
**) and #2465 (String.prototype.repeat,BigInt.asIntN/asUintN,Function.prototype.apply). The reporter and maintainer asked to also coverString.raw,Array.prototype.join,Array.prototype.sort, and any other affected sites, so this PR does the full sweep.Approach
Two complementary mechanisms already present in the codebase are reused:
ClrLimits.MaxArrayLength(0x7FFFFFC7). Converts a would-beOutOfMemoryExceptioninto a catchableRangeError, exactly asString.prototype.repeatalready does.ConstraintCheckInterval = 10_000idiom (already used byArray.prototype.fill/reverse/with, etc.):Check()in a native loop lets all three constraints fire: time (stopwatch), memory (allocations accumulate as the structure grows), and statements (the counter increments perCheck()).A single giant up-front allocation can't be interrupted before it OOMs, so it is either pre-capped (mechanism 1) or built incrementally with periodic checks (mechanism 2).
Changes
String.prototype.padStart/padEnd(the issue) — rewritten:ToLengthinstead ofToInt32so a largemaxLengthcan't wrap and slip past the size cap (spec step 2).fillString(and run itsToString) only after themaxLength <= stringLengthearly return — fixes an observable side-effect-ordering bug.RangeError("Invalid string length")when the result exceedsMaxArrayLength.Enumerable.Repeat+string.Join+Substring+ interpolation (~3× transient allocation).Array.from(the issue) — periodic checks inConstructArrayFromArrayLikeandConstructArrayFromIEnumerable.String.raw/fromCharCode/fromCodePoint— periodic checks in the element/char loops; size cap added tofromCharCode.Array.prototype&%TypedArray%.prototypebulk loops —join,sort(collection/write-back),map,filter,forEach,reduce/reduceRight,find*(viaFindWithCallback),slice,splice,unshift,shift,concat,toSpliced,toLocaleString,set, and the slowindexOf/lastIndexOf/includesscans. Dense primitive fast-path scans are intentionally left unguarded (they're tight, bounded byMaxArraySize, and the one place a per-iteration check would actually cost).RegExp.prototype—@@replace/@@split/@@matchouter match-collection loops.JSON—JSON.stringifybreadth (array/object/replacer) andJSON.parsearray/object/string-literal loops (depth was already capped; breadth was not).Not changed (already covered / fast paths)
ArrayBuffer/TypedArrayconstructors — already capped atint.MaxValue.String.prototype.repeatsingle-char path,new Array(N)— already capped/lazy.MaxArraySize-bounded.Performance
Reuses the
% ConstraintCheckIntervalidiom (RyuJIT lowers% constto a multiply+shift; the branch is predicted not-taken 9999/10000). The predicate is added only to loops whose body is already heavy (a JS call, propertyGet/Set,ToString, regexNextMatch), and the common small-input case runs the predicate a handful of times.Measured with BenchmarkDotNet (
--job short, net10.0, 1000 ops/benchmark), base (main) vs this branch:padStart(small)padEnd(small)filter(1000-elem)forEach(1000-elem)join(1000-elem)padStart/padEnd: ~60% fewer allocations and slightly faster — the rewrite removes the old 3× transient allocation.ShortRunconfig — between two baseline runsfilteralone varied 119,636 → 138,494 µs (±16%), which is larger than any of the before/after deltas.Testing
padStart/padEndconformance tests pass, confirming theToLength+ side-effect-ordering changes are spec-correct.ExecutionConstraintTestsfor the three issue repros plusString.raw,sort, an empty-callbackforEach,padStart/padEndcorrectness, and the fill-string side-effect ordering.🤖 Generated with Claude Code