Title: [183974] trunk
Revision
183974
Author
[email protected]
Date
2015-05-07 19:12:35 -0700 (Thu, 07 May 2015)

Log Message

GC has trouble with pathologically large array allocations
https://bugs.webkit.org/show_bug.cgi?id=144609

Reviewed by Geoffrey Garen.
Source/_javascript_Core:


The bug was that SlotVisitor::copyLater() would return early for oversize blocks (right
after pinning them), and would skip the accounting. The GC calculates the size of the heap
in tandem with the scan to save time, and that accounting was part of how the GC would
know how big the heap was. The GC would then think that oversize copied blocks use no
memory, and would then mess up its scheduling of the next GC.
        
Fixing this bug is harder than it seems. When running an eden GC, we figure out the heap
size by summing the size from the last collection and the size by walking the eden heap.
But this breaks when we eagerly delete objects that the last collection touched. We can do
that in one corner case: copied block reallocation. The old block will be deleted from old
space during the realloc and a new block will be allocated in new space. In order for the
GC to know that the size of old space actually shrank, we need a field to tell us how much
such shrinkage could occur. Since this is a very dirty corner case and it only works for
very particular reasons arising from the special properties of copied space (single owner,
and the realloc is used in places where the compiler already knows that it cannot register
allocate a pointer to the old block), I opted for an equally dirty shrinkage counter
devoted just to this case. It's called bytesRemovedFromOldSpaceDueToReallocation.
        
To test this, I needed to add an Option to force a particular RAM size in the GC. This
allows us to write tests that assert that the GC heap size is some value X, without
worrying about machine-to-machine variations due to GC heuristics changing based on RAM
size.

* heap/CopiedSpace.cpp:
(JSC::CopiedSpace::CopiedSpace): Initialize the dirty shrinkage counter.
(JSC::CopiedSpace::tryReallocateOversize): Bump the dirty shrinkage counter.
* heap/CopiedSpace.h:
(JSC::CopiedSpace::takeBytesRemovedFromOldSpaceDueToReallocation): Swap out the counter. Used by the GC when it does its accounting.
* heap/Heap.cpp:
(JSC::Heap::Heap): Allow the user to force the RAM size.
(JSC::Heap::updateObjectCounts): Use the dirty shrinkage counter to good effect. Also, make this code less confusing.
* heap/SlotVisitorInlines.h:
(JSC::SlotVisitor::copyLater): The early return for isOversize() was the bug. We still need to report these bytes as live. Otherwise the GC doesn't know that it owns this memory.
* jsc.cpp: Add size measuring hooks to write the largeish test.
(GlobalObject::finishCreation):
(functionGCAndSweep):
(functionFullGC):
(functionEdenGC):
(functionHeapSize):
* runtime/Options.h:
* tests/stress/new-array-storage-array-with-size.js: Fix this so that it actually allocates ArrayStorage arrays and tests the thing it was supposed to test.
* tests/stress/new-largeish-contiguous-array-with-size.js: Added. This tests what the other test accidentally started testing, but does so without running your system out of memory.
(foo):
(test):

Tools:

        
Add a --filter option that restricts the set of tests we run. I needed it to fix this bug
and it's a frequently requested feature.
        
Also add the ability to run a test pretending that your system has a particular RAM size.
This is useful for GC tests, and the new GC test that I added uses this.

* Scripts/run-_javascript_core-tests:
(runJSCStressTests):
* Scripts/run-jsc-stress-tests:

Modified Paths

Added Paths

Diff

Modified: trunk/Source/_javascript_Core/ChangeLog (183973 => 183974)


--- trunk/Source/_javascript_Core/ChangeLog	2015-05-08 02:05:15 UTC (rev 183973)
+++ trunk/Source/_javascript_Core/ChangeLog	2015-05-08 02:12:35 UTC (rev 183974)
@@ -1,3 +1,55 @@
+2015-05-07  Filip Pizlo  <[email protected]>
+
+        GC has trouble with pathologically large array allocations
+        https://bugs.webkit.org/show_bug.cgi?id=144609
+
+        Reviewed by Geoffrey Garen.
+
+        The bug was that SlotVisitor::copyLater() would return early for oversize blocks (right
+        after pinning them), and would skip the accounting. The GC calculates the size of the heap
+        in tandem with the scan to save time, and that accounting was part of how the GC would
+        know how big the heap was. The GC would then think that oversize copied blocks use no
+        memory, and would then mess up its scheduling of the next GC.
+        
+        Fixing this bug is harder than it seems. When running an eden GC, we figure out the heap
+        size by summing the size from the last collection and the size by walking the eden heap.
+        But this breaks when we eagerly delete objects that the last collection touched. We can do
+        that in one corner case: copied block reallocation. The old block will be deleted from old
+        space during the realloc and a new block will be allocated in new space. In order for the
+        GC to know that the size of old space actually shrank, we need a field to tell us how much
+        such shrinkage could occur. Since this is a very dirty corner case and it only works for
+        very particular reasons arising from the special properties of copied space (single owner,
+        and the realloc is used in places where the compiler already knows that it cannot register
+        allocate a pointer to the old block), I opted for an equally dirty shrinkage counter
+        devoted just to this case. It's called bytesRemovedFromOldSpaceDueToReallocation.
+        
+        To test this, I needed to add an Option to force a particular RAM size in the GC. This
+        allows us to write tests that assert that the GC heap size is some value X, without
+        worrying about machine-to-machine variations due to GC heuristics changing based on RAM
+        size.
+
+        * heap/CopiedSpace.cpp:
+        (JSC::CopiedSpace::CopiedSpace): Initialize the dirty shrinkage counter.
+        (JSC::CopiedSpace::tryReallocateOversize): Bump the dirty shrinkage counter.
+        * heap/CopiedSpace.h:
+        (JSC::CopiedSpace::takeBytesRemovedFromOldSpaceDueToReallocation): Swap out the counter. Used by the GC when it does its accounting.
+        * heap/Heap.cpp:
+        (JSC::Heap::Heap): Allow the user to force the RAM size.
+        (JSC::Heap::updateObjectCounts): Use the dirty shrinkage counter to good effect. Also, make this code less confusing.
+        * heap/SlotVisitorInlines.h:
+        (JSC::SlotVisitor::copyLater): The early return for isOversize() was the bug. We still need to report these bytes as live. Otherwise the GC doesn't know that it owns this memory.
+        * jsc.cpp: Add size measuring hooks to write the largeish test.
+        (GlobalObject::finishCreation):
+        (functionGCAndSweep):
+        (functionFullGC):
+        (functionEdenGC):
+        (functionHeapSize):
+        * runtime/Options.h:
+        * tests/stress/new-array-storage-array-with-size.js: Fix this so that it actually allocates ArrayStorage arrays and tests the thing it was supposed to test.
+        * tests/stress/new-largeish-contiguous-array-with-size.js: Added. This tests what the other test accidentally started testing, but does so without running your system out of memory.
+        (foo):
+        (test):
+
 2015-05-07  Saam Barati  <[email protected]>
 
         Global functions should be initialized as JSFunctions in byte code

Modified: trunk/Source/_javascript_Core/heap/CopiedSpace.cpp (183973 => 183974)


--- trunk/Source/_javascript_Core/heap/CopiedSpace.cpp	2015-05-08 02:05:15 UTC (rev 183973)
+++ trunk/Source/_javascript_Core/heap/CopiedSpace.cpp	2015-05-08 02:12:35 UTC (rev 183974)
@@ -38,6 +38,7 @@
     , m_inCopyingPhase(false)
     , m_shouldDoCopyPhase(false)
     , m_numberOfLoanedBlocks(0)
+    , m_bytesRemovedFromOldSpaceDueToReallocation(0)
 {
 }
 
@@ -155,9 +156,13 @@
 
     CopiedBlock* oldBlock = CopiedSpace::blockFor(oldPtr);
     if (oldBlock->isOversize()) {
-        if (oldBlock->isOld())
+        // FIXME: Eagerly deallocating the old space block probably buys more confusion than
+        // value.
+        // https://bugs.webkit.org/show_bug.cgi?id=144750
+        if (oldBlock->isOld()) {
+            m_bytesRemovedFromOldSpaceDueToReallocation += oldBlock->size();
             m_oldGen.oversizeBlocks.remove(oldBlock);
-        else
+        } else
             m_newGen.oversizeBlocks.remove(oldBlock);
         m_blockSet.remove(oldBlock);
         CopiedBlock::destroy(oldBlock);

Modified: trunk/Source/_javascript_Core/heap/CopiedSpace.h (183973 => 183974)


--- trunk/Source/_javascript_Core/heap/CopiedSpace.h	2015-05-08 02:05:15 UTC (rev 183973)
+++ trunk/Source/_javascript_Core/heap/CopiedSpace.h	2015-05-08 02:12:35 UTC (rev 183974)
@@ -86,6 +86,13 @@
     static CopiedBlock* blockFor(void*);
 
     Heap* heap() const { return m_heap; }
+    
+    size_t takeBytesRemovedFromOldSpaceDueToReallocation()
+    {
+        size_t result = 0;
+        std::swap(m_bytesRemovedFromOldSpaceDueToReallocation, result);
+        return result;
+    }
 
 private:
     static bool isOversize(size_t);
@@ -135,6 +142,8 @@
     Mutex m_loanedBlocksLock; 
     ThreadCondition m_loanedBlocksCondition;
     size_t m_numberOfLoanedBlocks;
+    
+    size_t m_bytesRemovedFromOldSpaceDueToReallocation;
 
     static const size_t s_maxAllocationSize = CopiedBlock::blockSize / 2;
     static const size_t s_initialBlockNum = 16;

Modified: trunk/Source/_javascript_Core/heap/Heap.cpp (183973 => 183974)


--- trunk/Source/_javascript_Core/heap/Heap.cpp	2015-05-08 02:05:15 UTC (rev 183973)
+++ trunk/Source/_javascript_Core/heap/Heap.cpp	2015-05-08 02:12:35 UTC (rev 183974)
@@ -314,7 +314,7 @@
 
 Heap::Heap(VM* vm, HeapType heapType)
     : m_heapType(heapType)
-    , m_ramSize(ramSize())
+    , m_ramSize(Options::forceRAMSize() ? Options::forceRAMSize() : ramSize())
     , m_minBytesPerCycle(minHeapSize(m_heapType, m_ramSize))
     , m_sizeAfterLastCollect(0)
     , m_sizeAfterLastFullCollect(0)
@@ -818,15 +818,18 @@
 #endif
         dataLogF("\nNumber of live Objects after GC %lu, took %.6f secs\n", static_cast<unsigned long>(visitCount), WTF::monotonicallyIncreasingTime() - gcStartTime);
     }
-
-    if (m_operationInProgress == EdenCollection) {
-        m_totalBytesVisited += m_slotVisitor.bytesVisited();
-        m_totalBytesCopied += m_slotVisitor.bytesCopied();
-    } else {
-        ASSERT(m_operationInProgress == FullCollection);
-        m_totalBytesVisited = m_slotVisitor.bytesVisited();
-        m_totalBytesCopied = m_slotVisitor.bytesCopied();
-    }
+    
+    size_t bytesRemovedFromOldSpaceDueToReallocation =
+        m_storageSpace.takeBytesRemovedFromOldSpaceDueToReallocation();
+    
+    if (m_operationInProgress == FullCollection) {
+        m_totalBytesVisited = 0;
+        m_totalBytesCopied = 0;
+    } else
+        m_totalBytesCopied -= bytesRemovedFromOldSpaceDueToReallocation;
+    
+    m_totalBytesVisited += m_slotVisitor.bytesVisited();
+    m_totalBytesCopied += m_slotVisitor.bytesCopied();
 #if ENABLE(PARALLEL_GC)
     m_totalBytesVisited += m_sharedData.childBytesVisited();
     m_totalBytesCopied += m_sharedData.childBytesCopied();

Modified: trunk/Source/_javascript_Core/heap/SlotVisitorInlines.h (183973 => 183974)


--- trunk/Source/_javascript_Core/heap/SlotVisitorInlines.h	2015-05-08 02:05:15 UTC (rev 183973)
+++ trunk/Source/_javascript_Core/heap/SlotVisitorInlines.h	2015-05-08 02:12:35 UTC (rev 183974)
@@ -239,8 +239,13 @@
     ASSERT(bytes);
     CopiedBlock* block = CopiedSpace::blockFor(ptr);
     if (block->isOversize()) {
+        ASSERT(bytes <= block->size());
+        // FIXME: We should be able to shrink the allocation if bytes went below the block size.
+        // For now, we just make sure that our accounting of how much memory we are actually using
+        // is correct.
+        // https://bugs.webkit.org/show_bug.cgi?id=144749
+        bytes = block->size();
         m_shared.m_copiedSpace->pin(block);
-        return;
     }
 
     ASSERT(heap()->m_storageSpace.contains(block));

Modified: trunk/Source/_javascript_Core/jsc.cpp (183973 => 183974)


--- trunk/Source/_javascript_Core/jsc.cpp	2015-05-08 02:05:15 UTC (rev 183973)
+++ trunk/Source/_javascript_Core/jsc.cpp	2015-05-08 02:12:35 UTC (rev 183974)
@@ -447,6 +447,7 @@
 static EncodedJSValue JSC_HOST_CALL functionGCAndSweep(ExecState*);
 static EncodedJSValue JSC_HOST_CALL functionFullGC(ExecState*);
 static EncodedJSValue JSC_HOST_CALL functionEdenGC(ExecState*);
+static EncodedJSValue JSC_HOST_CALL functionHeapSize(ExecState*);
 static EncodedJSValue JSC_HOST_CALL functionDeleteAllCompiledCode(ExecState*);
 #ifndef NDEBUG
 static EncodedJSValue JSC_HOST_CALL functionReleaseExecutableMemory(ExecState*);
@@ -586,6 +587,7 @@
         addFunction(vm, "gc", functionGCAndSweep, 0);
         addFunction(vm, "fullGC", functionFullGC, 0);
         addFunction(vm, "edenGC", functionEdenGC, 0);
+        addFunction(vm, "gcHeapSize", functionHeapSize, 0);
         addFunction(vm, "deleteAllCompiledCode", functionDeleteAllCompiledCode, 0);
 #ifndef NDEBUG
         addFunction(vm, "dumpCallFrame", functionDumpCallFrame, 0);
@@ -834,23 +836,29 @@
 {
     JSLockHolder lock(exec);
     exec->heap()->collectAllGarbage();
-    return JSValue::encode(jsUndefined());
+    return JSValue::encode(jsNumber(exec->heap()->sizeAfterLastFullCollection()));
 }
 
 EncodedJSValue JSC_HOST_CALL functionFullGC(ExecState* exec)
 {
     JSLockHolder lock(exec);
     exec->heap()->collect(FullCollection);
-    return JSValue::encode(jsUndefined());
+    return JSValue::encode(jsNumber(exec->heap()->sizeAfterLastFullCollection()));
 }
 
 EncodedJSValue JSC_HOST_CALL functionEdenGC(ExecState* exec)
 {
     JSLockHolder lock(exec);
     exec->heap()->collect(EdenCollection);
-    return JSValue::encode(jsUndefined());
+    return JSValue::encode(jsNumber(exec->heap()->sizeAfterLastEdenCollection()));
 }
 
+EncodedJSValue JSC_HOST_CALL functionHeapSize(ExecState* exec)
+{
+    JSLockHolder lock(exec);
+    return JSValue::encode(jsNumber(exec->heap()->size()));
+}
+
 EncodedJSValue JSC_HOST_CALL functionDeleteAllCompiledCode(ExecState* exec)
 {
     JSLockHolder lock(exec);

Modified: trunk/Source/_javascript_Core/runtime/Options.h (183973 => 183974)


--- trunk/Source/_javascript_Core/runtime/Options.h	2015-05-08 02:05:15 UTC (rev 183973)
+++ trunk/Source/_javascript_Core/runtime/Options.h	2015-05-08 02:12:35 UTC (rev 183974)
@@ -291,6 +291,7 @@
     v(gcLogLevel, logGC, GCLogging::None, "debugging option to log GC activity (0 = None, 1 = Basic, 2 = Verbose)") \
     v(bool, disableGC, false, nullptr) \
     v(unsigned, gcMaxHeapSize, 0, nullptr) \
+    v(unsigned, forceRAMSize, 0, nullptr) \
     v(bool, recordGCPauseTimes, false, nullptr) \
     v(bool, logHeapStatisticsAtExit, false, nullptr) \
     v(bool, enableTypeProfiler, false, nullptr) \

Modified: trunk/Source/_javascript_Core/tests/stress/new-array-storage-array-with-size.js (183973 => 183974)


--- trunk/Source/_javascript_Core/tests/stress/new-array-storage-array-with-size.js	2015-05-08 02:05:15 UTC (rev 183973)
+++ trunk/Source/_javascript_Core/tests/stress/new-array-storage-array-with-size.js	2015-05-08 02:12:35 UTC (rev 183974)
@@ -1,12 +1,15 @@
-// https://bugs.webkit.org/show_bug.cgi?id=144609
-//@ skip
-
 function foo(x) {
     return new Array(x);
 }
 
 noInline(foo);
 
+// Warm up up to create array storage.
+for (var i = 0; i < 10000; ++i) {
+    var array = foo(10);
+    array.__defineSetter__(0, function(v) { });
+}
+
 function test(size) {
     var result = foo(size);
     if (result.length != size)
@@ -22,5 +25,5 @@
 }
 
 for (var i = 0; i < 100000; ++i) {
-    test(1000000);
+    test(10);
 }

Added: trunk/Source/_javascript_Core/tests/stress/new-largeish-contiguous-array-with-size.js (0 => 183974)


--- trunk/Source/_javascript_Core/tests/stress/new-largeish-contiguous-array-with-size.js	                        (rev 0)
+++ trunk/Source/_javascript_Core/tests/stress/new-largeish-contiguous-array-with-size.js	2015-05-08 02:12:35 UTC (rev 183974)
@@ -0,0 +1,47 @@
+// We only need one run of this with any GC or JIT strategy. This test is not particularly fast.
+// Unfortunately, it needs to run for a while to test the thing it's testing.
+//@ slow!
+//@ runWithRAMSize(10000000)
+
+function foo(x) {
+    return new Array(x);
+}
+
+noInline(foo);
+
+function test(size) {
+    var result = foo(size);
+    if (result.length != size)
+        throw "Error: bad result: " + result;
+    var sawThings = false;
+    for (var s in result)
+        sawThings = true;
+    if (sawThings)
+        throw "Error: array is in bad state: " + result;
+    result[0] = "42.5";
+    if (result[0] != "42.5")
+        throw "Error: array is in weird state: " + result;
+}
+
+var result = gcHeapSize();
+
+for (var i = 0; i < 1000; ++i) {
+    // The test was written when we found that large array allocations weren't being accounted for
+    // in that part of the GC's accounting that determined the GC trigger. Consequently, the GC
+    // would run too infrequently in this loop and we would use an absurd amount of memory when this
+    // loop exited.
+    test(50000);
+}
+
+// Last time I tested, the heap should be 3725734 before and 125782 after. I don't want to enforce
+// exactly that. If you regress the accounting code, the GC heap size at this point will be much
+// more than that.
+var result = gcHeapSize();
+if (result > 10000000)
+    throw "Error: heap too big before forced GC: " + result;
+
+// Do a final check after GC, just for sanity.
+gc();
+result = gcHeapSize();
+if (result > 1000000)
+    throw "Error: heap too big after forced GC: " + result;

Modified: trunk/Tools/ChangeLog (183973 => 183974)


--- trunk/Tools/ChangeLog	2015-05-08 02:05:15 UTC (rev 183973)
+++ trunk/Tools/ChangeLog	2015-05-08 02:12:35 UTC (rev 183974)
@@ -1,3 +1,20 @@
+2015-05-07  Filip Pizlo  <[email protected]>
+
+        GC has trouble with pathologically large array allocations
+        https://bugs.webkit.org/show_bug.cgi?id=144609
+
+        Reviewed by Geoffrey Garen.
+        
+        Add a --filter option that restricts the set of tests we run. I needed it to fix this bug
+        and it's a frequently requested feature.
+        
+        Also add the ability to run a test pretending that your system has a particular RAM size.
+        This is useful for GC tests, and the new GC test that I added uses this.
+
+        * Scripts/run-_javascript_core-tests:
+        (runJSCStressTests):
+        * Scripts/run-jsc-stress-tests:
+
 2015-05-07  Csaba Osztrogonác  <[email protected]>
 
         [EFL] Bump EFL version to 1.14.0

Modified: trunk/Tools/Scripts/run-_javascript_core-tests (183973 => 183974)


--- trunk/Tools/Scripts/run-_javascript_core-tests	2015-05-08 02:05:15 UTC (rev 183973)
+++ trunk/Tools/Scripts/run-_javascript_core-tests	2015-05-08 02:12:35 UTC (rev 183974)
@@ -66,6 +66,7 @@
 my $buildJSCDefault = $buildJSC ? "will check" : "will not check";
 my $testapiDefault = $runTestAPI ? "will run" : "will not run";
 my $jscStressDefault = $runJSCStress ? "will run" : " will not run";
+my $filter;
 my $usage = <<EOF;
 Usage: $programName [options] [options to pass to build system]
   --help                        Show this help message
@@ -89,6 +90,7 @@
   --shell-runner                Uses the shell-based test runner instead of the default make-based runner.
                                 In general the shell runner is slower than the make runner.
   --make-runner                 Uses the faster make-based runner.
+  --filter                      Only run tests whose name matches the given regular _expression_.
 
 EOF
 
@@ -106,6 +108,7 @@
     'child-processes=s' => \$childProcesses,
     'shell-runner' => \$shellRunner,
     'make-runner' => \$makeRunner,
+    'filter=s' => \$filter,
     'help' => \$showHelp
 );
 
@@ -313,6 +316,11 @@
     if ($makeRunner) {
         push(@jscStressDriverCmd, "--make-runner");
     }
+    
+    if ($filter) {
+        push(@jscStressDriverCmd, "--filter");
+        push(@jscStressDriverCmd, $filter);
+    }
 
     # End option processing, the rest of the arguments are tests
     push(@jscStressDriverCmd, "--");

Modified: trunk/Tools/Scripts/run-jsc-stress-tests (183973 => 183974)


--- trunk/Tools/Scripts/run-jsc-stress-tests	2015-05-08 02:05:15 UTC (rev 183973)
+++ trunk/Tools/Scripts/run-jsc-stress-tests	2015-05-08 02:12:35 UTC (rev 183974)
@@ -105,6 +105,7 @@
 $remoteDirectory = nil
 $architecture = nil
 $hostOS = nil
+$filter = nil
 
 
 def usage
@@ -130,6 +131,7 @@
     puts "--remote                    Specify a remote host on which to run tests from command line argument."
     puts "--remote-config-file        Specify a remote host on which to run tests from JSON file."
     puts "--child-processes    (-c)   Specify the number of child processes."
+    puts "--filter                    Only run tests whose name matches the given regular _expression_."
     puts "--help               (-h)   Print this message."
     exit 1
 end
@@ -152,6 +154,7 @@
                ['--remote', GetoptLong::REQUIRED_ARGUMENT],
                ['--remote-config-file', GetoptLong::REQUIRED_ARGUMENT],
                ['--child-processes', '-c', GetoptLong::REQUIRED_ARGUMENT],
+               ['--filter', GetoptLong::REQUIRED_ARGUMENT],
                ['--verbose', '-v', GetoptLong::NO_ARGUMENT]).each {
     | opt, arg |
     case opt
@@ -191,6 +194,8 @@
         $remoteConfigFile = arg
     when '--child-processes'
         $numChildProcesses = arg.to_i
+    when '--filter'
+        $filter = Regexp.new(arg)
     when '--arch'
         $architecture = arg
     when '--os'
@@ -594,7 +599,11 @@
 
 def addRunCommand(kind, command, outputHandler, errorHandler)
     $didAddRunCommand = true
-    plan = Plan.new($benchmarkDirectory, command, baseOutputName(kind), outputHandler, errorHandler)
+    name = baseOutputName(kind)
+    if $filter and name !~ $filter
+        return
+    end
+    plan = Plan.new($benchmarkDirectory, command, name, outputHandler, errorHandler)
     if $numChildProcesses > 1 and $runCommandOptions[:isSlow]
         $runlist.unshift plan
     else
@@ -644,6 +653,10 @@
     run("default")
 end
 
+def runWithRAMSize(size)
+    run("ram-size-#{size}", "--forceRAMSize=#{size}")
+end
+
 def runNoLLInt
     run("no-llint", "--useLLInt=false")
 end
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to