Add Internal.h
This commit is contained in:
487
ConflictSet.cpp
487
ConflictSet.cpp
@@ -1,17 +1,15 @@
|
|||||||
#include "ConflictSet.h"
|
#include "ConflictSet.h"
|
||||||
|
#include "Internal.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <map>
|
|
||||||
#include <set>
|
|
||||||
#include <span>
|
#include <span>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#ifdef HAS_AVX
|
#ifdef HAS_AVX
|
||||||
#include <immintrin.h>
|
#include <immintrin.h>
|
||||||
@@ -21,445 +19,8 @@
|
|||||||
|
|
||||||
#define DEBUG_VERBOSE 0
|
#define DEBUG_VERBOSE 0
|
||||||
|
|
||||||
// GCOVR_EXCL_START
|
|
||||||
|
|
||||||
__attribute__((always_inline)) inline void *safe_malloc(size_t s) {
|
|
||||||
if (void *p = malloc(s)) {
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== BEGIN ARENA IMPL ====================
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
/// Group allocations with similar lifetimes to amortize the cost of malloc/free
|
|
||||||
struct Arena {
|
|
||||||
explicit Arena(int initialSize = 0);
|
|
||||||
/// O(log n) in the number of allocations
|
|
||||||
~Arena();
|
|
||||||
struct ArenaImpl;
|
|
||||||
Arena(const Arena &) = delete;
|
|
||||||
Arena &operator=(const Arena &) = delete;
|
|
||||||
Arena(Arena &&other) noexcept;
|
|
||||||
Arena &operator=(Arena &&other) noexcept;
|
|
||||||
|
|
||||||
ArenaImpl *impl = nullptr;
|
|
||||||
};
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
[[maybe_unused]] inline void operator delete(void *, std::align_val_t,
|
|
||||||
Arena &) {}
|
|
||||||
inline void *operator new(size_t size, std::align_val_t align, Arena &arena);
|
|
||||||
void *operator new(size_t size, std::align_val_t align, Arena *arena) = delete;
|
|
||||||
|
|
||||||
[[maybe_unused]] inline void operator delete(void *, Arena &) {}
|
|
||||||
inline void *operator new(size_t size, Arena &arena) {
|
|
||||||
return operator new(size, std::align_val_t(alignof(std::max_align_t)), arena);
|
|
||||||
}
|
|
||||||
inline void *operator new(size_t size, Arena *arena) = delete;
|
|
||||||
|
|
||||||
[[maybe_unused]] inline void operator delete[](void *, Arena &) {}
|
|
||||||
inline void *operator new[](size_t size, Arena &arena) {
|
|
||||||
return operator new(size, arena);
|
|
||||||
}
|
|
||||||
inline void *operator new[](size_t size, Arena *arena) = delete;
|
|
||||||
|
|
||||||
[[maybe_unused]] inline void operator delete[](void *, std::align_val_t,
|
|
||||||
Arena &) {}
|
|
||||||
inline void *operator new[](size_t size, std::align_val_t align, Arena &arena) {
|
|
||||||
return operator new(size, align, arena);
|
|
||||||
}
|
|
||||||
inline void *operator new[](size_t size, std::align_val_t align,
|
|
||||||
Arena *arena) = delete;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
/// align must be a power of two
|
|
||||||
template <class T> T *align_up(T *t, size_t align) {
|
|
||||||
auto unaligned = uintptr_t(t);
|
|
||||||
auto aligned = (unaligned + align - 1) & ~(align - 1);
|
|
||||||
return reinterpret_cast<T *>(reinterpret_cast<char *>(t) + aligned -
|
|
||||||
unaligned);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// align must be a power of two
|
|
||||||
constexpr inline int align_up(uint32_t unaligned, uint32_t align) {
|
|
||||||
return (unaligned + align - 1) & ~(align - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the smallest power of two >= x
|
|
||||||
[[maybe_unused]] constexpr inline uint32_t nextPowerOfTwo(uint32_t x) {
|
|
||||||
return x <= 1 ? 1 : 1 << (32 - __builtin_clz(x - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Arena::ArenaImpl {
|
|
||||||
Arena::ArenaImpl *prev;
|
|
||||||
int capacity;
|
|
||||||
int used;
|
|
||||||
uint8_t *begin() { return reinterpret_cast<uint8_t *>(this + 1); }
|
|
||||||
};
|
|
||||||
|
|
||||||
static_assert(sizeof(Arena::ArenaImpl) == 16);
|
|
||||||
static_assert(alignof(Arena::ArenaImpl) == 8);
|
|
||||||
|
|
||||||
Arena::Arena(int initialSize) : impl(nullptr) {
|
|
||||||
if (initialSize > 0) {
|
|
||||||
auto allocationSize = align_up(initialSize + sizeof(ArenaImpl), 16);
|
|
||||||
impl = (Arena::ArenaImpl *)safe_malloc(allocationSize);
|
|
||||||
impl->prev = nullptr;
|
|
||||||
impl->capacity = allocationSize - sizeof(ArenaImpl);
|
|
||||||
impl->used = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onDestroy(Arena::ArenaImpl *impl) {
|
|
||||||
while (impl) {
|
|
||||||
auto *prev = impl->prev;
|
|
||||||
free(impl);
|
|
||||||
impl = prev;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[[maybe_unused]] Arena::Arena(Arena &&other) noexcept
|
|
||||||
: impl(std::exchange(other.impl, nullptr)) {}
|
|
||||||
[[maybe_unused]] Arena &Arena::operator=(Arena &&other) noexcept {
|
|
||||||
onDestroy(impl);
|
|
||||||
impl = std::exchange(other.impl, nullptr);
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
Arena::~Arena() { onDestroy(impl); }
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
inline void *operator new(size_t size, std::align_val_t align, Arena &arena) {
|
|
||||||
int64_t aligned_size = size + size_t(align) - 1;
|
|
||||||
if (arena.impl == nullptr ||
|
|
||||||
(arena.impl->capacity - arena.impl->used) < aligned_size) {
|
|
||||||
auto allocationSize = align_up(
|
|
||||||
sizeof(Arena::ArenaImpl) +
|
|
||||||
std::max<int>(aligned_size,
|
|
||||||
(arena.impl ? std::max<int>(sizeof(Arena::ArenaImpl),
|
|
||||||
arena.impl->capacity * 2)
|
|
||||||
: 0)),
|
|
||||||
16);
|
|
||||||
auto *impl = (Arena::ArenaImpl *)safe_malloc(allocationSize);
|
|
||||||
impl->prev = arena.impl;
|
|
||||||
impl->capacity = allocationSize - sizeof(Arena::ArenaImpl);
|
|
||||||
impl->used = 0;
|
|
||||||
arena.impl = impl;
|
|
||||||
}
|
|
||||||
auto *result =
|
|
||||||
align_up(arena.impl->begin() + arena.impl->used, size_t(align));
|
|
||||||
auto usedDelta = (result - arena.impl->begin()) + size - arena.impl->used;
|
|
||||||
arena.impl->used += usedDelta;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
/// STL-friendly allocator using an arena
|
|
||||||
template <class T> struct ArenaAlloc {
|
|
||||||
typedef T value_type;
|
|
||||||
|
|
||||||
ArenaAlloc() = delete;
|
|
||||||
explicit ArenaAlloc(Arena *arena) : arena(arena) {}
|
|
||||||
|
|
||||||
Arena *arena;
|
|
||||||
|
|
||||||
template <class U> constexpr ArenaAlloc(const ArenaAlloc<U> &other) noexcept {
|
|
||||||
arena = other.arena;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] T *allocate(size_t n) {
|
|
||||||
if (n > 0xfffffffffffffffful / sizeof(T)) { // NOLINT
|
|
||||||
__builtin_unreachable();
|
|
||||||
}
|
|
||||||
|
|
||||||
return static_cast<T *>((void *)new (std::align_val_t(alignof(T)), *arena)
|
|
||||||
uint8_t[n * sizeof(T)]); // NOLINT
|
|
||||||
}
|
|
||||||
|
|
||||||
void deallocate(T *, size_t) noexcept {}
|
|
||||||
};
|
|
||||||
|
|
||||||
template <class T> using Vector = std::vector<T, ArenaAlloc<T>>;
|
|
||||||
template <class T> auto vector(Arena &arena) {
|
|
||||||
return Vector<T>(ArenaAlloc<T>(&arena));
|
|
||||||
}
|
|
||||||
template <class T> using Set = std::set<T, std::less<T>, ArenaAlloc<T>>;
|
|
||||||
template <class T> auto set(Arena &arena) {
|
|
||||||
return Set<T>(ArenaAlloc<T>(&arena));
|
|
||||||
}
|
|
||||||
|
|
||||||
template <class T, class U>
|
|
||||||
bool operator==(const ArenaAlloc<T> &lhs, const ArenaAlloc<U> &rhs) {
|
|
||||||
return lhs.arena == rhs.arena;
|
|
||||||
}
|
|
||||||
|
|
||||||
template <class T, class U>
|
|
||||||
bool operator!=(const ArenaAlloc<T> &lhs, const ArenaAlloc<U> &rhs) {
|
|
||||||
return !(lhs == rhs);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
// ==================== END ARENA IMPL ====================
|
|
||||||
|
|
||||||
// ==================== BEGIN ARBITRARY IMPL ====================
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
/// Think of `Arbitrary` as an attacker-controlled random number generator.
|
|
||||||
/// Usually you want your random number generator to be fair, so that you can
|
|
||||||
/// sensibly analyze probabilities. E.g. The analysis that shows that quicksort
|
|
||||||
/// is expected O(n log n) with a random pivot relies on the random pivot being
|
|
||||||
/// selected uniformly from a fair distribution.
|
|
||||||
///
|
|
||||||
/// Other times you want your randomness to be diabolically unfair, like when
|
|
||||||
/// looking for bugs and fuzzing. The random-number-like interface is still
|
|
||||||
/// convenient here, but you can potentially get much better coverage by
|
|
||||||
/// allowing the possibility of e.g. flipping heads 100 times in a row.
|
|
||||||
///
|
|
||||||
/// When it runs out of entropy, it always returns 0.
|
|
||||||
struct Arbitrary {
|
|
||||||
Arbitrary() = default;
|
|
||||||
|
|
||||||
explicit Arbitrary(std::span<const uint8_t> bytecode) : bytecode(bytecode) {}
|
|
||||||
|
|
||||||
/// Draws an arbitrary uint32_t
|
|
||||||
uint32_t next() { return consume<4>(); }
|
|
||||||
|
|
||||||
/// Draws an arbitrary element from [0, s)
|
|
||||||
uint32_t bounded(uint32_t s);
|
|
||||||
|
|
||||||
/// Fill `bytes` with `size` arbitrary bytes
|
|
||||||
void randomBytes(uint8_t *bytes, int size) {
|
|
||||||
int toFill = std::min<int>(size, bytecode.size());
|
|
||||||
if (toFill > 0) {
|
|
||||||
memcpy(bytes, bytecode.data(), toFill);
|
|
||||||
}
|
|
||||||
bytecode = bytecode.subspan(toFill, bytecode.size() - toFill);
|
|
||||||
memset(bytes + toFill, 0, size - toFill);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fill `bytes` with `size` random hex bytes
|
|
||||||
void randomHex(uint8_t *bytes, int size) {
|
|
||||||
for (int i = 0; i < size;) {
|
|
||||||
uint8_t arbitrary = consume<1>();
|
|
||||||
bytes[i++] = "0123456789abcdef"[arbitrary & 0xf];
|
|
||||||
arbitrary >>= 4;
|
|
||||||
if (i < size) {
|
|
||||||
bytes[i++] = "0123456789abcdef"[arbitrary & 0xf];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
template <class T, class = std::enable_if_t<std::is_trivially_copyable_v<T>>>
|
|
||||||
T randT() {
|
|
||||||
T t;
|
|
||||||
randomBytes((uint8_t *)&t, sizeof(T));
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool hasEntropy() const { return bytecode.size() != 0; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
uint8_t consumeByte() {
|
|
||||||
if (bytecode.size() == 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
auto result = bytecode[0];
|
|
||||||
bytecode = bytecode.subspan(1, bytecode.size() - 1);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
template <int kBytes> uint32_t consume() {
|
|
||||||
uint32_t result = 0;
|
|
||||||
static_assert(kBytes <= 4);
|
|
||||||
for (int i = 0; i < kBytes; ++i) {
|
|
||||||
result <<= 8;
|
|
||||||
result |= consumeByte();
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::span<const uint8_t> bytecode;
|
|
||||||
};
|
|
||||||
|
|
||||||
uint32_t Arbitrary::bounded(uint32_t s) {
|
|
||||||
if (s == 1) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
switch (32 - __builtin_clz(s - 1)) {
|
|
||||||
case 1:
|
|
||||||
case 2:
|
|
||||||
case 3:
|
|
||||||
case 4:
|
|
||||||
case 5:
|
|
||||||
case 6:
|
|
||||||
case 7:
|
|
||||||
case 8:
|
|
||||||
return consume<1>() % s;
|
|
||||||
case 9:
|
|
||||||
case 10:
|
|
||||||
case 11:
|
|
||||||
case 12:
|
|
||||||
case 13:
|
|
||||||
case 14:
|
|
||||||
case 15:
|
|
||||||
case 16:
|
|
||||||
return consume<2>() % s;
|
|
||||||
case 17:
|
|
||||||
case 18:
|
|
||||||
case 19:
|
|
||||||
case 20:
|
|
||||||
case 21:
|
|
||||||
case 22:
|
|
||||||
case 23:
|
|
||||||
case 24:
|
|
||||||
return consume<3>() % s;
|
|
||||||
default:
|
|
||||||
return consume<4>() % s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
// ==================== END ARBITRARY IMPL ====================
|
|
||||||
|
|
||||||
// ==================== BEGIN UTILITIES IMPL ====================
|
|
||||||
|
|
||||||
// Call Stepwise::step for each element of remaining until it returns true.
|
|
||||||
// Applies a permutation to `remaining` as a side effect.
|
|
||||||
template <class Stepwise> void runInterleaved(std::span<Stepwise> remaining) {
|
|
||||||
while (remaining.size() > 0) {
|
|
||||||
for (int i = 0; i < int(remaining.size());) {
|
|
||||||
bool done = remaining[i].step();
|
|
||||||
if (done) {
|
|
||||||
if (i != int(remaining.size()) - 1) {
|
|
||||||
using std::swap;
|
|
||||||
swap(remaining[i], remaining.back());
|
|
||||||
}
|
|
||||||
remaining = remaining.subspan(0, remaining.size() - 1);
|
|
||||||
} else {
|
|
||||||
++i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
template <class Stepwise> void runSequential(std::span<Stepwise> remaining) {
|
|
||||||
for (auto &r : remaining) {
|
|
||||||
while (!r.step()) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ReferenceImpl {
|
|
||||||
explicit ReferenceImpl(int64_t oldestVersion) : oldestVersion(oldestVersion) {
|
|
||||||
writeVersionMap[""] = oldestVersion;
|
|
||||||
}
|
|
||||||
void check(const ConflictSet::ReadRange *reads, ConflictSet::Result *results,
|
|
||||||
int count) const {
|
|
||||||
for (int i = 0; i < count; ++i) {
|
|
||||||
if (reads[i].readVersion < oldestVersion) {
|
|
||||||
results[i] = ConflictSet::TooOld;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
auto begin =
|
|
||||||
std::string((const char *)reads[i].begin.p, reads[i].begin.len);
|
|
||||||
auto end =
|
|
||||||
reads[i].end.len == 0
|
|
||||||
? begin + std::string("\x00", 1)
|
|
||||||
: std::string((const char *)reads[i].end.p, reads[i].end.len);
|
|
||||||
int64_t maxVersion = oldestVersion;
|
|
||||||
for (auto iter = --writeVersionMap.upper_bound(begin),
|
|
||||||
endIter = writeVersionMap.lower_bound(end);
|
|
||||||
iter != endIter; ++iter) {
|
|
||||||
maxVersion = std::max(maxVersion, iter->second);
|
|
||||||
}
|
|
||||||
results[i] = maxVersion > reads[i].readVersion ? ConflictSet::Conflict
|
|
||||||
: ConflictSet::Commit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
void addWrites(const ConflictSet::WriteRange *writes, int count) {
|
|
||||||
for (int i = 0; i < count; ++i) {
|
|
||||||
auto begin =
|
|
||||||
std::string((const char *)writes[i].begin.p, writes[i].begin.len);
|
|
||||||
auto end =
|
|
||||||
writes[i].end.len == 0
|
|
||||||
? begin + std::string("\x00", 1)
|
|
||||||
: std::string((const char *)writes[i].end.p, writes[i].end.len);
|
|
||||||
auto writeVersion = writes[i].writeVersion;
|
|
||||||
auto prevVersion = (--writeVersionMap.upper_bound(end))->second;
|
|
||||||
for (auto iter = writeVersionMap.lower_bound(begin),
|
|
||||||
endIter = writeVersionMap.lower_bound(end);
|
|
||||||
iter != endIter;) {
|
|
||||||
iter = writeVersionMap.erase(iter);
|
|
||||||
}
|
|
||||||
writeVersionMap[begin] = writeVersion;
|
|
||||||
writeVersionMap[end] = prevVersion;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void setOldestVersion(int64_t oldestVersion) {
|
|
||||||
this->oldestVersion = oldestVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
void printLogical(std::string &result) {
|
|
||||||
for (const auto &[k, v] : writeVersionMap) {
|
|
||||||
std::string key;
|
|
||||||
for (uint8_t c : k) {
|
|
||||||
key += "x";
|
|
||||||
key += "0123456789abcdef"[c / 16];
|
|
||||||
key += "0123456789abcdef"[c % 16];
|
|
||||||
}
|
|
||||||
result += key + " -> " + std::to_string(v) + "\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int64_t oldestVersion;
|
|
||||||
std::map<std::string, int64_t> writeVersionMap;
|
|
||||||
};
|
|
||||||
|
|
||||||
using Key = ConflictSet::Key;
|
|
||||||
|
|
||||||
[[maybe_unused]] Key toKey(Arena &arena, int n) {
|
|
||||||
constexpr int kMaxLength = 8;
|
|
||||||
int i = kMaxLength;
|
|
||||||
uint8_t *itoaBuf = new (arena) uint8_t[kMaxLength];
|
|
||||||
memset(itoaBuf, '0', kMaxLength);
|
|
||||||
do {
|
|
||||||
itoaBuf[--i] = "0123456789abcdef"[n % 16];
|
|
||||||
n /= 16;
|
|
||||||
} while (n);
|
|
||||||
return Key{itoaBuf, kMaxLength};
|
|
||||||
}
|
|
||||||
|
|
||||||
[[maybe_unused]] Key toKeyAfter(Arena &arena, int n) {
|
|
||||||
constexpr int kMaxLength = 8;
|
|
||||||
int i = kMaxLength;
|
|
||||||
uint8_t *itoaBuf = new (arena) uint8_t[kMaxLength + 1];
|
|
||||||
memset(itoaBuf, '0', kMaxLength);
|
|
||||||
itoaBuf[kMaxLength] = 0;
|
|
||||||
do {
|
|
||||||
itoaBuf[--i] = "0123456789abcdef"[n % 16];
|
|
||||||
n /= 16;
|
|
||||||
} while (n);
|
|
||||||
return Key{itoaBuf, kMaxLength + 1};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== END UTILITIES IMPL ====================
|
|
||||||
|
|
||||||
// ==================== BEGIN IMPLEMENTATION ====================
|
// ==================== BEGIN IMPLEMENTATION ====================
|
||||||
|
|
||||||
// GCOVR_EXCL_STOP
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
struct Entry {
|
struct Entry {
|
||||||
int64_t pointVersion;
|
int64_t pointVersion;
|
||||||
int64_t rangeVersion;
|
int64_t rangeVersion;
|
||||||
@@ -1103,7 +664,17 @@ struct Iterator {
|
|||||||
int cmp;
|
int cmp;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string_view getSearchPath(Arena &arena, Node *n);
|
std::string_view getSearchPath(Arena &arena, Node *n) {
|
||||||
|
if (n->parent == nullptr) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
auto result = vector<char>(arena);
|
||||||
|
for (; n->parent != nullptr; n = n->parent) {
|
||||||
|
result.push_back(n->parentsIndex);
|
||||||
|
}
|
||||||
|
std::reverse(result.begin(), result.end());
|
||||||
|
return std::string_view((const char *)&result[0], result.size()); // NOLINT
|
||||||
|
}
|
||||||
|
|
||||||
Iterator lastLeq(Node *n, const std::span<const uint8_t> key) {
|
Iterator lastLeq(Node *n, const std::span<const uint8_t> key) {
|
||||||
auto remaining = key;
|
auto remaining = key;
|
||||||
@@ -1172,11 +743,6 @@ void insert(Node **self_, std::span<const uint8_t> key, int64_t writeVersion) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string printable(std::string_view key);
|
|
||||||
std::string printable(const Key &key);
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
|
struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
|
||||||
void check(const ReadRange *reads, Result *result, int count) const {
|
void check(const ReadRange *reads, Result *result, int count) const {
|
||||||
for (int i = 0; i < count; ++i) {
|
for (int i = 0; i < count; ++i) {
|
||||||
@@ -1311,34 +877,6 @@ __attribute__((__visibility__("default"))) void ConflictSet_destroy(void *cs) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
std::string printable(std::string_view key) {
|
|
||||||
std::string result;
|
|
||||||
for (uint8_t c : key) {
|
|
||||||
result += "x";
|
|
||||||
result += "0123456789abcdef"[c / 16];
|
|
||||||
result += "0123456789abcdef"[c % 16];
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string printable(const Key &key) {
|
|
||||||
return printable(std::string_view((const char *)key.p, key.len));
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string_view getSearchPath(Arena &arena, Node *n) {
|
|
||||||
if (n->parent == nullptr) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
auto result = vector<char>(arena);
|
|
||||||
for (; n->parent != nullptr; n = n->parent) {
|
|
||||||
result.push_back(n->parentsIndex);
|
|
||||||
}
|
|
||||||
std::reverse(result.begin(), result.end());
|
|
||||||
return std::string_view((const char *)&result[0], result.size()); // NOLINT
|
|
||||||
}
|
|
||||||
|
|
||||||
void printLogical(std::string &result, Node *node) {
|
void printLogical(std::string &result, Node *node) {
|
||||||
Arena arena;
|
Arena arena;
|
||||||
for (Node *iter = node; iter != nullptr;) {
|
for (Node *iter = node; iter != nullptr;) {
|
||||||
@@ -1450,7 +988,6 @@ bool checkCorrectness(Node *node, ReferenceImpl &refImpl) {
|
|||||||
|
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
} // namespace
|
|
||||||
|
|
||||||
namespace std {
|
namespace std {
|
||||||
void __throw_length_error(const char *) { __builtin_unreachable(); }
|
void __throw_length_error(const char *) { __builtin_unreachable(); }
|
||||||
|
445
Internal.h
Normal file
445
Internal.h
Normal file
@@ -0,0 +1,445 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ConflictSet.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <map>
|
||||||
|
#include <set>
|
||||||
|
#include <span>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// GCOVR_EXCL_START
|
||||||
|
|
||||||
|
__attribute__((always_inline)) inline void *safe_malloc(size_t s) {
|
||||||
|
if (void *p = malloc(s)) {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== BEGIN ARENA IMPL ====================
|
||||||
|
|
||||||
|
/// Group allocations with similar lifetimes to amortize the cost of malloc/free
|
||||||
|
struct Arena {
|
||||||
|
explicit Arena(int initialSize = 0);
|
||||||
|
/// O(log n) in the number of allocations
|
||||||
|
~Arena();
|
||||||
|
struct ArenaImpl;
|
||||||
|
Arena(const Arena &) = delete;
|
||||||
|
Arena &operator=(const Arena &) = delete;
|
||||||
|
Arena(Arena &&other) noexcept;
|
||||||
|
Arena &operator=(Arena &&other) noexcept;
|
||||||
|
|
||||||
|
ArenaImpl *impl = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[maybe_unused]] inline void operator delete(void *, std::align_val_t,
|
||||||
|
Arena &) {}
|
||||||
|
inline void *operator new(size_t size, std::align_val_t align, Arena &arena);
|
||||||
|
void *operator new(size_t size, std::align_val_t align, Arena *arena) = delete;
|
||||||
|
|
||||||
|
[[maybe_unused]] inline void operator delete(void *, Arena &) {}
|
||||||
|
inline void *operator new(size_t size, Arena &arena) {
|
||||||
|
return operator new(size, std::align_val_t(alignof(std::max_align_t)), arena);
|
||||||
|
}
|
||||||
|
inline void *operator new(size_t size, Arena *arena) = delete;
|
||||||
|
|
||||||
|
[[maybe_unused]] inline void operator delete[](void *, Arena &) {}
|
||||||
|
inline void *operator new[](size_t size, Arena &arena) {
|
||||||
|
return operator new(size, arena);
|
||||||
|
}
|
||||||
|
inline void *operator new[](size_t size, Arena *arena) = delete;
|
||||||
|
|
||||||
|
[[maybe_unused]] inline void operator delete[](void *, std::align_val_t,
|
||||||
|
Arena &) {}
|
||||||
|
inline void *operator new[](size_t size, std::align_val_t align, Arena &arena) {
|
||||||
|
return operator new(size, align, arena);
|
||||||
|
}
|
||||||
|
inline void *operator new[](size_t size, std::align_val_t align,
|
||||||
|
Arena *arena) = delete;
|
||||||
|
|
||||||
|
/// align must be a power of two
|
||||||
|
template <class T> T *align_up(T *t, size_t align) {
|
||||||
|
auto unaligned = uintptr_t(t);
|
||||||
|
auto aligned = (unaligned + align - 1) & ~(align - 1);
|
||||||
|
return reinterpret_cast<T *>(reinterpret_cast<char *>(t) + aligned -
|
||||||
|
unaligned);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// align must be a power of two
|
||||||
|
constexpr inline int align_up(uint32_t unaligned, uint32_t align) {
|
||||||
|
return (unaligned + align - 1) & ~(align - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the smallest power of two >= x
|
||||||
|
[[maybe_unused]] constexpr inline uint32_t nextPowerOfTwo(uint32_t x) {
|
||||||
|
return x <= 1 ? 1 : 1 << (32 - __builtin_clz(x - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Arena::ArenaImpl {
|
||||||
|
Arena::ArenaImpl *prev;
|
||||||
|
int capacity;
|
||||||
|
int used;
|
||||||
|
uint8_t *begin() { return reinterpret_cast<uint8_t *>(this + 1); }
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(Arena::ArenaImpl) == 16);
|
||||||
|
static_assert(alignof(Arena::ArenaImpl) == 8);
|
||||||
|
|
||||||
|
inline Arena::Arena(int initialSize) : impl(nullptr) {
|
||||||
|
if (initialSize > 0) {
|
||||||
|
auto allocationSize = align_up(initialSize + sizeof(ArenaImpl), 16);
|
||||||
|
impl = (Arena::ArenaImpl *)safe_malloc(allocationSize);
|
||||||
|
impl->prev = nullptr;
|
||||||
|
impl->capacity = allocationSize - sizeof(ArenaImpl);
|
||||||
|
impl->used = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void onDestroy(Arena::ArenaImpl *impl) {
|
||||||
|
while (impl) {
|
||||||
|
auto *prev = impl->prev;
|
||||||
|
free(impl);
|
||||||
|
impl = prev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[[maybe_unused]] inline Arena::Arena(Arena &&other) noexcept
|
||||||
|
: impl(std::exchange(other.impl, nullptr)) {}
|
||||||
|
[[maybe_unused]] inline Arena &Arena::operator=(Arena &&other) noexcept {
|
||||||
|
onDestroy(impl);
|
||||||
|
impl = std::exchange(other.impl, nullptr);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Arena::~Arena() { onDestroy(impl); }
|
||||||
|
|
||||||
|
inline void *operator new(size_t size, std::align_val_t align, Arena &arena) {
|
||||||
|
int64_t aligned_size = size + size_t(align) - 1;
|
||||||
|
if (arena.impl == nullptr ||
|
||||||
|
(arena.impl->capacity - arena.impl->used) < aligned_size) {
|
||||||
|
auto allocationSize = align_up(
|
||||||
|
sizeof(Arena::ArenaImpl) +
|
||||||
|
std::max<int>(aligned_size,
|
||||||
|
(arena.impl ? std::max<int>(sizeof(Arena::ArenaImpl),
|
||||||
|
arena.impl->capacity * 2)
|
||||||
|
: 0)),
|
||||||
|
16);
|
||||||
|
auto *impl = (Arena::ArenaImpl *)safe_malloc(allocationSize);
|
||||||
|
impl->prev = arena.impl;
|
||||||
|
impl->capacity = allocationSize - sizeof(Arena::ArenaImpl);
|
||||||
|
impl->used = 0;
|
||||||
|
arena.impl = impl;
|
||||||
|
}
|
||||||
|
auto *result =
|
||||||
|
align_up(arena.impl->begin() + arena.impl->used, size_t(align));
|
||||||
|
auto usedDelta = (result - arena.impl->begin()) + size - arena.impl->used;
|
||||||
|
arena.impl->used += usedDelta;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// STL-friendly allocator using an arena
|
||||||
|
template <class T> struct ArenaAlloc {
|
||||||
|
typedef T value_type;
|
||||||
|
|
||||||
|
ArenaAlloc() = delete;
|
||||||
|
explicit ArenaAlloc(Arena *arena) : arena(arena) {}
|
||||||
|
|
||||||
|
Arena *arena;
|
||||||
|
|
||||||
|
template <class U> constexpr ArenaAlloc(const ArenaAlloc<U> &other) noexcept {
|
||||||
|
arena = other.arena;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] T *allocate(size_t n) {
|
||||||
|
if (n > 0xfffffffffffffffful / sizeof(T)) { // NOLINT
|
||||||
|
__builtin_unreachable();
|
||||||
|
}
|
||||||
|
|
||||||
|
return static_cast<T *>((void *)new (std::align_val_t(alignof(T)), *arena)
|
||||||
|
uint8_t[n * sizeof(T)]); // NOLINT
|
||||||
|
}
|
||||||
|
|
||||||
|
void deallocate(T *, size_t) noexcept {}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <class T> using Vector = std::vector<T, ArenaAlloc<T>>;
|
||||||
|
template <class T> auto vector(Arena &arena) {
|
||||||
|
return Vector<T>(ArenaAlloc<T>(&arena));
|
||||||
|
}
|
||||||
|
template <class T> using Set = std::set<T, std::less<T>, ArenaAlloc<T>>;
|
||||||
|
template <class T> auto set(Arena &arena) {
|
||||||
|
return Set<T>(ArenaAlloc<T>(&arena));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T, class U>
|
||||||
|
bool operator==(const ArenaAlloc<T> &lhs, const ArenaAlloc<U> &rhs) {
|
||||||
|
return lhs.arena == rhs.arena;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T, class U>
|
||||||
|
bool operator!=(const ArenaAlloc<T> &lhs, const ArenaAlloc<U> &rhs) {
|
||||||
|
return !(lhs == rhs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== END ARENA IMPL ====================
|
||||||
|
|
||||||
|
// ==================== BEGIN ARBITRARY IMPL ====================
|
||||||
|
|
||||||
|
/// Think of `Arbitrary` as an attacker-controlled random number generator.
|
||||||
|
/// Usually you want your random number generator to be fair, so that you can
|
||||||
|
/// sensibly analyze probabilities. E.g. The analysis that shows that quicksort
|
||||||
|
/// is expected O(n log n) with a random pivot relies on the random pivot being
|
||||||
|
/// selected uniformly from a fair distribution.
|
||||||
|
///
|
||||||
|
/// Other times you want your randomness to be diabolically unfair, like when
|
||||||
|
/// looking for bugs and fuzzing. The random-number-like interface is still
|
||||||
|
/// convenient here, but you can potentially get much better coverage by
|
||||||
|
/// allowing the possibility of e.g. flipping heads 100 times in a row.
|
||||||
|
///
|
||||||
|
/// When it runs out of entropy, it always returns 0.
|
||||||
|
struct Arbitrary {
|
||||||
|
Arbitrary() = default;
|
||||||
|
|
||||||
|
explicit Arbitrary(std::span<const uint8_t> bytecode) : bytecode(bytecode) {}
|
||||||
|
|
||||||
|
/// Draws an arbitrary uint32_t
|
||||||
|
uint32_t next() { return consume<4>(); }
|
||||||
|
|
||||||
|
/// Draws an arbitrary element from [0, s)
|
||||||
|
uint32_t bounded(uint32_t s);
|
||||||
|
|
||||||
|
/// Fill `bytes` with `size` arbitrary bytes
|
||||||
|
void randomBytes(uint8_t *bytes, int size) {
|
||||||
|
int toFill = std::min<int>(size, bytecode.size());
|
||||||
|
if (toFill > 0) {
|
||||||
|
memcpy(bytes, bytecode.data(), toFill);
|
||||||
|
}
|
||||||
|
bytecode = bytecode.subspan(toFill, bytecode.size() - toFill);
|
||||||
|
memset(bytes + toFill, 0, size - toFill);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fill `bytes` with `size` random hex bytes
|
||||||
|
void randomHex(uint8_t *bytes, int size) {
|
||||||
|
for (int i = 0; i < size;) {
|
||||||
|
uint8_t arbitrary = consume<1>();
|
||||||
|
bytes[i++] = "0123456789abcdef"[arbitrary & 0xf];
|
||||||
|
arbitrary >>= 4;
|
||||||
|
if (i < size) {
|
||||||
|
bytes[i++] = "0123456789abcdef"[arbitrary & 0xf];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T, class = std::enable_if_t<std::is_trivially_copyable_v<T>>>
|
||||||
|
T randT() {
|
||||||
|
T t;
|
||||||
|
randomBytes((uint8_t *)&t, sizeof(T));
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasEntropy() const { return bytecode.size() != 0; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint8_t consumeByte() {
|
||||||
|
if (bytecode.size() == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
auto result = bytecode[0];
|
||||||
|
bytecode = bytecode.subspan(1, bytecode.size() - 1);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <int kBytes> uint32_t consume() {
|
||||||
|
uint32_t result = 0;
|
||||||
|
static_assert(kBytes <= 4);
|
||||||
|
for (int i = 0; i < kBytes; ++i) {
|
||||||
|
result <<= 8;
|
||||||
|
result |= consumeByte();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::span<const uint8_t> bytecode;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline uint32_t Arbitrary::bounded(uint32_t s) {
|
||||||
|
if (s == 1) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
switch (32 - __builtin_clz(s - 1)) {
|
||||||
|
case 1:
|
||||||
|
case 2:
|
||||||
|
case 3:
|
||||||
|
case 4:
|
||||||
|
case 5:
|
||||||
|
case 6:
|
||||||
|
case 7:
|
||||||
|
case 8:
|
||||||
|
return consume<1>() % s;
|
||||||
|
case 9:
|
||||||
|
case 10:
|
||||||
|
case 11:
|
||||||
|
case 12:
|
||||||
|
case 13:
|
||||||
|
case 14:
|
||||||
|
case 15:
|
||||||
|
case 16:
|
||||||
|
return consume<2>() % s;
|
||||||
|
case 17:
|
||||||
|
case 18:
|
||||||
|
case 19:
|
||||||
|
case 20:
|
||||||
|
case 21:
|
||||||
|
case 22:
|
||||||
|
case 23:
|
||||||
|
case 24:
|
||||||
|
return consume<3>() % s;
|
||||||
|
default:
|
||||||
|
return consume<4>() % s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== END ARBITRARY IMPL ====================
|
||||||
|
|
||||||
|
// ==================== BEGIN UTILITIES IMPL ====================
|
||||||
|
|
||||||
|
// Call Stepwise::step for each element of remaining until it returns true.
|
||||||
|
// Applies a permutation to `remaining` as a side effect.
|
||||||
|
template <class Stepwise> void runInterleaved(std::span<Stepwise> remaining) {
|
||||||
|
while (remaining.size() > 0) {
|
||||||
|
for (int i = 0; i < int(remaining.size());) {
|
||||||
|
bool done = remaining[i].step();
|
||||||
|
if (done) {
|
||||||
|
if (i != int(remaining.size()) - 1) {
|
||||||
|
using std::swap;
|
||||||
|
swap(remaining[i], remaining.back());
|
||||||
|
}
|
||||||
|
remaining = remaining.subspan(0, remaining.size() - 1);
|
||||||
|
} else {
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <class Stepwise> void runSequential(std::span<Stepwise> remaining) {
|
||||||
|
for (auto &r : remaining) {
|
||||||
|
while (!r.step()) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ReferenceImpl {
|
||||||
|
explicit ReferenceImpl(int64_t oldestVersion) : oldestVersion(oldestVersion) {
|
||||||
|
writeVersionMap[""] = oldestVersion;
|
||||||
|
}
|
||||||
|
void check(const ConflictSet::ReadRange *reads, ConflictSet::Result *results,
|
||||||
|
int count) const {
|
||||||
|
for (int i = 0; i < count; ++i) {
|
||||||
|
if (reads[i].readVersion < oldestVersion) {
|
||||||
|
results[i] = ConflictSet::TooOld;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto begin =
|
||||||
|
std::string((const char *)reads[i].begin.p, reads[i].begin.len);
|
||||||
|
auto end =
|
||||||
|
reads[i].end.len == 0
|
||||||
|
? begin + std::string("\x00", 1)
|
||||||
|
: std::string((const char *)reads[i].end.p, reads[i].end.len);
|
||||||
|
int64_t maxVersion = oldestVersion;
|
||||||
|
for (auto iter = --writeVersionMap.upper_bound(begin),
|
||||||
|
endIter = writeVersionMap.lower_bound(end);
|
||||||
|
iter != endIter; ++iter) {
|
||||||
|
maxVersion = std::max(maxVersion, iter->second);
|
||||||
|
}
|
||||||
|
results[i] = maxVersion > reads[i].readVersion ? ConflictSet::Conflict
|
||||||
|
: ConflictSet::Commit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void addWrites(const ConflictSet::WriteRange *writes, int count) {
|
||||||
|
for (int i = 0; i < count; ++i) {
|
||||||
|
auto begin =
|
||||||
|
std::string((const char *)writes[i].begin.p, writes[i].begin.len);
|
||||||
|
auto end =
|
||||||
|
writes[i].end.len == 0
|
||||||
|
? begin + std::string("\x00", 1)
|
||||||
|
: std::string((const char *)writes[i].end.p, writes[i].end.len);
|
||||||
|
auto writeVersion = writes[i].writeVersion;
|
||||||
|
auto prevVersion = (--writeVersionMap.upper_bound(end))->second;
|
||||||
|
for (auto iter = writeVersionMap.lower_bound(begin),
|
||||||
|
endIter = writeVersionMap.lower_bound(end);
|
||||||
|
iter != endIter;) {
|
||||||
|
iter = writeVersionMap.erase(iter);
|
||||||
|
}
|
||||||
|
writeVersionMap[begin] = writeVersion;
|
||||||
|
writeVersionMap[end] = prevVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setOldestVersion(int64_t oldestVersion) {
|
||||||
|
this->oldestVersion = oldestVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
void printLogical(std::string &result) {
|
||||||
|
for (const auto &[k, v] : writeVersionMap) {
|
||||||
|
std::string key;
|
||||||
|
for (uint8_t c : k) {
|
||||||
|
key += "x";
|
||||||
|
key += "0123456789abcdef"[c / 16];
|
||||||
|
key += "0123456789abcdef"[c % 16];
|
||||||
|
}
|
||||||
|
result += key + " -> " + std::to_string(v) + "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int64_t oldestVersion;
|
||||||
|
std::map<std::string, int64_t> writeVersionMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
using Key = ConflictSet::Key;
|
||||||
|
|
||||||
|
[[maybe_unused]] static Key toKey(Arena &arena, int n) {
|
||||||
|
constexpr int kMaxLength = 8;
|
||||||
|
int i = kMaxLength;
|
||||||
|
uint8_t *itoaBuf = new (arena) uint8_t[kMaxLength];
|
||||||
|
memset(itoaBuf, '0', kMaxLength);
|
||||||
|
do {
|
||||||
|
itoaBuf[--i] = "0123456789abcdef"[n % 16];
|
||||||
|
n /= 16;
|
||||||
|
} while (n);
|
||||||
|
return Key{itoaBuf, kMaxLength};
|
||||||
|
}
|
||||||
|
|
||||||
|
[[maybe_unused]] static Key toKeyAfter(Arena &arena, int n) {
|
||||||
|
constexpr int kMaxLength = 8;
|
||||||
|
int i = kMaxLength;
|
||||||
|
uint8_t *itoaBuf = new (arena) uint8_t[kMaxLength + 1];
|
||||||
|
memset(itoaBuf, '0', kMaxLength);
|
||||||
|
itoaBuf[kMaxLength] = 0;
|
||||||
|
do {
|
||||||
|
itoaBuf[--i] = "0123456789abcdef"[n % 16];
|
||||||
|
n /= 16;
|
||||||
|
} while (n);
|
||||||
|
return Key{itoaBuf, kMaxLength + 1};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string printable(std::string_view key) {
|
||||||
|
std::string result;
|
||||||
|
for (uint8_t c : key) {
|
||||||
|
result += "x";
|
||||||
|
result += "0123456789abcdef"[c / 16];
|
||||||
|
result += "0123456789abcdef"[c % 16];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string printable(const Key &key) {
|
||||||
|
return printable(std::string_view((const char *)key.p, key.len));
|
||||||
|
}
|
||||||
|
|
||||||
|
// GCOVR_EXCL_STOP
|
Reference in New Issue
Block a user