From 34e03d1551ca80afc66c60e0763da56e083c50f9 Mon Sep 17 00:00:00 2001 From: hewei Date: Sat, 28 Jun 2025 17:54:49 +0800 Subject: [PATCH] optimize builtins finalizationregistry Issue: https://gitee.com/openharmony/arkcompiler_ets_runtime/issues/ICIHRI Signed-off-by: hewei Change-Id: I4457ba2eb147beb3d57cd76f89dba0677fd6dc77 --- .../builtins_finalization_registry.cpp | 12 +- .../dfx/hprof/tests/js_metadata_test.cpp | 23 +- ecmascript/dump.cpp | 16 +- ecmascript/js_finalization_registry.cpp | 265 +++++++++++------- ecmascript/js_finalization_registry.h | 30 +- ecmascript/js_type_metadata/cell_record.json | 12 +- .../js_finalization_registry.json | 4 +- ecmascript/object_factory.cpp | 7 +- ecmascript/tests/dump_test.cpp | 4 +- .../tests/js_finalization_registry_test.cpp | 42 ++- test/moduletest/BUILD.gn | 2 + test/moduletest/finalizationregistry/BUILD.gn | 18 ++ .../finalizationregistry.js | 125 +++++++++ 13 files changed, 393 insertions(+), 167 deletions(-) create mode 100644 test/moduletest/finalizationregistry/BUILD.gn create mode 100644 test/moduletest/finalizationregistry/finalizationregistry.js diff --git a/ecmascript/builtins/builtins_finalization_registry.cpp b/ecmascript/builtins/builtins_finalization_registry.cpp index 362e849dc4..6a09873e72 100644 --- a/ecmascript/builtins/builtins_finalization_registry.cpp +++ b/ecmascript/builtins/builtins_finalization_registry.cpp @@ -47,15 +47,16 @@ JSTaggedValue BuiltinsFinalizationRegistry::FinalizationRegistryConstructor(Ecma // 5. Set finalizationRegistry.[[Realm]] to fn.[[Realm]]. // 6. Set finalizationRegistry.[[CleanupCallback]] to cleanupCallback. finalization->SetCleanupCallback(thread, cleanupCallback); + // 7. Set finalizationRegistry.[[Cells]] to a new empty List. - JSHandle noUnregister(CellRecordVector::Create(thread)); - JSHandle maybeUnregister = LinkedHashMap::Create(thread); - finalization->SetNoUnregister(thread, noUnregister); - finalization->SetMaybeUnregister(thread, maybeUnregister); - JSHandle objValue(finalization); + JSHandle registeredCellsWithToken = LinkedHashMap::Create(thread); + finalization->SetRegisteredCells(thread, JSTaggedValue::Hole()); + finalization->SetRegisteredCellsWithToken(thread, registeredCellsWithToken); + // 8. Return finalizationRegistry. return finalization.GetTaggedValue(); } + // 26.2.3.2 JSTaggedValue BuiltinsFinalizationRegistry::Register(EcmaRuntimeCallInfo *argv) { @@ -95,6 +96,7 @@ JSTaggedValue BuiltinsFinalizationRegistry::Register(EcmaRuntimeCallInfo *argv) // 8. Return undefined. return JSTaggedValue::Undefined(); } + // 26.2.3.3 JSTaggedValue BuiltinsFinalizationRegistry::Unregister(EcmaRuntimeCallInfo *argv) { diff --git a/ecmascript/dfx/hprof/tests/js_metadata_test.cpp b/ecmascript/dfx/hprof/tests/js_metadata_test.cpp index 9c1827d2f4..e037a94840 100644 --- a/ecmascript/dfx/hprof/tests/js_metadata_test.cpp +++ b/ecmascript/dfx/hprof/tests/js_metadata_test.cpp @@ -167,7 +167,7 @@ public: {JSType::ASYNC_ITERATOR_RECORD, {"Iterator", "NextMethod", "ASYNC_ITERATOR_RECORD"}}, {JSType::BIGINT, {"BIGINT"}}, {JSType::BYTE_ARRAY, {"BYTE_ARRAY"}}, - {JSType::CELL_RECORD, {"WeakRefTarget", "HeldValue", "CELL_RECORD"}}, + {JSType::CELL_RECORD, {"WeakRefTarget", "HeldValue", "Next", "Prev", "CELL_RECORD"}}, {JSType::CLASS_INFO_EXTRACTOR, { "NonStaticKeys", "NonStaticProperties", "NonStaticElements", "StaticKeys", "StaticProperties", "StaticElements", @@ -250,8 +250,9 @@ public: {JSType::JS_DISPLAYNAMES, {"Locale", "IcuLDN", "JS_DISPLAYNAMES"}}, {JSType::JS_ERROR, {"Properties", "Elements", "JS_ERROR"}}, {JSType::JS_EVAL_ERROR, {"Properties", "Elements", "JS_EVAL_ERROR"}}, - {JSType::JS_FINALIZATION_REGISTRY, {"CleanupCallback", "NoUnregister", - "MaybeUnregister", "Next", "Prev", "JS_FINALIZATION_REGISTRY"}}, + {JSType::JS_FINALIZATION_REGISTRY, {"CleanupCallback", "RegisteredCells", + "RegisteredCellsWithToken", "Next", "Prev", + "JS_FINALIZATION_REGISTRY"}}, {JSType::JS_FLOAT32_ARRAY, {"ViewedArrayBufferOrByteArray", "TypedArrayName", "JS_FLOAT32_ARRAY"}}, {JSType::JS_FLOAT64_ARRAY, {"ViewedArrayBufferOrByteArray", "TypedArrayName", "JS_FLOAT64_ARRAY"}}, {JSType::JS_FORIN_ITERATOR, {"Object", "CachedHClass", "Keys", "JS_FORIN_ITERATOR"}}, @@ -440,6 +441,8 @@ public: {JSType::BYTE_ARRAY, {ByteArray::LAST_OFFSET - ByteArray::ARRAY_LENGTH_OFFSET}}, {JSType::CELL_RECORD, {CellRecord::WEAKREF_TARGET_OFFSET, CellRecord::HELD_VALUE_OFFSET, + CellRecord::NEXT_OFFSET, + CellRecord::PREV_OFFSET, CellRecord::SIZE - CellRecord::WEAKREF_TARGET_OFFSET}}, {JSType::CLASS_INFO_EXTRACTOR, { ClassInfoExtractor::PROTOTYPE_HCLASS_OFFSET, @@ -653,8 +656,8 @@ public: JSObject::SIZE - JSObject::PROPERTIES_OFFSET}}, {JSType::JS_FINALIZATION_REGISTRY, { JSFinalizationRegistry::CLEANUP_CALLBACK_OFFSET, - JSFinalizationRegistry::NO_UNREGISTER_OFFSET, - JSFinalizationRegistry::MAYBE_UNREGISTER_OFFSET, + JSFinalizationRegistry::CELLS_OFFSET, + JSFinalizationRegistry::CELLS_WITH_TOKEN_OFFSET, JSFinalizationRegistry::NEXT_OFFSET, JSFinalizationRegistry::PREV_OFFSET, JSFinalizationRegistry::SIZE - JSFinalizationRegistry::CLEANUP_CALLBACK_OFFSET}}, @@ -1273,7 +1276,9 @@ public: {JSType::BIGINT, {}}, {JSType::BYTE_ARRAY, {}}, {JSType::CELL_RECORD, {CellRecord::HELD_VALUE_OFFSET - CellRecord::WEAKREF_TARGET_OFFSET, - CellRecord::SIZE - CellRecord::HELD_VALUE_OFFSET}}, + CellRecord::NEXT_OFFSET- CellRecord::HELD_VALUE_OFFSET, + CellRecord::PREV_OFFSET - CellRecord::NEXT_OFFSET, + CellRecord::SIZE - CellRecord::PREV_OFFSET}}, {JSType::CLASS_INFO_EXTRACTOR, { ClassInfoExtractor::NON_STATIC_PROPERTIES_OFFSET - ClassInfoExtractor::PROTOTYPE_HCLASS_OFFSET, ClassInfoExtractor::NON_STATIC_ELEMENTS_OFFSET - ClassInfoExtractor::NON_STATIC_PROPERTIES_OFFSET, @@ -1436,9 +1441,9 @@ public: {JSType::JS_EVAL_ERROR, {JSObject::ELEMENTS_OFFSET - JSObject::PROPERTIES_OFFSET, JSObject::SIZE - JSObject::ELEMENTS_OFFSET}}, {JSType::JS_FINALIZATION_REGISTRY, { - JSFinalizationRegistry::NO_UNREGISTER_OFFSET - JSFinalizationRegistry::CLEANUP_CALLBACK_OFFSET, - JSFinalizationRegistry::MAYBE_UNREGISTER_OFFSET - JSFinalizationRegistry::NO_UNREGISTER_OFFSET, - JSFinalizationRegistry::NEXT_OFFSET - JSFinalizationRegistry::MAYBE_UNREGISTER_OFFSET, + JSFinalizationRegistry::CELLS_OFFSET - JSFinalizationRegistry::CLEANUP_CALLBACK_OFFSET, + JSFinalizationRegistry::CELLS_WITH_TOKEN_OFFSET - JSFinalizationRegistry::CELLS_OFFSET, + JSFinalizationRegistry::NEXT_OFFSET - JSFinalizationRegistry::CELLS_WITH_TOKEN_OFFSET, JSFinalizationRegistry::PREV_OFFSET - JSFinalizationRegistry::NEXT_OFFSET, JSFinalizationRegistry::SIZE - JSFinalizationRegistry::PREV_OFFSET}}, {JSType::JS_FLOAT32_ARRAY, { diff --git a/ecmascript/dump.cpp b/ecmascript/dump.cpp index 6a291a0c69..4162de98cf 100644 --- a/ecmascript/dump.cpp +++ b/ecmascript/dump.cpp @@ -2173,11 +2173,11 @@ void JSFinalizationRegistry::Dump(const JSThread *thread, std::ostream &os) cons os << " - CleanupCallback : "; GetCleanupCallback(thread).DumpTaggedValue(thread, os); os << "\n"; - os << " - NoUnregister : "; - GetNoUnregister(thread).Dump(thread, os); + os << " - RegisteredCells : "; + GetRegisteredCells(thread).Dump(thread, os); os << "\n"; - os << " - MaybeUnregister : "; - LinkedHashMap *map = LinkedHashMap::Cast(GetMaybeUnregister(thread).GetTaggedObject()); + os << " - RegisteredCellsWithToken : "; + LinkedHashMap *map = LinkedHashMap::Cast(GetRegisteredCellsWithToken(thread).GetTaggedObject()); os << " - elements: " << std::dec << map->NumberOfElements() << "\n"; os << " - deleted-elements: " << std::dec << map->NumberOfDeletedElements() << "\n"; os << " - capacity: " << std::dec << map->Capacity() << "\n"; @@ -5067,12 +5067,12 @@ void JSWeakRef::DumpForSnapshot(const JSThread *thread, std::vector & void JSFinalizationRegistry::DumpForSnapshot(const JSThread *thread, std::vector &vec) const { vec.emplace_back(CString("CleanupCallback"), GetCleanupCallback(thread)); - if (!(GetMaybeUnregister(thread).IsInvalidValue())) { - LinkedHashMap *map = LinkedHashMap::Cast(GetMaybeUnregister(thread).GetTaggedObject()); - vec.emplace_back(CString("MaybeUnregister"), GetMaybeUnregister(thread)); + vec.emplace_back(CString("GetRegisteredCells"), GetRegisteredCells(thread)); + if (!(GetRegisteredCellsWithToken(thread).IsInvalidValue())) { + LinkedHashMap *map = LinkedHashMap::Cast(GetRegisteredCellsWithToken(thread).GetTaggedObject()); + vec.emplace_back(CString("RegisteredCellsWithToken"), GetRegisteredCellsWithToken(thread)); map->DumpForSnapshot(thread, vec); } - vec.emplace_back(CString("Next"), GetNext(thread)); vec.emplace_back(CString("Prev"), GetPrev(thread)); JSObject::DumpForSnapshot(thread, vec); diff --git a/ecmascript/js_finalization_registry.cpp b/ecmascript/js_finalization_registry.cpp index 216070f963..ac29f00006 100644 --- a/ecmascript/js_finalization_registry.cpp +++ b/ecmascript/js_finalization_registry.cpp @@ -21,30 +21,6 @@ #include "ecmascript/linked_hash_table.h" namespace panda::ecmascript { -// -------------------------------CellRecordVector----------------------------------- -JSHandle CellRecordVector::Append(const JSThread *thread, const JSHandle &vector, - const JSHandle &value) -{ - JSHandle oldVector(vector); - JSHandle newVector = WeakVector::FillOrAppend(thread, oldVector, value); - return JSHandle(newVector); -} - -bool CellRecordVector::IsEmpty(const JSThread *thread) -{ - if (Empty()) { - return true; - } - for (uint32_t i = 0; i < GetEnd(); i++) { - JSTaggedValue value = Get(thread, i); - if (!value.IsHole()) { - return false; - } - } - return true; -} - -// ---------------------------JSFinalizationRegistry----------------------------------- void JSFinalizationRegistry::Register(JSThread *thread, JSHandle target, JSHandle heldValue, JSHandle unregisterToken, JSHandle obj) @@ -53,26 +29,44 @@ void JSFinalizationRegistry::Register(JSThread *thread, JSHandle JSHandle cellRecord = factory->NewCellRecord(); cellRecord->SetToWeakRefTarget(thread, target.GetTaggedValue()); cellRecord->SetHeldValue(thread, heldValue); - JSHandle cell(cellRecord); - // If unregisterToken is undefined, we use vector to store - // otherwise we use hash map to store to facilitate subsequent delete operations + if (!unregisterToken->IsUndefined()) { - JSHandle maybeUnregister(thread, obj->GetMaybeUnregister(thread)); - JSHandle array(thread, JSTaggedValue::Undefined()); - if (maybeUnregister->Has(thread, unregisterToken.GetTaggedValue())) { - array = JSHandle(thread, maybeUnregister->Get(thread, unregisterToken.GetTaggedValue())); - } else { - array = JSHandle(CellRecordVector::Create(thread)); + JSHandle registeredCellsWithToken(thread, obj->GetRegisteredCellsWithToken(thread)); + if (registeredCellsWithToken->Has(thread, unregisterToken.GetTaggedValue())) { + ASSERT(registeredCellsWithToken->Get(thread, unregisterToken.GetTaggedValue()).IsCellRecord()); + JSHandle headCell(thread, + registeredCellsWithToken->Get(thread, unregisterToken.GetTaggedValue())); + // insert cellRecord at the head of the list + cellRecord->SetNext(thread, headCell.GetTaggedValue()); + // assert the prev of current head cell is Hole + ASSERT(JSHandle::Cast(headCell)->GetPrev(thread).IsHole()); + JSHandle::Cast(headCell)->SetPrev(thread, cellRecord.GetTaggedValue()); } - array = CellRecordVector::Append(thread, array, cell); - JSHandle arrayValue(array); - maybeUnregister = LinkedHashMap::SetWeakRef(thread, maybeUnregister, unregisterToken, arrayValue); - obj->SetMaybeUnregister(thread, maybeUnregister); + // update linkedhashmap with the need head of key unregisterToken + registeredCellsWithToken = LinkedHashMap::SetWeakRef(thread, registeredCellsWithToken, + unregisterToken, + JSHandle::Cast(cellRecord)); } else { - JSHandle noUnregister(thread, obj->GetNoUnregister(thread)); - noUnregister = CellRecordVector::Append(thread, noUnregister, cell); - obj->SetNoUnregister(thread, noUnregister); + // get the head of cells list and push the new cell to the front of the list + JSTaggedValue registeredCellsVal = obj->GetRegisteredCells(thread); + if (registeredCellsVal.IsHole()) { + // If the cells list is empty, set the new cell as the head + obj->SetRegisteredCells(thread, cellRecord.GetTaggedValue()); + } else { + // If the cells list is not empty, set the new cell as the head and link it to the old head + JSHandle registeredCells(thread, registeredCellsVal); + cellRecord->SetNext(thread, registeredCells.GetTaggedValue()); + registeredCells->SetPrev(thread, cellRecord.GetTaggedValue()); + // Update the head of the cells list to the new cell + obj->SetRegisteredCells(thread, cellRecord.GetTaggedValue()); + } } + // cell is inserted at the head of the list, its prev is Hole + ASSERT(cellRecord->GetPrev(thread).IsHole()); + // cell->next == hole || cell->next->prev == cell + ASSERT(cellRecord->GetNext(thread).IsHole() || + JSHandle(thread, cellRecord->GetNext(thread))->GetPrev(thread) == cellRecord.GetTaggedValue()); + JSFinalizationRegistry::AddFinRegLists(thread, obj); } @@ -81,14 +75,15 @@ bool JSFinalizationRegistry::Unregister(JSThread *thread, JSHandle maybeUnregister(thread, obj->GetMaybeUnregister(thread)); - int entry = maybeUnregister->FindElement(thread, UnregisterToken.GetTaggedValue()); + + JSHandle registeredCellsWithToken(thread, obj->GetRegisteredCellsWithToken(thread)); + int entry = registeredCellsWithToken->FindElement(thread, UnregisterToken.GetTaggedValue()); if (entry == -1) { return false; } - maybeUnregister->RemoveEntry(thread, entry); - JSHandle newMaybeUnregister = LinkedHashMap::Shrink(thread, maybeUnregister); - obj->SetMaybeUnregister(thread, newMaybeUnregister); + registeredCellsWithToken->RemoveEntry(thread, entry); + JSHandle newRegisteredCellsWithToken = LinkedHashMap::Shrink(thread, registeredCellsWithToken); + obj->SetRegisteredCellsWithToken(thread, newRegisteredCellsWithToken); return true; } @@ -139,26 +134,97 @@ void JSFinalizationRegistry::CheckAndCall(JSThread *thread) } } -void DealCallBackOfMap(JSThread *thread, JSHandle &cellVect, - JSHandle &job, JSHandle &func) +/** + * Detach @p curCell from the intrusive doubly‑linked list with head @p headCell. + */ +inline void JSFinalizationRegistry::DetachCell(JSThread *thread, JSHandle &curCell, + JSMutableHandle &headCell) { - if (!cellVect->Empty()) { - uint32_t cellVectLen = cellVect->GetEnd(); - for (uint32_t i = 0; i < cellVectLen; ++i) { - JSTaggedValue value = cellVect->Get(thread, i); - if (value.IsHole()) { - continue; - } - JSHandle cellRecord(thread, value); - // if WeakRefTarget have been gc, set callback to job and delete - if (cellRecord->GetFromWeakRefTarget(thread).IsUndefined()) { - JSHandle argv = thread->GetEcmaVM()->GetFactory()->NewTaggedArray(1); - argv->Set(thread, 0, cellRecord->GetHeldValue(thread)); - job::MicroJobQueue::EnqueueJob(thread, job, job::QueueType::QUEUE_PROMISE, func, argv); - cellVect->Delete(thread, i); - } - } + // when we call this function, the linked list must not be empty, so its headCell must not be Hole + ASSERT(!headCell->IsHole()); + + /** + * 1. If cell is the head of the list, do something like this: + * (1) current state: + * cell <-> nextCell <-> nextNextCell <-> xxxxxx + * ^ headCell + * (2) remote cell from list and update the head: + * nextCell <-> nextNextCell <-> xxxxxx + * ^ headCell + * + * 2. If cell is not the head of the list, just remove it from the list: + * (1) current state: + * headCell <-> xxx <-> prevCell <-> cell <-> nextCell <-> xxxxxx + * (2) remote cell from list: + * headCell <-> xxx <-> prevCell <----------> nextCell <-> xxxxxx + */ + JSTaggedValue prevVal = curCell->GetPrev(thread); + JSTaggedValue nextVal = curCell->GetNext(thread); + + if (!nextVal.IsHole()) { + // curCell is not the tail of the list, update the prev of next cell + // curCell->next->prev = curCell->prev + ASSERT(nextVal.IsCellRecord()); + JSHandle nextCell(thread, nextVal); + nextCell->SetPrev(thread, prevVal); } + + if (!prevVal.IsHole()) { + // cell is not the head of the list, update the next of prev cell + // curCell->prev->next = curCell->next + ASSERT(prevVal.IsCellRecord()); + JSHandle prevCell(thread, prevVal); + prevCell->SetNext(thread, nextVal); + } else { + // cell is the head of the list, update head cell + headCell.Update(nextVal); + } +} + +/** + * Check the list of cells and enqueue jobs for those whose WeakRefTarget has been gc + * @param headCell The head cell of the linked list of cells. + * @return The JSTaggedValue representing new Head of the linked list. + */ +void JSFinalizationRegistry::CheckListAndEnqueueJob(JSThread *thread, + JSHandle &func, + JSMutableHandle &headCell) +{ + // 1. Assert: headCell is the head of the list. + // 2. While headCell is not empty, do the following: + // a. If headCell.[[WeakRefTarget]] is empty, then + // i. Perform ? HostCallJobCallback(callback, undefined, « headCell.[[HeldValue]] »). + // ii. Remove headCell from the list. + // b. Else, + // i. Set headCell to headCell.[[Next]]. + ASSERT(headCell->IsCellRecord()); + ASSERT(JSHandle::Cast(headCell)->GetPrev(thread).IsHole()); + + auto ecmaVm = thread->GetEcmaVM(); + JSHandle job = ecmaVm->GetMicroJobQueue(); + ObjectFactory *factory = ecmaVm->GetFactory(); + // curCell is used to traverse the linked list of cells + JSMutableHandle curCellVal(thread, headCell); + do { + auto curCell = JSHandle::Cast(curCellVal); + // If the WeakRefTarget of the cell is empty, it means that the object has been gc + if (curCell->GetFromWeakRefTarget(thread).IsUndefined()) { + // Set callback to job + JSHandle argv = factory->NewTaggedArray(1); + argv->Set(thread, 0, curCell->GetHeldValue(thread)); + job::MicroJobQueue::EnqueueJob(thread, job, job::QueueType::QUEUE_PROMISE, func, argv); + + JSHandle nextCell(thread, curCell->GetNext(thread)); + // remove curCell from linked list + DetachCell(thread, curCell, headCell); + // update curCellVal to the next cell in the linked list + curCellVal.Update(nextCell.GetTaggedValue()); + } else { + // If the WeakRefTarget of the cell is not empty, it means that the object has not been gc, just + // update curCell to the next cell in the linked list, also update retValue to false + curCellVal.Update(curCell->GetNext(thread)); + } + } while (!curCellVal.GetTaggedValue().IsHole()); // continue until curCell is the last cell in the linked list } bool JSFinalizationRegistry::CleanupFinalizationRegistry(JSThread *thread, JSHandle obj) @@ -170,52 +236,57 @@ bool JSFinalizationRegistry::CleanupFinalizationRegistry(JSThread *thread, JSHan // a. Choose any such cell. // b. Remove cell from finalizationRegistry.[[Cells]]. // c. Perform ? HostCallJobCallback(callback, undefined, « cell.[[HeldValue]] »). - // 4. Return unused. + // 4. Return true if finalizationRegistry.[[Cells]] is empty, and false otherwise. ASSERT(obj->IsECMAObject()); auto ecmaVm = thread->GetEcmaVM(); JSHandle job = ecmaVm->GetMicroJobQueue(); ObjectFactory *factory = ecmaVm->GetFactory(); JSHandle func(thread, obj->GetCleanupCallback(thread)); - JSHandle noUnregister(thread, obj->GetNoUnregister(thread)); - if (!noUnregister->Empty()) { - uint32_t noUnregisterLen = noUnregister->GetEnd(); - for (uint32_t i = 0; i < noUnregisterLen; ++i) { - JSTaggedValue value = noUnregister->Get(thread, i); - if (value.IsHole()) { - continue; - } - JSHandle cellRecord(thread, value); - // if WeakRefTarget have been gc, set callback to job and delete - if (cellRecord->GetFromWeakRefTarget(thread).IsUndefined()) { - JSHandle argv = factory->NewTaggedArray(1); - argv->Set(thread, 0, cellRecord->GetHeldValue(thread)); - job::MicroJobQueue::EnqueueJob(thread, job, job::QueueType::QUEUE_PROMISE, func, argv); - noUnregister->Delete(thread, i); - } + + // return true if all the objects registered on the current JSFinalizationRegistry object have been gc + bool retValue = true; + JSHandle registeredCells(thread, obj->GetRegisteredCells(thread)); + if (!registeredCells->IsHole()) { + ASSERT(registeredCells->IsCellRecord()); + + JSMutableHandle headCell(thread, registeredCells); + // Check the list of cells and enqueue jobs for those whose WeakRefTarget has been gc + CheckListAndEnqueueJob(thread, func, headCell); + // udpate the head of the linked list of cells + obj->SetRegisteredCells(thread, headCell.GetTaggedValue()); + if (!headCell->IsHole()) { + // If the head cell is not Hole, it means that there are still some cells in the list + retValue = false; } } - JSMutableHandle maybeUnregister(thread, obj->GetMaybeUnregister(thread)); + + JSMutableHandle registeredCellsWithToken(thread, obj->GetRegisteredCellsWithToken(thread)); int index = 0; - int totalElements = maybeUnregister->NumberOfElements() + maybeUnregister->NumberOfDeletedElements(); + int totalElements = registeredCellsWithToken->NumberOfElements() + + registeredCellsWithToken->NumberOfDeletedElements(); while (index < totalElements) { - if (!maybeUnregister->GetKey(thread, index++).IsHole()) { - JSHandle cellVect(thread, maybeUnregister->GetValue(thread, index - 1)); - DealCallBackOfMap(thread, cellVect, job, func); - if (!cellVect->Empty()) { - continue; + if (!registeredCellsWithToken->GetKey(thread, index).IsHole()) { + // if the value is Hole, it means that the entry has been deleted, the branch will not be reached + ASSERT(registeredCellsWithToken->GetValue(thread, index).IsCellRecord()); + + JSMutableHandle headCell(thread, registeredCellsWithToken->GetValue(thread, index)); + + CheckListAndEnqueueJob(thread, func, headCell); + if (!headCell->IsHole()) { + // If the head cell is not Hole, it means that there are still some cells in the list + retValue = false; + } else { + // If the head cell is Hole, it means that all the cells in the list have been gc + // remove the entry from the linked hash map + registeredCellsWithToken->RemoveEntry(thread, index); } - maybeUnregister->RemoveEntry(thread, index - 1); } + index++; } - JSHandle newMap = LinkedHashMap::Shrink(thread, maybeUnregister); - obj->SetMaybeUnregister(thread, newMap); - // Determine whether the objects registered on the current JSFinalizationRegistry object - // have all been gc - int remainSize = newMap->NumberOfElements() + newMap->NumberOfDeletedElements(); - if (noUnregister->IsEmpty(thread) && remainSize == 0) { - return true; - } - return false; + // After checking all the entries in the linked hash map, shrink it to remove any empty entries + auto newMap = LinkedHashMap::Shrink(thread, registeredCellsWithToken); + obj->SetRegisteredCellsWithToken(thread, newMap); + return retValue; } void JSFinalizationRegistry::AddFinRegLists(JSThread *thread, JSHandle next) diff --git a/ecmascript/js_finalization_registry.h b/ecmascript/js_finalization_registry.h index 76755df9d5..83257cdf6f 100644 --- a/ecmascript/js_finalization_registry.h +++ b/ecmascript/js_finalization_registry.h @@ -56,23 +56,14 @@ public: } static constexpr size_t WEAKREF_TARGET_OFFSET = Record::SIZE; ACCESSORS(WeakRefTarget, WEAKREF_TARGET_OFFSET, HELD_VALUE_OFFSET) - ACCESSORS(HeldValue, HELD_VALUE_OFFSET, SIZE) + ACCESSORS(HeldValue, HELD_VALUE_OFFSET, NEXT_OFFSET) + ACCESSORS(Next, NEXT_OFFSET, PREV_OFFSET) + ACCESSORS(Prev, PREV_OFFSET, SIZE) DECL_VISIT_OBJECT(WEAKREF_TARGET_OFFSET, SIZE) DECL_DUMP() }; -class CellRecordVector : public WeakVector { -public: - static CellRecordVector *Cast(TaggedObject *object) - { - return static_cast(object); - } - static JSHandle Append(const JSThread *thread, const JSHandle &vector, - const JSHandle &value); - bool IsEmpty(const JSThread *thread); -}; - class JSFinalizationRegistry : public JSObject { public: CAST_CHECK(JSFinalizationRegistry, IsJSFinalizationRegistry); @@ -86,13 +77,22 @@ public: static void AddFinRegLists(JSThread *thread, JSHandle next); static void CleanFinRegLists(JSThread *thread, JSHandle obj); static constexpr size_t CLEANUP_CALLBACK_OFFSET = JSObject::SIZE; - ACCESSORS(CleanupCallback, CLEANUP_CALLBACK_OFFSET, NO_UNREGISTER_OFFSET) - ACCESSORS(NoUnregister, NO_UNREGISTER_OFFSET, MAYBE_UNREGISTER_OFFSET) - ACCESSORS(MaybeUnregister, MAYBE_UNREGISTER_OFFSET, NEXT_OFFSET) + ACCESSORS(CleanupCallback, CLEANUP_CALLBACK_OFFSET, CELLS_OFFSET) + // RegisteredCells is a doubly-linked list that manages all registered cells. + ACCESSORS(RegisteredCells, CELLS_OFFSET, CELLS_WITH_TOKEN_OFFSET) + // RegisteredCellsWithToken is a linked hash map that manages cells with unregister tokens. + // The key is the unregister token, and the value is the head of the linked list of cells registered for that token. + ACCESSORS(RegisteredCellsWithToken, CELLS_WITH_TOKEN_OFFSET, NEXT_OFFSET) ACCESSORS(Next, NEXT_OFFSET, PREV_OFFSET) ACCESSORS(Prev, PREV_OFFSET, SIZE) DECL_VISIT_OBJECT_FOR_JS_OBJECT(JSObject, CLEANUP_CALLBACK_OFFSET, SIZE) DECL_DUMP() + +private: + static inline void CheckListAndEnqueueJob(JSThread *thread, JSHandle &func, + JSMutableHandle &headCell); + static inline void DetachCell(JSThread *thread, JSHandle &curCell, + JSMutableHandle &headCell); }; } // namespace #endif // ECMASCRIPT_JS_FINALIZATION_REGISTRY_H \ No newline at end of file diff --git a/ecmascript/js_type_metadata/cell_record.json b/ecmascript/js_type_metadata/cell_record.json index 5da40c0d35..39b86db414 100644 --- a/ecmascript/js_type_metadata/cell_record.json +++ b/ecmascript/js_type_metadata/cell_record.json @@ -10,9 +10,19 @@ "name": "HeldValue", "offset": 8, "size": 8 + }, + { + "name": "Next", + "offset": 16, + "size": 8 + }, + { + "name": "Prev", + "offset": 24, + "size": 8 } ], - "end_offset": 16, + "end_offset": 32, "parents": [ "RECORD" ] diff --git a/ecmascript/js_type_metadata/js_finalization_registry.json b/ecmascript/js_type_metadata/js_finalization_registry.json index 6e69bf67bb..fa9e1e66de 100644 --- a/ecmascript/js_type_metadata/js_finalization_registry.json +++ b/ecmascript/js_type_metadata/js_finalization_registry.json @@ -7,12 +7,12 @@ "size": 8 }, { - "name": "NoUnregister", + "name": "RegisteredCells", "offset": 8, "size": 8 }, { - "name": "MaybeUnregister", + "name": "RegisteredCellsWithToken", "offset": 16, "size": 8 }, diff --git a/ecmascript/object_factory.cpp b/ecmascript/object_factory.cpp index 49433f5e52..e6178dcda2 100644 --- a/ecmascript/object_factory.cpp +++ b/ecmascript/object_factory.cpp @@ -1505,8 +1505,9 @@ void ObjectFactory::InitializeJSObject(const JSHandle &obj, const JSHa break; case JSType::JS_FINALIZATION_REGISTRY: JSFinalizationRegistry::Cast(*obj)->SetCleanupCallback(thread_, JSTaggedValue::Undefined()); - JSFinalizationRegistry::Cast(*obj)->SetNoUnregister(thread_, JSTaggedValue::Undefined()); - JSFinalizationRegistry::Cast(*obj)->SetMaybeUnregister(thread_, JSTaggedValue::Undefined()); + JSFinalizationRegistry::Cast(*obj)->SetRegisteredCells(thread_, JSTaggedValue::Hole()); + JSFinalizationRegistry::Cast(*obj)->SetRegisteredCellsWithToken( + thread_, JSTaggedValue::Hole()); JSFinalizationRegistry::Cast(*obj)->SetNext(thread_, JSTaggedValue::Null()); JSFinalizationRegistry::Cast(*obj)->SetPrev(thread_, JSTaggedValue::Null()); break; @@ -5158,6 +5159,8 @@ JSHandle ObjectFactory::NewCellRecord() JSHandle obj(thread_, header); obj->SetWeakRefTarget(thread_, JSTaggedValue::Undefined()); obj->SetHeldValue(thread_, JSTaggedValue::Undefined()); + obj->SetNext(thread_, JSTaggedValue::Hole()); + obj->SetPrev(thread_, JSTaggedValue::Hole()); return obj; } diff --git a/ecmascript/tests/dump_test.cpp b/ecmascript/tests/dump_test.cpp index 16ac3791bb..be886d478a 100644 --- a/ecmascript/tests/dump_test.cpp +++ b/ecmascript/tests/dump_test.cpp @@ -720,12 +720,12 @@ HWTEST_F_L0(EcmaDumpTest, HeapProfileDump) JSHandle jsFinalizationRegistry = JSHandle::Cast(factory->NewJSObjectWithInit(finalizationRegistryClass)); JSHandle weakLinkedMap(LinkedHashMap::Create(thread)); - jsFinalizationRegistry->SetMaybeUnregister(thread, weakLinkedMap); + jsFinalizationRegistry->SetRegisteredCellsWithToken(thread, weakLinkedMap); DUMP_FOR_HANDLE(jsFinalizationRegistry); break; } case JSType::CELL_RECORD: { - CHECK_DUMP_FIELDS(Record::SIZE, CellRecord::SIZE, 2U); + CHECK_DUMP_FIELDS(Record::SIZE, CellRecord::SIZE, 4U); JSHandle cellRecord = factory->NewCellRecord(); DUMP_FOR_HANDLE(cellRecord); break; diff --git a/ecmascript/tests/js_finalization_registry_test.cpp b/ecmascript/tests/js_finalization_registry_test.cpp index 5bfa6ffbb8..c7b3ba6258 100644 --- a/ecmascript/tests/js_finalization_registry_test.cpp +++ b/ecmascript/tests/js_finalization_registry_test.cpp @@ -87,20 +87,16 @@ HWTEST_F_L0(JSFinalizationRegistryTest, Register_001) JSHandle constructor = CreateFinalizationRegistry(thread); JSHandle finaRegObj(thread, constructor.GetTaggedValue()); - // If unregisterToken is undefined, use vector to store - JSHandle cellRecord = factory->NewCellRecord(); - cellRecord->SetToWeakRefTarget(thread, target.GetTaggedValue()); - cellRecord->SetHeldValue(thread, heldValue); - JSHandle cell(cellRecord); - JSHandle expectNoUnregister(thread, finaRegObj->GetNoUnregister(thread)); - expectNoUnregister = CellRecordVector::Append(thread, expectNoUnregister, cell); - JSFinalizationRegistry::Register(thread, target, heldValue, unregisterToken, finaRegObj); - JSHandle noUnregister(thread, finaRegObj->GetNoUnregister(thread)); JSHandle finRegLists(thread, vm->GetFinRegLists()); EXPECT_EQ(finRegLists.GetTaggedValue().GetRawData(), JSHandle::Cast(finaRegObj).GetTaggedValue().GetRawData()); - EXPECT_EQ(expectNoUnregister.GetTaggedValue().GetRawData(), noUnregister.GetTaggedValue().GetRawData()); + + JSHandle actualRegisteredCells(thread, finaRegObj->GetRegisteredCells(thread)); + auto actualTarget = actualRegisteredCells->GetFromWeakRefTarget(thread); + auto actualHeldValue = actualRegisteredCells->GetHeldValue(thread); + EXPECT_EQ(target->GetRawData(), actualTarget.GetRawData()); + EXPECT_EQ(heldValue->GetRawData(), actualHeldValue.GetRawData()); EXPECT_EQ(testValue, 0); } @@ -121,20 +117,15 @@ HWTEST_F_L0(JSFinalizationRegistryTest, Register_002) JSHandle constructor = CreateFinalizationRegistry(thread); JSHandle finaRegObj(thread, constructor.GetTaggedValue()); - // If unregisterToken is not undefined, use hash map to store to facilitate subsequent delete operations - JSHandle cellRecord = factory->NewCellRecord(); - cellRecord->SetToWeakRefTarget(thread, target.GetTaggedValue()); - cellRecord->SetHeldValue(thread, heldValue); - JSHandle cell(cellRecord); - JSHandle array = JSHandle(CellRecordVector::Create(thread)); - array = CellRecordVector::Append(thread, array, cell); - JSHandle arrayValue(array); - JSHandle expectMaybeUnregister(thread, finaRegObj->GetMaybeUnregister(thread)); - expectMaybeUnregister = LinkedHashMap::SetWeakRef(thread, expectMaybeUnregister, unregisterToken, arrayValue); - JSFinalizationRegistry::Register(thread, target, heldValue, unregisterToken, finaRegObj); - JSHandle maybeUnregister(thread, finaRegObj->GetMaybeUnregister(thread)); - EXPECT_EQ(expectMaybeUnregister.GetTaggedValue().GetRawData(), maybeUnregister.GetTaggedValue().GetRawData()); + + JSHandle registeredCellsWithToken(thread, finaRegObj->GetRegisteredCellsWithToken(thread)); + auto actualCell = registeredCellsWithToken->Get(thread, unregisterToken.GetTaggedValue()); + auto actualTarget = JSHandle(thread, actualCell)->GetFromWeakRefTarget(thread); + auto actualHeldValue = JSHandle(thread, actualCell)->GetHeldValue(thread); + + EXPECT_EQ(target->GetRawData(), actualTarget.GetRawData()); + EXPECT_EQ(heldValue->GetRawData(), actualHeldValue.GetRawData()); JSHandle finRegLists(thread, vm->GetFinRegLists()); EXPECT_EQ(finRegLists.GetTaggedValue().GetRawData(), @@ -166,9 +157,8 @@ static void RegisterUnRegisterTestCommon(JSThread *thread, bool testUnRegister = cellRecord->SetToWeakRefTarget(thread, target.GetTaggedValue()); cellRecord->SetHeldValue(thread, heldValue); JSHandle cell(cellRecord); - JSHandle noUnregister(thread, finaRegObj->GetNoUnregister(thread)); - noUnregister = CellRecordVector::Append(thread, noUnregister, cell); - finaRegObj->SetNoUnregister(thread, noUnregister); + finaRegObj->SetRegisteredCells(thread, cell.GetTaggedValue()); + JSFinalizationRegistry::AddFinRegLists(thread, finaRegObj); JSHandle finRegLists(thread, vm->GetFinRegLists()); EXPECT_EQ(finRegLists.GetTaggedValue(), JSHandle::Cast(finaRegObj).GetTaggedValue()); diff --git a/test/moduletest/BUILD.gn b/test/moduletest/BUILD.gn index 40c51f208e..b08d371a1d 100644 --- a/test/moduletest/BUILD.gn +++ b/test/moduletest/BUILD.gn @@ -221,6 +221,7 @@ group("ark_js_assert_moduletest") { "addpropertybyname", "cowarray", "decodeuricomponent", + "finalizationregistry", "isin", "jsonparser", "jsonstringifier", @@ -360,6 +361,7 @@ group("ark_asm_assert_test") { "addpropertybyname", "cowarray", "decodeuricomponent", + "finalizationregistry", "isin", "jsonparser", "jsonstringifier", diff --git a/test/moduletest/finalizationregistry/BUILD.gn b/test/moduletest/finalizationregistry/BUILD.gn new file mode 100644 index 0000000000..a68aedfd3f --- /dev/null +++ b/test/moduletest/finalizationregistry/BUILD.gn @@ -0,0 +1,18 @@ +# Copyright (c) 2025 Huawei Device Co., Ltd. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import("//arkcompiler/ets_runtime/test/test_helper.gni") + +host_moduletest_assert_action("finalizationregistry") { + deps = [] +} diff --git a/test/moduletest/finalizationregistry/finalizationregistry.js b/test/moduletest/finalizationregistry/finalizationregistry.js new file mode 100644 index 0000000000..28babd2f42 --- /dev/null +++ b/test/moduletest/finalizationregistry/finalizationregistry.js @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// ------------------------------------------------- +// 1. Basic finalization – callback invoked after GC +// ------------------------------------------------- +{ + let called = false; + const registry = new FinalizationRegistry((heldValue) => { + called = heldValue; + }); + + (function () { + const target = {}; + registry.register(target, 'basic'); + })(); + + ArkTools.forceFullGC(); + ArkTools.excutePendingJob(); + + assert_equal(called, 'basic'); +} + +// --------------------------------------------------- +// 2. Holdings propagation – value travels to callback +// --------------------------------------------------- +{ + const received = []; + const meta = { 'id': 42 }; + const registry = new FinalizationRegistry(heldValue => received.push(heldValue)); + + (function () { + const obj = {}; + registry.register(obj, meta); + })(); + + ArkTools.forceFullGC(); + ArkTools.excutePendingJob(); + + assert_equal(received.length, 1); + assert_equal(received[0], meta); +} + +// ----------------------------------------------------- +// 3. unregister() – prevent callback when token removed +// ----------------------------------------------------- +{ + const calls = []; + const reg = new FinalizationRegistry((heldValue) => { calls.push(heldValue); }); + const token = {}; + + (function () { + const obj = {}; + reg.register(obj, 'data1', token); + reg.unregister(token); + obj.b = 1; // keep the object alive + })(); + + (function () { + const obj = {}; + reg.register(obj, 'data2'); + })(); + + ArkTools.forceFullGC(); + ArkTools.excutePendingJob(); + + assert_equal(calls, ['data2']); +} + +// ---------------------------------------------------- +// 4. Multiple targets – each triggers its own callback +// ---------------------------------------------------- +{ + const calls = []; + const reg = new FinalizationRegistry(h => calls.push(h)); + + (function () { + const a = {}, b = {}; + reg.register(a, 'A'); + reg.register(b, 'B'); + })(); + + ArkTools.forceFullGC(); + ArkTools.excutePendingJob(); + + // callback will be invoked randomly, so we need sort the results + assert_equal(calls.sort(), ['A', 'B']); +} + +// ----------------------------------------------------------- +// 5. Shared token – unregister removes all associated targets +// ----------------------------------------------------------- +{ + let count = 0; + const reg = new FinalizationRegistry(() => { count++; }); + const token = {}; + + (function () { + const a = {}, b = {}; + reg.register(a, 'X', token); + reg.register(b, 'Y', token); + reg.unregister(token); // removes both + a.a = 1; // keep the objects alive + b.b = 2; // keep the objects alive + })(); + + ArkTools.forceFullGC(); + ArkTools.excutePendingJob(); + + assert_equal(count, 0); +} + +test_end(); \ No newline at end of file -- Gitee