aboutsummaryrefslogtreecommitdiffstats
path: root/src/qml/memory
diff options
context:
space:
mode:
Diffstat (limited to 'src/qml/memory')
-rw-r--r--src/qml/memory/design.md125
-rw-r--r--src/qml/memory/qv4mm.cpp576
-rw-r--r--src/qml/memory/qv4mm_p.h194
-rw-r--r--src/qml/memory/qv4mmdefs_p.h19
-rw-r--r--src/qml/memory/qv4stacklimits.cpp20
-rw-r--r--src/qml/memory/qv4writebarrier.cpp36
-rw-r--r--src/qml/memory/qv4writebarrier_p.h142
7 files changed, 935 insertions, 177 deletions
diff --git a/src/qml/memory/design.md b/src/qml/memory/design.md
new file mode 100644
index 0000000000..1b4eb8eac4
--- /dev/null
+++ b/src/qml/memory/design.md
@@ -0,0 +1,125 @@
+The V4 Garbage Collector
+========================
+
+ChangeLog
+---------
+- < 6.8: There was little documentation, and the gc was STW mark&sweep
+- 6.8: The gc became incremental (with a stop-the-world sweep phase)
+- 6.8: Sweep was made incremental, too
+
+
+Glossary:
+------------
+- V4: The ECMAScript engine used in Qt (qtdeclarative)
+- gc: abbreviation for garbage collector
+- mutator: the actual user application (in constrast to the collector)
+- roots: A set of pointers from which the gc process starts
+- fast roots: A subset of the roots which are not protected by a barrier
+- write barrier: A set of instructions executed on each write
+- stop the world: not concurrent
+- concurrent gc: gc and mutator can run "at the same time"; this can either mean
+ + incremental: collector and mutator run in the same thread, but in certain time intervals the mutator is paused, a chunk of the collector is executed, and then the mutator resumes. Repeats until the gc cycle is finished
+ + parallel: gc and mutator operations run in different threads
+- precise: a gc is precise if for every value it can know whether it's a pointer to a heap object (a non-precise gc can't in general distinguish pointers from pointer-sized numbers)
+- floating garbage: items that are not live, but nevertheless end up surviving the gc cycle
+- generational: generational refers to dividing objects into different "generations" based on how many collection cycles they survived. This technique is used in garbage collection to improve performance by focusing on collecting the most recently created objects first.
+- moving: A gc is moving if it can relocate objects in memory. Care must be taken to update pointers pointing to them.
+
+
+Overview:
+---------
+
+Since Qt 6.8, V4 uses an incremental, precise mark-and-sweep gc algorithm. It is neither generational nor moving.
+
+In the mark phase, each heap-item can be in one of three states:
+1. unvisited ("white"): The gc has not seen this item at all
+2. seen ("grey"): All grey items have been discovered by the gc, but items directly reachable from them have (potentially) not been visited.
+3. finished ("black"): Not only has the item been seen, but also all items directly reachable from it have been seen.
+
+Items are black if they have their corresponding bit set in the black-object bitmap. They are grey if they are stored at least once in the MarkStack, a stack data structure. Items are white if they are neither grey nor black. Note that black items must never be pushed to the MarkStack (otherwise we could easily end up with endless cycles), but items already _on_ the MarkStack can be black:
+If an item has been pushed multiple times before it has been popped, this can happen. It causes some additional work to revisit its fields, but that is safe, as after popping the item will be black, and thus we won't keep on repeatedly pushing the same item on the mark stack.
+
+The roots consist of
+- the engine-global objects (namely the internal classes for the JS globals)
+- all strings (and symbols) stored in the identifier table and
+- all actively linked compilation units.
+- Moreover, the values on the JS stack are also treated as roots; more precisely as fast roots.
+- Additionally, all persistent values (everything stored in a QJSValue as well as all bound functions of QQmlBindings) are added to the roots.
+- Lastly, all QObjectWrapper of QObjects with C++ ownership, or which are rooted in or parented to a QObject with C++ ownership are added to the root set.
+
+All roots are added to the MarkStack. Then, during mark phase, entries are:
+1. popped from the markstack
+2. All heap-objects reachable from them are added to the MarkStack (unless they are already black)
+
+To avoid that values that were on the heap during the start of the gc cycle, then moved to the stack before they could be visited and consequently freed even though they are still live, the stack is rescanned before the sweep phase.
+
+To avoid that unmarked heap-items are moved from one heap item (or the stack) to an already marked heap-item (and consequently end up hidden from the gc), we employ a Dijkstra style write barrier: Any item that becomes reachable from another heap-item is marked grey (unless already black).
+
+While a gc cycle is ongoing, allocations are black, meaning every allocated object is considered to be live (until the next gc cycle is started).
+This is currently required as compilation units linked to the engine while the gc is running are not protected by the write barrier or another mechanism. It also helps to reduce the amount of work to be done when rescanning the JS stack (as it helps to ensure that most items are already black at that point).
+
+
+The gc state machine
+--------------------
+
+To facilitate incremental garbage collection, the gc algorithm is divided into the following stages:
+
+1. markStart, the atomic initialization phase, in which the MarkStack is initialized, and a flag is set on the engine indicating that incremental gc is active
+2. markGlobalObject, an atomic phase in which the global object, the engine's identifier table and the currently linked compilation units are marked
+3. markJSStack, an atomic phase in which the JS stack is marked
+4. initMarkPersistentValues: Atomic phase. If there are persistent values, some setup is done for the next phase.
+5. markPersistentValues: An interruptible phase in which all persistent values are marked.
+6. initMarkWeakValues: Atomic phase. If there are weak values, some setup is done for the next phase
+7. markWeakValues: An interruptible phase which takes care of marking the QObjectWrappers
+8. markDrain: An interrupible phase. While the MarkStack is not empty, the marking algorithm runs.
+9. markReady: An atomic phase which currently does nothing, but could be used for e.g. logging statistics
+10. initCallDestroyObjects: An atomic phase, in which the stack is rescanned, the MarkStack is drained once more. This ensures that all live objects are really marked.
+ Afterwards, the iteration over all the QObjectWrappers is prepared.
+11. callDestroyObject: An interruptible phase, were we call destroyObject of all non-marked QObjectWrapper.
+12. freeWeakMaps: An atomic phase in which we remove references to dead objects from live weak maps.
+13. freeWeakSets: Same as the last phase, but for weak sets
+14: handleQObjectWrappers: An atomic phase in which pending references to QObjectWrappers are cleared
+15. multiple sweep phases: Atomic phases, in which do the actual sweeping to free up memory. Note that this will also call destroy on objects marked with `V4_NEEDS_DESTROY`. There is one phase for the various allocators (identifier table, block allocator, huge item allocator, IC allocator)
+16. updateMetaData: Updates the black bitmaps, the usage statistics, and marks the gc cycle as done.
+17. invalid, the "not-running" stage of the state machine.
+
+To avoid constantly having to query the timer, even interruptible phases run for a fixed amount of steps before checking whether there's a timemout.
+
+Most steps are straight-forward, only the persistent and weak value phases require some explanation as to why it's safe to interrupt the process: The important thing to note is that we never remove elements from the structure while we're undergoing gc, and that we only ever append at the end. So we will see any new values that might be added.
+
+Persistent Values
+-----------------
+As shown in the diagram above, the handling of persistent values is interruptible (both for "real" persistent values, and also for weak vaules which also are stored in a `PersistentValueStorage` data structure.
+This is done by storing the `PersistentValueStorage::Iterator` in the gc state machine. That in turn raises two questions: Is the iterator safe against invalidation? And do we actually keep track of newly added persistent values?
+
+The latter part is easy to answer: Any newly added weak value is marked when we are in a gc cycle, so the marking part is handled. Sweeping only cares about unmarked values, so that's safe too.
+To answer the question about iterator validity, we have to look at the `PersistentValueStorage` data structure. Conceptionally, it's a forward-list of `Page`s (arrays of `QV4::Value`). Pages are ref-counted, and only unliked from the list if the ref-count reaches zero. Moreover, iterators also increase the ref-count.
+Therefore, as long as we iterate over the list, we don't risk having the pointer point to a deleted `Page` – even if all values in it have been freed. Freeing values is unproblematic for the gc – it will simply encounter `undefined` values, something it is already prepared to handle.
+Pages are also kept in the list while they are not deleted, so iteration works as expected. The only adjustment we need to do is to disable an optimization: When searching for a Page with an empty slot, we have
+to (potentially) travese the whole `PersistentValueStorage`. To avoid that, the first Page with empty slots is normally moved to the front of the list. However, that would mean that we could potentially skip over it during the marking phase. We sidestep that issue by simply disabling the optimization. This area could
+easily be improved in the future by keeping track of the first page with free slots in a different way.
+
+Custom marking
+---------------
+
+Some parts of the engine have to deviate from the general scheme described in the overview, as they don't integrate with the normal WriteBarrier. They are wrapped in the callback of the `QV4::WriteBarrier::markCustom` function, so that they can easily be found via "Find references".
+
+1. `QJSValue::encode`. QJSValues act as roots, and don't act as normal heap-items. When the current value of a QJSValue is overwritten with another heap-item, we also mark the new object. That aligns nicely with the gc barrier.
+2. The same applies to `PersistentValue::set`.
+3. The identifier table is another root; if a new string is inserted there during gc, it is (conservatively) marked black.
+4. PropertyKeys should for all intents and purposes use a write barrier (and have a deleted operator=). But them being an ad-hoc union type of numbers, Strings and Symbols, which has the additional requirements of having to be trivial, it turned out to be easier to simply mark them in `SharedInternalClassDataPrivate<PropertyKey>::set` (for PropertyKeys that had already been allocated), and on the fact that we allocate black for newly created PropertyKeys.
+5. `QV4::Heap::InternalClass` also requires special handling, as it uses plain members to Heap objects, notably to the prototype and to the parent internal class. As the class is somewhat special in any case (due to the usage of `SharedInternalClassData` and especially due to the usage of `SharedInternalClassData<PropertyKey>`, see notes on PropertyKey above), we use some bespoke sections for now. This could probably be cleaned up.
+
+Motivation for using a Dijkstra style barrier:
+----------------------------------------------
+- Deletion barriers are hard to support with the current PropertyKey design
+- Steele style barriers cause more work (have to revisit more objects), and as long as we have black allocations it doesn't make much sense to optimize for a minimal amount of floating garbage.
+
+Sweep Phase and finalizers:
+---------------------------
+A story for another day
+
+Allocator design:
+-----------------
+Your explanation is in another castle.
+
diff --git a/src/qml/memory/qv4mm.cpp b/src/qml/memory/qv4mm.cpp
index 5c3be27014..d740969131 100644
--- a/src/qml/memory/qv4mm.cpp
+++ b/src/qml/memory/qv4mm.cpp
@@ -25,6 +25,8 @@
#include "qv4mapobject_p.h"
#include "qv4setobject_p.h"
+#include <chrono>
+
//#define MM_STATS
#if !defined(MM_STATS) && !defined(QT_NO_DEBUG)
@@ -54,10 +56,8 @@
#include <pthread_np.h>
#endif
-Q_LOGGING_CATEGORY(lcGcStats, "qt.qml.gc.statistics")
-Q_DECLARE_LOGGING_CATEGORY(lcGcStats)
-Q_LOGGING_CATEGORY(lcGcAllocatorStats, "qt.qml.gc.allocatorStats")
-Q_DECLARE_LOGGING_CATEGORY(lcGcAllocatorStats)
+Q_STATIC_LOGGING_CATEGORY(lcGcStats, "qt.qml.gc.statistics")
+Q_STATIC_LOGGING_CATEGORY(lcGcAllocatorStats, "qt.qml.gc.allocatorStats")
using namespace WTF;
@@ -671,6 +671,229 @@ void HugeItemAllocator::freeAll()
}
}
+namespace {
+using ExtraData = GCStateInfo::ExtraData;
+GCState markStart(GCStateMachine *that, ExtraData &)
+{
+ //Initialize the mark stack
+ that->mm->m_markStack = std::make_unique<MarkStack>(that->mm->engine);
+ that->mm->engine->isGCOngoing = true;
+ return MarkGlobalObject;
+}
+
+GCState markGlobalObject(GCStateMachine *that, ExtraData &)
+{
+ that->mm->engine->markObjects(that->mm->m_markStack.get());
+ return MarkJSStack;
+}
+
+GCState markJSStack(GCStateMachine *that, ExtraData &)
+{
+ that->mm->collectFromJSStack(that->mm->markStack());
+ return InitMarkPersistentValues;
+}
+
+GCState initMarkPersistentValues(GCStateMachine *that, ExtraData &stateData)
+{
+ if (!that->mm->m_persistentValues)
+ return InitMarkWeakValues; // no persistent values to mark
+ stateData = GCIteratorStorage { that->mm->m_persistentValues->begin() };
+ return MarkPersistentValues;
+}
+
+static constexpr int markLoopIterationCount = 1024;
+
+bool wasDrainNecessary(MarkStack *ms, QDeadlineTimer deadline)
+{
+ if (ms->remainingBeforeSoftLimit() > markLoopIterationCount)
+ return false;
+ // drain
+ ms->drain(deadline);
+ return true;
+}
+
+GCState markPersistentValues(GCStateMachine *that, ExtraData &stateData) {
+ auto markStack = that->mm->markStack();
+ if (wasDrainNecessary(markStack, that->deadline) && that->deadline.hasExpired())
+ return MarkPersistentValues;
+ PersistentValueStorage::Iterator& it = get<GCIteratorStorage>(stateData).it;
+ // avoid repeatedly hitting the timer constantly by batching iterations
+ for (int i = 0; i < markLoopIterationCount; ++i) {
+ if (!it.p)
+ return InitMarkWeakValues;
+ if (Managed *m = (*it).as<Managed>())
+ m->mark(markStack);
+ ++it;
+ }
+ return MarkPersistentValues;
+}
+
+GCState initMarkWeakValues(GCStateMachine *that, ExtraData &stateData)
+{
+ stateData = GCIteratorStorage { that->mm->m_weakValues->begin() };
+ return MarkWeakValues;
+}
+
+GCState markWeakValues(GCStateMachine *that, ExtraData &stateData)
+{
+ auto markStack = that->mm->markStack();
+ if (wasDrainNecessary(markStack, that->deadline) && that->deadline.hasExpired())
+ return MarkWeakValues;
+ PersistentValueStorage::Iterator& it = get<GCIteratorStorage>(stateData).it;
+ // avoid repeatedly hitting the timer constantly by batching iterations
+ for (int i = 0; i < markLoopIterationCount; ++i) {
+ if (!it.p)
+ return MarkDrain;
+ QObjectWrapper *qobjectWrapper = (*it).as<QObjectWrapper>();
+ ++it;
+ if (!qobjectWrapper)
+ continue;
+ QObject *qobject = qobjectWrapper->object();
+ if (!qobject)
+ continue;
+ bool keepAlive = QQmlData::keepAliveDuringGarbageCollection(qobject);
+
+ if (!keepAlive) {
+ if (QObject *parent = qobject->parent()) {
+ while (parent->parent())
+ parent = parent->parent();
+ keepAlive = QQmlData::keepAliveDuringGarbageCollection(parent);
+ }
+ }
+
+ if (keepAlive)
+ qobjectWrapper->mark(that->mm->markStack());
+ }
+ return MarkWeakValues;
+}
+
+GCState markDrain(GCStateMachine *that, ExtraData &)
+{
+ if (that->deadline.isForever()) {
+ that->mm->markStack()->drain();
+ return MarkReady;
+ }
+ auto drainState = that->mm->m_markStack->drain(that->deadline);
+ return drainState == MarkStack::DrainState::Complete
+ ? MarkReady
+ : MarkDrain;
+}
+
+GCState markReady(GCStateMachine *, ExtraData &)
+{
+ //Possibility to do some clean up, stat printing, etc...
+ return InitCallDestroyObjects;
+}
+
+/** \!internal
+collects new references from the stack, then drains the mark stack again
+*/
+void redrain(GCStateMachine *that)
+{
+ that->mm->collectFromJSStack(that->mm->markStack());
+ that->mm->m_markStack->drain();
+}
+
+GCState initCallDestroyObjects(GCStateMachine *that, ExtraData &stateData)
+{
+ // as we don't have a deletion barrier, we need to rescan the stack
+ redrain(that);
+ if (!that->mm->m_weakValues)
+ return FreeWeakMaps; // no need to call destroy objects
+ stateData = GCIteratorStorage { that->mm->m_weakValues->begin() };
+ return CallDestroyObjects;
+}
+GCState callDestroyObject(GCStateMachine *that, ExtraData &stateData)
+{
+ PersistentValueStorage::Iterator& it = get<GCIteratorStorage>(stateData).it;
+ // destroyObject might call user code, which really shouldn't call back into the gc
+ auto oldState = std::exchange(that->mm->gcBlocked, QV4::MemoryManager::Blockness::InCriticalSection);
+ auto cleanup = qScopeGuard([&]() {
+ that->mm->gcBlocked = oldState;
+ });
+ // avoid repeatedly hitting the timer constantly by batching iterations
+ for (int i = 0; i < markLoopIterationCount; ++i) {
+ if (!it.p)
+ return FreeWeakMaps;
+ Managed *m = (*it).managed();
+ ++it;
+ if (!m || m->markBit())
+ continue;
+ // we need to call destroyObject on qobjectwrappers now, so that they can emit the destroyed
+ // signal before we start sweeping the heap
+ if (QObjectWrapper *qobjectWrapper = m->as<QObjectWrapper>())
+ qobjectWrapper->destroyObject(/*lastSweep =*/false);
+ }
+ return CallDestroyObjects;
+}
+
+void freeWeakMaps(MemoryManager *mm)
+{
+ for (auto [map, lastMap] = std::tuple {mm->weakMaps, &mm->weakMaps }; map; map = map->nextWeakMap) {
+ if (!map->isMarked())
+ continue;
+ map->removeUnmarkedKeys();
+ *lastMap = map;
+ lastMap = &map->nextWeakMap;
+ }
+}
+
+GCState freeWeakMaps(GCStateMachine *that, ExtraData &)
+{
+ freeWeakMaps(that->mm);
+ return FreeWeakSets;
+}
+
+void freeWeakSets(MemoryManager *mm)
+{
+ for (auto [set, lastSet] = std::tuple {mm->weakSets, &mm->weakSets}; set; set = set->nextWeakSet) {
+
+ if (!set->isMarked())
+ continue;
+ set->removeUnmarkedKeys();
+ *lastSet = set;
+ lastSet = &set->nextWeakSet;
+ }
+}
+
+GCState freeWeakSets(GCStateMachine *that, ExtraData &)
+{
+ freeWeakSets(that->mm);
+ return HandleQObjectWrappers;
+}
+
+GCState handleQObjectWrappers(GCStateMachine *that, ExtraData &)
+{
+ that->mm->cleanupDeletedQObjectWrappersInSweep();
+ return DoSweep;
+}
+
+GCState doSweep(GCStateMachine *that, ExtraData &)
+{
+ auto mm = that->mm;
+
+ mm->engine->identifierTable->sweep();
+ mm->blockAllocator.sweep();
+ mm->hugeItemAllocator.sweep(that->mm->gcCollectorStats ? increaseFreedCountForClass : nullptr);
+ mm->icAllocator.sweep();
+
+ // reset all black bits
+ mm->blockAllocator.resetBlackBits();
+ mm->hugeItemAllocator.resetBlackBits();
+ mm->icAllocator.resetBlackBits();
+
+ mm->usedSlotsAfterLastFullSweep = mm->blockAllocator.usedSlotsAfterLastSweep + mm->icAllocator.usedSlotsAfterLastSweep;
+ mm->gcBlocked = MemoryManager::Unblocked;
+ mm->m_markStack.reset();
+ mm->engine->isGCOngoing = false;
+
+ mm->updateUnmanagedHeapSizeGCLimit();
+
+ return Invalid;
+}
+
+}
+
MemoryManager::MemoryManager(ExecutionEngine *engine)
: engine(engine)
@@ -691,6 +914,70 @@ MemoryManager::MemoryManager(ExecutionEngine *engine)
memset(statistics.allocations, 0, sizeof(statistics.allocations));
if (gcStats)
blockAllocator.allocationStats = statistics.allocations;
+
+ gcStateMachine = std::make_unique<GCStateMachine>();
+ gcStateMachine->mm = this;
+
+ gcStateMachine->stateInfoMap[GCState::MarkStart] = {
+ markStart,
+ false,
+ };
+ gcStateMachine->stateInfoMap[GCState::MarkGlobalObject] = {
+ markGlobalObject,
+ false,
+ };
+ gcStateMachine->stateInfoMap[GCState::MarkJSStack] = {
+ markJSStack,
+ false,
+ };
+ gcStateMachine->stateInfoMap[GCState::InitMarkPersistentValues] = {
+ initMarkPersistentValues,
+ false,
+ };
+ gcStateMachine->stateInfoMap[GCState::MarkPersistentValues] = {
+ markPersistentValues,
+ false,
+ };
+ gcStateMachine->stateInfoMap[GCState::InitMarkWeakValues] = {
+ initMarkWeakValues,
+ false,
+ };
+ gcStateMachine->stateInfoMap[GCState::MarkWeakValues] = {
+ markWeakValues,
+ false,
+ };
+ gcStateMachine->stateInfoMap[GCState::MarkDrain] = {
+ markDrain,
+ false,
+ };
+ gcStateMachine->stateInfoMap[GCState::MarkReady] = {
+ markReady,
+ false,
+ };
+ gcStateMachine->stateInfoMap[GCState::InitCallDestroyObjects] = {
+ initCallDestroyObjects,
+ false,
+ };
+ gcStateMachine->stateInfoMap[GCState::CallDestroyObjects] = {
+ callDestroyObject,
+ false,
+ };
+ gcStateMachine->stateInfoMap[GCState::FreeWeakMaps] = {
+ freeWeakMaps,
+ false,
+ };
+ gcStateMachine->stateInfoMap[GCState::FreeWeakSets] = {
+ freeWeakSets,
+ true, // ensure that handleQObjectWrappers runs in isolation
+ };
+ gcStateMachine->stateInfoMap[GCState::HandleQObjectWrappers] = {
+ handleQObjectWrappers,
+ false,
+ };
+ gcStateMachine->stateInfoMap[GCState::DoSweep] = {
+ doSweep,
+ false,
+ };
}
Heap::Base *MemoryManager::allocString(std::size_t unmanagedSize)
@@ -704,13 +991,6 @@ Heap::Base *MemoryManager::allocString(std::size_t unmanagedSize)
HeapItem *m = allocate(&blockAllocator, stringSize);
memset(m, 0, stringSize);
- if (gcBlocked) {
- // If the gc is running right now, it will not have a chance to mark the newly created item
- // and may therefore sweep it right away.
- // Protect the new object from the current GC run to avoid this.
- m->as<Heap::Base>()->setMarkBit();
- }
-
return *m;
}
@@ -726,13 +1006,6 @@ Heap::Base *MemoryManager::allocData(std::size_t size)
HeapItem *m = allocate(&blockAllocator, size);
memset(m, 0, size);
- if (gcBlocked) {
- // If the gc is running right now, it will not have a chance to mark the newly created item
- // and may therefore sweep it right away.
- // Protect the new object from the current GC run to avoid this.
- m->as<Heap::Base>()->setMarkBit();
- }
-
return *m;
}
@@ -790,6 +1063,7 @@ MarkStack::MarkStack(ExecutionEngine *engine)
void MarkStack::drain()
{
+ // we're not calling drain(QDeadlineTimer::Forever) as that has higher overhead
while (m_top > m_base) {
Heap::Base *h = pop();
++markStackSize;
@@ -798,91 +1072,85 @@ void MarkStack::drain()
}
}
-void MemoryManager::collectRoots(MarkStack *markStack)
+MarkStack::DrainState MarkStack::drain(QDeadlineTimer deadline)
{
- engine->markObjects(markStack);
-
-// qDebug() << " mark stack after engine->mark" << (engine->jsStackTop - markBase);
-
- collectFromJSStack(markStack);
-
-// qDebug() << " mark stack after js stack collect" << (engine->jsStackTop - markBase);
- m_persistentValues->mark(markStack);
-
-// qDebug() << " mark stack after persistants" << (engine->jsStackTop - markBase);
-
- // Preserve QObject ownership rules within JavaScript: A parent with c++ ownership
- // keeps all of its children alive in JavaScript.
-
- // Do this _after_ collectFromStack to ensure that processing the weak
- // managed objects in the loop down there doesn't make then end up as leftovers
- // on the stack and thus always get collected.
- for (PersistentValueStorage::Iterator it = m_weakValues->begin(); it != m_weakValues->end(); ++it) {
- QObjectWrapper *qobjectWrapper = (*it).as<QObjectWrapper>();
- if (!qobjectWrapper)
- continue;
- QObject *qobject = qobjectWrapper->object();
- if (!qobject)
- continue;
- bool keepAlive = QQmlData::keepAliveDuringGarbageCollection(qobject);
-
- if (!keepAlive) {
- if (QObject *parent = qobject->parent()) {
- while (parent->parent())
- parent = parent->parent();
-
- keepAlive = QQmlData::keepAliveDuringGarbageCollection(parent);
- }
+ do {
+ for (int i = 0; i <= markLoopIterationCount * 10; ++i) {
+ if (m_top == m_base)
+ return DrainState::Complete;
+ Heap::Base *h = pop();
+ ++markStackSize;
+ Q_ASSERT(h); // at this point we should only have Heap::Base objects in this area on the stack. If not, weird things might happen.
+ h->internalClass->vtable->markObjects(h, this);
}
+ } while (!deadline.hasExpired());
+ return DrainState::Ongoing;
+}
- if (keepAlive)
- qobjectWrapper->mark(markStack);
+void MemoryManager::onEventLoop()
+{
+ if (engine->inShutdown)
+ return;
+ if (gcBlocked == InCriticalSection) {
+ QMetaObject::invokeMethod(engine->publicEngine, [this]{
+ onEventLoop();
+ }, Qt::QueuedConnection);
+ return;
+ }
+ if (gcStateMachine->inProgress()) {
+ gcStateMachine->step();
}
}
-void MemoryManager::mark()
+
+void MemoryManager::setGCTimeLimit(int timeMs)
{
- markStackSize = 0;
- MarkStack markStack(engine);
- collectRoots(&markStack);
- // dtor of MarkStack drains
+ gcStateMachine->timeLimit = std::chrono::milliseconds(timeMs);
}
void MemoryManager::sweep(bool lastSweep, ClassDestroyStatsCallback classCountPtr)
{
+
for (PersistentValueStorage::Iterator it = m_weakValues->begin(); it != m_weakValues->end(); ++it) {
Managed *m = (*it).managed();
if (!m || m->markBit())
continue;
// we need to call destroyObject on qobjectwrappers now, so that they can emit the destroyed
// signal before we start sweeping the heap
- if (QObjectWrapper *qobjectWrapper = (*it).as<QObjectWrapper>())
+ if (QObjectWrapper *qobjectWrapper = (*it).as<QObjectWrapper>()) {
qobjectWrapper->destroyObject(lastSweep);
- }
-
- // remove objects from weak maps and sets
- Heap::MapObject *map = weakMaps;
- Heap::MapObject **lastMap = &weakMaps;
- while (map) {
- if (map->isMarked()) {
- map->removeUnmarkedKeys();
- *lastMap = map;
- lastMap = &map->nextWeakMap;
}
- map = map->nextWeakMap;
}
- Heap::SetObject *set = weakSets;
- Heap::SetObject **lastSet = &weakSets;
- while (set) {
- if (set->isMarked()) {
- set->removeUnmarkedKeys();
- *lastSet = set;
- lastSet = &set->nextWeakSet;
- }
- set = set->nextWeakSet;
+ freeWeakMaps(this);
+ freeWeakSets(this);
+
+ cleanupDeletedQObjectWrappersInSweep();
+
+ if (!lastSweep) {
+ engine->identifierTable->sweep();
+ blockAllocator.sweep(/*classCountPtr*/);
+ hugeItemAllocator.sweep(classCountPtr);
+ icAllocator.sweep(/*classCountPtr*/);
}
+ // reset all black bits
+ blockAllocator.resetBlackBits();
+ hugeItemAllocator.resetBlackBits();
+ icAllocator.resetBlackBits();
+
+ usedSlotsAfterLastFullSweep = blockAllocator.usedSlotsAfterLastSweep + icAllocator.usedSlotsAfterLastSweep;
+ updateUnmanagedHeapSizeGCLimit();
+ gcBlocked = MemoryManager::Unblocked;
+}
+
+/*
+ \internal
+ Helper function used in sweep to clean up the (to-be-freed) QObjectWrapper
+ Used both in MemoryManager::sweep, and the corresponding gc statemachine phase
+*/
+void MemoryManager::cleanupDeletedQObjectWrappersInSweep()
+{
// onDestruction handlers may have accessed other QObject wrappers and reset their value, so ensure
// that they are all set to undefined.
for (PersistentValueStorage::Iterator it = m_weakValues->begin(); it != m_weakValues->end(); ++it) {
@@ -915,14 +1183,6 @@ void MemoryManager::sweep(bool lastSweep, ClassDestroyStatsCallback classCountPt
++it;
}
}
-
-
- if (!lastSweep) {
- engine->identifierTable->sweep();
- blockAllocator.sweep(/*classCountPtr*/);
- hugeItemAllocator.sweep(classCountPtr);
- icAllocator.sweep(/*classCountPtr*/);
- }
}
bool MemoryManager::shouldRunGC() const
@@ -962,15 +1222,44 @@ static size_t dumpBins(BlockAllocator *b, const char *title)
return totalSlotMem*Chunk::SlotSize;
}
+/*!
+ \internal
+ Precondition: Incremental garbage collection must be currently active
+ Finishes incremental garbage collection, unless in a critical section
+ Code entering a critical section is expected to check if we need to
+ force a gc completion, and to trigger the gc again if necessary
+ when exiting the critcial section.
+ Returns \c true if the gc cycle completed, false otherwise.
+ */
+bool MemoryManager::tryForceGCCompletion()
+{
+ if (gcBlocked == InCriticalSection)
+ return false;
+ const bool incrementalGCIsAlreadyRunning = m_markStack != nullptr;
+ Q_ASSERT(incrementalGCIsAlreadyRunning);
+ auto oldTimeLimit = std::exchange(gcStateMachine->timeLimit, std::chrono::microseconds::max());
+ while (gcStateMachine->inProgress()) {
+ gcStateMachine->step();
+ }
+ gcStateMachine->timeLimit = oldTimeLimit;
+ return true;
+}
+
+void MemoryManager::runFullGC()
+{
+ runGC();
+ const bool incrementalGCStillRunning = m_markStack != nullptr;
+ if (incrementalGCStillRunning)
+ tryForceGCCompletion();
+}
+
void MemoryManager::runGC()
{
- if (gcBlocked) {
-// qDebug() << "Not running GC.";
+ if (gcBlocked != Unblocked) {
return;
}
- QScopedValueRollback<bool> gcBlocker(gcBlocked, true);
-// qDebug() << "runGC";
+ gcBlocked = MemoryManager::NormalBlocked;
if (gcStats) {
statistics.maxReservedMem = qMax(statistics.maxReservedMem, getAllocatedMem());
@@ -978,8 +1267,7 @@ void MemoryManager::runGC()
}
if (!gcCollectorStats) {
- mark();
- sweep();
+ gcStateMachine->step();
} else {
bool triggeredByUnmanagedHeap = (unmanagedHeapSize > unmanagedHeapSizeGCLimit);
size_t oldUnmanagedSize = unmanagedHeapSize;
@@ -1003,13 +1291,11 @@ void MemoryManager::runGC()
QElapsedTimer t;
t.start();
- mark();
+ gcStateMachine->step();
qint64 markTime = t.nsecsElapsed()/1000;
t.restart();
- sweep(false, increaseFreedCountForClass);
const size_t usedAfter = getUsedMem();
const size_t largeItemsAfter = getLargeItemsMem();
- qint64 sweepTime = t.nsecsElapsed()/1000;
if (triggeredByUnmanagedHeap) {
qDebug(stats) << "triggered by unmanaged heap:";
@@ -1021,7 +1307,6 @@ void MemoryManager::runGC()
+ dumpBins(&icAllocator, "InternalClasss");
qDebug(stats) << "Marked object in" << markTime << "us.";
qDebug(stats) << " " << markStackSize << "objects marked";
- qDebug(stats) << "Sweeped object in" << sweepTime << "us.";
// sort our object types by number of freed instances
MMStatsHash freedObjectStats;
@@ -1059,21 +1344,6 @@ void MemoryManager::runGC()
if (gcStats)
statistics.maxUsedMem = qMax(statistics.maxUsedMem, getUsedMem() + getLargeItemsMem());
-
- if (aggressiveGC) {
- // ensure we don't 'loose' any memory
- Q_ASSERT(blockAllocator.allocatedMem()
- == blockAllocator.usedMem() + dumpBins(&blockAllocator, nullptr));
- Q_ASSERT(icAllocator.allocatedMem()
- == icAllocator.usedMem() + dumpBins(&icAllocator, nullptr));
- }
-
- usedSlotsAfterLastFullSweep = blockAllocator.usedSlotsAfterLastSweep + icAllocator.usedSlotsAfterLastSweep;
-
- // reset all black bits
- blockAllocator.resetBlackBits();
- hugeItemAllocator.resetBlackBits();
- icAllocator.resetBlackBits();
}
size_t MemoryManager::getUsedMem() const
@@ -1091,6 +1361,29 @@ size_t MemoryManager::getLargeItemsMem() const
return hugeItemAllocator.usedMem();
}
+void MemoryManager::updateUnmanagedHeapSizeGCLimit()
+{
+ if (3*unmanagedHeapSizeGCLimit <= 4 * unmanagedHeapSize) {
+ // more than 75% full, raise limit
+ unmanagedHeapSizeGCLimit = std::max(unmanagedHeapSizeGCLimit,
+ unmanagedHeapSize) * 2;
+ } else if (unmanagedHeapSize * 4 <= unmanagedHeapSizeGCLimit) {
+ // less than 25% full, lower limit
+ unmanagedHeapSizeGCLimit = qMax(std::size_t(MinUnmanagedHeapSizeGCLimit),
+ unmanagedHeapSizeGCLimit/2);
+ }
+
+ if (aggressiveGC && !engine->inShutdown) {
+ // ensure we don't 'loose' any memory
+ // but not during shutdown, because than we skip parts of sweep
+ // and use freeAll instead
+ Q_ASSERT(blockAllocator.allocatedMem()
+ == blockAllocator.usedMem() + dumpBins(&blockAllocator, nullptr));
+ Q_ASSERT(icAllocator.allocatedMem()
+ == icAllocator.usedMem() + dumpBins(&icAllocator, nullptr));
+ }
+}
+
void MemoryManager::registerWeakMap(Heap::MapObject *map)
{
map->nextWeakMap = weakMaps;
@@ -1106,10 +1399,22 @@ void MemoryManager::registerWeakSet(Heap::SetObject *set)
MemoryManager::~MemoryManager()
{
delete m_persistentValues;
-
dumpStats();
+ // do one last non-incremental sweep to clean up C++ objects
+ // first, abort any on-going incremental gc operation
+ setGCTimeLimit(-1);
+ if (engine->isGCOngoing) {
+ engine->isGCOngoing = false;
+ m_markStack.reset();
+ gcStateMachine->state = GCState::Invalid;
+ blockAllocator.resetBlackBits();
+ hugeItemAllocator.resetBlackBits();
+ icAllocator.resetBlackBits();
+ }
+ // then sweep
sweep(/*lastSweep*/true);
+
blockAllocator.freeAll();
hugeItemAllocator.freeAll();
icAllocator.freeAll();
@@ -1153,6 +1458,49 @@ void MemoryManager::collectFromJSStack(MarkStack *markStack) const
}
}
+GCStateMachine::GCStateMachine()
+{
+ // base assumption: target 60fps, use at most 1/3 of time for gc
+ timeLimit = std::chrono::milliseconds { (1000 / 60) / 3 };
+}
+
+void GCStateMachine::transition() {
+ if (timeLimit.count() > 0) {
+ deadline = QDeadlineTimer(timeLimit);
+ bool deadlineExpired = false;
+ while (!(deadlineExpired = deadline.hasExpired()) && state != GCState::Invalid) {
+ if (state > GCState::InitCallDestroyObjects) {
+ /* initCallDestroyObjects is the last action which drains the mark
+ stack by default. But as our write-barrier might end up putting
+ objects on the markStack which still reference other objects.
+ Especially when we call user code triggered by Component.onDestruction,
+ but also when we run into a timeout.
+ We don't redrain before InitCallDestroyObjects, as that would
+ potentially lead to useless busy-work (e.g., if the last referencs
+ to objects are removed while the mark phase is running)
+ */
+ redrain(this);
+ }
+ GCStateInfo& stateInfo = stateInfoMap[int(state)];
+ state = stateInfo.execute(this, stateData);
+ if (stateInfo.breakAfter)
+ break;
+ }
+ if (deadlineExpired)
+ handleTimeout(state);
+ if (state != GCState::Invalid)
+ QMetaObject::invokeMethod(mm->engine->publicEngine, [this]{
+ mm->onEventLoop();
+ }, Qt::QueuedConnection);
+ } else {
+ deadline = QDeadlineTimer::Forever;
+ while (state != GCState::Invalid) {
+ GCStateInfo& stateInfo = stateInfoMap[int(state)];
+ state = stateInfo.execute(this, stateData);
+ }
+ }
+}
+
} // namespace QV4
QT_END_NAMESPACE
diff --git a/src/qml/memory/qv4mm_p.h b/src/qml/memory/qv4mm_p.h
index 378b36369d..ef0cd0c36c 100644
--- a/src/qml/memory/qv4mm_p.h
+++ b/src/qml/memory/qv4mm_p.h
@@ -28,6 +28,73 @@ QT_BEGIN_NAMESPACE
namespace QV4 {
+enum GCState {
+ MarkStart = 0,
+ MarkGlobalObject,
+ MarkJSStack,
+ InitMarkPersistentValues,
+ MarkPersistentValues,
+ InitMarkWeakValues,
+ MarkWeakValues,
+ MarkDrain,
+ MarkReady,
+ InitCallDestroyObjects,
+ CallDestroyObjects,
+ FreeWeakMaps,
+ FreeWeakSets,
+ HandleQObjectWrappers,
+ DoSweep,
+ Invalid,
+ Count,
+};
+
+struct GCData { virtual ~GCData(){};};
+
+struct GCIteratorStorage {
+ PersistentValueStorage::Iterator it{nullptr, 0};
+};
+struct GCStateMachine;
+
+struct GCStateInfo {
+ using ExtraData = std::variant<std::monostate, GCIteratorStorage>;
+ GCState (*execute)(GCStateMachine *, ExtraData &) = nullptr; // Function to execute for this state, returns true if ready to transition
+ bool breakAfter{false};
+};
+
+struct GCStateMachine {
+ using ExtraData = GCStateInfo::ExtraData;
+ GCState state{GCState::Invalid};
+ std::chrono::microseconds timeLimit{};
+ QDeadlineTimer deadline;
+ std::array<GCStateInfo, GCState::Count> stateInfoMap;
+ MemoryManager *mm = nullptr;
+ ExtraData stateData; // extra date for specific states
+
+ GCStateMachine();
+
+ inline void step() {
+ if (!inProgress()) {
+ reset();
+ }
+ transition();
+ }
+
+ inline bool inProgress() {
+ return state != GCState::Invalid;
+ }
+
+ inline void reset() {
+ state = GCState::MarkStart;
+ }
+
+ Q_QML_EXPORT void transition();
+
+ inline void handleTimeout(GCState state) {
+ Q_UNUSED(state);
+ }
+};
+
+
struct ChunkAllocator;
struct MemorySegment;
@@ -112,11 +179,22 @@ public:
MemoryManager(ExecutionEngine *engine);
~MemoryManager();
+ template <typename ToBeMarked>
+ friend struct GCCriticalSection;
+
// TODO: this is only for 64bit (and x86 with SSE/AVX), so exend it for other architectures to be slightly more efficient (meaning, align on 8-byte boundaries).
// Note: all occurrences of "16" in alloc/dealloc are also due to the alignment.
constexpr static inline std::size_t align(std::size_t size)
{ return (size + Chunk::SlotSize - 1) & ~(Chunk::SlotSize - 1); }
+ /* NOTE: allocManaged comes in various overloads. If size is not passed explicitly
+ sizeof(ManagedType::Data) is used for size. However, there are quite a few cases
+ where we allocate more than sizeof(ManagedType::Data); that's generally the case
+ when the Object has a ValueArray member.
+ If no internal class pointer is provided, ManagedType::defaultInternalClass(engine)
+ will be used as the internal class.
+ */
+
template<typename ManagedType>
inline typename ManagedType::Data *allocManaged(std::size_t size, Heap::InternalClass *ic)
{
@@ -130,12 +208,24 @@ public:
}
template<typename ManagedType>
+ inline typename ManagedType::Data *allocManaged(Heap::InternalClass *ic)
+ {
+ return allocManaged<ManagedType>(sizeof(typename ManagedType::Data), ic);
+ }
+
+ template<typename ManagedType>
inline typename ManagedType::Data *allocManaged(std::size_t size, InternalClass *ic)
{
return allocManaged<ManagedType>(size, ic->d());
}
template<typename ManagedType>
+ inline typename ManagedType::Data *allocManaged(InternalClass *ic)
+ {
+ return allocManaged<ManagedType>(sizeof(typename ManagedType::Data), ic);
+ }
+
+ template<typename ManagedType>
inline typename ManagedType::Data *allocManaged(std::size_t size)
{
Scope scope(engine);
@@ -143,6 +233,15 @@ public:
return allocManaged<ManagedType>(size, ic);
}
+ template<typename ManagedType>
+ inline typename ManagedType::Data *allocManaged()
+ {
+ auto constexpr size = sizeof(typename ManagedType::Data);
+ Scope scope(engine);
+ Scoped<InternalClass> ic(scope, ManagedType::defaultInternalClass(engine));
+ return allocManaged<ManagedType>(size, ic);
+ }
+
template <typename ObjectType>
typename ObjectType::Data *allocateObject(Heap::InternalClass *ic)
{
@@ -208,12 +307,14 @@ public:
typename ManagedType::Data *alloc(Args&&... args)
{
Scope scope(engine);
- Scoped<ManagedType> t(scope, allocManaged<ManagedType>(sizeof(typename ManagedType::Data)));
+ Scoped<ManagedType> t(scope, allocManaged<ManagedType>());
t->d_unchecked()->init(std::forward<Args>(args)...);
return t->d();
}
void runGC();
+ bool tryForceGCCompletion();
+ void runFullGC();
void dumpStats() const;
@@ -225,6 +326,9 @@ public:
// and InternalClassDataPrivate<PropertyAttributes>.
void changeUnmanagedHeapSizeUsage(qptrdiff delta) { unmanagedHeapSize += delta; }
+ // called at the end of a gc cycle
+ void updateUnmanagedHeapSizeGCLimit();
+
template<typename ManagedType>
typename ManagedType::Data *allocIC()
{
@@ -235,6 +339,12 @@ public:
void registerWeakMap(Heap::MapObject *map);
void registerWeakSet(Heap::SetObject *set);
+ void onEventLoop();
+
+ //GC related methods
+ void setGCTimeLimit(int timeMs);
+ MarkStack* markStack() { return m_markStack.get(); }
+
protected:
/// expects size to be aligned
Heap::Base *allocString(std::size_t unmanagedSize);
@@ -246,33 +356,34 @@ private:
MinUnmanagedHeapSizeGCLimit = 128 * 1024
};
+public:
void collectFromJSStack(MarkStack *markStack) const;
- void mark();
void sweep(bool lastSweep = false, ClassDestroyStatsCallback classCountPtr = nullptr);
+ void cleanupDeletedQObjectWrappersInSweep();
+ bool isAboveUnmanagedHeapLimit()
+ {
+ const bool incrementalGCIsAlreadyRunning = m_markStack != nullptr;
+ const bool aboveUnmanagedHeapLimit = incrementalGCIsAlreadyRunning
+ ? unmanagedHeapSize > 3 * unmanagedHeapSizeGCLimit / 2
+ : unmanagedHeapSize > unmanagedHeapSizeGCLimit;
+ return aboveUnmanagedHeapLimit;
+ }
+private:
bool shouldRunGC() const;
- void collectRoots(MarkStack *markStack);
HeapItem *allocate(BlockAllocator *allocator, std::size_t size)
{
+ const bool incrementalGCIsAlreadyRunning = m_markStack != nullptr;
+
bool didGCRun = false;
if (aggressiveGC) {
- runGC();
+ runFullGC();
didGCRun = true;
}
- if (unmanagedHeapSize > unmanagedHeapSizeGCLimit) {
+ if (isAboveUnmanagedHeapLimit()) {
if (!didGCRun)
- runGC();
-
- if (3*unmanagedHeapSizeGCLimit <= 4 * unmanagedHeapSize) {
- // more than 75% full, raise limit
- unmanagedHeapSizeGCLimit = std::max(unmanagedHeapSizeGCLimit,
- unmanagedHeapSize) * 2;
- } else if (unmanagedHeapSize * 4 <= unmanagedHeapSizeGCLimit) {
- // less than 25% full, lower limit
- unmanagedHeapSizeGCLimit = qMax(std::size_t(MinUnmanagedHeapSizeGCLimit),
- unmanagedHeapSizeGCLimit/2);
- }
+ incrementalGCIsAlreadyRunning ? (void) tryForceGCCompletion() : runGC();
didGCRun = true;
}
@@ -300,11 +411,15 @@ public:
Heap::MapObject *weakMaps = nullptr;
Heap::SetObject *weakSets = nullptr;
+ std::unique_ptr<GCStateMachine> gcStateMachine{nullptr};
+ std::unique_ptr<MarkStack> m_markStack{nullptr};
+
std::size_t unmanagedHeapSize = 0; // 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;
std::size_t usedSlotsAfterLastFullSweep = 0;
- bool gcBlocked = false;
+ enum Blockness : quint8 {Unblocked, NormalBlocked, InCriticalSection };
+ Blockness gcBlocked = Unblocked;
bool aggressiveGC = false;
bool gcStats = false;
bool gcCollectorStats = false;
@@ -320,6 +435,51 @@ public:
} statistics;
};
+/*!
+ \internal
+ GCCriticalSection prevets the gc from running, until it is destructed.
+ In its dtor, it runs a check whether we've reached the unmanaegd heap limit,
+ and triggers a gc run if necessary.
+ Lastly, it can optionally mark an object passed to it before runnig the gc.
+ */
+template <typename ToBeMarked = void>
+struct GCCriticalSection {
+ Q_DISABLE_COPY_MOVE(GCCriticalSection)
+
+ Q_NODISCARD_CTOR GCCriticalSection(QV4::ExecutionEngine *engine, ToBeMarked *toBeMarked = nullptr)
+ : m_engine(engine)
+ , m_oldState(std::exchange(engine->memoryManager->gcBlocked, MemoryManager::InCriticalSection))
+ , m_toBeMarked(toBeMarked)
+ {
+ // disallow nested critical sections
+ Q_ASSERT(m_oldState != MemoryManager::InCriticalSection);
+ }
+ ~GCCriticalSection()
+ {
+ m_engine->memoryManager->gcBlocked = m_oldState;
+ if (m_oldState != MemoryManager::Unblocked)
+ if constexpr (!std::is_same_v<ToBeMarked, void>)
+ if (m_toBeMarked)
+ m_toBeMarked->markObjects(m_engine->memoryManager->markStack());
+ /* because we blocked the gc, we might be using too much memoryon the unmanaged heap
+ and did not run the normal fixup logic. So recheck again, and trigger a gc run
+ if necessary*/
+ if (!m_engine->memoryManager->isAboveUnmanagedHeapLimit())
+ return;
+ if (!m_engine->isGCOngoing) {
+ m_engine->memoryManager->runGC();
+ } else {
+ [[maybe_unused]] bool gcFinished = m_engine->memoryManager->tryForceGCCompletion();
+ Q_ASSERT(gcFinished);
+ }
+ }
+
+private:
+ QV4::ExecutionEngine *m_engine;
+ MemoryManager::Blockness m_oldState;
+ ToBeMarked *m_toBeMarked;
+};
+
}
QT_END_NAMESPACE
diff --git a/src/qml/memory/qv4mmdefs_p.h b/src/qml/memory/qv4mmdefs_p.h
index a7fc7bb7cb..b77b615e6c 100644
--- a/src/qml/memory/qv4mmdefs_p.h
+++ b/src/qml/memory/qv4mmdefs_p.h
@@ -21,6 +21,8 @@
QT_BEGIN_NAMESPACE
+class QDeadlineTimer;
+
namespace QV4 {
struct MarkStack;
@@ -227,9 +229,9 @@ Q_STATIC_ASSERT(sizeof(HeapItem) == Chunk::SlotSize);
Q_STATIC_ASSERT(QT_POINTER_SIZE*8 == Chunk::Bits);
Q_STATIC_ASSERT((1 << Chunk::BitShift) == Chunk::Bits);
-struct Q_QML_PRIVATE_EXPORT MarkStack {
+struct Q_QML_EXPORT MarkStack {
MarkStack(ExecutionEngine *engine);
- ~MarkStack() { drain(); }
+ ~MarkStack() { /* we drain manually */ }
void push(Heap::Base *m) {
*(m_top++) = m;
@@ -250,17 +252,28 @@ struct Q_QML_PRIVATE_EXPORT MarkStack {
}
}
+ bool isEmpty() const { return m_top == m_base; }
+
+ qptrdiff remainingBeforeSoftLimit() const
+ {
+ return m_softLimit - m_top;
+ }
+
ExecutionEngine *engine() const { return m_engine; }
+ void drain();
+ enum class DrainState { Ongoing, Complete };
+ DrainState drain(QDeadlineTimer deadline);
private:
Heap::Base *pop() { return *(--m_top); }
- void drain();
Heap::Base **m_top = nullptr;
Heap::Base **m_base = nullptr;
Heap::Base **m_softLimit = nullptr;
Heap::Base **m_hardLimit = nullptr;
+
ExecutionEngine *m_engine = nullptr;
+
quintptr m_drainRecursion = 0;
};
diff --git a/src/qml/memory/qv4stacklimits.cpp b/src/qml/memory/qv4stacklimits.cpp
index 663ece09a2..288a6fd347 100644
--- a/src/qml/memory/qv4stacklimits.cpp
+++ b/src/qml/memory/qv4stacklimits.cpp
@@ -31,6 +31,8 @@
# include <unistd.h>
#elif defined(Q_OS_INTEGRITY)
# include <INTEGRITY.h>
+#elif defined(Q_OS_VXWORKS)
+# include <taskLib.h>
#elif defined(Q_OS_WASM)
# include <emscripten/stack.h>
#endif
@@ -147,12 +149,19 @@ StackProperties stackProperties()
static_assert(Q_STACK_GROWTH_DIRECTION < 0);
StackProperties stackProperties()
{
+ // MinGW complains about out of bounds array access in compiler headers
+ QT_WARNING_PUSH
+ QT_WARNING_DISABLE_GCC("-Warray-bounds")
+
// Get the stack base.
# ifdef _WIN64
PNT_TIB64 pTib = reinterpret_cast<PNT_TIB64>(NtCurrentTeb());
# else
PNT_TIB pTib = reinterpret_cast<PNT_TIB>(NtCurrentTeb());
# endif
+
+ QT_WARNING_POP
+
quint8 *stackBase = reinterpret_cast<quint8 *>(pTib->StackBase);
// Get the stack limit. tib->StackLimit is the size of the
@@ -226,6 +235,15 @@ StackProperties stackProperties()
return createStackProperties(reinterpret_cast<void *>(base), size);
}
+#elif defined(Q_OS_VXWORKS)
+
+StackProperties stackProperties()
+{
+ TASK_DESC taskDescription;
+ taskInfoGet(taskIdSelf(), &taskDescription);
+ return createStackProperties(taskDescription.td_pStackBase, taskDescription.td_stackSize);
+}
+
#else
StackProperties stackPropertiesGeneric(qsizetype stackSize = 0)
@@ -234,8 +252,8 @@ StackProperties stackPropertiesGeneric(qsizetype stackSize = 0)
pthread_t thread = pthread_self();
pthread_attr_t sattr;
- pthread_attr_init(&sattr);
# if defined(PTHREAD_NP_H) || defined(_PTHREAD_NP_H_) || defined(Q_OS_NETBSD)
+ pthread_attr_init(&sattr);
pthread_attr_get_np(thread, &sattr);
# else
pthread_getattr_np(thread, &sattr);
diff --git a/src/qml/memory/qv4writebarrier.cpp b/src/qml/memory/qv4writebarrier.cpp
new file mode 100644
index 0000000000..d7e56212ca
--- /dev/null
+++ b/src/qml/memory/qv4writebarrier.cpp
@@ -0,0 +1,36 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+#include <private/qv4value_p.h>
+#include <private/qv4mm_p.h>
+
+QT_BEGIN_NAMESPACE
+
+namespace {
+ void markHeapBase(QV4::MarkStack* markStack, QV4::Heap::Base *base){
+ if (!base)
+ return;
+ base->mark(markStack);
+ }
+}
+namespace QV4 {
+
+void WriteBarrier::write_slowpath(EngineBase *engine, Heap::Base *base, ReturnedValue *slot, ReturnedValue value)
+{
+ Q_UNUSED(base);
+ Q_UNUSED(slot);
+ MarkStack * markStack = engine->memoryManager->markStack();
+ if constexpr (isInsertionBarrier)
+ markHeapBase(markStack, Value::fromReturnedValue(value).heapObject());
+}
+
+void WriteBarrier::write_slowpath(EngineBase *engine, Heap::Base *base, Heap::Base **slot, Heap::Base *value)
+{
+ Q_UNUSED(base);
+ Q_UNUSED(slot);
+ MarkStack * markStack = engine->memoryManager->markStack();
+ if constexpr (isInsertionBarrier)
+ markHeapBase(markStack, value);
+}
+
+}
+QT_END_NAMESPACE
diff --git a/src/qml/memory/qv4writebarrier_p.h b/src/qml/memory/qv4writebarrier_p.h
index 813d360841..ddee183982 100644
--- a/src/qml/memory/qv4writebarrier_p.h
+++ b/src/qml/memory/qv4writebarrier_p.h
@@ -15,57 +15,115 @@
//
#include <private/qv4global_p.h>
+#include <private/qv4enginebase_p.h>
QT_BEGIN_NAMESPACE
-#define WRITEBARRIER_none 1
-
-#define WRITEBARRIER(x) (1/WRITEBARRIER_##x == 1)
-
namespace QV4 {
struct EngineBase;
-
-namespace WriteBarrier {
-
-enum Type {
- NoBarrier,
- Barrier
+typedef quint64 ReturnedValue;
+
+struct WriteBarrier {
+
+ static constexpr bool isInsertionBarrier = true;
+
+ Q_ALWAYS_INLINE static void write(EngineBase *engine, Heap::Base *base, ReturnedValue *slot, ReturnedValue value)
+ {
+ if (engine->isGCOngoing)
+ write_slowpath(engine, base, slot, value);
+ *slot = value;
+ }
+ Q_QML_EXPORT Q_NEVER_INLINE static void write_slowpath(
+ EngineBase *engine, Heap::Base *base,
+ ReturnedValue *slot, ReturnedValue value);
+
+ Q_ALWAYS_INLINE static void write(EngineBase *engine, Heap::Base *base, Heap::Base **slot, Heap::Base *value)
+ {
+ if (engine->isGCOngoing)
+ write_slowpath(engine, base, slot, value);
+ *slot = value;
+ }
+ Q_QML_EXPORT Q_NEVER_INLINE static void write_slowpath(
+ EngineBase *engine, Heap::Base *base,
+ Heap::Base **slot, Heap::Base *value);
+
+ // MemoryManager isn't a complete type here, so make Engine a template argument
+ // so that we can still call engine->memoryManager->markStack()
+ template<typename F, typename Engine = EngineBase>
+ static void markCustom(Engine *engine, F &&markFunction) {
+ if (engine->isGCOngoing)
+ (std::forward<F>(markFunction))(engine->memoryManager->markStack());
+ }
+
+ // HeapObjectWrapper(Base) are helper classes to ensure that
+ // we always use a WriteBarrier when setting heap-objects
+ // they are also trivial; if triviality is not required, use Pointer instead
+ struct HeapObjectWrapperBase
+ {
+ // enum class avoids accidental construction via brace-init
+ enum class PointerWrapper : quintptr {};
+ PointerWrapper wrapped;
+
+ void clear() { wrapped = PointerWrapper(quintptr(0)); }
+ };
+
+ template<typename HeapType>
+ struct HeapObjectWrapperCommon : HeapObjectWrapperBase
+ {
+ HeapType *get() const { return reinterpret_cast<HeapType *>(wrapped); }
+ operator HeapType *() const { return get(); }
+ HeapType * operator->() const { return get(); }
+
+ template <typename ConvertibleToHeapType>
+ void set(QV4::EngineBase *engine, ConvertibleToHeapType *heapObject)
+ {
+ WriteBarrier::markCustom(engine, [heapObject](QV4::MarkStack *ms){
+ if (heapObject)
+ heapObject->mark(ms);
+ });
+ wrapped = static_cast<HeapObjectWrapperBase::PointerWrapper>(quintptr(heapObject));
+ }
+ };
+
+ // all types are trivial; we however want to block copies bypassing the write barrier
+ // therefore, all members use a PhantomTag to reduce the likelihood
+ template<typename HeapType, int PhantomTag>
+ struct HeapObjectWrapper : HeapObjectWrapperCommon<HeapType> {};
+
+ /* similar Heap::Pointer, but without the Base conversion (and its inUse assert)
+ and for storing references in engine classes stored on the native heap
+ Stores a "non-owning" reference to a heap-item (in the C++ sense), but should
+ generally mark the heap-item; therefore set goes through a write-barrier
+ */
+ template<typename T>
+ struct Pointer
+ {
+ Pointer() = default;
+ ~Pointer() = default;
+ Q_DISABLE_COPY_MOVE(Pointer)
+ T* operator->() const { return get(); }
+ operator T* () const { return get(); }
+
+ void set(EngineBase *e, T *newVal) {
+ WriteBarrier::markCustom(e, [newVal](QV4::MarkStack *ms) {
+ if (newVal)
+ newVal->mark(ms);
+ });
+ ptr = newVal;
+ }
+
+ T* get() const { return ptr; }
+
+
+
+ private:
+ T *ptr = nullptr;
+ };
};
-enum NewValueType {
- Primitive,
- Object,
- Unknown
-};
-
-// ### this needs to be filled with a real memory fence once marking is concurrent
+ // ### this needs to be filled with a real memory fence once marking is concurrent
Q_ALWAYS_INLINE void fence() {}
-#if WRITEBARRIER(none)
-
-template <NewValueType type>
-static constexpr inline bool isRequired() {
- return false;
-}
-
-inline void write(EngineBase *engine, Heap::Base *base, ReturnedValue *slot, ReturnedValue value)
-{
- Q_UNUSED(engine);
- Q_UNUSED(base);
- *slot = value;
-}
-
-inline void write(EngineBase *engine, Heap::Base *base, Heap::Base **slot, Heap::Base *value)
-{
- Q_UNUSED(engine);
- Q_UNUSED(base);
- *slot = value;
-}
-
-#endif
-
-}
-
}
QT_END_NAMESPACE