From 133442b6f94e165610eb20d7acd4c2910c4b4439 Mon Sep 17 00:00:00 2001 From: Bart Wyatt Date: Tue, 1 Aug 2017 17:13:33 -0400 Subject: [PATCH] Add naive scheduler to block production add basic tests for scheduler --- libraries/chain/CMakeLists.txt | 1 + libraries/chain/block_schedule.cpp | 281 ++++++++++++++++++ libraries/chain/chain_controller.cpp | 90 ++++-- .../include/eos/chain/block_schedule.hpp | 64 ++++ tests/common/expect.hpp | 73 +++++ tests/tests/block_schedule_tests.cpp | 257 ++++++++++++++++ 6 files changed, 734 insertions(+), 32 deletions(-) create mode 100644 libraries/chain/block_schedule.cpp create mode 100644 libraries/chain/include/eos/chain/block_schedule.hpp create mode 100644 tests/common/expect.hpp create mode 100644 tests/tests/block_schedule_tests.cpp diff --git a/libraries/chain/CMakeLists.txt b/libraries/chain/CMakeLists.txt index eaa351a51..1f9dc4ed6 100644 --- a/libraries/chain/CMakeLists.txt +++ b/libraries/chain/CMakeLists.txt @@ -4,6 +4,7 @@ file(GLOB HEADERS "include/eos/chain/*.hpp") add_library( eos_chain chain_controller.cpp wasm_interface.cpp + block_schedule.cpp fork_database.cpp diff --git a/libraries/chain/block_schedule.cpp b/libraries/chain/block_schedule.cpp new file mode 100644 index 000000000..31963ae16 --- /dev/null +++ b/libraries/chain/block_schedule.cpp @@ -0,0 +1,281 @@ +/* + * Copyright (c) 2017, Respective Authors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include + +namespace eos { namespace chain { + +static uint next_power_of_two(uint input) { + if (input == 0) { + return 0; + } + + uint result = input; + result--; + result |= result >> 1; + result |= result >> 2; + result |= result >> 4; + result |= result >> 8; + result |= result >> 16; + result++; + return result; +}; + +typedef std::hash account_hash; +static account_hash account_hasher; + +struct schedule_entry { + schedule_entry(uint _cycle, uint _thread, SignedTransaction const * _transaction) + : cycle(_cycle) + , thread(_thread) + , transaction(_transaction) + {} + + uint cycle; + uint thread; + SignedTransaction const *transaction; + + friend bool operator<( schedule_entry const &l, schedule_entry const &r ) { + if (l.cycle < r.cycle) { + return true; + } else if (l.cycle == r.cycle) { + return l.thread < r.thread; + } + + return false; + } +}; + +static block_schedule from_entries(vector &entries) { + // sort in reverse to save allocations, this should put the highest thread index + // for the highest cycle index first meaning the naive resize in the loop below + // is usually the largest and only resize + auto reverse = [](schedule_entry const &l, schedule_entry const &r) { + return !(l < r); + }; + + std::sort(entries.begin(), entries.end(), reverse); + + block_schedule result; + for(auto const & entry : entries) { + if (result.cycles.size() <= entry.cycle) { + result.cycles.resize(entry.cycle + 1); + } + + auto &cycle = result.cycles.at(entry.cycle); + + if (cycle.size() <= entry.thread) { + cycle.resize(entry.thread + 1); + } + + // because we are traversing the schedule in reverse to save + // allocations, we cannot emplace_back as that would reverse + // the transactions in a thread + auto &thread = cycle.at(entry.thread); + thread.user_input.emplace(thread.user_input.begin(), entry.transaction); + } + + return result; +} + +template +auto initialize_pointer_vector(CONTAINER const &c) { + vector result; + result.reserve(c.size()); + for (auto const &t : c) { + result.emplace_back(&t); + } + + return result; +} + +struct block_size_skipper { + size_t current_size; + size_t const max_size; + + bool should_skip(SignedTransaction const *t) const { + size_t transaction_size = fc::raw::pack_size(*t); + // postpone transaction if it would make block too big + if( transaction_size + current_size > max_size ) { + return true; + } else { + return false; + } + } + + void apply(SignedTransaction const *t) { + size_t transaction_size = fc::raw::pack_size(*t); + current_size += transaction_size; + } +}; + +auto make_skipper(const global_property_object &properties) { + static const size_t max_block_header_size = fc::raw::pack_size( signed_block_header() ) + 4; + auto maximum_block_size = properties.configuration.maxBlockSize; + return block_size_skipper { max_block_header_size, (size_t)maximum_block_size }; +} + +block_schedule block_schedule::by_threading_conflicts( + deque const &transactions, + const global_property_object &properties + ) +{ + static uint const MAX_TXS_PER_THREAD = 4; + auto skipper = make_skipper(properties); + + uint HASH_SIZE = std::max(4096, next_power_of_two(transactions.size() / 8)); + vector> assigned_threads(HASH_SIZE); + + vector schedule; + schedule.reserve(transactions.size()); + + auto current = initialize_pointer_vector(transactions); + vector postponed; + postponed.reserve(transactions.size()); + + vector txs_per_thread; + txs_per_thread.reserve(HASH_SIZE); + + int cycle = 0; + bool scheduled = true; + while (scheduled) { + scheduled = false; + uint next_thread = 0; + + for (auto t : current) { + // skip ? + if (skipper.should_skip(t)) { + continue; + } + + auto assigned_to = optional(); + bool postpone = false; + + for (const auto &a : t->scope) { + uint hash_index = account_hasher(a) % HASH_SIZE; + if (assigned_to && assigned_threads[hash_index] && assigned_to != assigned_threads[hash_index]) { + postpone = true; + postponed.push_back(t); + break; + } + + if (assigned_threads[hash_index]) + { + assigned_to = assigned_threads[hash_index]; + } + } + + if (!postpone) { + if (!assigned_to) { + assigned_to = next_thread++; + txs_per_thread.resize(next_thread); + } + + if (txs_per_thread[*assigned_to] < MAX_TXS_PER_THREAD) { + for (const auto &a : t->scope) + { + uint hash_index = account_hasher(a) % HASH_SIZE; + assigned_threads[hash_index] = assigned_to; + } + + txs_per_thread[*assigned_to]++; + schedule.emplace_back(cycle, *assigned_to, t); + scheduled = true; + skipper.apply(t); + } else { + postponed.push_back(t); + } + } + } + + current.resize(0); + txs_per_thread.resize(0); + assigned_threads.resize(0); + assigned_threads.resize(HASH_SIZE); + std::swap(current, postponed); + ++cycle; + + } + return from_entries(schedule); +} + +block_schedule block_schedule::by_cycling_conflicts( + deque const &transactions, + const global_property_object &properties + ) +{ + auto skipper = make_skipper(properties); + + uint HASH_SIZE = std::max(4096, next_power_of_two(transactions.size() / 8)); + vector schedule; + schedule.reserve(transactions.size()); + + auto current = initialize_pointer_vector(transactions); + vector postponed; + postponed.reserve(transactions.size()); + + int cycle = 0; + vector used(HASH_SIZE); + bool scheduled = true; + while (scheduled) { + scheduled = false; + int thread = 0; + for (auto t : current) { + // skip ? + if (skipper.should_skip(t)) { + continue; + } + + bool u = false; + for (const auto &a : t->scope) { + uint hash_index = account_hasher(a) % HASH_SIZE; + if (used[hash_index]) { + u = true; + postponed.push_back(t); + break; + } + } + + if (!u) { + for (const auto &a : t->scope) { + uint hash_index = account_hasher(a) % HASH_SIZE; + used[hash_index] = true; + } + + schedule.emplace_back(cycle, thread++, t); + scheduled = true; + skipper.apply(t); + } + } + + current.resize(0); + used.resize(0); + used.resize(HASH_SIZE); + std::swap(current, postponed); + ++cycle; + } + + return from_entries(schedule); +} + +} /* namespace chain */ } /* namespace eos */ diff --git a/libraries/chain/chain_controller.cpp b/libraries/chain/chain_controller.cpp index da96bb55a..f71163a45 100644 --- a/libraries/chain/chain_controller.cpp +++ b/libraries/chain/chain_controller.cpp @@ -26,6 +26,7 @@ #include #include +#include #include #include #include @@ -292,11 +293,7 @@ signed_block chain_controller::_generate_block( if( !(skip & skip_producer_signature) ) FC_ASSERT( producer_obj.signing_key == block_signing_private_key.get_public_key() ); - static const size_t max_block_header_size = fc::raw::pack_size( signed_block_header() ) + 4; - auto maximum_block_size = get_global_properties().configuration.maxBlockSize; - size_t total_block_size = max_block_header_size; - - signed_block pending_block; + auto schedule = block_schedule::by_threading_conflicts(_pending_transactions, get_global_properties()); // // The following code throws away existing pending_tx_session and @@ -312,45 +309,74 @@ signed_block chain_controller::_generate_block( _pending_tx_session.reset(); _pending_tx_session = _db.start_undo_session(true); - uint64_t postponed_tx_count = 0; - // pop pending state (reset to head block state) - for( const SignedTransaction& tx : _pending_transactions ) - { - size_t new_total_size = total_block_size + fc::raw::pack_size( tx ); - - // postpone transaction if it would make block too big - if( new_total_size >= maximum_block_size ) - { - postponed_tx_count++; - continue; - } - + auto process_one = [this](SignedTransaction const *tx, database &db) -> optional { try { - auto temp_session = _db.start_undo_session(true); - _apply_transaction(tx); - temp_session.squash(); - - total_block_size += fc::raw::pack_size(tx); - if (pending_block.cycles.empty()) { - pending_block.cycles.resize(1); - pending_block.cycles.back().resize(1); - } - pending_block.cycles.back().back().user_input.emplace_back(tx); -#warning TODO: Populate generated blocks with generated transactions + auto temp_session = db.start_undo_session(true); + auto ptx = _apply_transaction(*tx); + temp_session.squash(); + + return optional(ptx); } catch ( const fc::exception& e ) { - // Do nothing, transaction will not be re-applied - wlog( "Transaction was not processed while generating block due to ${e}", ("e", e) ); - wlog( "The transaction was ${t}", ("t", tx) ); + // Do nothing, transaction will not be re-applied + wlog( "Transaction was not processed while generating block due to ${e}", ("e", e) ); + wlog( "The transaction was ${t}", ("t", *tx) ); } + + return optional(); + }; + + signed_block pending_block; + pending_block.cycles.reserve(schedule.cycles.size()); + + size_t invalid_transaction_count = 0; + size_t valid_transaction_count = 0; + + for (const auto &c : schedule.cycles) { + cycle block_cycle; + block_cycle.reserve(c.size()); + + for (const auto &t : c) { + thread block_thread; + block_thread.generated_input.reserve(t.generated_input.size()); + for (const auto &trx : t.generated_input) { +#warning TODO: Process generated transaction + } + + block_thread.user_input.reserve(t.user_input.size()); + for (const auto &trx : t.user_input) { + auto processed = process_one(trx, _db); + if (processed) { + block_thread.user_input.emplace_back(*processed); + valid_transaction_count++; + } else { + invalid_transaction_count++; + } + } + + if (!(block_thread.generated_input.empty() && block_thread.user_input.empty())) { + block_cycle.emplace_back(std::move(block_thread)); + } + } + + if (!block_cycle.empty()) { + pending_block.cycles.emplace_back(std::move(block_cycle)); + } } + + size_t postponed_tx_count = _pending_transactions.size() - valid_transaction_count - invalid_transaction_count; if( postponed_tx_count > 0 ) { wlog( "Postponed ${n} transactions due to block size limit", ("n", postponed_tx_count) ); } + if( invalid_transaction_count > 0 ) + { + wlog( "Postponed ${n} transactions errors when processing", ("n", invalid_transaction_count) ); + } + _pending_tx_session.reset(); // We have temporarily broken the invariant that diff --git a/libraries/chain/include/eos/chain/block_schedule.hpp b/libraries/chain/include/eos/chain/block_schedule.hpp new file mode 100644 index 000000000..33b15c4b4 --- /dev/null +++ b/libraries/chain/include/eos/chain/block_schedule.hpp @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2017, Respective Authors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once +#include +#include + +namespace eos { namespace chain { + + struct thread_schedule { + vector generated_input; + vector user_input; + }; + + using cycle_schedule = vector; + + /** + * @class block_schedule + * @brief represents a proposed order of execution for a generated block + */ + struct block_schedule + { + vector cycles; + + // Algorithms + + /** + * A greedy scheduler that attempts to make short threads to resolve scope contention before + * falling back on cycles + * @return the block scheduler + */ + static block_schedule by_threading_conflicts(deque const &transactions, const global_property_object& properties); + + /** + * A greedy scheduler that attempts uses future cycles to resolve scope contention + * @return the block scheduler + */ + static block_schedule by_cycling_conflicts(deque const &transactions, const global_property_object& properties); + }; + +} } // eos::chain + +FC_REFLECT(eos::chain::thread_schedule, (generated_input)(user_input) ) +FC_REFLECT(eos::chain::block_schedule, (cycles)) diff --git a/tests/common/expect.hpp b/tests/common/expect.hpp new file mode 100644 index 000000000..e366c9513 --- /dev/null +++ b/tests/common/expect.hpp @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2017, Respective Authors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once + +namespace eos { namespace test { + +template +struct expector { + expector(EVAL _eval, T const &_expected, char const * const _msg) + : expected(_expected) + , eval(_eval) + , msg(_msg) + {} + + template + void operator() (INPUT const &input) { + BOOST_TEST_INFO(msg); + auto actual = eval(input); + BOOST_CHECK_EQUAL(PRED()(actual, expected), true); + } + + T expected; + EVAL eval; + char const * const msg; +}; + + +template +auto expect(EVAL &&eval, T expected, char const * const msg) { + return expector(eval, expected, msg); +} + +template +auto expect(EVAL &&eval, T expected, char const * const msg) { + return expector, EVAL, T>(eval, expected, msg); +} + +template +auto expect(EVAL &&eval, char const * const msg) { + return expector, EVAL, bool>(eval, true, msg); +} + +#define _NUM_ARGS(A,B,C,N, ...) N +#define NUM_ARGS(...) _NUM_ARGS(__VA_ARGS__, 3, 2, 1) +#define EXPECT(...) EXPECT_(NUM_ARGS(__VA_ARGS__),__VA_ARGS__) +#define EXPECT_(N, ...) EXPECT__(N, __VA_ARGS__) +#define EXPECT__(N, ...) EXPECT_##N(__VA_ARGS__) +#define EXPECT_3(P, E, T) eos::test::expect

(E,T,"EXPECT<" #P ">(" #E "," #T ")") +#define EXPECT_2(E, T) eos::test::expect(E,T,"EXPECT(" #E "," #T ")") +#define EXPECT_1(E) eos::test::expect(E,"EXPECT(" #E ")") + +}} \ No newline at end of file diff --git a/tests/tests/block_schedule_tests.cpp b/tests/tests/block_schedule_tests.cpp new file mode 100644 index 000000000..6fbf08ddd --- /dev/null +++ b/tests/tests/block_schedule_tests.cpp @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2017, Respective Authors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include + +#include +#include "../common/expect.hpp" + +using namespace eos; +using namespace chain; + +/* + * Policy based Fixtures for chain properties + */ +class common_fixture { +public: + struct test_transaction { + test_transaction(std::initializer_list const &_scopes) + : scopes(_scopes) + { + } + + std::initializer_list const &scopes; + }; + +protected: + auto create_pending( std::initializer_list const &transactions ) { + std::deque result; + for (auto const &t: transactions) { + SignedTransaction st; + st.scope.reserve(t.scopes.size()); + st.scope.insert(st.scope.end(), t.scopes.begin(), t.scopes.end()); + result.emplace_back(st); + } + return result; + } +}; + +template +class compose_fixture: public common_fixture { +public: + template + void schedule_and_validate(SCHED_FN sched_fn, std::initializer_list const &transactions, VALIDATORS ...validators) { + try { + auto pending = create_pending(transactions); + auto schedule = sched_fn(pending, properties_policy.properties); + validate(schedule, validators...); + } FC_LOG_AND_RETHROW() + } + +private: + template + void validate(block_schedule const &schedule, VALIDATOR validator) { + validator(schedule); + } + + template + void validate(block_schedule const &schedule, VALIDATOR validator, VALIDATORS ... others) { + validate(schedule, validator); + validate(schedule, others...); + } + + + PROPERTIES_POLICY properties_policy; +}; + +static void null_global_property_object_constructor(global_property_object const &) +{} + +static chainbase::allocator null_global_property_object_allocator(nullptr); + +struct base_properties { + base_properties() + : properties(null_global_property_object_constructor, null_global_property_object_allocator) + { + } + + global_property_object properties; +}; + +struct default_properties : public base_properties { + default_properties() { + properties.configuration.maxBlockSize = 256 * 1024; + } +}; + +struct small_block_properties : public base_properties { + small_block_properties() { + properties.configuration.maxBlockSize = 512; + } +}; + +typedef compose_fixture default_fixture; + +/* + * Evaluators for expect + */ +static uint transaction_count(block_schedule const &schedule) { + uint result = 0; + for (auto const &c : schedule.cycles) { + for (auto const &t: c) { + result += t.generated_input.size(); + result += t.user_input.size(); + } + } + + return result; +} + +static uint cycle_count(block_schedule const &schedule) { + return schedule.cycles.size(); +} + +static bool schedule_is_valid(block_schedule const &schedule) { + for (auto const &c : schedule.cycles) { + std::vector scope_in_use; + for (auto const &t: c) { + std::set thread_bits; + size_t max_bit = 0; + for(auto const &gi: t.generated_input) { + #warning TODO: Process generated transaction + } + + for(auto const &ui: t.user_input) { + for (auto const &s : ui->scope) { + size_t bit = boost::numeric_cast((uint64_t)s); + thread_bits.emplace(bit); + max_bit = std::max(max_bit, bit); + } + } + + if (scope_in_use.size() <= max_bit) { + scope_in_use.resize(max_bit + 1); + } + + for ( auto b: thread_bits ) { + if(scope_in_use.at(b)) { + return false; + }; + scope_in_use.at(b) = true; + } + } + } + + return true; +} + +/* + * Test Cases + */ + +BOOST_AUTO_TEST_SUITE(block_schedule_tests) + +BOOST_FIXTURE_TEST_CASE(no_conflicts, default_fixture) { + // ensure the scheduler can handle basic non-conflicted transactions + // in a single cycle + schedule_and_validate( + block_schedule::by_threading_conflicts, + { + {0x1ULL, 0x2ULL}, + {0x3ULL, 0x4ULL}, + {0x5ULL, 0x6ULL}, + {0x7ULL, 0x8ULL}, + {0x9ULL, 0xAULL} + }, + EXPECT(schedule_is_valid), + EXPECT(transaction_count, 5), + EXPECT(cycle_count, 1) + ); +} + +BOOST_FIXTURE_TEST_CASE(some_conflicts, default_fixture) { + // ensure the scheduler can handle conflicted transactions + // using multiple cycles + schedule_and_validate( + block_schedule::by_threading_conflicts, + { + {0x1ULL, 0x2ULL}, + {0x3ULL, 0x2ULL}, + {0x5ULL, 0x1ULL}, + {0x7ULL, 0x1ULL}, + {0x1ULL, 0x7ULL} + }, + EXPECT(schedule_is_valid), + EXPECT(transaction_count, 5), + EXPECT(std::greater, cycle_count, 1) + ); +} + +BOOST_FIXTURE_TEST_CASE(basic_cycle, default_fixture) { + // ensure the scheduler can handle a basic scope cycle + schedule_and_validate( + block_schedule::by_threading_conflicts, + { + {0x1ULL, 0x2ULL}, + {0x2ULL, 0x3ULL}, + {0x3ULL, 0x1ULL}, + }, + EXPECT(schedule_is_valid), + EXPECT(transaction_count, 3) + ); +} + +BOOST_FIXTURE_TEST_CASE(small_block, compose_fixture) { + // ensure the scheduler can handle basic block size restrictions + schedule_and_validate( + block_schedule::by_threading_conflicts, + { + {0x1ULL, 0x2ULL}, + {0x3ULL, 0x4ULL}, + {0x5ULL, 0x6ULL}, + {0x7ULL, 0x8ULL}, + {0x9ULL, 0xAULL}, + {0xBULL, 0xCULL}, + {0xDULL, 0xEULL}, + {0x11ULL, 0x12ULL}, + {0x13ULL, 0x14ULL}, + {0x15ULL, 0x16ULL}, + {0x17ULL, 0x18ULL}, + {0x19ULL, 0x1AULL}, + {0x1BULL, 0x1CULL}, + {0x1DULL, 0x1EULL}, + {0x21ULL, 0x22ULL}, + {0x23ULL, 0x24ULL}, + {0x25ULL, 0x26ULL}, + {0x27ULL, 0x28ULL}, + {0x29ULL, 0x2AULL}, + {0x2BULL, 0x2CULL}, + {0x2DULL, 0x2EULL}, + }, + EXPECT(schedule_is_valid), + EXPECT(std::less, transaction_count, 21) + ); +} + +BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file -- GitLab