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