From 47f406b7868a1bb8e6681e6270fcbcc9e543229e Mon Sep 17 00:00:00 2001 From: Green Sky Date: Thu, 22 Dec 2022 23:51:59 +0100 Subject: [PATCH] corredted v1 results, v2 using hint for seach and last insert index cache as hint --- CMakeLists.txt | 1 + bench/CMakeLists.txt | 11 + bench/README.md | 14 +- bench/v1_jpaper.cpp | 2 +- bench/v2_jpaper.cpp | 211 +++++++++++++++++ version2/CMakeLists.txt | 26 +++ version2/green_crdt/v2/list.hpp | 385 ++++++++++++++++++++++++++++++++ version2/test1.cpp | 214 ++++++++++++++++++ 8 files changed, 860 insertions(+), 4 deletions(-) create mode 100644 bench/v2_jpaper.cpp create mode 100644 version2/CMakeLists.txt create mode 100644 version2/green_crdt/v2/list.hpp create mode 100644 version2/test1.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6df6786..fd7eb68 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,7 @@ add_subdirectory(./prototyping EXCLUDE_FROM_ALL) add_subdirectory(./version0) add_subdirectory(./version1) +add_subdirectory(./version2) add_subdirectory(./bench) diff --git a/bench/CMakeLists.txt b/bench/CMakeLists.txt index 1476793..1a4114a 100644 --- a/bench/CMakeLists.txt +++ b/bench/CMakeLists.txt @@ -24,3 +24,14 @@ target_link_libraries(crdt_bench_jpaper_v1 PUBLIC nlohmann_json::nlohmann_json ) +######################################## + +add_executable(crdt_bench_jpaper_v2 + ./v2_jpaper.cpp +) + +target_link_libraries(crdt_bench_jpaper_v2 PUBLIC + crdt_version2 + nlohmann_json::nlohmann_json +) + diff --git a/bench/README.md b/bench/README.md index e4c6c3e..909f87d 100644 --- a/bench/README.md +++ b/bench/README.md @@ -34,11 +34,19 @@ the json contains: - g++9 -O3 -DNDEBUG : - 8m7s ~533 ops/s -## version1 +## version1 - actor index - g++9 -g -O2 : - - 5m23s ~804 ops/s + - 4m1s ~1077 ops/s - g++9 -O3 -DNDEBUG : - - 4m7s ~1051 ops/s + - 4m5s ~1060 ops/s + +## version2 - find with hint, cache last insert and use as hint + +- g++9 -g -O2 : + - 3m38s ~1191 ops/s + +- g++9 -O3 -DNDEBUG : + - 3m43s ~1164 ops/s diff --git a/bench/v1_jpaper.cpp b/bench/v1_jpaper.cpp index 813a572..702136d 100644 --- a/bench/v1_jpaper.cpp +++ b/bench/v1_jpaper.cpp @@ -1,4 +1,4 @@ -#define EXTRA_ASSERTS 1 +#define EXTRA_ASSERTS 0 #include #include diff --git a/bench/v2_jpaper.cpp b/bench/v2_jpaper.cpp new file mode 100644 index 0000000..aa1c24b --- /dev/null +++ b/bench/v2_jpaper.cpp @@ -0,0 +1,211 @@ +#define EXTRA_ASSERTS 0 + +#include +#include + +#include +#include +#include +#include +#include + +using ActorID = std::array; +using List = GreenCRDT::V2::List; + +template<> +struct std::hash { + std::size_t operator()(ActorID const& s) const noexcept { + static_assert(sizeof(size_t) == 8); + // TODO: maybe shuffle the indices a bit + return + (static_cast(s[0]) << 8*0) | + (static_cast(s[1]) << 8*1) | + (static_cast(s[2]) << 8*2) | + (static_cast(s[3]) << 8*3) | + (static_cast(s[4]) << 8*4) | + (static_cast(s[5]) << 8*5) | + (static_cast(s[6]) << 8*6) | + (static_cast(s[7]) << 8*7) + ; + } +}; + +// for dev, benching in debug is usefull, but only if the ammount of asserts is reasonable +#if !defined(extra_assert) + #if defined(EXTRA_ASSERTS) && EXTRA_ASSERTS == 1 + #define extra_assert(...) assert(__VA_ARGS__) + #else + #define extra_assert(...) void(0) + #endif +#endif + +namespace detail { + uint8_t nib_from_hex(char c) { + extra_assert((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')); + + if (c >= '0' && c <= '9') { + return static_cast(c) - '0'; + } else if (c >= 'a' && c <= 'f') { + return (static_cast(c) - 'a') + 10u; + } else { + return 0u; + } + } +} // detail + +static ActorID ActorIDFromStr(std::string_view str) { + extra_assert(str.size() == 32*2); + ActorID tmp; + + for (size_t i = 0; i < tmp.size(); i++) { + tmp[i] = detail::nib_from_hex(str[i*2]) << 4 | detail::nib_from_hex(str[i*2+1]); + } + + return tmp; +} + +// seq@ID type format used in the json +struct JObj { + ActorID id; + uint64_t seq {0}; +}; + +static JObj JObjFromStr(std::string_view str) { + extra_assert(str.size() > 32*2 + 1); + + size_t at_pos = str.find_first_of('@'); + auto seq_sv = str.substr(0, at_pos); + auto id_sv = str.substr(at_pos+1); + + assert(seq_sv.size() != 0); + assert(id_sv.size() == 32*2); + + uint64_t tmp_seq {0}; + for (size_t i = 0; i < seq_sv.size(); i++) { + assert(seq_sv[i] >= '0' && seq_sv[i] <= '9'); + tmp_seq *= 10; + tmp_seq += seq_sv[i] - '0'; + } + + return {ActorIDFromStr(id_sv), tmp_seq}; +} + +int main(void) { + List list; + + std::ifstream file {"../res/paper.json"}; + std::cout << "start reading...\n"; + + uint64_t g_total_inserts {0}; + uint64_t g_total_deletes {0}; + //uint64_t g_seq_inserts {0}; // the opsec are not sequentially growing for inserts, so we sidestep + std::unordered_map g_seq_inserts {0}; // the opsec are not sequentially growing for inserts, so we sidestep + std::unordered_map> map_seq; // maps json op_seq -> lits id seq + + for (std::string line; std::getline(file, line); ) { + nlohmann::json j_entry = nlohmann::json::parse(line); + const ActorID actor = ActorIDFromStr(static_cast(j_entry["actor"])); + const size_t actor_idx = list.findActor(actor).value_or(0u); + uint64_t op_seq = j_entry["startOp"]; + for (const auto& j_op : j_entry["ops"]) { + if (j_op["action"] == "set") { + const auto obj = JObjFromStr(static_cast(j_op["obj"])); + if (obj.seq != 1) { + // skip all non text edits (create text doc, curser etc) + continue; + } + + if (j_op["insert"]) { + const auto& j_parent = j_op["key"]; + extra_assert(!j_parent.is_null()); + if (j_parent == "_head") { + uint64_t tmp_seq {g_seq_inserts[actor]++}; + bool r = list.add( + {actor, tmp_seq}, + static_cast(j_op["value"]).front(), + std::nullopt, + std::nullopt + ); + assert(r); + map_seq[actor][op_seq] = tmp_seq; + g_total_inserts++; + } else { // we have a parrent + extra_assert(static_cast(j_op["value"]).size() == 1); + + size_t hint_last_insert {0}; + if (list.last_inserted_idx.count(actor_idx)) { + hint_last_insert = list.last_inserted_idx[actor_idx]; + } + + // split parent into seq and actor + const auto parent_left = JObjFromStr(static_cast(j_parent)); + auto idx_opt = list.findIdx({parent_left.id, map_seq[parent_left.id][parent_left.seq]}, hint_last_insert); + assert(idx_opt.has_value()); + + std::optional parent_left_id; + { + const auto& tmp_parent_left_id = list.list.at(idx_opt.value()).id; + parent_left_id = {list._actors[tmp_parent_left_id.actor_idx], tmp_parent_left_id.seq}; + } + + std::optional parent_right_id; + if (idx_opt.value()+1 < list.list.size()) { + const auto& tmp_parent_right_id = list.list.at(idx_opt.value()+1).id; + parent_right_id = {list._actors[tmp_parent_right_id.actor_idx], tmp_parent_right_id.seq}; + } + + uint64_t tmp_seq {g_seq_inserts[actor]++}; + bool r = list.add( + {actor, tmp_seq}, + static_cast(j_op["value"]).front(), + parent_left_id, + parent_right_id + ); + assert(r); + map_seq[actor][op_seq] = tmp_seq; + g_total_inserts++; + } + } else { + // i think this is curser movement + } + } else if (j_op["action"] == "del") { + const auto list_id = JObjFromStr(static_cast(j_op["key"])); + bool r = list.del({list_id.id, map_seq[list_id.id][list_id.seq]}); + assert(r); + g_total_deletes++; + } else if (j_op["action"] == "makeText") { + // doc.clear(); + } else if (j_op["action"] == "makeMap") { + // no idea + } else { + std::cout << "op: " << j_op << "\n"; + } + + op_seq++; + } + } + + std::cout << "\ndoc size (with tombstones): " << list.list.size() << "\n"; + std::cout << "doc size: " << list.doc_size << "\n"; + std::cout << "total inserts: " << g_total_inserts << "\n"; + std::cout << "total deletes: " << g_total_deletes << "\n"; + std::cout << "total ops: " << g_total_inserts + g_total_deletes << "\n"; + + //std::cout << "find_hint: " << list._stat_find_with_hint << "\n"; + //std::cout << "find_hint_hit: " << list._stat_find_with_hint_hit << "\n"; + + // checked, looks correct +#if 0 + std::cout << "doc text:\n"; + // simple print + for (const auto& it : list.list) { + if (it.value) { + std::cout << it.value.value(); + } + } + std::cout << "\n"; +#endif + + return 0; +} + diff --git a/version2/CMakeLists.txt b/version2/CMakeLists.txt new file mode 100644 index 0000000..8a6b01a --- /dev/null +++ b/version2/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.9 FATAL_ERROR) + +project(crdt_version2 CXX C) + +add_library(crdt_version2 INTERFACE) + +target_compile_features(crdt_version2 INTERFACE cxx_std_17) + +target_include_directories(crdt_version2 INTERFACE "${PROJECT_SOURCE_DIR}") + +######################################## + +add_executable(v2_test1 + ./test1.cpp +) + +target_link_libraries(v2_test1 PUBLIC crdt_version2) + +######################################## + +#add_executable(v2_test2 + #./test2.cpp +#) + +#target_link_libraries(v2_test2 PUBLIC crdt_version2) + diff --git a/version2/green_crdt/v2/list.hpp b/version2/green_crdt/v2/list.hpp new file mode 100644 index 0000000..20dcb16 --- /dev/null +++ b/version2/green_crdt/v2/list.hpp @@ -0,0 +1,385 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +#if !defined(extra_assert) + #if defined(EXTRA_ASSERTS) && EXTRA_ASSERTS == 1 + #define extra_assert(...) assert(__VA_ARGS__) + #else + #define extra_assert(...) void(0) + #endif +#endif + +namespace GreenCRDT::V2 { + +template +struct List { + // for public interface + struct ListID { + ActorType id; + uint64_t seq{0}; // strictly increasing for that actor + + bool operator<(const ListID& rhs) const { + if (seq < rhs.seq) { + return true; + } else if (seq > rhs.seq) { + return false; + } else { // == + return id < rhs.id; + } + } + + bool operator==(const ListID& rhs) const { + return seq == rhs.seq && id == rhs.id; + } + + bool operator!=(const ListID& rhs) const { + return seq != rhs.seq || id != rhs.id; + } + }; + + struct ListIDInternal { + size_t actor_idx{0}; + uint64_t seq{0}; // strictly increasing for that actor + + bool operator==(const ListIDInternal& rhs) const { + return seq == rhs.seq && actor_idx == rhs.actor_idx; + } + }; + + // internally the index into this array is used to refer to an actor + std::vector _actors; + + // TODO: replace with SoA + struct Entry { + ListIDInternal id; + + // Yjs + std::optional parent_left; + std::optional parent_right; + + // might be deleted (yes, *sigh*, crtds need tombstones) + std::optional value; + }; + + // TODO: use something better, edit: this seems fine + std::vector list; + + // number of not deleted entries + size_t doc_size {0}; + + // TODO: actor index instead of map + std::unordered_map last_seen_seq; + + // caching only, contains the last index an actor inserted at + std::unordered_map last_inserted_idx; + + //size_t _stat_find_with_hint{0}; + //size_t _stat_find_with_hint_hit{0}; + + std::optional findActor(const ActorType& actor) const { + for (size_t i = 0; i < _actors.size(); i++) { + if (_actors[i] == actor) { + return i; + } + } + return std::nullopt; + } + + std::optional findIdx(const ListIDInternal& list_id) const { + extra_assert(verify()); + + for (size_t i = 0; i < list.size(); i++) { + if (list[i].id == list_id) { + return i; + } + } + + return std::nullopt; + } + + // search close to hint first + std::optional findIdx(const ListIDInternal& list_id, size_t hint) const { + extra_assert(verify()); + + //_stat_find_with_hint++; + + // TODO: find some good magic values here + // total: 364150 + // 2-9 hits: 360164 (3m54) + // 1-9 hits: 360161 (3m53) + // 1-2 hits: 359800 (3m55s) + // 0-2 hits: 359763 (3m54s) + // changed from loop to single if: + // 1-2 hits: 359800 (3m50s) + // 1-4 hits: 359928 (3m51s) (after cond reorder: 3m49s) + static constexpr size_t c_hint_pre = 1; + static constexpr size_t c_hint_post = 4; + + { // go back 2, so we dont miss // TODO: is this really needed + //for (size_t i = 0; hint > 0 && i < c_hint_pre; hint--, i++) {} + if (hint >= c_hint_pre) { + hint -= c_hint_pre; + } + } + + const size_t max_at_hint = hint + c_hint_post; // how many positions we check at hint, before falling back to full lookup + + for (size_t i = hint; i <= max_at_hint && i < list.size(); i++) { + if (list[i].id == list_id) { + //_stat_find_with_hint_hit++; + return i; + } + } + + // fall back to normal search + // TODO: in some cases we scan the list twice now!! + return findIdx(list_id); + } + + std::optional findIdx(const ListID& list_id) const { + extra_assert(verify()); + + const auto actor_idx_opt = findActor(list_id.id); + if (!actor_idx_opt.has_value()) { + return std::nullopt; + } + + const ListIDInternal tmp_id {actor_idx_opt.value(), list_id.seq}; + + return findIdx(tmp_id); + } + + std::optional findIdx(const ListID& list_id, size_t hint) const { + extra_assert(verify()); + + const auto actor_idx_opt = findActor(list_id.id); + if (!actor_idx_opt.has_value()) { + return std::nullopt; + } + + const ListIDInternal tmp_id {actor_idx_opt.value(), list_id.seq}; + + return findIdx(tmp_id, hint); + } + + // returns false if missing OPs + // based on YjsMod https://github.com/josephg/reference-crdts/blob/9f4f9c3a97b497e2df8ae4473d1e521d3c3bf2d2/crdts.ts#L293-L348 + // which is a modified Yjs(YATA) algo + // TODO: idx_hint + bool add(const ListID& list_id, const ValueType& value, const std::optional& parent_left, const std::optional& parent_right) { + extra_assert(verify()); + + size_t actor_idx {0}; + { // new actor? + // add, even if op fails + const auto actor_opt = findActor(list_id.id); + if (!actor_opt.has_value()) { + actor_idx = _actors.size(); + last_inserted_idx[_actors.size()] = 0; // hack + _actors.push_back(list_id.id); + } else { + actor_idx = actor_opt.value(); + } + } + + // check actor op order + if (!last_seen_seq.count(actor_idx)) { + // we dont know this actor yet, first seq needs to be 0 + if (list_id.seq != 0) { + return false; + } + } else { + // making sure we dont skip operations by that actor + if (list_id.seq != last_seen_seq.at(actor_idx) + 1) { + return false; + } + } + + size_t insert_idx = 0; + if (list.empty()) { + if (parent_left.has_value() || parent_right.has_value()) { + // empty, missing parents + return false; + } + } else { + // find left + std::optional left_idx_opt = std::nullopt; + if (parent_left.has_value()) { + left_idx_opt = findIdx(parent_left.value(), last_inserted_idx[actor_idx]); + if (!left_idx_opt.has_value()) { + // missing parent left + return false; + } + + // we insert before the it, so we need to go past the left parent + insert_idx = left_idx_opt.value() + 1; + } // else insert_idx = 0 + const size_t left_idx_hint = insert_idx; + + // find right + size_t right_idx = list.size(); + if (parent_right.has_value()) { + auto tmp_right = findIdx(parent_right.value(), left_idx_hint); + if (!tmp_right.has_value()) { + return false; + } + right_idx = tmp_right.value(); + } + + bool scanning {false}; + + for(size_t i = insert_idx;; i++) { + if (!scanning) { + insert_idx = i; + } + // if right parent / end of doc, insert + if (insert_idx == right_idx) { + break; + } + // we ran past right o.o ? + if (insert_idx == list.size()) { + break; + } + + const Entry& at_i = list[i]; + // parents left and right + std::optional i_left_idx {std::nullopt}; + if (at_i.parent_left.has_value()) { + i_left_idx = findIdx(at_i.parent_left.value(), left_idx_hint); + if (!i_left_idx.has_value()) { + assert(false && "item in list with unknown parent left!!"); + return false; + } + } + + // possibility map + // + // | ir < r | ir == r | ir > r + // ------------------------------------- + // il < l | insert | insert | insert + // il == l | ? | agentfallback | ? + // il > l | skip | skip | skip + + if (i_left_idx < left_idx_opt) { + break; + } else if (i_left_idx == left_idx_opt) { + // get i parent_right + size_t i_right_idx = list.size(); + if (at_i.parent_right.has_value()) { + auto tmp_right = findIdx(at_i.parent_right.value(), insert_idx); + if (!tmp_right.has_value()) { + assert(false && "item in list with unknown parent right!!"); + return false; + } + i_right_idx = tmp_right.value(); + } + + if (i_right_idx < right_idx) { + scanning = true; + } else if (i_right_idx == right_idx) { + // actor id tie breaker + if (_actors[actor_idx] < _actors[at_i.id.actor_idx]) { + break; + } else { + scanning = false; + } + } else { // i_right_idx > right_idx + scanning = false; + } + } else { // il > l + // do nothing + } + } + } + + { // actual insert + Entry new_entry; + + new_entry.id.actor_idx = actor_idx; + new_entry.id.seq = list_id.seq; + + if (parent_left.has_value()) { + new_entry.parent_left = ListIDInternal{findActor(parent_left.value().id).value(), parent_left.value().seq}; + } + + if (parent_right.has_value()) { + new_entry.parent_right = ListIDInternal{findActor(parent_right.value().id).value(), parent_right.value().seq}; + } + + new_entry.value = value; + + list.emplace(list.begin() + insert_idx, new_entry); + last_inserted_idx[actor_idx] = insert_idx; + } + + doc_size++; + last_seen_seq[actor_idx] = list_id.seq; + + extra_assert(verify()); + return true; + } + + // returns false if not found + bool del(const ListID& id) { + extra_assert(verify()); + + auto actor_idx_opt = findActor(id.id); + if (!actor_idx_opt.has_value()) { + // we dont have anything with that actor + return false; + } + + const ListIDInternal tmp_id {actor_idx_opt.value(), id.seq}; + + for (auto& it : list) { + if (it.id == tmp_id) { + if (it.value.has_value()) { + it.value.reset(); + + doc_size--; + extra_assert(verify()); + return true; + } else { + extra_assert(verify()); + return false; // TODO: allow double deletes?,,,, need ids + } + } + } + + extra_assert(verify()); + return false; + } + + std::vector getArray(void) const { + std::vector array; + for (const auto& e : list) { + if (e.value.has_value()) { + array.push_back(e.value.value()); + } + } + + return array; + } + + // TODO: only in debug? + bool verify(void) const { + size_t actual_size = 0; + for (const auto& it : list) { + if (it.value.has_value()) { + actual_size++; + } + } + //assert(doc_size == actual_size); + return doc_size == actual_size; + } +}; + +} // GreenCRDT::V1 + diff --git a/version2/test1.cpp b/version2/test1.cpp new file mode 100644 index 0000000..abe4be8 --- /dev/null +++ b/version2/test1.cpp @@ -0,0 +1,214 @@ +#define EXTRA_ASSERTS 1 +#include + +#include +#include +#include +#include +#include +#include + +// single letter actor, for testing only +using Actor = char; +using ListType = GreenCRDT::V2::List; + +namespace std { +bool operator==(const std::vector& lhs, const std::string_view& rhs) { + if (lhs.size() != rhs.size()) { + return false; + } + + for (size_t i = 0; i < rhs.size(); i++) { + if (lhs[i] != rhs[i]) { + return false; + } + } + + return true; +} +} // namespace std + +void testSingle1(void) { + ListType list; + + assert(list.add({'A', 0}, 'a', std::nullopt, std::nullopt)); + assert(list.add({'A', 1}, 'b', ListType::ListID{'A', 0u}, std::nullopt)); + + assert(list.getArray() == "ab"); +} + + +void testConcurrent1(void) { + // agent_a < agent_b + + // concurrent insert of first element + { // variant 1, a then b + ListType list; + assert(list.add({'A', 0}, 'a', std::nullopt, std::nullopt)); + assert(list.add({'B', 0}, 'b', std::nullopt, std::nullopt)); + + assert(list.getArray() == "ab"); + } + { // variant 2, b then a + ListType list; + assert(list.add({'B', 0}, 'b', std::nullopt, std::nullopt)); + assert(list.add({'A', 0}, 'a', std::nullopt, std::nullopt)); + + assert(list.getArray() == "ab"); + } +} + +struct AddOp { + ListType::ListID id; + char value; + std::optional parent_left; + std::optional parent_right; +}; + +void randomAddPermutations(const std::vector& ops, const std::string& expected) { + // TODO: more then 1k? + for (size_t i = 0; i < 1000; i++) { + std::minstd_rand rng(1337 + i); + std::vector ops_todo(ops.size()); + std::iota(ops_todo.begin(), ops_todo.end(), 0u); + + size_t attempts {0}; + + ListType list; + do { + size_t idx = rng() % ops_todo.size(); + + if (list.add(ops[ops_todo[idx]].id, ops[ops_todo[idx]].value, ops[ops_todo[idx]].parent_left, ops[ops_todo[idx]].parent_right)) { + // only remove if it was possible -> returned true; + ops_todo.erase(ops_todo.begin()+idx); + } + + attempts++; + assert(attempts < 10'000); // in case we run into an endless loop + } while (!ops_todo.empty()); + + assert(list.getArray() == expected); + } +} + +void testInterleave1(void) { + const std::vector ops { + {{'A', 0u}, 'a', std::nullopt, std::nullopt}, + {{'A', 1u}, 'a', ListType::ListID{'A', 0u}, std::nullopt}, + {{'A', 2u}, 'a', ListType::ListID{'A', 1u}, std::nullopt}, + {{'B', 0u}, 'b', std::nullopt, std::nullopt}, + {{'B', 1u}, 'b', ListType::ListID{'B', 0u}, std::nullopt}, + {{'B', 2u}, 'b', ListType::ListID{'B', 1u}, std::nullopt}, + }; + + randomAddPermutations(ops, "aaabbb"); +} + +void testInterleave2(void) { + const std::vector ops { + {{'A', 0u}, 'a', std::nullopt, std::nullopt}, + {{'A', 1u}, 'a', std::nullopt, ListType::ListID{'A', 0u}}, + {{'A', 2u}, 'a', std::nullopt, ListType::ListID{'A', 1u}}, + {{'B', 0u}, 'b', std::nullopt, std::nullopt}, + {{'B', 1u}, 'b', std::nullopt, ListType::ListID{'B', 0u}}, + {{'B', 2u}, 'b', std::nullopt, ListType::ListID{'B', 1u}}, + }; + + randomAddPermutations(ops, "aaabbb"); +} + +void testConcurrent2(void) { + const std::vector ops { + {{'A', 0u}, 'a', std::nullopt, std::nullopt}, + {{'C', 0u}, 'c', std::nullopt, std::nullopt}, + {{'B', 0u}, 'b', std::nullopt, std::nullopt}, + {{'D', 0u}, 'd', ListType::ListID{'A', 0u}, ListType::ListID{'C', 0u}}, + }; + + randomAddPermutations(ops, "adbc"); +} + +void testMain1(void) { + ListType list; + + static_assert('0' < '1'); + + const std::vector a0_ops { + {{'0', 0u}, 'a', std::nullopt, std::nullopt}, + {{'0', 1u}, 'b', ListType::ListID{'0', 0u}, std::nullopt}, + {{'0', 2u}, 'c', ListType::ListID{'0', 1u}, std::nullopt}, + {{'0', 3u}, 'd', ListType::ListID{'0', 1u}, ListType::ListID{'0', 2u}}, + }; + + const std::vector a1_ops { + // knows of a0 up to {a0, 1} + {{'1', 0u}, 'z', ListType::ListID{'0', 0u}, ListType::ListID{'0', 1u}}, + {{'1', 1u}, 'y', ListType::ListID{'0', 1u}, std::nullopt}, + }; + + { // the ez, in order stuff + // a0 insert first char, 'a', since its the first, we dont have any parents + assert(list.add(a0_ops[0].id, a0_ops[0].value, a0_ops[0].parent_left, a0_ops[0].parent_right)); + assert(list.getArray() == "a"); + + // a0 insert secound char, 'b' after 'a', no parents to right + assert(list.add(a0_ops[1].id, a0_ops[1].value, a0_ops[1].parent_left, a0_ops[1].parent_right)); + assert(list.getArray() == "ab"); + + // a0 insert 'c' after 'b', no parents to right + assert(list.add(a0_ops[2].id, a0_ops[2].value, a0_ops[2].parent_left, a0_ops[2].parent_right)); + assert(list.getArray() == "abc"); + + // a0 insert 'd' after 'b', 'c' parent right + assert(list.add(a0_ops[3].id, a0_ops[3].value, a0_ops[3].parent_left, a0_ops[3].parent_right)); + assert(list.getArray() == "abdc"); + + // a1 insert 'z' after 'a', 'b' parent right + assert(list.add(a1_ops[0].id, a1_ops[0].value, a1_ops[0].parent_left, a1_ops[0].parent_right)); + assert(list.getArray() == "azbdc"); + } + + std::cout << "done with ez\n"; + + { // a1 was not uptodate only had 0,1 of a0 + // a1 insert 'y' after 'b', no parent right + assert(list.add(a1_ops[1].id, a1_ops[1].value, a1_ops[1].parent_left, a1_ops[1].parent_right)); + assert(list.getArray() == "azbdcy"); + } + + std::cout << "\ndoc size (with tombstones): " << list.list.size() << "\n"; + std::cout << "\ndoc size: " << list.doc_size << "\n"; + std::cout << "doc text:\n"; + + const auto tmp_array = list.getArray(); + std::cout << std::string_view(tmp_array.data(), tmp_array.size()) << "\n"; +} + +int main(void) { + std::cout << "testSingle1:\n"; + testSingle1(); + std::cout << std::string(40, '-') << "\n"; + + std::cout << "testConcurrent1:\n"; + testConcurrent1(); + std::cout << std::string(40, '-') << "\n"; + + std::cout << "testInterleave1:\n"; + testInterleave1(); + std::cout << std::string(40, '-') << "\n"; + + std::cout << "testInterleave2:\n"; + testInterleave2(); + std::cout << std::string(40, '-') << "\n"; + + std::cout << "testConcurrent2:\n"; + testConcurrent2(); + std::cout << std::string(40, '-') << "\n"; + + std::cout << "testMain1:\n"; + testMain1(); + std::cout << std::string(40, '-') << "\n"; + + return 0; +} +