| /* |
| * Copyright (C) 2020-2021 Apple, Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "JSFinalizationRegistry.h" |
| |
| #include "AbstractSlotVisitor.h" |
| #include "DeferredWorkTimer.h" |
| #include "JSCInlines.h" |
| #include "JSInternalFieldObjectImplInlines.h" |
| |
| namespace JSC { |
| |
| const ClassInfo JSFinalizationRegistry::s_info = { "FinalizationRegistry", &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSFinalizationRegistry) }; |
| |
| Structure* JSFinalizationRegistry::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) |
| { |
| return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); |
| } |
| |
| JSFinalizationRegistry* JSFinalizationRegistry::create(VM& vm, Structure* structure, JSObject* callback) |
| { |
| JSFinalizationRegistry* instance = new (NotNull, allocateCell<JSFinalizationRegistry>(vm)) JSFinalizationRegistry(vm, structure); |
| instance->finishCreation(vm, structure->globalObject(), callback); |
| return instance; |
| } |
| |
| void JSFinalizationRegistry::finishCreation(VM& vm, JSGlobalObject* globalObject, JSObject* callback) |
| { |
| Base::finishCreation(vm); |
| ASSERT(callback->isCallable(vm)); |
| auto values = initialValues(); |
| for (unsigned index = 0; index < values.size(); ++index) |
| Base::internalField(index).setWithoutWriteBarrier(values[index]); |
| internalField(Field::Callback).setWithoutWriteBarrier(callback); |
| |
| // Make sure we init the DOM wrapper for our document since it must be allocated before finalizeUnconditionally is called. finalizeUnconditionally, |
| // is called during the GC flip so no JS objects can be allocated there. This only works because we no longer weakly hold on to DOM wrappers. |
| globalObject->globalObjectMethodTable()->currentScriptExecutionOwner(globalObject); |
| } |
| |
| template<typename Visitor> |
| void JSFinalizationRegistry::visitChildrenImpl(JSCell* cell, Visitor& visitor) |
| { |
| Base::visitChildren(cell, visitor); |
| |
| auto* thisObject = jsCast<JSFinalizationRegistry*>(cell); |
| |
| Locker locker { thisObject->cellLock() }; |
| for (const auto& iter : thisObject->m_liveRegistrations) { |
| for (auto& registration : iter.value) |
| visitor.append(registration.holdings); |
| } |
| for (auto& registration : thisObject->m_noUnregistrationLive) |
| visitor.append(registration.holdings); |
| for (const auto& iter : thisObject->m_deadRegistrations) { |
| for (auto& holdings : iter.value) |
| visitor.append(holdings); |
| } |
| for (auto& holdings : thisObject->m_noUnregistrationDead) |
| visitor.append(holdings); |
| |
| size_t totalBufferSizesInBytes = thisObject->m_deadRegistrations.capacity() * sizeof(typename decltype(thisObject->m_deadRegistrations)::KeyValuePairType); |
| totalBufferSizesInBytes += thisObject->m_liveRegistrations.capacity() * sizeof(typename decltype(thisObject->m_deadRegistrations)::KeyValuePairType); |
| totalBufferSizesInBytes += thisObject->m_noUnregistrationLive.capacity() * sizeof(decltype(thisObject->m_noUnregistrationLive.takeLast())); |
| totalBufferSizesInBytes += thisObject->m_noUnregistrationDead.capacity() * sizeof(decltype(thisObject->m_noUnregistrationLive.takeLast())); |
| visitor.vm().heap.reportExtraMemoryVisited(totalBufferSizesInBytes); |
| } |
| |
| DEFINE_VISIT_CHILDREN(JSFinalizationRegistry); |
| |
| void JSFinalizationRegistry::destroy(JSCell* table) |
| { |
| static_cast<JSFinalizationRegistry*>(table)->~JSFinalizationRegistry(); |
| } |
| |
| void JSFinalizationRegistry::finalizeUnconditionally(VM& vm) |
| { |
| Locker locker { cellLock() }; |
| |
| #if ASSERT_ENABLED |
| for (const auto& iter : m_deadRegistrations) |
| RELEASE_ASSERT(iter.value.size()); |
| #endif |
| |
| bool readiedCell = false; |
| m_noUnregistrationLive.removeAllMatching([&] (const Registration& reg) { |
| ASSERT(!reg.holdings.get().isCell() || vm.heap.isMarked(reg.holdings.get().asCell())); |
| if (!vm.heap.isMarked(reg.target)) { |
| m_noUnregistrationDead.append(reg.holdings); |
| readiedCell = true; |
| return true; |
| } |
| return false; |
| }); |
| |
| m_liveRegistrations.removeIf([&] (auto& bucket) -> bool { |
| ASSERT(bucket.value.size()); |
| |
| bool keyIsDead = !vm.heap.isMarked(bucket.key); |
| DeadRegistrations* deadList = nullptr; |
| auto getDeadList = [&] () -> DeadRegistrations& { |
| if (UNLIKELY(!deadList)) |
| deadList = &m_deadRegistrations.add(bucket.key, DeadRegistrations()).iterator->value; |
| return *deadList; |
| }; |
| |
| bucket.value.removeAllMatching([&] (const Registration& reg) { |
| ASSERT(!reg.holdings.get().isCell() || vm.heap.isMarked(reg.holdings.get().asCell())); |
| if (!vm.heap.isMarked(reg.target)) { |
| if (keyIsDead) |
| m_noUnregistrationDead.append(reg.holdings); |
| else |
| getDeadList().append(reg.holdings); |
| readiedCell = true; |
| return true; |
| } |
| |
| if (keyIsDead) { |
| m_noUnregistrationLive.append(reg); |
| return true; |
| } |
| |
| return false; |
| }); |
| |
| return !bucket.value.size(); |
| }); |
| |
| if (!m_hasAlreadyScheduledWork && (readiedCell || deadCount(locker))) { |
| auto ticket = vm.deferredWorkTimer->addPendingWork(vm, this, { }); |
| ASSERT(vm.deferredWorkTimer->hasPendingWork(ticket)); |
| vm.deferredWorkTimer->scheduleWorkSoon(ticket, [this](DeferredWorkTimer::Ticket) { |
| JSGlobalObject* globalObject = this->globalObject(); |
| this->m_hasAlreadyScheduledWork = false; |
| this->runFinalizationCleanup(globalObject); |
| }); |
| m_hasAlreadyScheduledWork = true; |
| } |
| } |
| |
| void JSFinalizationRegistry::runFinalizationCleanup(JSGlobalObject* globalObject) |
| { |
| VM& vm = globalObject->vm(); |
| auto scope = DECLARE_THROW_SCOPE(vm); |
| |
| while (JSValue value = takeDeadHoldingsValue()) { |
| MarkedArgumentBuffer args; |
| args.append(value); |
| call(globalObject, callback(), args, "This should not be visible: please report a bug to bugs.webkit.org"); |
| RETURN_IF_EXCEPTION(scope, void()); |
| } |
| } |
| |
| JSValue JSFinalizationRegistry::takeDeadHoldingsValue() |
| { |
| Locker locker { cellLock() }; |
| JSValue result; |
| if (m_noUnregistrationDead.size()) |
| result = m_noUnregistrationDead.takeLast().get(); |
| else { |
| auto iter = m_deadRegistrations.begin(); |
| if (iter == m_deadRegistrations.end()) |
| return JSValue(); |
| ASSERT(iter->value.size()); |
| result = iter->value.takeLast().get(); |
| if (!iter->value.size()) |
| m_deadRegistrations.remove(iter); |
| } |
| |
| #if ASSERT_ENABLED |
| for (const auto& iter : m_deadRegistrations) |
| RELEASE_ASSERT(iter.value.size()); |
| #endif |
| |
| return result; |
| } |
| |
| void JSFinalizationRegistry::registerTarget(VM& vm, JSObject* target, JSValue holdings, JSValue token) |
| { |
| Locker locker { cellLock() }; |
| Registration registration; |
| registration.target = target; |
| registration.holdings.setWithoutWriteBarrier(holdings); |
| if (token.isUndefined()) |
| m_noUnregistrationLive.append(WTFMove(registration)); |
| else { |
| auto result = m_liveRegistrations.add(jsSecureCast<JSObject*>(vm, token), LiveRegistrations()); |
| result.iterator->value.append(WTFMove(registration)); |
| } |
| vm.writeBarrier(this); |
| } |
| |
| bool JSFinalizationRegistry::unregister(VM&, JSObject* token) |
| { |
| // We don't need to write barrier ourselves here because we will only point to less things after this finishes. |
| Locker locker { cellLock() }; |
| bool result = m_liveRegistrations.remove(token); |
| result |= m_deadRegistrations.remove(token); |
| |
| return result; |
| } |
| |
| size_t JSFinalizationRegistry::liveCount(const Locker<JSCellLock>&) |
| { |
| size_t count = m_noUnregistrationLive.size(); |
| for (const auto& iter : m_liveRegistrations) |
| count += iter.value.size(); |
| |
| return count; |
| } |
| |
| size_t JSFinalizationRegistry::deadCount(const Locker<JSCellLock>&) |
| { |
| size_t count = m_noUnregistrationDead.size(); |
| for (const auto& iter : m_deadRegistrations) |
| count += iter.value.size(); |
| |
| return count; |
| } |
| |
| } |
| |
| |