V4: track C++ heap usage for Strings in the MemoryManager
authorErik Verbruggen <erik.verbruggen@digia.com>
Fri, 3 Jul 2015 11:20:18 +0000 (13:20 +0200)
committerErik Verbruggen <erik.verbruggen@theqtcompany.com>
Fri, 10 Jul 2015 13:52:18 +0000 (13:52 +0000)
... and do a GC run when it exceeds a threshold. The issue with Strings
is that they hold on to QString instances that store the real content.
However, the GC only sees the light-weight JS handle, and doesn't take
the size of the backing content into account. So it could happen that
big QStrings accumulate in the heap as long as the GC didn't reach its
threshold.

The newly introduced unmanaged heap threshold is upped by a factor of
two when exceeded, and lowered by a factor of 2 when the used heap space
falls below a quarter of the threshold. Also grow the threshold if there
is enough space after running the GC, but another GC run would be
triggered for the next allocation.

There is a special case for Heap::String::append, because this method
will copy the data from the left and right substrings into a new
QString. To track this, append notifies the memory manager directly of
the new length. The pointer to the memory manager is stored in
Heap::String, growing it from 40 bytes to 48 bytes (which makes it still
fit in the same bucket, so no extra memory is allocated).

Task-number: QTBUG-42002
Change-Id: I71313915e593a9908a2b227b0bc4d768e375ee17
Reviewed-by: Simon Hausmann <simon.hausmann@theqtcompany.com>
src/qml/jsruntime/qv4engine.cpp
src/qml/jsruntime/qv4function.cpp
src/qml/jsruntime/qv4mm.cpp
src/qml/jsruntime/qv4mm_p.h
src/qml/jsruntime/qv4runtime.cpp
src/qml/jsruntime/qv4string.cpp
src/qml/jsruntime/qv4string_p.h

index 2dbdcfc526fa2c4b439e81d703890bd7efb00a44..0f2b44fac6dd20683658580d2fbbbd2372098c9d 100644 (file)
@@ -538,7 +538,7 @@ Heap::Object *ExecutionEngine::newObject(InternalClass *internalClass, QV4::Obje
 Heap::String *ExecutionEngine::newString(const QString &s)
 {
     Scope scope(this);
-    return ScopedString(scope, memoryManager->alloc<String>(s))->d();
+    return ScopedString(scope, memoryManager->allocWithStringData<String>(s.length() * sizeof(QChar), s))->d();
 }
 
 Heap::String *ExecutionEngine::newIdentifier(const QString &text)
index efe6c7c22618c2ca9e52e01244437b896fecf8d2..be63b31e1e86cedcfe04db1bbb342127b682b3d0 100644 (file)
@@ -66,7 +66,8 @@ Function::Function(ExecutionEngine *engine, CompiledData::CompilationUnit *unit,
                 break;
             }
             // duplicate arguments, need some trick to store them
-            arg = engine->memoryManager->alloc<String>(arg->d(), engine->newString(QString(0xfffe)));
+            MemoryManager *mm = engine->memoryManager;
+            arg = mm->alloc<String>(mm, arg->d(), engine->newString(QString(0xfffe)));
         }
     }
 
index d5576b400a4af76a861de9abdf2e829f198a6bc6..64491e13769b0d2035421d0705f6b0571adad3d5 100644 (file)
@@ -95,6 +95,8 @@ struct MemoryManager::Data
     uint maxShift;
     std::size_t maxChunkSize;
     QVector<PageAllocation> heapChunks;
+    std::size_t unmanagedHeapSize; // the amount of bytes of heap that is not managed by the memory manager, but which is held onto by managed items.
+    std::size_t unmanagedHeapSizeGCLimit;
 
     struct LargeItem {
         LargeItem *next;
@@ -123,6 +125,8 @@ struct MemoryManager::Data
         , totalAlloc(0)
         , maxShift(6)
         , maxChunkSize(32*1024)
+        , unmanagedHeapSize(0)
+        , unmanagedHeapSizeGCLimit(64 * 1024)
         , largeItems(0)
         , totalLargeItemsAllocated(0)
         , deletable(0)
@@ -157,8 +161,10 @@ struct MemoryManager::Data
 
 namespace {
 
-bool sweepChunk(MemoryManager::Data::ChunkHeader *header, uint *itemsInUse, ExecutionEngine *engine)
+bool sweepChunk(MemoryManager::Data::ChunkHeader *header, uint *itemsInUse, ExecutionEngine *engine, std::size_t *unmanagedHeapSize)
 {
+    Q_ASSERT(unmanagedHeapSize);
+
     bool isEmpty = true;
     Heap::Base *tail = &header->freeItems;
 //    qDebug("chunkStart @ %p, size=%x, pos=%x", header->itemStart, header->itemSize, header->itemSize>>4);
@@ -167,8 +173,8 @@ bool sweepChunk(MemoryManager::Data::ChunkHeader *header, uint *itemsInUse, Exec
 #endif
     for (char *item = header->itemStart; item <= header->itemEnd; item += header->itemSize) {
         Heap::Base *m = reinterpret_cast<Heap::Base *>(item);
-//        qDebug("chunk @ %p, size = %lu, in use: %s, mark bit: %s",
-//               item, m->size, (m->inUse ? "yes" : "no"), (m->markBit ? "true" : "false"));
+//        qDebug("chunk @ %p, in use: %s, mark bit: %s",
+//               item, (m->inUse() ? "yes" : "no"), (m->isMarked() ? "true" : "false"));
 
         Q_ASSERT((qintptr) item % 16 == 0);
 
@@ -183,6 +189,13 @@ bool sweepChunk(MemoryManager::Data::ChunkHeader *header, uint *itemsInUse, Exec
 #ifdef V4_USE_VALGRIND
                 VALGRIND_ENABLE_ERROR_REPORTING;
 #endif
+                if (std::size_t(header->itemSize) == MemoryManager::align(sizeof(Heap::String)) && m->gcGetVtable()->isString) {
+                    std::size_t heapBytes = static_cast<Heap::String *>(m)->retainedTextSize();
+                    Q_ASSERT(*unmanagedHeapSize >= heapBytes);
+//                    qDebug() << "-- it's a string holding on to" << heapBytes << "bytes";
+                    *unmanagedHeapSize -= heapBytes;
+                }
+
                 if (m->gcGetVtable()->destroy)
                     m->gcGetVtable()->destroy(m);
 
@@ -219,7 +232,7 @@ MemoryManager::MemoryManager(ExecutionEngine *engine)
     m_d->engine = engine;
 }
 
-Heap::Base *MemoryManager::allocData(std::size_t size)
+Heap::Base *MemoryManager::allocData(std::size_t size, std::size_t unmanagedSize)
 {
     if (m_d->aggressiveGC)
         runGC();
@@ -230,11 +243,27 @@ Heap::Base *MemoryManager::allocData(std::size_t size)
     Q_ASSERT(size >= 16);
     Q_ASSERT(size % 16 == 0);
 
+//    qDebug() << "unmanagedHeapSize:" << m_d->unmanagedHeapSize << "limit:" << m_d->unmanagedHeapSizeGCLimit << "unmanagedSize:" << unmanagedSize;
+    m_d->unmanagedHeapSize += unmanagedSize;
+    bool didGCRun = false;
+    if (m_d->unmanagedHeapSize > m_d->unmanagedHeapSizeGCLimit) {
+        runGC();
+
+        if (m_d->unmanagedHeapSizeGCLimit <= m_d->unmanagedHeapSize)
+            m_d->unmanagedHeapSizeGCLimit = std::max(m_d->unmanagedHeapSizeGCLimit, m_d->unmanagedHeapSize) * 2;
+        else if (m_d->unmanagedHeapSize * 4 <= m_d->unmanagedHeapSizeGCLimit)
+            m_d->unmanagedHeapSizeGCLimit /= 2;
+        else if (m_d->unmanagedHeapSizeGCLimit - m_d->unmanagedHeapSize < 5 * unmanagedSize)
+            // try preventing running the GC all the time when we're just below the threshold limit and manage to collect just enough to do this one allocation
+            m_d->unmanagedHeapSizeGCLimit += std::max(std::size_t(8 * 1024), 5 * unmanagedSize);
+        didGCRun = true;
+    }
+
     size_t pos = size >> 4;
 
     // doesn't fit into a small bucket
     if (size >= MemoryManager::Data::MaxItemSize) {
-        if (m_d->totalLargeItemsAllocated > 8 * 1024 * 1024)
+        if (!didGCRun && m_d->totalLargeItemsAllocated > 8 * 1024 * 1024)
             runGC();
 
         // we use malloc for this
@@ -257,7 +286,7 @@ Heap::Base *MemoryManager::allocData(std::size_t size)
     }
 
     // try to free up space, otherwise allocate
-    if (m_d->allocCount[pos] > (m_d->availableItems[pos] >> 1) && m_d->totalAlloc > (m_d->totalItems >> 1) && !m_d->aggressiveGC) {
+    if (!didGCRun && m_d->allocCount[pos] > (m_d->availableItems[pos] >> 1) && m_d->totalAlloc > (m_d->totalItems >> 1) && !m_d->aggressiveGC) {
         runGC();
         header = m_d->nonFullChunks[pos];
         if (header) {
@@ -404,7 +433,7 @@ void MemoryManager::sweep(bool lastSweep)
 
     for (int i = 0; i < m_d->heapChunks.size(); ++i) {
         Data::ChunkHeader *header = reinterpret_cast<Data::ChunkHeader *>(m_d->heapChunks[i].base());
-        chunkIsEmpty[i] = sweepChunk(header, &itemsInUse[header->itemSize >> 4], m_d->engine);
+        chunkIsEmpty[i] = sweepChunk(header, &itemsInUse[header->itemSize >> 4], m_d->engine, &m_d->unmanagedHeapSize);
     }
 
     QVector<PageAllocation>::iterator chunkIter = m_d->heapChunks.begin();
@@ -553,6 +582,11 @@ size_t MemoryManager::getLargeItemsMem() const
     return total;
 }
 
+void MemoryManager::growUnmanagedHeapSizeUsage(size_t delta)
+{
+    m_d->unmanagedHeapSize += delta;
+}
+
 MemoryManager::~MemoryManager()
 {
     delete m_persistentValues;
index 00b41b796af36650c0967f63c0f19feaa1f76bf4..7f6d2edba31b635f819fa1482f5a7959e9ee028d 100644 (file)
@@ -83,10 +83,10 @@ public:
     { return (size + 15) & ~0xf; }
 
     template<typename ManagedType>
-    inline typename ManagedType::Data *allocManaged(std::size_t size)
+    inline typename ManagedType::Data *allocManaged(std::size_t size, std::size_t unmanagedSize = 0)
     {
         size = align(size);
-        Heap::Base *o = allocData(size);
+        Heap::Base *o = allocData(size, unmanagedSize);
         o->vtable = ManagedType::staticVTable();
         return static_cast<typename ManagedType::Data *>(o);
     }
@@ -109,6 +109,15 @@ public:
         return t->d();
     }
 
+    template <typename ManagedType, typename Arg1>
+    typename ManagedType::Data *allocWithStringData(std::size_t unmanagedSize, Arg1 arg1)
+    {
+        Scope scope(engine());
+        Scoped<ManagedType> t(scope, allocManaged<ManagedType>(sizeof(typename ManagedType::Data), unmanagedSize));
+        (void)new (t->d()) typename ManagedType::Data(this, arg1);
+        return t->d();
+    }
+
     template <typename ManagedType, typename Arg1, typename Arg2>
     typename ManagedType::Data *alloc(Arg1 arg1, Arg2 arg2)
     {
@@ -159,10 +168,12 @@ public:
     size_t getAllocatedMem() const;
     size_t getLargeItemsMem() const;
 
+    void growUnmanagedHeapSizeUsage(size_t delta); // called when a JS object grows itself. Specifically: Heap::String::append
+
 protected:
     /// expects size to be aligned
     // TODO: try to inline
-    Heap::Base *allocData(std::size_t size);
+    Heap::Base *allocData(std::size_t size, std::size_t unmanagedSize);
 
 #ifdef DETAILED_MM_STATS
     void willAllocate(std::size_t size);
index c31de6a9f0235c295439f20f7dbe6abd2c62af18..b66e917b8740ac7d0f0bb0cf5495dc805c90e486 100644 (file)
@@ -521,7 +521,8 @@ QV4::ReturnedValue RuntimeHelpers::addHelper(ExecutionEngine *engine, const Valu
             return pright->asReturnedValue();
         if (!pright->stringValue()->d()->length())
             return pleft->asReturnedValue();
-        return (engine->memoryManager->alloc<String>(pleft->stringValue()->d(), pright->stringValue()->d()))->asReturnedValue();
+        MemoryManager *mm = engine->memoryManager;
+        return (mm->alloc<String>(mm, pleft->stringValue()->d(), pright->stringValue()->d()))->asReturnedValue();
     }
     double x = RuntimeHelpers::toNumber(pleft);
     double y = RuntimeHelpers::toNumber(pright);
@@ -537,7 +538,8 @@ QV4::ReturnedValue Runtime::addString(ExecutionEngine *engine, const Value &left
             return right.asReturnedValue();
         if (!right.stringValue()->d()->length())
             return left.asReturnedValue();
-        return (engine->memoryManager->alloc<String>(left.stringValue()->d(), right.stringValue()->d()))->asReturnedValue();
+        MemoryManager *mm = engine->memoryManager;
+        return (mm->alloc<String>(mm, left.stringValue()->d(), right.stringValue()->d()))->asReturnedValue();
     }
 
     Scope scope(engine);
@@ -554,7 +556,8 @@ QV4::ReturnedValue Runtime::addString(ExecutionEngine *engine, const Value &left
         return pright->asReturnedValue();
     if (!pright->stringValue()->d()->length())
         return pleft->asReturnedValue();
-    return (engine->memoryManager->alloc<String>(pleft->stringValue()->d(), pright->stringValue()->d()))->asReturnedValue();
+    MemoryManager *mm = engine->memoryManager;
+    return (mm->alloc<String>(mm, pleft->stringValue()->d(), pright->stringValue()->d()))->asReturnedValue();
 }
 
 void Runtime::setProperty(ExecutionEngine *engine, const Value &object, int nameIndex, const Value &value)
index 20dd84420cddac39838f4267bafaff6b208db85b..6d55eb2c18c5e8512734ebf90157e0a9ad52e3eb 100644 (file)
@@ -117,7 +117,8 @@ bool String::isEqualTo(Managed *t, Managed *o)
 }
 
 
-Heap::String::String(const QString &t)
+Heap::String::String(MemoryManager *mm, const QString &t)
+    : mm(mm)
 {
     subtype = String::StringType_Unknown;
 
@@ -129,7 +130,8 @@ Heap::String::String(const QString &t)
     len = text->size;
 }
 
-Heap::String::String(String *l, String *r)
+Heap::String::String(MemoryManager *mm, String *l, String *r)
+    : mm(mm)
 {
     subtype = String::StringType_Unknown;
 
@@ -187,6 +189,7 @@ void Heap::String::simplifyString() const
     text->ref.ref();
     identifier = 0;
     largestSubLength = 0;
+    mm->growUnmanagedHeapSizeUsage(size_t(text->size) * sizeof(QChar));
 }
 
 void Heap::String::createHashValue() const
index 5a0c83b4b9737b912501008bfb7e41f964e64d56..1cf8f51a292a6df1958796ce33db9be9a18bdb37 100644 (file)
@@ -53,8 +53,8 @@ struct Q_QML_PRIVATE_EXPORT String : Base {
         StringType_ArrayIndex
     };
 
-    String(const QString &text);
-    String(String *l, String *n);
+    String(MemoryManager *mm, const QString &text);
+    String(MemoryManager *mm, String *l, String *n);
     ~String() {
         if (!largestSubLength && !text->ref.deref())
             QStringData::deallocate(text);
@@ -66,6 +66,9 @@ struct Q_QML_PRIVATE_EXPORT String : Base {
                  len == (uint)text->size);
         return len;
     }
+    std::size_t retainedTextSize() const {
+        return largestSubLength ? 0 : (std::size_t(text->size) * sizeof(QChar));
+    }
     void createHashValue() const;
     inline unsigned hashValue() const {
         if (subtype == StringType_Unknown)
@@ -107,6 +110,7 @@ struct Q_QML_PRIVATE_EXPORT String : Base {
     mutable uint stringHash;
     mutable uint largestSubLength;
     uint len;
+    MemoryManager *mm;
 private:
     static void append(const String *data, QChar *ch);
 };