已合并
shrink handle/primitive storage proportionally #15032
shrink handle/primitive storage proportionally #15032
已合并
wwzm-jjw创建于 26 天前
7 个文件变更+456-10
Mecmascript/ecma_vm.cpp+56-10
@@ -15,6 +15,7 @@
15 15 
16#include "ecmascript/ecma_vm.h"16#include "ecmascript/ecma_vm.h"
17 17 
18+#include <algorithm>
18#include <cmath>19#include <cmath>
19#include "common_components/taskpool/taskpool.h"20#include "common_components/taskpool/taskpool.h"
20#include "ecmascript/base/config.h"21#include "ecmascript/base/config.h"
@@ -85,6 +86,7 @@ namespace panda::ecmascript {
85using RandomGenerator = base::RandomGenerator;86using RandomGenerator = base::RandomGenerator;
86using PGOProfilerManager = pgo::PGOProfilerManager;87using PGOProfilerManager = pgo::PGOProfilerManager;
87using JitTools = ohos::JitTools;88using JitTools = ohos::JitTools;
89+ 
88constexpr const char* HEAP_MEM_PRESSURE_PROCESS = "ProcessHeapMemPressure";90constexpr const char* HEAP_MEM_PRESSURE_PROCESS = "ProcessHeapMemPressure";
89constexpr const char* HEAP_MEM_PRESSURE_LOCAL = "LocalHeapMemPressure";91constexpr const char* HEAP_MEM_PRESSURE_LOCAL = "LocalHeapMemPressure";
90constexpr const char* HEAP_MEM_PRESSURE_SHARED = "SharedHeapMemPressure";92constexpr const char* HEAP_MEM_PRESSURE_SHARED = "SharedHeapMemPressure";
@@ -1265,6 +1267,40 @@ void EcmaVM::DeleteHandleStorage()
1265 handleScopeStorageNext_ = handleScopeStorageEnd_ = nullptr;1267 handleScopeStorageNext_ = handleScopeStorageEnd_ = nullptr;
1266}1268}
1267 1269 
1270+bool EcmaVM::FreeStorageNodesTask::Run([[maybe_unused]] uint32_t threadIndex)
1271+{
1272+ std::string traceName =
1273+ "EcmaVM::FreeStorageNodesTask::Run freed " + std::to_string(nodes_.size()) + " nodes";
1274+ ECMA_BYTRACE_NAME(HITRACE_LEVEL_COMMERCIAL, HITRACE_TAG_ARK, traceName.c_str(), "");
1275+ for (auto *node : nodes_) {
1276+ delete node;
1277+ }
1278+ return true;
1279+}
1280+ 
1281+void EcmaVM::FreeStorageNodes(std::vector<StorageNode *> &nodes, int32_t toDelete)
1282+{
1283+ // The caller (mutator thread) owns `nodes`, so detaching via pop_back here is
1284+ // race-free. Only the heap free of detached nodes is posted to the taskpool,
1285+ // which never touches `nodes` afterwards, avoiding vector races with the caller.
1286+ auto *taskpool = common::Taskpool::GetCurrentTaskpool();
1287+ bool asyncFree = thread_->IsMainThreadFast() && toDelete > ASYNC_SHRINK_NODE_THRESHOLD && taskpool != nullptr;
1288+ if (asyncFree) {
1289+ std::vector<StorageNode *> toFree;
1290+ toFree.reserve(toDelete);
1291+ for (int32_t i = 0; i < toDelete; i++) {
1292+ toFree.push_back(nodes.back());
1293+ nodes.pop_back();
1294+ }
1295+ taskpool->PostTask(std::make_unique<FreeStorageNodesTask>(std::move(toFree)));
1296+ } else {
1297+ for (int32_t i = 0; i < toDelete; i++) {
1298+ delete nodes.back();
1299+ nodes.pop_back();
1300+ }
1301+ }
1302+}
1303+ 
1268void EcmaVM::ShrinkHandleStorage(int prevIndex)1304void EcmaVM::ShrinkHandleStorage(int prevIndex)
1269{1305{
1270 currentHandleStorageIndex_ = prevIndex;1306 currentHandleStorageIndex_ = prevIndex;
@@ -1288,11 +1324,16 @@ void EcmaVM::ShrinkHandleStorage(int prevIndex)
1288 }1324 }
1289#endif1325#endif
1290 1326 
1291- if (lastIndex > MIN_HANDLE_STORAGE_SIZE && currentHandleStorageIndex_ < MIN_HANDLE_STORAGE_SIZE) {1327+ if (lastIndex > MIN_HANDLE_STORAGE_SIZE) {
1292- for (int i = MIN_HANDLE_STORAGE_SIZE; i < lastIndex; i++) {1328+ int32_t totalNodes = lastIndex + 1;
1293- auto node = handleStorageNodes_.back();1329+ int32_t currentUsage = currentHandleStorageIndex_ + 1;
1294- delete node;1330+ int32_t targetNodes = std::max(MIN_HANDLE_STORAGE_SIZE, currentUsage * SHRINK_HEADROOM_FACTOR);
1295- handleStorageNodes_.pop_back();1331+ int32_t toDelete = totalNodes - targetNodes;
1332+ // Only shrink when at least a quarter of the nodes would be freed, to avoid churn.
1333+ if (toDelete >= std::max(totalNodes / SHRINK_HYSTERESIS_DIVISOR, SHRINK_MIN_FREE_NODES)) {
1334+ LOG_ECMA(DEBUG) << "ShrinkHandleStorage currentUsage:" << currentUsage
1335+ << " totalNodes:" << totalNodes << " targetNodes:" << targetNodes;
1336+ FreeStorageNodes(handleStorageNodes_, toDelete);
1296 }1337 }
1297 }1338 }
1298}1339}
@@ -1415,11 +1456,16 @@ void EcmaVM::ShrinkPrimitiveStorage(int prevIndex)
1415 }1456 }
1416#endif1457#endif
1417 1458 
1418- if (lastIndex > MIN_PRIMITIVE_STORAGE_SIZE && currentPrimitiveStorageIndex_ < MIN_PRIMITIVE_STORAGE_SIZE) {1459+ if (lastIndex > MIN_PRIMITIVE_STORAGE_SIZE) {
1419- for (int i = MIN_PRIMITIVE_STORAGE_SIZE; i < lastIndex; i++) {1460+ int32_t totalNodes = lastIndex + 1;
1420- auto node = primitiveStorageNodes_.back();1461+ int32_t currentUsage = currentPrimitiveStorageIndex_ + 1;
1421- delete node;1462+ int32_t targetNodes = std::max(MIN_PRIMITIVE_STORAGE_SIZE, currentUsage * SHRINK_HEADROOM_FACTOR);
1422- primitiveStorageNodes_.pop_back();1463+ int32_t toDelete = totalNodes - targetNodes;
1464+ // Only shrink when at least a quarter of the nodes would be freed, to avoid churn.
1465+ if (toDelete >= std::max(totalNodes / SHRINK_HYSTERESIS_DIVISOR, SHRINK_MIN_FREE_NODES)) {
1466+ LOG_ECMA(DEBUG) << "ShrinkPrimitiveStorage currentUsage:" << currentUsage
1467+ << " totalNodes:" << totalNodes << " targetNodes:" << targetNodes;
1468+ FreeStorageNodes(primitiveStorageNodes_, toDelete);
1423 }1469 }
1424 }1470 }
1425}1471}
Mecmascript/ecma_vm.h+33-0
@@ -42,6 +42,7 @@
42#include "ecmascript/mem/gc_key_stats.h"42#include "ecmascript/mem/gc_key_stats.h"
43#include "ecmascript/mem/gc_stats.h"43#include "ecmascript/mem/gc_stats.h"
44#include "ecmascript/mem/heap_region_allocator.h"44#include "ecmascript/mem/heap_region_allocator.h"
45+#include "common_components/taskpool/task.h"
45#include "ecmascript/js_tagged_value_wrapper.h"46#include "ecmascript/js_tagged_value_wrapper.h"
46#include "ecmascript/napi/include/dfx_jsnapi.h"47#include "ecmascript/napi/include/dfx_jsnapi.h"
47#include "ecmascript/patch/patch_loader.h"48#include "ecmascript/patch/patch_loader.h"
@@ -473,6 +474,11 @@ public:
473 return currentHandleStorageIndex_;474 return currentHandleStorageIndex_;
474 }475 }
475 476 
477+ size_t GetHandleStorageNodesSize() const
478+ {
479+ return handleStorageNodes_.size();
480+ }
481+ 
476 JSTaggedType *GetPrimitiveScopeStorageNext() const482 JSTaggedType *GetPrimitiveScopeStorageNext() const
477 {483 {
478 return primitiveScopeStorageNext_;484 return primitiveScopeStorageNext_;
@@ -498,6 +504,11 @@ public:
498 return currentPrimitiveStorageIndex_;504 return currentPrimitiveStorageIndex_;
499 }505 }
500 506 
507+ size_t GetPrimitiveStorageNodesSize() const
508+ {
509+ return primitiveStorageNodes_.size();
510+ }
511+ 
501 uintptr_t *ExpandHandleStorage();512 uintptr_t *ExpandHandleStorage();
502 void ShrinkHandleStorage(int prevIndex);513 void ShrinkHandleStorage(int prevIndex);
503 void DeleteHandleStorage();514 void DeleteHandleStorage();
@@ -1940,8 +1951,30 @@ private:
1940 // HandleScope1951 // HandleScope
1941 static const uint32_t NODE_BLOCK_SIZE_LOG2 = 10;1952 static const uint32_t NODE_BLOCK_SIZE_LOG2 = 10;
1942 static const uint32_t NODE_BLOCK_SIZE = 1U << NODE_BLOCK_SIZE_LOG2;1953 static const uint32_t NODE_BLOCK_SIZE = 1U << NODE_BLOCK_SIZE_LOG2;
1954+ using StorageNode = std::array<JSTaggedType, NODE_BLOCK_SIZE>;
1943 static constexpr uint32_t SO_LOAD_FAILURE_CAPACITY = 20;1955 static constexpr uint32_t SO_LOAD_FAILURE_CAPACITY = 20;
1944 static constexpr int32_t MIN_HANDLE_STORAGE_SIZE = 2;1956 static constexpr int32_t MIN_HANDLE_STORAGE_SIZE = 2;
1957+ // Retain SHRINK_HEADROOM_FACTOR * active usage nodes after a shrink.
1958+ static constexpr int32_t SHRINK_HEADROOM_FACTOR = 2;
1959+ // Only shrink when at least total / SHRINK_HYSTERESIS_DIVISOR nodes would be freed.
1960+ static constexpr int32_t SHRINK_HYSTERESIS_DIVISOR = 4;
1961+ // Never shrink for fewer than this many nodes to avoid trivial churn on small storages.
1962+ static constexpr int32_t SHRINK_MIN_FREE_NODES = 4;
1963+ // On the main thread, freeing more than this many nodes is offloaded to the taskpool.
1964+ static constexpr int32_t ASYNC_SHRINK_NODE_THRESHOLD = 1000;
1965+ void FreeStorageNodes(std::vector<StorageNode *> &nodes, int32_t toDelete);
1966+ // Detached storage nodes freed asynchronously by the taskpool (see FreeStorageNodes).
1967+ class FreeStorageNodesTask : public common::Task {
1968+ public:
1969+ explicit FreeStorageNodesTask(std::vector<StorageNode *> nodes)
1970+ : common::Task(common::GLOBAL_TASK_ID), nodes_(std::move(nodes)) {}
1971+ ~FreeStorageNodesTask() override = default;
1972+ bool Run(uint32_t threadIndex) override;
1973+ NO_COPY_SEMANTIC(FreeStorageNodesTask);
1974+ NO_MOVE_SEMANTIC(FreeStorageNodesTask);
1975+ private:
1976+ std::vector<StorageNode *> nodes_;
1977+ };
1945 JSTaggedType *handleScopeStorageNext_ {nullptr};1978 JSTaggedType *handleScopeStorageNext_ {nullptr};
1946 JSTaggedType *handleScopeStorageEnd_ {nullptr};1979 JSTaggedType *handleScopeStorageEnd_ {nullptr};
1947 std::vector<std::array<JSTaggedType, NODE_BLOCK_SIZE> *> handleStorageNodes_ {};1980 std::vector<std::array<JSTaggedType, NODE_BLOCK_SIZE> *> handleStorageNodes_ {};
Mecmascript/mem/gc_stats.cpp+10-0
@@ -173,11 +173,21 @@ void GCStats::PrintGCStatistic()
173 }173 }
174 // print verbose gc statsistics174 // print verbose gc statsistics
175 PrintVerboseGCStatistic();175 PrintVerboseGCStatistic();
176+ PrintStorageStatistic();
176 }177 }
177 GCFinishTrace();178 GCFinishTrace();
178 InitializeRecordList();179 InitializeRecordList();
179}180}
180 181 
182+void GCStats::PrintStorageStatistic()
183+{
184+ LOG_GC(INFO) << "HandleStorage currentUsage:" << (heap_->GetEcmaVM()->GetCurrentHandleStorageIndex() + 1)
185+ << " totalNodes:" << heap_->GetEcmaVM()->GetHandleStorageNodesSize()
186+ << " PrimitiveStorage currentUsage:"
187+ << (heap_->GetEcmaVM()->GetCurrentPrimitiveStorageIndex() + 1)
188+ << " totalNodes:" << heap_->GetEcmaVM()->GetPrimitiveStorageNodesSize();
189+}
190+ 
181const char *GCStats::GCReasonToString()191const char *GCStats::GCReasonToString()
182{192{
183 return GCReasonToString(gcReason_);193 return GCReasonToString(gcReason_);
Mecmascript/mem/gc_stats.h+1-0
@@ -284,6 +284,7 @@ public:
284protected:284protected:
285 bool CheckIfNeedPrint(GCType type);285 bool CheckIfNeedPrint(GCType type);
286 void PrintVerboseGCStatistic();286 void PrintVerboseGCStatistic();
287+ void PrintStorageStatistic();
287 void PrintGCDurationStatistic();288 void PrintGCDurationStatistic();
288 void PrintGCSummaryStatistic(GCType type = GCType::START);289 void PrintGCSummaryStatistic(GCType type = GCType::START);
289 void InitializeRecordList();290 void InitializeRecordList();
Mecmascript/tests/BUILD.gn+29-0
@@ -1505,6 +1505,33 @@ host_unittest_action("JS_EcmaHandleScope_Test") {
1505 deps += hiviewdfx_deps1505 deps += hiviewdfx_deps
1506}1506}
1507 1507 
1508+host_unittest_action("JS_EcmaVMStorageShrink_Test") {
1509+ module_out_path = module_output_path
1510+ 
1511+ sources = [
1512+ # test file
1513+ "ecma_vm_storage_shrink_test.cpp",
1514+ ]
1515+ 
1516+ configs = [
1517+ "../../:ecma_test_config",
1518+ "../../:icu_path_test_config",
1519+ ]
1520+ 
1521+ deps = [ "../../:libark_jsruntime_test" ]
1522+ 
1523+ # hiviewdfx libraries
1524+ external_deps = hiviewdfx_ext_deps
1525+ external_deps += [
1526+ "icu:shared_icui18n",
1527+ "icu:shared_icuuc",
1528+ "runtime_core:libarkassembler_static",
1529+ "runtime_core:libarkverifier",
1530+ "zlib:libz",
1531+ ]
1532+ deps += hiviewdfx_deps
1533+}
1534+ 
1508host_unittest_action("JS_MapIterator_Test") {1535host_unittest_action("JS_MapIterator_Test") {
1509 module_out_path = module_output_path1536 module_out_path = module_output_path
1510 1537 
@@ -4098,6 +4125,7 @@ group("unittest") {
4098 ":JS_GlueRegs_Test",4125 ":JS_GlueRegs_Test",
4099 ":JS_Handle_Test",4126 ":JS_Handle_Test",
4100 ":JS_EcmaHandleScope_Test",4127 ":JS_EcmaHandleScope_Test",
4128+ ":JS_EcmaVMStorageShrink_Test",
4101 ":JS_Hclass_Test",4129 ":JS_Hclass_Test",
4102 ":JS_Iterator_Test",4130 ":JS_Iterator_Test",
4103 ":JS_LayoutInfo_Test",4131 ":JS_LayoutInfo_Test",
@@ -4297,6 +4325,7 @@ group("host_unittest") {
4297 ":JS_GlueRegs_TestAction",4325 ":JS_GlueRegs_TestAction",
4298 ":JS_Handle_TestAction",4326 ":JS_Handle_TestAction",
4299 ":JS_EcmaHandleScope_TestAction",4327 ":JS_EcmaHandleScope_TestAction",
4328+ ":JS_EcmaVMStorageShrink_TestAction",
4300 ":JS_Hclass_TestAction",4329 ":JS_Hclass_TestAction",
4301 ":JS_Iterator_TestAction",4330 ":JS_Iterator_TestAction",
4302 ":JS_LayoutInfo_TestAction",4331 ":JS_LayoutInfo_TestAction",
Aecmascript/tests/ecma_vm_storage_shrink_test.cpp+322-0
@@ -0,0 +1,322 @@
1+/*
dwhuawei
dwhuaweidwhuawei25 天前

新增的test文件要加到xml里面

likedislike
2+ * Copyright (c) 2026 Huawei Device Co., Ltd.
3+ * Licensed under the Apache License, Version 2.0 (the "License");
4+ * you may not use this file except in compliance with the License.
5+ * You may obtain a copy of the License at
6+ *
7+ * http://www.apache.org/licenses/LICENSE-2.0
8+ *
9+ * Unless required by applicable law or agreed to in writing, software
10+ * distributed under the License is distributed on an "AS IS" BASIS,
11+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+ * See the License for the specific language governing permissions and
13+ * limitations under the License.
14+ */
15+ 
16+#include <algorithm>
17+ 
18+#include "ecmascript/ecma_vm.h"
19+#include "ecmascript/tests/test_helper.h"
20+#include "gtest/gtest.h"
21+ 
22+using namespace panda::ecmascript;
23+ 
24+namespace panda::test {
25+namespace {
26+// Mirrors the private EcmaVM constants in ecmascript/ecma_vm.h.
27+constexpr int32_t MIN_HANDLE_STORAGE_SIZE = 2;
28+constexpr int32_t MIN_PRIMITIVE_STORAGE_SIZE = 2;
29+ 
30+// Shrink retains SHRINK_HEADROOM_FACTOR * active usage nodes (>= MIN).
31+constexpr int32_t SHRINK_HEADROOM_FACTOR = 2;
32+ 
33+// Reset helper grows MIN + RESET_GROW_MARGIN nodes so the shrink collapses to MIN.
34+// Must be large enough that toDelete >= the production fixed floor (SHRINK_MIN_FREE_NODES).
35+constexpr int32_t RESET_GROW_MARGIN = 4;
36+constexpr size_t RESET_BASELINE_NODES = static_cast<size_t>(MIN_HANDLE_STORAGE_SIZE);
37+ 
38+// prevIndex values driving each scenario.
39+constexpr int32_t PREV_INDEX_LOW = 1; // usage 2 -> target 4
40+constexpr int32_t PREV_INDEX_PROPORTIONAL = 4; // usage 5 -> target 10
41+constexpr int32_t PREV_INDEX_HEADROOM_EXCEEDS = 9; // usage 10 -> target 20 == total
42+constexpr int32_t PREV_INDEX_HYSTERESIS_BAND = 7; // usage 8 -> target 16, frees 4 < total/4=5
43+ 
44+// Growth targets (final node counts) per scenario.
45+constexpr size_t GROW_NODES_SMALL = 10;
46+constexpr size_t GROW_NODES_LARGE = 30;
47+constexpr size_t GROW_NODES_NO_SHRINK = 20; // headroom * (prev+1) == total
48+constexpr size_t GROW_NODES_HYSTERESIS_BELOW = 13; // prev=4: frees 3 < max(3,4)=4 -> no shrink
49+constexpr size_t GROW_NODES_HYSTERESIS_AT = 14; // prev=4: frees 4 >= max(3,4)=4 -> shrink
50+ 
51+// Expected retained node count after a proportional shrink to 2x active usage.
52+inline size_t ShrinkTargetNodes(int32_t prevIndex, int32_t minNodes)
53+{
54+ return static_cast<size_t>(std::max(minNodes, (prevIndex + 1) * SHRINK_HEADROOM_FACTOR));
55+}
56+ 
57+void GrowHandleStorageTo(EcmaVM *vm, size_t target)
58+{
59+ while (vm->GetHandleStorageNodesSize() < target) {
60+ vm->ExpandHandleStorage();
61+ }
62+}
63+ 
64+void GrowPrimitiveStorageTo(EcmaVM *vm, size_t target)
65+{
66+ while (vm->GetPrimitiveStorageNodesSize() < target) {
67+ vm->ExpandPrimitiveStorage();
68+ }
69+}
70+ 
71+// Collapse to a deterministic baseline of MIN nodes (index 0) regardless of
72+// the non-deterministic node count left by VM initialization.
73+void ResetHandleStorageToMin(EcmaVM *vm)
74+{
75+ GrowHandleStorageTo(vm, static_cast<size_t>(MIN_HANDLE_STORAGE_SIZE + RESET_GROW_MARGIN));
76+ vm->ShrinkHandleStorage(0);
77+}
78+ 
79+void ResetPrimitiveStorageToMin(EcmaVM *vm)
80+{
81+ GrowPrimitiveStorageTo(vm, static_cast<size_t>(MIN_PRIMITIVE_STORAGE_SIZE + RESET_GROW_MARGIN));
82+ vm->ShrinkPrimitiveStorage(0);
83+}
84+} // namespace
85+ 
86+class EcmaVMStorageShrinkTest : public BaseTestWithScope<false> {
87+};
88+ 
89+// --- Handle storage ---
90+ 
91+HWTEST_F_L0(EcmaVMStorageShrinkTest, HandleStorage_LowUsageShrinkToHeadroom)
92+{
93+ // Low usage still shrinks, but to 2x usage (floored at MIN), not to MIN+1.
94+ ResetHandleStorageToMin(instance);
95+ GrowHandleStorageTo(instance, GROW_NODES_SMALL);
96+ ASSERT_EQ(instance->GetHandleStorageNodesSize(), GROW_NODES_SMALL);
97+ 
98+ instance->ShrinkHandleStorage(PREV_INDEX_LOW);
99+ 
100+ EXPECT_EQ(instance->GetCurrentHandleStorageIndex(), PREV_INDEX_LOW);
101+ EXPECT_EQ(instance->GetHandleStorageNodesSize(), ShrinkTargetNodes(PREV_INDEX_LOW, MIN_HANDLE_STORAGE_SIZE));
102+ EXPECT_LT(instance->GetCurrentHandleStorageIndex(),
103+ static_cast<int32_t>(instance->GetHandleStorageNodesSize()));
104+}
105+ 
106+HWTEST_F_L0(EcmaVMStorageShrinkTest, HandleStorage_ProportionalShrink)
107+{
108+ // usage <= 3/8 of total -> frees >= total/4 -> shrink to 2x usage.
109+ ResetHandleStorageToMin(instance);
110+ GrowHandleStorageTo(instance, GROW_NODES_LARGE);
111+ ASSERT_EQ(instance->GetHandleStorageNodesSize(), GROW_NODES_LARGE);
112+ 
113+ instance->ShrinkHandleStorage(PREV_INDEX_PROPORTIONAL);
114+ 
115+ EXPECT_EQ(instance->GetCurrentHandleStorageIndex(), PREV_INDEX_PROPORTIONAL);
116+ EXPECT_EQ(instance->GetHandleStorageNodesSize(),
117+ ShrinkTargetNodes(PREV_INDEX_PROPORTIONAL, MIN_HANDLE_STORAGE_SIZE));
118+ EXPECT_LT(instance->GetCurrentHandleStorageIndex(),
119+ static_cast<int32_t>(instance->GetHandleStorageNodesSize()));
120+}
121+ 
122+HWTEST_F_L0(EcmaVMStorageShrinkTest, HandleStorage_NoShrinkWhenHeadroomExceedsTotal)
123+{
124+ // 2x usage == total -> nothing to free.
125+ ResetHandleStorageToMin(instance);
126+ GrowHandleStorageTo(instance, GROW_NODES_NO_SHRINK);
127+ ASSERT_EQ(instance->GetHandleStorageNodesSize(), GROW_NODES_NO_SHRINK);
128+ 
129+ instance->ShrinkHandleStorage(PREV_INDEX_HEADROOM_EXCEEDS);
130+ 
131+ EXPECT_EQ(instance->GetCurrentHandleStorageIndex(), PREV_INDEX_HEADROOM_EXCEEDS);
132+ EXPECT_EQ(instance->GetHandleStorageNodesSize(), GROW_NODES_NO_SHRINK);
133+ EXPECT_LT(instance->GetCurrentHandleStorageIndex(),
134+ static_cast<int32_t>(instance->GetHandleStorageNodesSize()));
135+}
136+ 
137+HWTEST_F_L0(EcmaVMStorageShrinkTest, HandleStorage_NoShrinkBelowHysteresis)
138+{
139+ // Frees a positive but < total/4 amount -> hysteresis keeps it to avoid churn.
140+ ResetHandleStorageToMin(instance);
141+ GrowHandleStorageTo(instance, GROW_NODES_NO_SHRINK);
142+ ASSERT_EQ(instance->GetHandleStorageNodesSize(), GROW_NODES_NO_SHRINK);
143+ 
144+ instance->ShrinkHandleStorage(PREV_INDEX_HYSTERESIS_BAND);
145+ 
146+ EXPECT_EQ(instance->GetCurrentHandleStorageIndex(), PREV_INDEX_HYSTERESIS_BAND);
147+ EXPECT_EQ(instance->GetHandleStorageNodesSize(), GROW_NODES_NO_SHRINK);
148+ EXPECT_LT(instance->GetCurrentHandleStorageIndex(),
149+ static_cast<int32_t>(instance->GetHandleStorageNodesSize()));
150+}
151+ 
152+HWTEST_F_L0(EcmaVMStorageShrinkTest, HandleStorage_BelowMinGuardNoShrink)
153+{
154+ // lastIndex <= MIN -> outer guard (lastIndex > MIN) is false, no shrink.
155+ ResetHandleStorageToMin(instance);
156+ ASSERT_EQ(instance->GetHandleStorageNodesSize(), RESET_BASELINE_NODES);
157+ 
158+ instance->ShrinkHandleStorage(0);
159+ 
160+ EXPECT_EQ(instance->GetCurrentHandleStorageIndex(), 0);
161+ EXPECT_EQ(instance->GetHandleStorageNodesSize(), RESET_BASELINE_NODES);
162+}
163+ 
164+HWTEST_F_L0(EcmaVMStorageShrinkTest, HandleStorage_HysteresisBoundary)
165+{
166+ // prev=4 -> target 10. total=13: frees 3 < max(3,4)=4 -> no shrink.
167+ ResetHandleStorageToMin(instance);
168+ GrowHandleStorageTo(instance, GROW_NODES_HYSTERESIS_BELOW);
169+ ASSERT_EQ(instance->GetHandleStorageNodesSize(), GROW_NODES_HYSTERESIS_BELOW);
170+ instance->ShrinkHandleStorage(PREV_INDEX_PROPORTIONAL);
171+ EXPECT_EQ(instance->GetHandleStorageNodesSize(), GROW_NODES_HYSTERESIS_BELOW);
172+ EXPECT_EQ(instance->GetCurrentHandleStorageIndex(), PREV_INDEX_PROPORTIONAL);
173+ 
174+ // total=14: frees 4 >= max(3,4)=4 -> shrink to 2x usage.
175+ GrowHandleStorageTo(instance, GROW_NODES_HYSTERESIS_AT);
176+ ASSERT_EQ(instance->GetHandleStorageNodesSize(), GROW_NODES_HYSTERESIS_AT);
177+ instance->ShrinkHandleStorage(PREV_INDEX_PROPORTIONAL);
178+ EXPECT_EQ(instance->GetHandleStorageNodesSize(),
179+ ShrinkTargetNodes(PREV_INDEX_PROPORTIONAL, MIN_HANDLE_STORAGE_SIZE));
180+ EXPECT_EQ(instance->GetCurrentHandleStorageIndex(), PREV_INDEX_PROPORTIONAL);
181+ EXPECT_LT(instance->GetCurrentHandleStorageIndex(),
182+ static_cast<int32_t>(instance->GetHandleStorageNodesSize()));
183+}
184+ 
185+HWTEST_F_L0(EcmaVMStorageShrinkTest, HandleStorage_IndependentOfPrimitiveIndex)
186+{
187+ // Handle shrink must use the handle index, never the primitive index.
188+ ResetHandleStorageToMin(instance);
189+ ResetPrimitiveStorageToMin(instance);
190+ // Inflate primitive storage so currentPrimitiveStorageIndex_ is large and unrelated.
191+ GrowPrimitiveStorageTo(instance, GROW_NODES_LARGE);
192+ GrowHandleStorageTo(instance, GROW_NODES_LARGE);
193+ ASSERT_EQ(instance->GetHandleStorageNodesSize(), GROW_NODES_LARGE);
194+ 
195+ instance->ShrinkHandleStorage(PREV_INDEX_PROPORTIONAL);
196+ 
197+ EXPECT_EQ(instance->GetHandleStorageNodesSize(),
198+ ShrinkTargetNodes(PREV_INDEX_PROPORTIONAL, MIN_HANDLE_STORAGE_SIZE));
199+ EXPECT_EQ(instance->GetCurrentHandleStorageIndex(), PREV_INDEX_PROPORTIONAL);
200+}
201+ 
202+// --- Primitive storage ---
203+ 
204+HWTEST_F_L0(EcmaVMStorageShrinkTest, PrimitiveStorage_LowUsageShrinkToHeadroom)
205+{
206+ ResetPrimitiveStorageToMin(instance);
207+ GrowPrimitiveStorageTo(instance, GROW_NODES_SMALL);
208+ ASSERT_EQ(instance->GetPrimitiveStorageNodesSize(), GROW_NODES_SMALL);
209+ 
210+ instance->ShrinkPrimitiveStorage(PREV_INDEX_LOW);
211+ 
212+ EXPECT_EQ(instance->GetCurrentPrimitiveStorageIndex(), PREV_INDEX_LOW);
213+ EXPECT_EQ(instance->GetPrimitiveStorageNodesSize(), ShrinkTargetNodes(PREV_INDEX_LOW, MIN_PRIMITIVE_STORAGE_SIZE));
214+ EXPECT_LT(instance->GetCurrentPrimitiveStorageIndex(),
215+ static_cast<int32_t>(instance->GetPrimitiveStorageNodesSize()));
216+}
217+ 
218+HWTEST_F_L0(EcmaVMStorageShrinkTest, PrimitiveStorage_ProportionalShrink)
219+{
220+ ResetPrimitiveStorageToMin(instance);
221+ GrowPrimitiveStorageTo(instance, GROW_NODES_LARGE);
222+ ASSERT_EQ(instance->GetPrimitiveStorageNodesSize(), GROW_NODES_LARGE);
223+ 
224+ instance->ShrinkPrimitiveStorage(PREV_INDEX_PROPORTIONAL);
225+ 
226+ EXPECT_EQ(instance->GetCurrentPrimitiveStorageIndex(), PREV_INDEX_PROPORTIONAL);
227+ EXPECT_EQ(instance->GetPrimitiveStorageNodesSize(),
228+ ShrinkTargetNodes(PREV_INDEX_PROPORTIONAL, MIN_PRIMITIVE_STORAGE_SIZE));
229+ EXPECT_LT(instance->GetCurrentPrimitiveStorageIndex(),
230+ static_cast<int32_t>(instance->GetPrimitiveStorageNodesSize()));
231+}
232+ 
233+HWTEST_F_L0(EcmaVMStorageShrinkTest, PrimitiveStorage_NoShrinkWhenHeadroomExceedsTotal)
234+{
235+ ResetPrimitiveStorageToMin(instance);
236+ GrowPrimitiveStorageTo(instance, GROW_NODES_NO_SHRINK);
237+ ASSERT_EQ(instance->GetPrimitiveStorageNodesSize(), GROW_NODES_NO_SHRINK);
238+ 
239+ instance->ShrinkPrimitiveStorage(PREV_INDEX_HEADROOM_EXCEEDS);
240+ 
241+ EXPECT_EQ(instance->GetCurrentPrimitiveStorageIndex(), PREV_INDEX_HEADROOM_EXCEEDS);
242+ EXPECT_EQ(instance->GetPrimitiveStorageNodesSize(), GROW_NODES_NO_SHRINK);
243+ EXPECT_LT(instance->GetCurrentPrimitiveStorageIndex(),
244+ static_cast<int32_t>(instance->GetPrimitiveStorageNodesSize()));
245+}
246+ 
247+HWTEST_F_L0(EcmaVMStorageShrinkTest, PrimitiveStorage_NoShrinkBelowHysteresis)
248+{
249+ ResetPrimitiveStorageToMin(instance);
250+ GrowPrimitiveStorageTo(instance, GROW_NODES_NO_SHRINK);
251+ ASSERT_EQ(instance->GetPrimitiveStorageNodesSize(), GROW_NODES_NO_SHRINK);
252+ 
253+ instance->ShrinkPrimitiveStorage(PREV_INDEX_HYSTERESIS_BAND);
254+ 
255+ EXPECT_EQ(instance->GetCurrentPrimitiveStorageIndex(), PREV_INDEX_HYSTERESIS_BAND);
256+ EXPECT_EQ(instance->GetPrimitiveStorageNodesSize(), GROW_NODES_NO_SHRINK);
257+ EXPECT_LT(instance->GetCurrentPrimitiveStorageIndex(),
258+ static_cast<int32_t>(instance->GetPrimitiveStorageNodesSize()));
259+}
260+ 
261+HWTEST_F_L0(EcmaVMStorageShrinkTest, PrimitiveStorage_BelowMinGuardNoShrink)
262+{
263+ // lastIndex <= MIN -> outer guard (lastIndex > MIN) is false, no shrink.
264+ ResetPrimitiveStorageToMin(instance);
265+ ASSERT_EQ(instance->GetPrimitiveStorageNodesSize(),
266+ static_cast<size_t>(MIN_PRIMITIVE_STORAGE_SIZE));
267+ 
268+ instance->ShrinkPrimitiveStorage(0);
269+ 
270+ EXPECT_EQ(instance->GetCurrentPrimitiveStorageIndex(), 0);
271+ EXPECT_EQ(instance->GetPrimitiveStorageNodesSize(),
272+ static_cast<size_t>(MIN_PRIMITIVE_STORAGE_SIZE));
273+}
274+ 
275+HWTEST_F_L0(EcmaVMStorageShrinkTest, PrimitiveStorage_HysteresisBoundary)
276+{
277+ ResetPrimitiveStorageToMin(instance);
278+ GrowPrimitiveStorageTo(instance, GROW_NODES_HYSTERESIS_BELOW);
279+ ASSERT_EQ(instance->GetPrimitiveStorageNodesSize(), GROW_NODES_HYSTERESIS_BELOW);
280+ instance->ShrinkPrimitiveStorage(PREV_INDEX_PROPORTIONAL);
281+ EXPECT_EQ(instance->GetPrimitiveStorageNodesSize(), GROW_NODES_HYSTERESIS_BELOW);
282+ EXPECT_EQ(instance->GetCurrentPrimitiveStorageIndex(), PREV_INDEX_PROPORTIONAL);
283+ 
284+ GrowPrimitiveStorageTo(instance, GROW_NODES_HYSTERESIS_AT);
285+ ASSERT_EQ(instance->GetPrimitiveStorageNodesSize(), GROW_NODES_HYSTERESIS_AT);
286+ instance->ShrinkPrimitiveStorage(PREV_INDEX_PROPORTIONAL);
287+ EXPECT_EQ(instance->GetPrimitiveStorageNodesSize(),
288+ ShrinkTargetNodes(PREV_INDEX_PROPORTIONAL, MIN_PRIMITIVE_STORAGE_SIZE));
289+ EXPECT_EQ(instance->GetCurrentPrimitiveStorageIndex(), PREV_INDEX_PROPORTIONAL);
290+ EXPECT_LT(instance->GetCurrentPrimitiveStorageIndex(),
291+ static_cast<int32_t>(instance->GetPrimitiveStorageNodesSize()));
292+}
293+ 
294+// P0 regression: ShrinkPrimitiveStorage must key off currentPrimitiveStorageIndex_,
295+// not currentHandleStorageIndex_. The buggy version computed targetNodes from the
296+// handle index; with a small handle index it deleted primitive nodes still in use
297+// (use-after-free), and with a large handle index it skipped shrinking. In both
298+// cases the resulting node count differs from 2x the primitive usage.
299+HWTEST_F_L0(EcmaVMStorageShrinkTest, PrimitiveStorage_UsesPrimitiveIndexNotHandleIndex)
300+{
301+ // Pin handle storage to index 0 so the buggy code path (which read the handle
302+ // index) would compute a minimal target and delete the active primitive node.
303+ ResetHandleStorageToMin(instance);
304+ ResetPrimitiveStorageToMin(instance);
305+ GrowPrimitiveStorageTo(instance, GROW_NODES_LARGE);
306+ ASSERT_EQ(instance->GetPrimitiveStorageNodesSize(), GROW_NODES_LARGE);
307+ ASSERT_EQ(instance->GetCurrentHandleStorageIndex(), 0);
308+ 
309+ instance->ShrinkPrimitiveStorage(PREV_INDEX_PROPORTIONAL);
310+ 
311+ // Fixed: target = headroom * (prevIndex + 1).
312+ // Buggy (handle index 0): target collapses to MIN and the active primitive
313+ // node dangles; the node count differs from the proportional target.
314+ EXPECT_EQ(instance->GetPrimitiveStorageNodesSize(),
315+ ShrinkTargetNodes(PREV_INDEX_PROPORTIONAL, MIN_PRIMITIVE_STORAGE_SIZE));
316+ EXPECT_EQ(instance->GetCurrentPrimitiveStorageIndex(), PREV_INDEX_PROPORTIONAL);
317+ // Active primitive index must stay within the allocated node range.
318+ EXPECT_LT(instance->GetCurrentPrimitiveStorageIndex(),
319+ static_cast<int32_t>(instance->GetPrimitiveStorageNodesSize()));
320+}
321+ 
322+} // namespace panda::test
Mtest/resource/js_runtime/ohos_test.xml+5-0
@@ -1398,6 +1398,11 @@
1398 <option name="push" value="arkcompiler/ets_runtime/libark_jsruntime_test.so -> /data/test" src="out"/>1398 <option name="push" value="arkcompiler/ets_runtime/libark_jsruntime_test.so -> /data/test" src="out"/>
1399 </preparer>1399 </preparer>
1400 </target>1400 </target>
1401+ <target name="JS_EcmaVMStorageShrink_Test">
1402+ <preparer>
1403+ <option name="push" value="arkcompiler/ets_runtime/libark_jsruntime_test.so -> /data/test" src="out"/>
1404+ </preparer>
1405+ </target>
1401 <target name="JS_Map_Test">1406 <target name="JS_Map_Test">
1402 <preparer>1407 <preparer>
1403 <option name="push" value="arkcompiler/ets_runtime/libark_jsruntime_test.so -> /data/test" src="out"/>1408 <option name="push" value="arkcompiler/ets_runtime/libark_jsruntime_test.so -> /data/test" src="out"/>