Repository navigation
[JSC] A catch OSR entrypoint needs a sample of every operand #772
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
ede86ed
6b15c0c
7a52d8f
d1b9040
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| //@ defaultRun; run("eager-catch-liveness", "--useLazyCatchLiveness=false") | ||
|
|
||
| // A catch OSR entrypoint must not give the DFG an operand without a prediction. Later phases take SpecNone for code | ||
| // that never ran: a double use of such a value became DoubleRep(NotCellNorBigIntUse), which is ToNumber, not a check. | ||
|
|
||
| function thrower(shouldThrow) { | ||
| if (shouldThrow) throw new Error("thrown"); | ||
| } | ||
| noInline(thrower); | ||
|
|
||
| function doubleArray() { | ||
| const array = [0.5, 1.5]; | ||
| array[0] = 2.5; | ||
| return array; | ||
| } | ||
| noInline(doubleArray); | ||
|
|
||
| const oddValues = [null, true, undefined, false]; | ||
|
|
||
| function check(name, value, result) { | ||
| if (!Object.is(result, value) && result !== String(value)) | ||
| throw new Error(name + ": saw " + result + " (" + typeof result + ") in place of " + value); | ||
| } | ||
|
|
||
| // With Options::useLazyCatchLiveness() these catches run before their value-profile buffer exists and not again | ||
| // until the DFG has compiled the function, so the buffer has no sample at all. | ||
| const neverSampled = { | ||
| localAfterCatch(value, shouldThrow) { | ||
| const t = value.p; | ||
| try { | ||
| thrower(shouldThrow); | ||
| } catch {} | ||
| return String(t); | ||
| }, | ||
|
|
||
| localInFinally(value, shouldThrow) { | ||
| const t = value.p; | ||
| let result; | ||
| try { | ||
| try { | ||
| thrower(shouldThrow); | ||
| } finally { | ||
| result = String(t); | ||
| } | ||
| } catch {} | ||
| return result; | ||
| }, | ||
|
|
||
| localInNewArrayInCatch(value, shouldThrow) { | ||
| const t = value.p; | ||
| try { | ||
| thrower(shouldThrow); | ||
| } catch { | ||
| return [t][0]; | ||
| } | ||
| return t; | ||
| }, | ||
|
|
||
| argumentInNewArrayInCatch(value, shouldThrow, t) { | ||
| try { | ||
| thrower(shouldThrow); | ||
| } catch { | ||
| return [t][0]; | ||
| } | ||
| return t; | ||
| }, | ||
| }; | ||
|
|
||
| for (const name in neverSampled) { | ||
| const f = neverSampled[name]; | ||
| noInline(f); | ||
| f({ p: 0.5 }, true, 0.5); | ||
| for (let i = 0; i < testLoopCount; ++i) f({ p: i + 0.5 }, false, i + 0.5); | ||
| for (const value of oddValues) check(name, value, f({ p: value }, true, value)); | ||
| } | ||
|
|
||
| // Whatever the option says: t is in its TDZ in each sample that reaches the prediction, and an empty bucket is no sample. | ||
| function sampledWhileEmpty(skipDeclaration, value, shouldThrow, array) { | ||
| switch (skipDeclaration) { | ||
| case 0: | ||
| let t = value.p; | ||
| case 1: | ||
| try { | ||
| thrower(shouldThrow); | ||
| } catch { | ||
| if (!skipDeclaration) { | ||
| array[0] = t; | ||
| return array[0]; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| noInline(sampledWhileEmpty); | ||
|
|
||
| for (let i = 0; i < 10; ++i) sampledWhileEmpty(0, { p: i + 0.5 }, true, doubleArray()); | ||
| for (let i = 0; i < testLoopCount; ++i) { | ||
| sampledWhileEmpty(0, { p: i + 0.5 }, false, doubleArray()); | ||
| if (!(i % 20)) sampledWhileEmpty(1, { p: 0.5 }, true, doubleArray()); | ||
| } | ||
| for (const value of oddValues) | ||
| check("sampledWhileEmpty", value, sampledWhileEmpty(0, { p: value }, true, doubleArray())); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3254,7 +3254,7 @@ JSC_DEFINE_JIT_OPERATION(operationTryOSREnterAtCatchAndValueProfile, UGPRPair, ( | |||||||||||||||||||||||||||||||||||||
| if (!buffer) | ||||||||||||||||||||||||||||||||||||||
| OPERATION_RETURN(scope, encodeResult(nullptr, nullptr)); | ||||||||||||||||||||||||||||||||||||||
| buffer->forEach([&] (ValueProfileAndVirtualRegister& profile) { | ||||||||||||||||||||||||||||||||||||||
| profile.m_buckets[0] = JSValue::encode(callFrame->uncheckedR(profile.m_operand).jsValue()); | ||||||||||||||||||||||||||||||||||||||
| profile.sample(callFrame->uncheckedR(profile.m_operand).jsValue()); | ||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 (optional) Under default lazy catch liveness, a rarely firing catch never gets a DFG or FTL OSR entrypoint, so every caught exception OSR-exits to baseline permanently. The new bail at DFGByteCodeParser.cpp:10241 skips the entrypoint when the buffer has no sample. operationTryOSREnterAtCatchAndValueProfile returns at JITOperations.cpp:3246 before sampling whenever a replacement exists, so the buffer never gains a sample and the FTL plan bails too. Fix: when the replacement has no entrypoint for this catch, still create and sample the buffer so a later plan can make one. The base made an entrypoint with SpecNone predictions instead. Why this was flaggedTrigger: Options::useLazyCatchLiveness() is on (OptionsList.h:722 default true). A function's catch runs once in LLInt/Baseline before the first DFG threshold. operationOptimize creates the buffer and delays once (JITOperations.cpp:3137-3143). The catch does not fire in that window, so at parse time every operand's computeUpdatedPrediction() is SpecNone and the parser takes NEXT_OPCODE at DFGByteCodeParser.cpp:10237-10241: no catch entrypoint. From then on each caught exception OSR-exits to the baseline op_catch, which calls operationTryOSREnterAtCatchAndValueProfile; with a DFG/FTL replacement it returns at JITOperations.cpp:3240-3246 and never reaches the sample() loop at 3256-3258, so the buffer stays unsampled. The later FTL plan parses the same baseline block and bails identically. Exception exits never jettison (ExitKind.cpp:34-36; DFGOSRExitCompilerCommon.cpp:44-50), so nothing ever recompiles with an entrypoint; the handler and the rest of the call run in baseline on every exception. On the base branch the entrypoint was made (with SpecNone predictions, the bug this PR fixes). Verification: Triggered when Options::useLazyCatchLiveness() is on (default true, OptionsList.h:722) and a catch executes before the first DFG threshold but not again during the single warm-up delay; the catch then never becomes a DFG or FTL OSR entrypoint. DFGByteCodeParser.cpp:10237-10241: if any operand's computeUpdatedPrediction() is SpecNone the parser does NEXT_OPCODE(op_catch) and never sets isCatchEntrypoint.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not in this PR. I built and measured exactly this: on a refused entry the operation samples, and the lazy option stops deferring the buffer once the block has optimized code. It does end the stuck state, but the effect on run time goes both ways. One loop of 1e9 iterations whose handler first runs after tier-up, release + LTO, median of 5 fresh processes, ms:
Without it the first exceptions are nearly free and each later one costs more. With it the loop settles at about twice the time of one that never throws: the FTL code that has the catch entrypoint runs that loop slower (18 ms against 11 ms for 5 million iterations). In another loop the entrypoint made no difference, and I do not know yet which loops it applies to. It also changes the state that a catch that first runs after tier-up has always been in, with the option on or off. So it gets its own PR. What leaving it out costs here is in the Performance section of the description. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win Keep profiling when the replacement has no catch entrypoint. If lazy catch liveness leaves the buffer unsampled at compile time, the parser now rejects the catch entrypoint until every operand has a prediction. (github.com) When a DFG/FTL replacement exists, this operation returns from the 🤖 Prompt for AI Agents
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not in this PR. I built and measured exactly this: on a refused entry the operation samples, and the lazy option stops deferring the buffer once the block has optimized code. It does end the stuck state, but the effect on run time goes both ways. One loop of 1e9 iterations whose handler first runs after tier-up, release + LTO, median of 5 fresh processes, ms:
Without it the first exceptions are nearly free and each later one costs more. With it the loop settles at about twice the time of one that never throws: the FTL code that has the catch entrypoint runs that loop slower (18 ms against 11 ms for 5 million iterations). In another loop the entrypoint made no difference, and I do not know yet which loops it applies to. It also changes the state that a catch that first runs after tier-up has always been in, with the option on or off. So it gets its own PR. What leaving it out costs here is in the Performance section of the description. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Your measurements show a trade-off: collecting samples after refused catch OSR entry removes the profiling gap, but the resulting FTL catch entrypoint can slow normal loop execution. This also changes existing behavior for catches that first execute after tier-up. Keeping this change in a separate PR makes sense. No change is needed here for this finding. Would you like me to open a follow-up issue for the separate PR? ✏️ Learnings added
You are interacting with an AI system. |
||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| OPERATION_RETURN(scope, encodeResult(nullptr, nullptr)); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.