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