Skip to content
Permalink
Browse files

OSR entry into DFG has problems with lexical scoping

https://bugs.webkit.org/show_bug.cgi?id=159687

Reviewed by Saam Barati.
Source/JavaScriptCore:

        
What a fun bug! It turns out that uses of lexical scoping, like "let", may sometimes cause us
to not be able to OSR enter into a loop from baseline to DFG. The bug is in a mitigation for
a different bug, which in turn had a mitigation for yet another bug, so the story here is a
long one.
        
DFG OSR entry has long had a mitigation for the following bug: the DFG bytecode parser may
choose to make us always OSR exit at some instruction if it thinks that it doesn't have
enough profiling for that instruction. We will do this if some kinds of put_by_id only
execute once, for example. This causes problems for loopy benchmarks like this:
        
    put_by_id(something crazy);
    for (var i = 0; i < bigNumber; ++i) simpleMath;
        
In this case, the put_by_id will have only executed once, and since it did something crazy
that one time, the bytecode parser will replace it with ForceOSRExit.
        
This creates an OSR entry bug: DFG CFA will then prove that the loop is unreachable, and will
tell OSR entry that it's impossible to enter into that loop.
        
We mitigated this bug a long time ago by recording mustHandleValues for loops at which we
want to enter. We inject these values into DFG CFA and we force CFA to recognize that the
loop is reachable even if CFA wanted to prove that it wasn't.
        
But this leads to another bug: we need to scrape the values from the stack inside
operationOptimize() and then we need to reason about them in the compiler. Some of those
values may be garbage, which would cause pandemonium inside the compiler. We also mitigated
this bug, by only recording the "vars", since those are guaranteed to be reset by op_enter.
        
And that's where the lexical scoping bug happens: "let" bound variables aren't part of the
"vars". DFG will see that they are live, but mustHandleValues will not have anything for
those variables, so CFA will prove that the values are Bottom. Then OSR entry will always
fail because no value is ever a subset of Bottom.
        
The first part of the fix is to ensure that mustHandleValues record all of the values on the
stack (i.e. within m_numCalleeLocals, rather than just m_numVars). But this creates a second
problem: we may record garbage. This patch includes a better fix for the garbage: before
touching mustHandleValues we run the bytecode liveness analysis and clear any values that are
not live. This ensures that we clear the garbage.
        
This is an enormous speed-up on microbenchmarks that use lexical scoping and have some crazy
put_by_id in the lead-up to the hot loop.

* dfg/DFGCFAPhase.cpp:
(JSC::DFG::CFAPhase::run):
* dfg/DFGOSREntry.cpp:
(JSC::DFG::prepareOSREntry):
* dfg/DFGPlan.cpp:
(JSC::DFG::Plan::compileInThreadImpl):
(JSC::DFG::Plan::checkLivenessAndVisitChildren):
(JSC::DFG::Plan::cancel):
(JSC::DFG::Plan::cleanMustHandleValuesIfNecessary):
* dfg/DFGPlan.h:
(JSC::DFG::Plan::canTierUpAndOSREnter):
* jit/JITOperations.cpp:

LayoutTests:


* js/regress/script-tests/strict-osr-entry.js: Added.
(let.o.apply_):
* js/regress/strict-osr-entry-expected.txt: Added.
* js/regress/strict-osr-entry.html: Added.



git-svn-id: http://svn.webkit.org/repository/webkit/trunk@203356 268f45cc-cd09-0410-ab3c-d52691b4dbfc
  • Loading branch information
fpizlo@apple.com
fpizlo@apple.com committed Jul 18, 2016
1 parent ccc104b commit e66e8921c44e622574b0120bccb58aec7ebb0b03
@@ -1,3 +1,15 @@
2016-07-12 Filip Pizlo <fpizlo@apple.com>

OSR entry into DFG has problems with lexical scoping
https://bugs.webkit.org/show_bug.cgi?id=159687

Reviewed by Saam Barati.

* js/regress/script-tests/strict-osr-entry.js: Added.
(let.o.apply_):
* js/regress/strict-osr-entry-expected.txt: Added.
* js/regress/strict-osr-entry.html: Added.

2016-07-18 Youenn Fablet <youenn@apple.com>

[Streams API] ReadableStream should throw a RangeError in case of NaN highWaterMark
@@ -0,0 +1,17 @@
"use strict";

(function() {
let o = {
apply_(a, b) {
return a + b;
}
};

let result = 0;
for (let i = 0; i < 10000000; ++i)
result = o.apply_(result, 1);

if (result != 10000000)
throw new "Bad result: " + result;
})();

@@ -0,0 +1,10 @@
JSRegress/strict-osr-entry

On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".


PASS no exception thrown
PASS successfullyParsed is true

TEST COMPLETE

@@ -0,0 +1,12 @@
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
<head>
<script src="../../resources/js-test-pre.js"></script>
</head>
<body>
<script src="../../resources/regress-pre.js"></script>
<script src="script-tests/strict-osr-entry.js"></script>
<script src="../../resources/regress-post.js"></script>
<script src="../../resources/js-test-post.js"></script>
</body>
</html>
@@ -1,3 +1,65 @@
2016-07-12 Filip Pizlo <fpizlo@apple.com>

OSR entry into DFG has problems with lexical scoping
https://bugs.webkit.org/show_bug.cgi?id=159687

Reviewed by Saam Barati.

What a fun bug! It turns out that uses of lexical scoping, like "let", may sometimes cause us
to not be able to OSR enter into a loop from baseline to DFG. The bug is in a mitigation for
a different bug, which in turn had a mitigation for yet another bug, so the story here is a
long one.

DFG OSR entry has long had a mitigation for the following bug: the DFG bytecode parser may
choose to make us always OSR exit at some instruction if it thinks that it doesn't have
enough profiling for that instruction. We will do this if some kinds of put_by_id only
execute once, for example. This causes problems for loopy benchmarks like this:

put_by_id(something crazy);
for (var i = 0; i < bigNumber; ++i) simpleMath;

In this case, the put_by_id will have only executed once, and since it did something crazy
that one time, the bytecode parser will replace it with ForceOSRExit.

This creates an OSR entry bug: DFG CFA will then prove that the loop is unreachable, and will
tell OSR entry that it's impossible to enter into that loop.

We mitigated this bug a long time ago by recording mustHandleValues for loops at which we
want to enter. We inject these values into DFG CFA and we force CFA to recognize that the
loop is reachable even if CFA wanted to prove that it wasn't.

But this leads to another bug: we need to scrape the values from the stack inside
operationOptimize() and then we need to reason about them in the compiler. Some of those
values may be garbage, which would cause pandemonium inside the compiler. We also mitigated
this bug, by only recording the "vars", since those are guaranteed to be reset by op_enter.

And that's where the lexical scoping bug happens: "let" bound variables aren't part of the
"vars". DFG will see that they are live, but mustHandleValues will not have anything for
those variables, so CFA will prove that the values are Bottom. Then OSR entry will always
fail because no value is ever a subset of Bottom.

The first part of the fix is to ensure that mustHandleValues record all of the values on the
stack (i.e. within m_numCalleeLocals, rather than just m_numVars). But this creates a second
problem: we may record garbage. This patch includes a better fix for the garbage: before
touching mustHandleValues we run the bytecode liveness analysis and clear any values that are
not live. This ensures that we clear the garbage.

This is an enormous speed-up on microbenchmarks that use lexical scoping and have some crazy
put_by_id in the lead-up to the hot loop.

* dfg/DFGCFAPhase.cpp:
(JSC::DFG::CFAPhase::run):
* dfg/DFGOSREntry.cpp:
(JSC::DFG::prepareOSREntry):
* dfg/DFGPlan.cpp:
(JSC::DFG::Plan::compileInThreadImpl):
(JSC::DFG::Plan::checkLivenessAndVisitChildren):
(JSC::DFG::Plan::cancel):
(JSC::DFG::Plan::cleanMustHandleValuesIfNecessary):
* dfg/DFGPlan.h:
(JSC::DFG::Plan::canTierUpAndOSREnter):
* jit/JITOperations.cpp:

2016-07-18 Youenn Fablet <youenn@apple.com>

REGRESSION(r202975): --minimal build is broken
@@ -80,6 +80,9 @@ class CFAPhase : public Phase {
} while (m_changed);

if (m_graph.m_form != SSA) {
if (m_verbose)
dataLog(" Widening state at OSR entry block.\n");

ASSERT(!m_changed);

// Widen the abstract values at the block that serves as the must-handle OSR entry.
@@ -93,14 +96,23 @@ class CFAPhase : public Phase {
if (block->bytecodeBegin != m_graph.m_plan.osrEntryBytecodeIndex)
continue;

if (m_verbose)
dataLog(" Found must-handle block: ", *block, "\n");

bool changed = false;
for (size_t i = m_graph.m_plan.mustHandleValues.size(); i--;) {
int operand = m_graph.m_plan.mustHandleValues.operandForIndex(i);
JSValue value = m_graph.m_plan.mustHandleValues[i];
Node* node = block->variablesAtHead.operand(operand);
if (!node)
if (!node) {
if (m_verbose)
dataLog(" Not live: ", VirtualRegister(operand), "\n");
continue;
}

if (m_verbose)
dataLog(" Widening ", VirtualRegister(operand), " with ", value, "\n");

AbstractValue& target = block->valuesAtHead.operand(operand);
changed |= target.mergeOSREntryValue(m_graph, value);
target.fixTypeForRepresentation(
@@ -228,7 +228,7 @@ void* prepareOSREntry(ExecState* exec, CodeBlock* codeBlock, unsigned bytecodeIn
if (!entry->m_expectedValues.local(local).validate(exec->registers()[localOffset].asanUnsafeJSValue())) {
if (Options::verboseOSR()) {
dataLog(
" OSR failed because variable ", localOffset, " is ",
" OSR failed because variable ", VirtualRegister(localOffset), " is ",
exec->registers()[localOffset].asanUnsafeJSValue(), ", expected ",
entry->m_expectedValues.local(local), ".\n");
}
@@ -238,6 +238,8 @@ void Plan::compileInThread(LongLivedState& longLivedState, ThreadData* threadDat

Plan::CompilationPath Plan::compileInThreadImpl(LongLivedState& longLivedState)
{
cleanMustHandleValuesIfNecessary();

if (verboseCompilationEnabled(mode) && osrEntryBytecodeIndex != UINT_MAX) {
dataLog("\n");
dataLog("Compiler must handle OSR entry from bc#", osrEntryBytecodeIndex, " with values: ", mustHandleValues, "\n");
@@ -626,7 +628,8 @@ void Plan::checkLivenessAndVisitChildren(SlotVisitor& visitor)
{
if (!isKnownToBeLiveDuringGC())
return;


cleanMustHandleValuesIfNecessary();
for (unsigned i = mustHandleValues.size(); i--;)
visitor.appendUnbarrieredValue(&mustHandleValues[i]);

@@ -675,6 +678,29 @@ void Plan::cancel()
stage = Cancelled;
}

void Plan::cleanMustHandleValuesIfNecessary()
{
LockHolder locker(mustHandleValueCleaningLock);

if (!mustHandleValuesMayIncludeGarbage)
return;

mustHandleValuesMayIncludeGarbage = false;

if (!codeBlock)
return;

if (!mustHandleValues.numberOfLocals())
return;

FastBitVector liveness = codeBlock->alternative()->livenessAnalysis().getLivenessInfoAtBytecodeOffset(osrEntryBytecodeIndex);

for (unsigned local = mustHandleValues.numberOfLocals(); local--;) {
if (!liveness.get(local))
mustHandleValues.local(local) = jsUndefined();
}
}

} } // namespace JSC::DFG

#endif // ENABLE(DFG_JIT)
@@ -78,6 +78,8 @@ struct Plan : public ThreadSafeRefCounted<Plan> {

bool canTierUpAndOSREnter() const { return !tierUpAndOSREnterBytecodes.isEmpty(); }

void cleanMustHandleValuesIfNecessary();

// Warning: pretty much all of the pointer fields in this object get nulled by cancel(). So, if
// you're writing code that is callable on the cancel path, be sure to null check everything!

@@ -90,6 +92,8 @@ struct Plan : public ThreadSafeRefCounted<Plan> {
CompilationMode mode;
const unsigned osrEntryBytecodeIndex;
Operands<JSValue> mustHandleValues;
bool mustHandleValuesMayIncludeGarbage { true };
Lock mustHandleValueCleaningLock;

ThreadData* threadData;

@@ -1353,7 +1353,7 @@ SlowPathReturnType JIT_OPERATION operationOptimize(ExecState* exec, int32_t byte

unsigned numVarsWithValues;
if (bytecodeIndex)
numVarsWithValues = codeBlock->m_numVars;
numVarsWithValues = codeBlock->m_numCalleeLocals;
else
numVarsWithValues = 0;
Operands<JSValue> mustHandleValues(codeBlock->numParameters(), numVarsWithValues);

0 comments on commit e66e892

Please sign in to comment.
You can’t perform that action at this time.