-
Notifications
You must be signed in to change notification settings - Fork 15.1k
[libc++] Implement C++20 atomic_ref #76647
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 77 commits
f173470
3d8d9db
f05a599
6e01477
1575d41
2767cf0
d79c427
722217b
1a0463f
5e5a5cd
d5525ad
a974df0
4172781
02220ba
8cc91fb
28e4f55
0391ec0
82fcdad
4fec87c
f763a3a
259afbf
f0cbfec
46ab572
f9edc9f
2438001
255559f
8ebfe2d
a745599
07ecf37
ba4ec71
b476c80
21bf066
6df2fa8
05dad2c
991ccb6
bd870b0
c10501c
6d310d8
7d8a1c8
89424db
956f4b3
6d5c9de
8ad10b6
ee7cb80
6ef3362
ed91c8c
dee15ac
79876f0
5c0c3c9
a795159
54220e5
c185cfd
55ee31a
bc5a5df
4616b46
fe1027c
e2b1529
d935cb4
dabe768
3ec8d68
54b6230
8a29a08
e2e4c6c
becd644
4d7c3a8
e4da1e0
e09ac7a
cee65ac
7e68d6c
3cc65a5
f64b44f
64941f1
8101aff
ef5fb5a
e656ca2
9c20e50
919e8c9
cce18df
6815d2b
049b3d7
505ddb5
a593fa0
0a03362
41b1eff
4279d70
12ecd1d
d7ff8c9
7492f36
3d6645b
75d3a66
9b81d05
6ae8d85
8fef446
856d2dd
e39d261
ee8a50e
47d3cde
9c85f18
233f568
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,356 @@ | ||
| // -*- C++ -*- | ||
| //===----------------------------------------------------------------------===// | ||
| // | ||
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| // See https://llvm.org/LICENSE.txt for license information. | ||
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
| // | ||
| // Kokkos v. 4.0 | ||
| // Copyright (2022) National Technology & Engineering | ||
| // Solutions of Sandia, LLC (NTESS). | ||
| // | ||
| // Under the terms of Contract DE-NA0003525 with NTESS, | ||
| // the U.S. Government retains certain rights in this software. | ||
| // | ||
| //===---------------------------------------------------------------------===// | ||
|
|
||
| #ifndef _LIBCPP___ATOMIC_ATOMIC_REF_H | ||
| #define _LIBCPP___ATOMIC_ATOMIC_REF_H | ||
|
|
||
| #include <__assert> | ||
| #include <__atomic/atomic_sync.h> | ||
| #include <__atomic/check_memory_order.h> | ||
| #include <__atomic/to_gcc_order.h> | ||
| #include <__concepts/arithmetic.h> | ||
| #include <__concepts/same_as.h> | ||
| #include <__config> | ||
| #include <__memory/addressof.h> | ||
| #include <__type_traits/has_unique_object_representation.h> | ||
| #include <__type_traits/is_trivially_copyable.h> | ||
| #include <cstddef> | ||
| #include <cstdint> | ||
| #include <cstring> | ||
|
|
||
| #if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER) | ||
| # pragma GCC system_header | ||
| #endif | ||
|
|
||
| _LIBCPP_PUSH_MACROS | ||
| #include <__undef_macros> | ||
|
|
||
| _LIBCPP_BEGIN_NAMESPACE_STD | ||
|
|
||
| #if _LIBCPP_STD_VER >= 20 | ||
|
|
||
| template <class _Tp> | ||
| struct __atomic_ref_base { | ||
| protected: | ||
| _Tp* __ptr_; | ||
|
|
||
| _LIBCPP_HIDE_FROM_ABI __atomic_ref_base(_Tp& __obj) : __ptr_(std::addressof(__obj)) {} | ||
|
|
||
| private: | ||
| _LIBCPP_HIDE_FROM_ABI static _Tp* __clear_padding(_Tp& __val) noexcept { | ||
| _Tp* __ptr = std::addressof(__val); | ||
| # if __has_builtin(__builtin_clear_padding) | ||
| __builtin_clear_padding(__ptr); | ||
| # endif | ||
| return __ptr; | ||
| } | ||
|
|
||
| _LIBCPP_HIDE_FROM_ABI static bool __compare_exchange( | ||
| _Tp* __ptr, _Tp* __expected, _Tp* __desired, bool __is_weak, int __success, int __failure) noexcept { | ||
| if constexpr ( | ||
| # if __has_builtin(__builtin_clear_padding) | ||
| has_unique_object_representations_v<_Tp> || same_as<_Tp, float> || same_as<_Tp, double> | ||
| # else | ||
| true // NOLINT(readability-simplify-boolean-expr) | ||
| # endif | ||
| ) { | ||
| return __atomic_compare_exchange(__ptr, __expected, __desired, __is_weak, __success, __failure); | ||
| } else { // _Tp has padding bits and __builtin_clear_padding is available | ||
| __clear_padding(*__desired); | ||
| _Tp __copy = *__expected; | ||
| __clear_padding(__copy); | ||
| // The algorithm we use here is basically to perform `__atomic_compare_exchange` on the | ||
| // values until it has either succeeded, or failed because the value representation of the | ||
| // objects involved was different. This is why we loop around __atomic_compare_exchange: | ||
| // we basically loop until its failure is caused by the value representation of the objects | ||
| // being different, not only their object representation. | ||
| while (true) { | ||
| _Tp __prev = __copy; | ||
| if (__atomic_compare_exchange(__ptr, std::addressof(__copy), __desired, __is_weak, __success, __failure)) { | ||
| return true; | ||
| } | ||
| _Tp __curr = __copy; | ||
| if (std::memcmp(__clear_padding(__prev), __clear_padding(__curr), sizeof(_Tp)) != 0) { | ||
| // Value representation without padding bits do not compare equal -> | ||
| // write the current content of *ptr into *expected | ||
| std::memcpy(__expected, std::addressof(__copy), sizeof(_Tp)); | ||
| return false; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| friend struct __atomic_waitable_traits<__atomic_ref_base<_Tp>>; | ||
|
|
||
| public: | ||
| using value_type = _Tp; | ||
|
|
||
| static constexpr size_t required_alignment = alignof(_Tp); | ||
|
|
||
| static constexpr bool is_always_lock_free = | ||
ldionne marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| __atomic_always_lock_free(sizeof(_Tp), reinterpret_cast<void*>(-required_alignment)); | ||
dalg24 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| _LIBCPP_HIDE_FROM_ABI bool is_lock_free() const noexcept { return __atomic_is_lock_free(sizeof(_Tp), __ptr_); } | ||
|
|
||
| _LIBCPP_HIDE_FROM_ABI void store(_Tp __desired, memory_order __order = memory_order::seq_cst) const noexcept | ||
| _LIBCPP_CHECK_STORE_MEMORY_ORDER(__order) { | ||
| _LIBCPP_ASSERT_ARGUMENT_WITHIN_DOMAIN( | ||
| __order == memory_order::relaxed || __order == memory_order::release || __order == memory_order::seq_cst, | ||
| "atomic_ref: memory order argument to atomic store operation is invalid"); | ||
| __atomic_store(__ptr_, __clear_padding(__desired), std::__to_gcc_order(__order)); | ||
| } | ||
|
|
||
| _LIBCPP_HIDE_FROM_ABI _Tp operator=(_Tp __desired) const noexcept { | ||
| store(__desired); | ||
| return __desired; | ||
| } | ||
|
|
||
| _LIBCPP_HIDE_FROM_ABI _Tp load(memory_order __order = memory_order::seq_cst) const noexcept | ||
| _LIBCPP_CHECK_LOAD_MEMORY_ORDER(__order) { | ||
| _LIBCPP_ASSERT_ARGUMENT_WITHIN_DOMAIN( | ||
| __order == memory_order::relaxed || __order == memory_order::consume || __order == memory_order::acquire || | ||
| __order == memory_order::seq_cst, | ||
| "atomic_ref: memory order argument to atomic load operation is invalid"); | ||
| alignas(_Tp) byte __mem[sizeof(_Tp)]; | ||
| auto* __ret = reinterpret_cast<_Tp*>(__mem); | ||
| __atomic_load(__ptr_, __ret, std::__to_gcc_order(__order)); | ||
| return *__ret; | ||
| } | ||
|
|
||
| _LIBCPP_HIDE_FROM_ABI operator _Tp() const noexcept { return load(); } | ||
|
|
||
| _LIBCPP_HIDE_FROM_ABI _Tp exchange(_Tp __desired, memory_order __order = memory_order::seq_cst) const noexcept { | ||
| alignas(_Tp) byte __mem[sizeof(_Tp)]; | ||
| auto* __ret = reinterpret_cast<_Tp*>(__mem); | ||
| __atomic_exchange(__ptr_, __clear_padding(__desired), __ret, std::__to_gcc_order(__order)); | ||
| return *__ret; | ||
| } | ||
|
|
||
| _LIBCPP_HIDE_FROM_ABI bool | ||
| compare_exchange_weak(_Tp& __expected, _Tp __desired, memory_order __success, memory_order __failure) const noexcept | ||
dalg24 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| _LIBCPP_CHECK_EXCHANGE_MEMORY_ORDER(__success, __failure) { | ||
| _LIBCPP_ASSERT_ARGUMENT_WITHIN_DOMAIN( | ||
| __failure == memory_order::relaxed || __failure == memory_order::consume || | ||
| __failure == memory_order::acquire || __failure == memory_order::seq_cst, | ||
| "atomic_ref: failure memory order argument to weak atomic compare-and-exchange operation is invalid"); | ||
| return __compare_exchange( | ||
| __ptr_, | ||
| std::addressof(__expected), | ||
| std::addressof(__desired), | ||
| true, | ||
| std::__to_gcc_order(__success), | ||
| std::__to_gcc_order(__failure)); | ||
| } | ||
| _LIBCPP_HIDE_FROM_ABI bool | ||
| compare_exchange_strong(_Tp& __expected, _Tp __desired, memory_order __success, memory_order __failure) const noexcept | ||
| _LIBCPP_CHECK_EXCHANGE_MEMORY_ORDER(__success, __failure) { | ||
| _LIBCPP_ASSERT_ARGUMENT_WITHIN_DOMAIN( | ||
| __failure == memory_order::relaxed || __failure == memory_order::consume || | ||
| __failure == memory_order::acquire || __failure == memory_order::seq_cst, | ||
| "atomic_ref: failure memory order argument to strong atomic compare-and-exchange operation is invalid"); | ||
| return __compare_exchange( | ||
| __ptr_, | ||
| std::addressof(__expected), | ||
| std::addressof(__desired), | ||
| false, | ||
| std::__to_gcc_order(__success), | ||
| std::__to_gcc_order(__failure)); | ||
| } | ||
|
|
||
| _LIBCPP_HIDE_FROM_ABI bool | ||
| compare_exchange_weak(_Tp& __expected, _Tp __desired, memory_order __order = memory_order::seq_cst) const noexcept { | ||
| return __compare_exchange( | ||
| __ptr_, | ||
| std::addressof(__expected), | ||
| std::addressof(__desired), | ||
| true, | ||
| std::__to_gcc_order(__order), | ||
| std::__to_gcc_failure_order(__order)); | ||
| } | ||
| _LIBCPP_HIDE_FROM_ABI bool | ||
| compare_exchange_strong(_Tp& __expected, _Tp __desired, memory_order __order = memory_order::seq_cst) const noexcept { | ||
| return __compare_exchange( | ||
| __ptr_, | ||
| std::addressof(__expected), | ||
| std::addressof(__desired), | ||
| false, | ||
| std::__to_gcc_order(__order), | ||
| std::__to_gcc_failure_order(__order)); | ||
| } | ||
|
|
||
| _LIBCPP_HIDE_FROM_ABI void wait(_Tp __old, memory_order __order = memory_order::seq_cst) const noexcept | ||
| _LIBCPP_CHECK_WAIT_MEMORY_ORDER(__order) { | ||
| _LIBCPP_ASSERT_ARGUMENT_WITHIN_DOMAIN( | ||
| __order == memory_order::relaxed || __order == memory_order::consume || __order == memory_order::acquire || | ||
| __order == memory_order::seq_cst, | ||
| "atomic_ref: memory order argument to atomic wait operation is invalid"); | ||
| std::__atomic_wait(*this, __old, __order); | ||
| } | ||
| _LIBCPP_HIDE_FROM_ABI void notify_one() const noexcept { std::__atomic_notify_one(*this); } | ||
| _LIBCPP_HIDE_FROM_ABI void notify_all() const noexcept { std::__atomic_notify_all(*this); } | ||
| }; | ||
|
|
||
| template <class _Tp> | ||
| struct __atomic_waitable_traits<__atomic_ref_base<_Tp>> { | ||
| static _LIBCPP_HIDE_FROM_ABI _Tp __atomic_load(const __atomic_ref_base<_Tp>& __a, memory_order __order) { | ||
| return __a.load(__order); | ||
| } | ||
| static _LIBCPP_HIDE_FROM_ABI const _Tp* __atomic_contention_address(const __atomic_ref_base<_Tp>& __a) { | ||
| return __a.__ptr_; | ||
| } | ||
| }; | ||
|
|
||
| template <class _Tp> | ||
| struct atomic_ref : public __atomic_ref_base<_Tp> { | ||
| static_assert(is_trivially_copyable_v<_Tp>, "std::atomic_ref<T> requires that 'T' be a trivially copyable type"); | ||
|
|
||
| using __base = __atomic_ref_base<_Tp>; | ||
|
|
||
| _LIBCPP_HIDE_FROM_ABI explicit atomic_ref(_Tp& __obj) : __base(__obj) { | ||
| _LIBCPP_ASSERT_ARGUMENT_WITHIN_DOMAIN( | ||
| reinterpret_cast<uintptr_t>(std::addressof(__obj)) % __base::required_alignment == 0, | ||
| "atomic_ref ctor: referenced object must be aligned to required_alignment"); | ||
| } | ||
|
|
||
| _LIBCPP_HIDE_FROM_ABI atomic_ref(const atomic_ref&) noexcept = default; | ||
|
|
||
| _LIBCPP_HIDE_FROM_ABI _Tp operator=(_Tp __desired) const noexcept { return __base::operator=(__desired); } | ||
|
|
||
| atomic_ref& operator=(const atomic_ref&) = delete; | ||
| }; | ||
|
|
||
| template <class _Tp> | ||
| requires(std::integral<_Tp> && !std::same_as<bool, _Tp>) | ||
| struct atomic_ref<_Tp> : public __atomic_ref_base<_Tp> { | ||
| using __base = __atomic_ref_base<_Tp>; | ||
|
|
||
| using difference_type = __base::value_type; | ||
|
|
||
| _LIBCPP_HIDE_FROM_ABI explicit atomic_ref(_Tp& __obj) : __base(__obj) { | ||
dalg24 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| _LIBCPP_ASSERT_ARGUMENT_WITHIN_DOMAIN( | ||
| reinterpret_cast<uintptr_t>(std::addressof(__obj)) % __base::required_alignment == 0, | ||
| "atomic_ref ctor: referenced object must be aligned to required_alignment"); | ||
| } | ||
|
|
||
| _LIBCPP_HIDE_FROM_ABI atomic_ref(const atomic_ref&) noexcept = default; | ||
|
|
||
| _LIBCPP_HIDE_FROM_ABI _Tp operator=(_Tp __desired) const noexcept { return __base::operator=(__desired); } | ||
|
|
||
| atomic_ref& operator=(const atomic_ref&) = delete; | ||
|
|
||
| _LIBCPP_HIDE_FROM_ABI _Tp fetch_add(_Tp __arg, memory_order __order = memory_order_seq_cst) const noexcept { | ||
| return __atomic_fetch_add(this->__ptr_, __arg, std::__to_gcc_order(__order)); | ||
| } | ||
| _LIBCPP_HIDE_FROM_ABI _Tp fetch_sub(_Tp __arg, memory_order __order = memory_order_seq_cst) const noexcept { | ||
| return __atomic_fetch_sub(this->__ptr_, __arg, std::__to_gcc_order(__order)); | ||
| } | ||
| _LIBCPP_HIDE_FROM_ABI _Tp fetch_and(_Tp __arg, memory_order __order = memory_order_seq_cst) const noexcept { | ||
| return __atomic_fetch_and(this->__ptr_, __arg, std::__to_gcc_order(__order)); | ||
| } | ||
| _LIBCPP_HIDE_FROM_ABI _Tp fetch_or(_Tp __arg, memory_order __order = memory_order_seq_cst) const noexcept { | ||
| return __atomic_fetch_or(this->__ptr_, __arg, std::__to_gcc_order(__order)); | ||
| } | ||
| _LIBCPP_HIDE_FROM_ABI _Tp fetch_xor(_Tp __arg, memory_order __order = memory_order_seq_cst) const noexcept { | ||
| return __atomic_fetch_xor(this->__ptr_, __arg, std::__to_gcc_order(__order)); | ||
| } | ||
|
|
||
| _LIBCPP_HIDE_FROM_ABI _Tp operator++(int) const noexcept { return fetch_add(_Tp(1)); } | ||
| _LIBCPP_HIDE_FROM_ABI _Tp operator--(int) const noexcept { return fetch_sub(_Tp(1)); } | ||
| _LIBCPP_HIDE_FROM_ABI _Tp operator++() const noexcept { return fetch_add(_Tp(1)) + _Tp(1); } | ||
| _LIBCPP_HIDE_FROM_ABI _Tp operator--() const noexcept { return fetch_sub(_Tp(1)) - _Tp(1); } | ||
| _LIBCPP_HIDE_FROM_ABI _Tp operator+=(_Tp __arg) const noexcept { return fetch_add(__arg) + __arg; } | ||
| _LIBCPP_HIDE_FROM_ABI _Tp operator-=(_Tp __arg) const noexcept { return fetch_sub(__arg) - __arg; } | ||
| _LIBCPP_HIDE_FROM_ABI _Tp operator&=(_Tp __arg) const noexcept { return fetch_and(__arg) & __arg; } | ||
| _LIBCPP_HIDE_FROM_ABI _Tp operator|=(_Tp __arg) const noexcept { return fetch_or(__arg) | __arg; } | ||
| _LIBCPP_HIDE_FROM_ABI _Tp operator^=(_Tp __arg) const noexcept { return fetch_xor(__arg) ^ __arg; } | ||
| }; | ||
|
|
||
| template <class _Tp> | ||
| requires std::floating_point<_Tp> | ||
| struct atomic_ref<_Tp> : public __atomic_ref_base<_Tp> { | ||
| using __base = __atomic_ref_base<_Tp>; | ||
|
|
||
| using difference_type = __base::value_type; | ||
|
|
||
| _LIBCPP_HIDE_FROM_ABI explicit atomic_ref(_Tp& __obj) : __base(__obj) { | ||
| _LIBCPP_ASSERT_ARGUMENT_WITHIN_DOMAIN( | ||
| reinterpret_cast<uintptr_t>(std::addressof(__obj)) % __base::required_alignment == 0, | ||
| "atomic_ref ctor: referenced object must be aligned to required_alignment"); | ||
| } | ||
|
|
||
| _LIBCPP_HIDE_FROM_ABI atomic_ref(const atomic_ref&) noexcept = default; | ||
|
|
||
| _LIBCPP_HIDE_FROM_ABI _Tp operator=(_Tp __desired) const noexcept { return __base::operator=(__desired); } | ||
|
|
||
| atomic_ref& operator=(const atomic_ref&) = delete; | ||
|
|
||
| _LIBCPP_HIDE_FROM_ABI _Tp fetch_add(_Tp __arg, memory_order __order = memory_order_seq_cst) const noexcept { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not an issue. we do have builtins for RMW operations for floating points in clang. but i guess gcc does not have it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Honestly I don't remember. IIRC I did this it because I had seen failures in some builds in the CI. I am not sure how we would guard against versions of the builtins that support atomic operations on fp. |
||
| _Tp __old = this->load(memory_order_relaxed); | ||
| _Tp __new = __old + __arg; | ||
| while (!this->compare_exchange_weak(__old, __new, __order, memory_order_relaxed)) { | ||
| __new = __old + __arg; | ||
| } | ||
| return __old; | ||
| } | ||
| _LIBCPP_HIDE_FROM_ABI _Tp fetch_sub(_Tp __arg, memory_order __order = memory_order_seq_cst) const noexcept { | ||
| _Tp __old = this->load(memory_order_relaxed); | ||
| _Tp __new = __old - __arg; | ||
| while (!this->compare_exchange_weak(__old, __new, __order, memory_order_relaxed)) { | ||
| __new = __old - __arg; | ||
| } | ||
| return __old; | ||
| } | ||
|
|
||
| _LIBCPP_HIDE_FROM_ABI _Tp operator+=(_Tp __arg) const noexcept { return fetch_add(__arg) + __arg; } | ||
| _LIBCPP_HIDE_FROM_ABI _Tp operator-=(_Tp __arg) const noexcept { return fetch_sub(__arg) - __arg; } | ||
| }; | ||
|
|
||
| template <class _Tp> | ||
| struct atomic_ref<_Tp*> : public __atomic_ref_base<_Tp*> { | ||
| using __base = __atomic_ref_base<_Tp*>; | ||
|
|
||
| using difference_type = ptrdiff_t; | ||
|
|
||
| _LIBCPP_HIDE_FROM_ABI explicit atomic_ref(_Tp*& __ptr) : __base(__ptr) {} | ||
|
|
||
| _LIBCPP_HIDE_FROM_ABI _Tp* operator=(_Tp* __desired) const noexcept { return __base::operator=(__desired); } | ||
|
|
||
| atomic_ref& operator=(const atomic_ref&) = delete; | ||
|
|
||
| _LIBCPP_HIDE_FROM_ABI _Tp* fetch_add(ptrdiff_t __arg, memory_order __order = memory_order_seq_cst) const noexcept { | ||
| return __atomic_fetch_add(this->__ptr_, __arg * sizeof(_Tp), std::__to_gcc_order(__order)); | ||
| } | ||
| _LIBCPP_HIDE_FROM_ABI _Tp* fetch_sub(ptrdiff_t __arg, memory_order __order = memory_order_seq_cst) const noexcept { | ||
| return __atomic_fetch_sub(this->__ptr_, __arg * sizeof(_Tp), std::__to_gcc_order(__order)); | ||
| } | ||
|
|
||
| _LIBCPP_HIDE_FROM_ABI _Tp* operator++(int) const noexcept { return fetch_add(1); } | ||
| _LIBCPP_HIDE_FROM_ABI _Tp* operator--(int) const noexcept { return fetch_sub(1); } | ||
| _LIBCPP_HIDE_FROM_ABI _Tp* operator++() const noexcept { return fetch_add(1) + 1; } | ||
| _LIBCPP_HIDE_FROM_ABI _Tp* operator--() const noexcept { return fetch_sub(1) - 1; } | ||
| _LIBCPP_HIDE_FROM_ABI _Tp* operator+=(ptrdiff_t __arg) const noexcept { return fetch_add(__arg) + __arg; } | ||
| _LIBCPP_HIDE_FROM_ABI _Tp* operator-=(ptrdiff_t __arg) const noexcept { return fetch_sub(__arg) - __arg; } | ||
| }; | ||
|
|
||
| _LIBCPP_CTAD_SUPPORTED_FOR_TYPE(atomic_ref); | ||
|
|
||
| #endif // _LIBCPP_STD_VER >= 20 | ||
|
|
||
| _LIBCPP_END_NAMESPACE_STD | ||
|
|
||
| _LIBCPP_POP_MACROS | ||
|
|
||
| #endif // _LIBCPP__ATOMIC_ATOMIC_REF_H | ||
Uh oh!
There was an error while loading. Please reload this page.