• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /**
2  * Copyright (c) 2021-2025 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 "ets_coroutine.h"
17 #include "types/ets_string.h"
18 #include "intrinsics/helpers/ets_to_string_cache.h"
19 #include "intrinsics/helpers/ets_intrinsics_helpers.h"
20 #include "ets_vm.h"
21 #include "gtest/gtest.h"
22 #include "runtime/include/runtime.h"
23 #include "runtime/include/thread.h"
24 #include "tests/runtime/types/ets_test_mirror_classes.h"
25 
26 #include <array>
27 #include <thread>
28 #include <random>
29 #include <sstream>
30 
31 #include "plugins/ets/runtime/intrinsics/helpers/ets_to_string_cache.cpp"
32 
33 namespace ark::ets::test {
34 
35 static constexpr uint32_t TEST_THREADS = 8;
36 static constexpr uint32_t TEST_ITERS = 1;
37 static constexpr uint32_t TEST_ARRAY_SIZE = 1000;
38 static constexpr int32_t VALUE_RANGE = 1000;
39 
40 enum GenType { COPY, SHUFFLE, INDEPENDENT };
41 
42 class EtsToStringCacheTest : public testing::Test {
43 public:
EtsToStringCacheTest(const char * gcType=nullptr,bool cacheEnabled=true)44     explicit EtsToStringCacheTest(const char *gcType = nullptr, bool cacheEnabled = true)
45         : engine_(std::random_device {}())
46     {
47         // Logger::InitializeStdLogging(Logger::Level::INFO, Logger::ComponentMaskFromString("runtime") |
48         // Logger::ComponentMaskFromString("coroutines"));
49 
50         RuntimeOptions options;
51         options.SetShouldLoadBootPandaFiles(true);
52         options.SetShouldInitializeIntrinsics(false);
53         options.SetLoadRuntimes({"ets"});
54         options.SetUseStringCaches(cacheEnabled);
55 
56         auto stdlib = std::getenv("PANDA_STD_LIB");
57         if (stdlib == nullptr) {
58             std::cerr << "PANDA_STD_LIB env variable should be set and point to etsstdlib.abc" << std::endl;
59             std::abort();
60         }
61         options.SetBootPandaFiles({stdlib});
62 
63         if (gcType != nullptr) {
64             options.SetGcType(gcType);
65         }
66         options.SetCompilerEnableJit(false);
67         if (!Runtime::Create(options)) {
68             UNREACHABLE();
69         }
70     }
71 
~EtsToStringCacheTest()72     ~EtsToStringCacheTest() override
73     {
74         Runtime::Destroy();
75     }
76 
77     NO_COPY_SEMANTIC(EtsToStringCacheTest);
78     NO_MOVE_SEMANTIC(EtsToStringCacheTest);
79 
SetUp()80     void SetUp() override
81     {
82         ASSERT(Runtime::GetCurrent() != nullptr);
83         ASSERT(PandaEtsVM::GetCurrent() != nullptr);
84         mainCoro_ = EtsCoroutine::CastFromThread(PandaEtsVM::GetCurrent()->GetCoroutineManager()->GetMainThread());
85         ASSERT(mainCoro_ != nullptr);
86     }
TestMainLoop(double value,bool needCheck)87     void TestMainLoop(double value, [[maybe_unused]] bool needCheck)
88     {
89         auto etsVm = mainCoro_->GetPandaVM();
90         auto *cache = etsVm->GetDoubleToStringCache();
91         auto *etsCoro = EtsCoroutine::GetCurrent();
92         [[maybe_unused]] auto [str, result] = cache->GetOrCacheImpl(etsCoro, value);
93 #ifndef NDEBUG
94         // don't always check to increase pressure
95         if (needCheck) {
96             ASSERT(!str->IsUtf16());
97             auto res = str->GetMutf8();
98 
99             intrinsics::helpers::FpToStringDecimalRadix(value,
100                                                         [&res](std::string_view expected) { ASSERT(expected == res); });
101 
102             auto resValue = PandaStringToD(res);
103             auto eps = std::numeric_limits<double>::epsilon() * 2 * std::abs(value);
104             ASSERT(std::abs(resValue - value) < eps);
105         }
106 #endif
107     }
108 
TestConcurrentInsertion(const std::array<double,TEST_ARRAY_SIZE> & values)109     void TestConcurrentInsertion(const std::array<double, TEST_ARRAY_SIZE> &values)
110     {
111         auto runtime = Runtime::GetCurrent();
112         auto coro = mainCoro_->GetCoroutineManager()->CreateEntrypointlessCoroutine(
113             runtime, runtime->GetPandaVM(), true, "worker", Coroutine::Type::MUTATOR,
114             CoroutinePriority::DEFAULT_PRIORITY);
115         std::mt19937 engine(std::random_device {}());
116         std::uniform_real_distribution<> dis(-VALUE_RANGE, VALUE_RANGE);
117         std::bernoulli_distribution bern(1.0 / TEST_THREADS);
118         coro->ManagedCodeBegin();
119         ASSERT(coro == EtsCoroutine::GetCurrent());
120         for (uint32_t i = 0; i < TEST_ITERS; i++) {
121             for (auto value : values) {
122                 bool needCheck = bern(engine);
123                 TestMainLoop(value, needCheck);
124             }
125         }
126 
127         coro->ManagedCodeEnd();
128         mainCoro_->GetCoroutineManager()->DestroyEntrypointlessCoroutine(coro);
129     }
130 
SetupSimple()131     void SetupSimple()
132     {
133         std::uniform_real_distribution<double> dist(-VALUE_RANGE, VALUE_RANGE);
134         std::generate(values_.begin(), values_.end(), [&dist, this]() { return dist(engine_); });
135     }
136 
SetupExp()137     void SetupExp()
138     {
139         std::uniform_int_distribution<int> dist(-VALUE_RANGE, VALUE_RANGE);
140         std::generate(values_.begin(), values_.end(), [&dist, this]() { return std::pow(2U, dist(engine_)); });
141     }
142 
SetupRepeatedHashes()143     void SetupRepeatedHashes()
144     {
145         std::uniform_real_distribution<double> dist(-VALUE_RANGE, VALUE_RANGE);
146 
147         std::generate(values_.begin(), values_.end(), [&dist, this]() {
148             constexpr auto UNIQUE_HASHES = 3;
149             double value;
150             do {
151                 value = dist(engine_);
152             } while (DoubleToStringCache::GetIndex(value) >= UNIQUE_HASHES);
153             return value;
154         });
155     }
156 
SetupRepeatedHashesAndValues()157     void SetupRepeatedHashesAndValues()
158     {
159         std::uniform_real_distribution<double> dist(-VALUE_RANGE, VALUE_RANGE);
160 
161         static constexpr auto UNIQUE_VALUES = 8;
162         std::generate(values_.begin(), values_.begin() + UNIQUE_VALUES, [&dist, this]() {
163             constexpr auto UNIQUE_HASHES = 2;
164             double value;
165             do {
166                 value = dist(engine_);
167             } while (DoubleToStringCache::GetIndex(value) >= UNIQUE_HASHES);
168             return value;
169         });
170         for (size_t i = UNIQUE_VALUES; i < values_.size(); i++) {
171             values_[i] = values_[i - UNIQUE_VALUES];
172         }
173     }
174 
SetupUniqueHashes()175     void SetupUniqueHashes()
176     {
177         std::uniform_real_distribution<double> dist(-VALUE_RANGE, VALUE_RANGE);
178 
179         std::unordered_map<uint32_t, double> cacheIndexToValue;
180         while (cacheIndexToValue.size() < DoubleToStringCache::SIZE / 2U) {
181             auto value = dist(engine_);
182             cacheIndexToValue[DoubleToStringCache::GetIndex(value)] = value;
183         }
184         auto it = cacheIndexToValue.begin();
185         std::generate(values_.begin(), values_.end(), [&cacheIndexToValue, &it]() {
186             if (it == cacheIndexToValue.end()) {
187                 it = cacheIndexToValue.begin();
188             }
189             return (*it++).second;
190         });
191     }
192 
GetMainCoro()193     EtsCoroutine *GetMainCoro()
194     {
195         return mainCoro_;
196     }
197 
GetEngine()198     std::mt19937 &GetEngine()
199     {
200         return engine_;
201     }
202 
203     template <typename T>
CheckCacheElementMembers()204     static void CheckCacheElementMembers()
205     {
206         ASSERT(detail::EtsToStringCacheElement<T>::STRING_OFFSET ==
207                MEMBER_OFFSET(detail::EtsToStringCacheElement<T>, data_));
208 
209         // We can call "classLinker->GetClass" only with MutatorLock or with disabled GC.
210         // So just for testing is necessary add "MutatorLock"
211         // NOTE: In the main place of use (in initialization VM), during execution method
212         // "EtsToStringCacheElement<T>::GetClass" GC is not started
213         PandaVM::GetCurrent()->GetMutatorLock()->WriteLock();
214         auto *klass = detail::EtsToStringCacheElement<T>::GetClass(EtsCoroutine::GetCurrent());
215         std::vector<MirrorFieldInfo> members {
216             MirrorFieldInfo("string", detail::EtsToStringCacheElement<T>::STRING_OFFSET),
217             MirrorFieldInfo("lock", detail::EtsToStringCacheElement<T>::FLAG_OFFSET),
218             MIRROR_FIELD_INFO(detail::EtsToStringCacheElement<T>, number_, "number")};
219         MirrorFieldInfo::CompareMemberOffsets(klass, members);
220         PandaVM::GetCurrent()->GetMutatorLock()->Unlock();
221     }
222 
223 protected:
DoTest(void (EtsToStringCacheTest::* setup)(),GenType genType)224     void DoTest(void (EtsToStringCacheTest::*setup)(), GenType genType)
225     {
226         (this->*setup)();
227         for (uint32_t i = 0; i < TEST_THREADS; i++) {
228             if (genType == GenType::SHUFFLE) {
229                 std::shuffle(values_.begin(), values_.end(), GetEngine());
230             } else if (genType == GenType::INDEPENDENT && i > 0) {
231                 (this->*setup)();
232             }
233             threadValues_[i] = values_;
234         }
235 
236         for (uint32_t i = 0; i < TEST_THREADS; i++) {
237             threads_[i] = std::thread([this, i]() { TestConcurrentInsertion(threadValues_[i]); });
238         }
239 
240         for (uint32_t i = 0; i < TEST_THREADS; i++) {
241             threads_[i].join();
242         }
243     }
244 
245 private:
246     std::array<double, TEST_ARRAY_SIZE> values_ {};
247     std::array<std::array<double, TEST_ARRAY_SIZE>, TEST_THREADS> threadValues_ {};
248 
249     std::array<std::thread, TEST_THREADS> threads_;
250     std::mt19937 engine_;
251     EtsCoroutine *mainCoro_ {};
252 };
253 
254 // NOLINTNEXTLINE(fuchsia-multiple-inheritance)
255 class EtsToStringCacheParamTest : public EtsToStringCacheTest,
256                                   public testing::WithParamInterface<std::tuple<const char *, GenType>> {
257 public:
EtsToStringCacheParamTest()258     EtsToStringCacheParamTest() : EtsToStringCacheTest(std::get<0>(GetParam())) {}
259 
DoTest(void (EtsToStringCacheTest::* setup)())260     void DoTest(void (EtsToStringCacheTest::*setup)())
261     {
262         EtsToStringCacheTest::DoTest(setup, std::get<1>(GetParam()));
263     }
264 };
265 
TEST_P(EtsToStringCacheParamTest,ConcurrentInsertion)266 TEST_P(EtsToStringCacheParamTest, ConcurrentInsertion)
267 {
268     DoTest(&EtsToStringCacheTest::SetupSimple);
269 }
270 
TEST_P(EtsToStringCacheParamTest,ConcurrentInsertionExp)271 TEST_P(EtsToStringCacheParamTest, ConcurrentInsertionExp)
272 {
273     DoTest(&EtsToStringCacheTest::SetupExp);
274 }
275 
TEST_P(EtsToStringCacheParamTest,ConcurrentInsertionRepeatedHashes)276 TEST_P(EtsToStringCacheParamTest, ConcurrentInsertionRepeatedHashes)
277 {
278     DoTest(&EtsToStringCacheTest::SetupRepeatedHashes);
279 }
280 
TEST_P(EtsToStringCacheParamTest,ConcurrentInsertionRepeatedHashesAndValues)281 TEST_P(EtsToStringCacheParamTest, ConcurrentInsertionRepeatedHashesAndValues)
282 {
283     DoTest(&EtsToStringCacheTest::SetupRepeatedHashesAndValues);
284 }
285 
TEST_P(EtsToStringCacheParamTest,ConcurrentInsertionUniqueHashes)286 TEST_P(EtsToStringCacheParamTest, ConcurrentInsertionUniqueHashes)
287 {
288     DoTest(&EtsToStringCacheTest::SetupUniqueHashes);
289 }
290 
291 INSTANTIATE_TEST_SUITE_P(EtsToStringCacheTestSuite, EtsToStringCacheParamTest,
292                          testing::Combine(testing::Values("stw", "g1-gc"),
293                                           testing::Values(GenType::COPY, GenType::SHUFFLE, GenType::INDEPENDENT)));
294 
TEST_F(EtsToStringCacheTest,BitcastTestCached)295 TEST_F(EtsToStringCacheTest, BitcastTestCached)
296 {
297     auto &engine = GetEngine();
298     auto coro = GetMainCoro();
299     auto etsVm = coro->GetPandaVM();
300     std::uniform_int_distribution<uint32_t> dis(0, std::numeric_limits<uint32_t>::max());
301     coro->ManagedCodeBegin();
302     auto etsCoro = EtsCoroutine::GetCurrent();
303     ASSERT(coro == etsCoro);
304     auto *cache = etsVm->GetDoubleToStringCache();
305 
306     for (uint32_t i = 0; i < TEST_ARRAY_SIZE; i++) {
307         auto longValue = (static_cast<uint64_t>(dis(engine)) << BITS_PER_UINT32) | dis(engine);
308         auto value = bit_cast<double>(longValue);
309         auto *str = cache->GetOrCache(etsCoro, value);
310         ASSERT(!str->IsUtf16());
311         auto res = str->GetMutf8();
312 
313         bool correct;
314         auto eps = std::numeric_limits<double>::epsilon() * std::abs(value);
315         double resValue = 0;
316         if (std::isnan(value)) {
317             correct = res == "NaN";
318         } else {
319             std::istringstream iss {std::string(res)};
320             iss >> resValue;
321             correct = std::abs(resValue - value) <= eps;
322         }
323 
324         if (!correct) {
325             std::cerr << std::setprecision(intrinsics::helpers::DOUBLE_MAX_PRECISION) << "Error:\n"
326                       << "long: " << std::hex << longValue << "\n"
327                       << "double: " << value << "\n"
328                       << "str: " << res << "\n"
329                       << "delta: " << std::abs(resValue - value) << "\n"
330                       << "eps: " << eps << std::endl;
331             UNREACHABLE();
332         }
333     }
334 
335     coro->ManagedCodeEnd();
336 }
337 
TEST_F(EtsToStringCacheTest,BitcastTestRaw)338 TEST_F(EtsToStringCacheTest, BitcastTestRaw)
339 {
340     auto &engine = GetEngine();
341     std::uniform_int_distribution<uint32_t> dis(0, std::numeric_limits<uint32_t>::max());
342 
343     for (uint32_t i = 0; i < TEST_ARRAY_SIZE; i++) {
344         auto longValue = (static_cast<uint64_t>(dis(engine)) << BITS_PER_UINT32) | dis(engine);
345         auto value = bit_cast<double>(longValue);
346         std::string res;
347         intrinsics::helpers::FpToStringDecimalRadix(value, [&res](std::string_view expected) { res = expected; });
348 
349         bool correct;
350         auto eps = std::numeric_limits<double>::epsilon() * std::abs(value);
351         double resValue = 0;
352         if (std::isnan(value)) {
353             correct = res == "NaN";
354         } else {
355             std::istringstream iss {std::string(res)};
356             iss >> resValue;
357             correct = std::abs(resValue - value) <= eps;
358         }
359 
360         if (!correct) {
361             std::cerr << std::setprecision(intrinsics::helpers::DOUBLE_MAX_PRECISION) << "Error:\n"
362                       << "long: " << std::hex << longValue << "\n"
363                       << "double: " << value << "\n"
364                       << "str: " << res << "\n"
365                       << "delta: " << std::abs(resValue - value) << "\n"
366                       << "eps: " << eps << std::endl;
367             UNREACHABLE();
368         }
369     }
370 }
371 
SymEq(float x,float y)372 [[maybe_unused]] static bool SymEq(float x, float y)
373 {
374     if (std::isnan(x)) {
375         return std::isnan(y);
376     }
377     auto delta = std::abs(x - y);
378     auto eps = std::abs(x) * (2 * std::numeric_limits<float>::epsilon());
379     return delta <= eps;
380 }
381 
TEST_F(EtsToStringCacheTest,BitcastTestFloat)382 TEST_F(EtsToStringCacheTest, BitcastTestFloat)
383 {
384     auto &engine = GetEngine();
385     std::uniform_int_distribution<uint32_t> dis(0, std::numeric_limits<uint32_t>::max());
386 
387     for (uint32_t i = 0; i < TEST_ARRAY_SIZE; i++) {
388         auto intValue = dis(engine);
389         auto value = bit_cast<float>(intValue);
390         if (std::isnan(value)) {
391             continue;
392         }
393         {
394             std::string resFloat;
395             intrinsics::helpers::FpToStringDecimalRadix(value, [&resFloat](std::string_view str) { resFloat = str; });
396             float parsedFromFloat = 0;
397             std::istringstream iss {resFloat};
398             iss >> parsedFromFloat;
399             ASSERT_DO(SymEq(value, parsedFromFloat), std::cerr << "Error:\n"
400                                                                << "float value: " << value << "\n"
401                                                                << "float str: " << resFloat << "\n"
402                                                                << "float parsed: " << parsedFromFloat << "\n");
403         }
404         {
405             auto doubleValue = static_cast<double>(value);
406             std::string resDouble;
407             intrinsics::helpers::FpToStringDecimalRadix(doubleValue,
408                                                         [&resDouble](std::string_view str) { resDouble = str; });
409             float parsedFromDouble = 0;
410             std::istringstream iss {resDouble};
411             iss >> parsedFromDouble;
412             ASSERT(SymEq(value, parsedFromDouble));
413             ASSERT_DO(SymEq(value, parsedFromDouble), std::cerr << "Error:\n"
414                                                                 << "float value: " << value << "\n"
415                                                                 << "double str: " << resDouble << "\n"
416                                                                 << "float parsed: " << parsedFromDouble << "\n");
417         }
418     }
419 }
420 
TEST_F(EtsToStringCacheTest,ToStringCacheElementLayout)421 TEST_F(EtsToStringCacheTest, ToStringCacheElementLayout)
422 {
423     CheckCacheElementMembers<EtsDouble>();
424     CheckCacheElementMembers<EtsFloat>();
425     CheckCacheElementMembers<EtsLong>();
426 }
427 
428 // NOLINTNEXTLINE(fuchsia-multiple-inheritance)
429 class EtsToStringCacheCLITest : public EtsToStringCacheTest, public testing::WithParamInterface<bool> {
430 public:
EtsToStringCacheCLITest()431     EtsToStringCacheCLITest() : EtsToStringCacheTest(nullptr, GetParam()) {}
432 };
433 
TEST_P(EtsToStringCacheCLITest,TestCacheUsage)434 TEST_P(EtsToStringCacheCLITest, TestCacheUsage)
435 {
436     auto etsVm = GetMainCoro()->GetPandaVM();
437     auto *doubleCache = etsVm->GetDoubleToStringCache();
438     auto *floatCache = etsVm->GetFloatToStringCache();
439     auto *longCache = etsVm->GetLongToStringCache();
440     if (!GetParam()) {  // cache disabled
441         ASSERT_EQ(doubleCache, nullptr);
442         ASSERT_EQ(floatCache, nullptr);
443         ASSERT_EQ(longCache, nullptr);
444     } else {  // cache enabled
445         ASSERT_NE(doubleCache, nullptr);
446         ASSERT_NE(floatCache, nullptr);
447         ASSERT_NE(longCache, nullptr);
448     }
449 }
450 
451 INSTANTIATE_TEST_SUITE_P(EtsToStringCacheCLITestSuite, EtsToStringCacheCLITest, testing::Values(true, false));
452 
453 }  // namespace ark::ets::test
454