diff --git a/test/cases.cc b/test/cases.cc index 09da2103..1311f12e 100644 --- a/test/cases.cc +++ b/test/cases.cc @@ -14,8 +14,8 @@ #include "test.h" -void configure_actor(unsigned &lastid, const actor_testcase testcase, - const char *id_cstr, const actor_params ¶ms) { +void configure_actor(unsigned &last_space_id, const actor_testcase testcase, + const char *space_id_cstr, const actor_params ¶ms) { unsigned wait4id = 0; if (params.waitfor_nops) { @@ -33,40 +33,64 @@ void configure_actor(unsigned &lastid, const actor_testcase testcase, failure("No previous waitable actor for %u-ops\n", params.waitfor_nops); } - unsigned id = 0; - if (!id_cstr || strcmp(id_cstr, "auto") == 0) - id = lastid + 1; + unsigned space_id = 0; + if (!space_id_cstr || strcmp(space_id_cstr, "auto") == 0) + space_id = last_space_id + 1; else { char *end = nullptr; errno = 0; - id = strtoul(id_cstr, &end, 0); + space_id = strtoul(space_id_cstr, &end, 0); if (errno) - failure_perror("Expects an integer value for actor-id\n", errno); + failure_perror("Expects an integer value for space-id\n", errno); if (end && *end) - failure("The '%s' is unexpected for actor-id\n", end); + failure("The '%s' is unexpected for space-id\n", end); } - if (id < 1 || id > ACTOR_ID_MAX) - failure("Invalid actor-id %u\n", id); - lastid = id; + if (space_id > ACTOR_ID_MAX) + failure("Invalid space-id %u\n", space_id); + last_space_id = space_id; - log_trace("configure_actor: %u for %s", id, testcase2str(testcase)); - global::actors.emplace_back(actor_config(testcase, params, id, wait4id)); + log_trace("configure_actor: space %u for %s", space_id, + testcase2str(testcase)); + global::actors.emplace_back( + actor_config(testcase, params, space_id, wait4id)); global::databases.insert(params.pathname_db); } void testcase_setup(const char *casename, actor_params ¶ms, - unsigned &lastid) { + unsigned &last_space_id) { if (strcmp(casename, "basic") == 0) { log_notice(">>> testcase_setup(%s)", casename); - configure_actor(lastid, ac_hill, nullptr, params); - configure_actor(lastid, ac_jitter, nullptr, params); - configure_actor(lastid, ac_jitter, nullptr, params); - configure_actor(lastid, ac_jitter, nullptr, params); + configure_actor(last_space_id, ac_jitter, nullptr, params); + configure_actor(last_space_id, ac_hill, nullptr, params); + configure_actor(last_space_id, ac_jitter, nullptr, params); + configure_actor(last_space_id, ac_hill, nullptr, params); + configure_actor(last_space_id, ac_jitter, nullptr, params); + configure_actor(last_space_id, ac_hill, nullptr, params); log_notice("<<< testcase_setup(%s): done", casename); } else { failure("unknown testcase `%s`", casename); } } +void keycase_setup(const char *casename, actor_params ¶ms) { + if (strcmp(casename, "random") == 0 || strcmp(casename, "prng") == 0) { + log_notice(">>> keycase_setup(%s)", casename); + params.keygen.keycase = kc_random; + // TODO + log_notice("<<< keycase_setup(%s): done", casename); + } else if (strcmp(casename, "dashes") == 0 || + strcmp(casename, "aside") == 0) { + log_notice(">>> keycase_setup(%s)", casename); + params.keygen.keycase = kc_dashes; + // TODO + log_notice("<<< keycase_setup(%s): done", casename); + } else if (strcmp(casename, "custom") == 0) { + log_notice("=== keycase_setup(%s): skip", casename); + params.keygen.keycase = kc_custom; + } else { + failure("unknown keycase `%s`", casename); + } +} + /* TODO */ diff --git a/test/config.cc b/test/config.cc index 743e022a..d2e6dd12 100644 --- a/test/config.cc +++ b/test/config.cc @@ -176,6 +176,16 @@ bool parse_option(int argc, char *const argv[], int &narg, const char *option, return true; } +bool parse_option(int argc, char *const argv[], int &narg, const char *option, + uint8_t &value, const uint8_t minval, const uint8_t maxval) { + + uint64_t huge; + if (!parse_option(argc, argv, narg, option, huge, no_scale, minval, maxval)) + return false; + value = (uint8_t)huge; + return true; +} + bool parse_option(int argc, char *const argv[], int &narg, const char *option, bool &value) { const char *value_cstr = NULL; @@ -268,6 +278,8 @@ void dump(const char *title) { logging::local_suffix indent(title); for (auto i = global::actors.begin(); i != global::actors.end(); ++i) { + const std::string tableid = + i->space_id ? "MAINDB" : ("SUB#" + std::to_string(i->space_id)); log_info("#%u, testcase %s, space_id/table %u\n", i->actor_id, testcase2str(i->testcase), i->space_id); indent.push(); @@ -284,8 +296,6 @@ void dump(const char *title) { dump_verbs("mode", i->params.mode_flags, mode_bits); dump_verbs("table", i->params.table_flags, table_bits); - log_info("seed %u\n", i->params.seed); - if (i->params.test_nops) log_info("iterations/records %u\n", i->params.test_nops); else @@ -298,6 +308,8 @@ void dump(const char *title) { log_info("threads %u\n", i->params.nthreads); + log_info("keygen.case: %s\n", keygencase2str(i->params.keygen.keycase)); + log_info("keygen.seed: %u\n", i->params.keygen.seed); log_info("key: minlen %u, maxlen %u\n", i->params.keylen_min, i->params.keylen_max); log_info("data: minlen %u, maxlen %u\n", i->params.datalen_min, diff --git a/test/config.h b/test/config.h index c0a04f93..91ea4a24 100644 --- a/test/config.h +++ b/test/config.h @@ -34,6 +34,15 @@ enum actor_status { const char *testcase2str(const actor_testcase); const char *status2str(actor_status status); +enum keygen_case { + kc_random, /* [ 6.. 2.. 7.. 4.. 0.. 1.. 5.. 3.. ] */ + kc_dashes, /* [ 0123.. 4567.. ] */ + kc_custom, + /* TODO: more cases */ +}; + +const char *keygencase2str(const keygen_case); + //----------------------------------------------------------------------------- namespace config { @@ -65,17 +74,129 @@ bool parse_option(int argc, char *const argv[], int &narg, const char *option, unsigned &value, const scale_mode scale, const unsigned minval = 0, const unsigned maxval = INT32_MAX); +bool parse_option(int argc, char *const argv[], int &narg, const char *option, + uint8_t &value, const uint8_t minval = 0, + const uint8_t maxval = 255); + //----------------------------------------------------------------------------- #pragma pack(push, 1) +struct keygen_params_pod { + keygen_case keycase; + + /* Параметры генератора пар key-value. + * + * Ключи и значения генерируются по задаваемым параметрам на основе "плоской" + * исходной координаты. При этом, в общем случае, в процессе тестов исходная + * координата последовательно итерируется в заданном диапазоне, а необходимые + * паттерны/последовательности/узоры получаются за счет преобразования + * исходной координаты, согласно описанным ниже параметрам. + * + * Стоит отметить, что порядок описания параметров для удобства совпадает с + * порядком их использования, т.е. с порядком соответствующих преобразований. + * + * Второе важное замечание касается ограничений одновременной координированной + * генерации паттеров как для ключей, так и для значений. Суть в том, что + * такая возможность не нужна по следующим причинам: + * - libmdbx поддерживает два существенно различающихся вида таблиц, + * "уникальные" (без дубликатов и без multi-value), и так называемые + * "с дубликатами" (c multi-value). + * - Для таблиц "без дубликатов" только размер связанных к ключами значений + * (данных) оказывает влияния на работу движка, непосредственно содержимое + * данных не анализируется движком и не оказывает влияния на его работу. + * - Для таблиц "с дубликатами", при наличии более одного значения для + * некоторого ключа, формируется дочернее btree-поддерево. Это дерево + * формируется в отдельном "кусте" страниц и обслуживается независимо + * от окружения родительского ключа. + * - Таким образом, паттерн генерации значений имеет смысл только для + * таблиц "с дубликатами" и только в контексте одного значения ключа. + * Иначе говоря, нет смысла в со-координации генерации паттернов для + * ключей и значений. Более того, генерацию значений всегда необходимо + * рассматривать в контексте связки с одним значением ключа. + * + * width: + * Большинство тестов предполагают создание или итерирование некоторого + * количества записей. При этом требуется итерирование или генерация + * значений и ключей из некоторого ограниченного пространства вариантов. + * + * Параметр width задает такую ширину пространства вариантов в битах. + * Таким образом мощность пространства вариантов (пока) всегда равна + * степени двойки. Это ограничение можно снять, но ценой увеличения + * вычислительной сложности, включая потерю простоты и прозрачности. + * + * С другой стороны, не-битовый width может быть полезен: + * - Позволит генерировать ключи/значения в точно задаваемом диапазоне. + * Например, перебрать в псевдо-случайном порядке 10001 значение. + * - Позволит поровну разделять заданное пространство (диапазон) + * ключей/значений между количеством потоков некратным степени двойки. + * + * mesh и seed: + * Позволяют получить псевдо-случайные последовательности ключей/значений. + * Параметр mesh задает сколько младших бит исходной плоской координаты + * будет "перемешано" (инъективно отображено), а параметр seed позволяет + * выбрать конкретный вариант "перемешивания". + * + * Перемешивание выполняется при ненулевом значении mesh. Перемешивание + * реализуется посредством применения двух инъективных функций для + * заданного количества бит: + * - применяется первая инъективная функция; + * - к результату добавляется salt полученный из seed; + * - применяется вторая инъективная функция; + * + * Следует отметить, что mesh умышленно позволяет перемешать только младшую + * часть, что при ненулевом значении split (см далее) не позволяет получать + * псевдо-случайные значений ключей без псевдо-случайности в значениях. + * + * Такое ограничение соответствуют внутренней алгоритмике libmdbx. Проще + * говоря мы можем проверить движок псевдо-случайной последовательностью + * ключей на таблицах без дубликатов (без multi-value), а затем проверить + * корректность работу псевдо-случайной последовательностью значений на + * таблицах с дубликатами (с multi-value), опционально добавляя + * псевдо-случайности к последовательности ключей. Однако, нет смысла + * генерировать псевдо-случайные ключи, одновременно с формированием + * какого-либо паттерна в значениях, так как содержимое в данных либо + * не будет иметь значения (для таблиц без дубликатов), либо будет + * обрабатываться в отдельных btree-поддеревьях. + * + * rotate и offset: + * Для проверки слияния и разделения страниц внутри движка требуются + * генерация ключей/значений в виде не-смежных последовательностей, как-бы + * в виде "пунктира", который постепенно заполняет весь заданных диапазон. + * + * Параметры позволяют генерировать такой "пунктир". Соответственно rotate + * задает циклический сдвиг вправо, а offset задает смещение, точнее говоря + * сложение по модулю внутри диапазона заданного посредством width. + * + * Например, при rotate равном 1 (циклический сдвиг вправо на 1 бит), + * четные и нечетные исходные значения сложатся в две линейные + * последовательности, которые постепенно закроют старшую и младшую + * половины диапазона. + * + * split: + * Для таблиц без дубликатов (без multi-value ключей) фактически требуется + * генерация только ключей, а данные могут быть постоянным. Но для таблиц с + * дубликатами (с multi-value ключами) также требуется генерация значений. + * + * Ненулевое значение параметра split фактически включает генерацию значений, + * при этом значение split определяет сколько бит исходного абстрактного + * номера будет отрезано для генерации значения. + */ + + uint8_t width; + uint8_t mesh; + uint8_t rotate; + uint8_t split; + uint32_t seed; + uint64_t offset; +}; + struct actor_params_pod { unsigned loglevel; size_t mode_flags; size_t table_flags; uint64_t size; - unsigned seed; unsigned test_duration; unsigned test_nops; @@ -91,10 +212,11 @@ struct actor_params_pod { unsigned delaystart; unsigned waitfor_nops; - bool drop_table; - unsigned max_readers; unsigned max_tables; + keygen_params_pod keygen; + + bool drop_table; }; struct actor_config_pod { diff --git a/test/hill.cc b/test/hill.cc index 0a7d2fd7..daa6e04e 100644 --- a/test/hill.cc +++ b/test/hill.cc @@ -27,7 +27,196 @@ bool testcase_hill::setup() { bool testcase_hill::run() { db_open(); - /* TODO */ + + txn_begin(false); + MDB_dbi dbi = db_table_open(true); + txn_end(false); + + /* LY: тест "холмиком": + * - сначала наполняем таблицу циклическими CRUD-манипуляциями, + * которые в каждом цикле делают несколько операций, включая удаление, + * но в результате добавляют записи. + * - затем очищаем таблицу также CRUD-манипуляциями, но уже с другой + * пропорцией удалений. + * + * При этом очень многое зависит от порядка перебора ключей: + * - (псевдо)случайное распределение требуется лишь для полноты картины, + * но в целом не покрывает важных кейсов. + * - кроме (псевдо)случайного перебора требуется последовательное + * итерирование ключей интервалами различной ширины, с тем чтобы + * проверить различные варианты как разделения, так и слияния страниц + * внутри движка. + * - при не-уникальных ключах (MDB_DUPSORT с подвариантами), для каждого + * повтора внутри движка формируется вложенное btree-дерево, + * соответственно требуется соблюдение аналогичных принципов + * итерирования для значений. + */ + + /* TODO: работа в несколько потоков */ + keyvalue_maker.setup(config.params, 0 /* thread_number */); + + keygen::buffer a_key = keygen::alloc(config.params.keylen_max); + keygen::buffer a_data_0 = keygen::alloc(config.params.datalen_max); + keygen::buffer a_data_1 = keygen::alloc(config.params.datalen_max); + keygen::buffer b_key = keygen::alloc(config.params.keylen_max); + keygen::buffer b_data = keygen::alloc(config.params.datalen_max); + + const unsigned insert_flags = (config.params.table_flags & MDB_DUPSORT) + ? MDB_NODUPDATA + : MDB_NODUPDATA | MDB_NOOVERWRITE; + const unsigned update_flags = MDB_CURRENT | MDB_NODUPDATA | MDB_NOOVERWRITE; + + uint64_t serial_count = 0; + unsigned txn_nops = 0; + if (!txn_guard) + txn_begin(false); + + while (should_continue()) { + const keygen::serial_t a_serial = serial_count; + if (unlikely(!keyvalue_maker.increment(serial_count, 1))) + failure("uphill: unexpected key-space overflow"); + + const keygen::serial_t b_serial = serial_count; + assert(b_serial > a_serial); + + // создаем первую запись из пары + const keygen::serial_t age_shift = UINT64_C(1) << (a_serial % 31); + log_trace("uphill: insert-a (age %" PRIu64 ") %" PRIu64, age_shift, + a_serial); + generate_pair(a_serial, a_key, a_data_1, age_shift); + int rc = mdbx_put(txn_guard.get(), dbi, &a_key->value, &a_data_1->value, + insert_flags); + if (unlikely(rc != MDB_SUCCESS)) + failure_perror("mdbx_put(insert-a.1)", rc); + + if (++txn_nops >= config.params.batch_write) { + txn_restart(false, false); + txn_nops = 0; + } + + // создаем вторую запись из пары + log_trace("uphill: insert-b %" PRIu64, b_serial); + generate_pair(b_serial, b_key, b_data, 0); + rc = mdbx_put(txn_guard.get(), dbi, &b_key->value, &b_data->value, + insert_flags); + if (unlikely(rc != MDB_SUCCESS)) + failure_perror("mdbx_put(insert-b)", rc); + + if (++txn_nops >= config.params.batch_write) { + txn_restart(false, false); + txn_nops = 0; + } + + // обновляем данные в первой записи + log_trace("uphill: update-a (age %" PRIu64 "->0) %" PRIu64, age_shift, + a_serial); + generate_pair(a_serial, a_key, a_data_0, 0); + rc = mdbx_replace(txn_guard.get(), dbi, &a_key->value, &a_data_0->value, + &a_data_1->value, update_flags); + if (unlikely(rc != MDB_SUCCESS)) + failure_perror("mdbx_put(update-a: 1->0)", rc); + + if (++txn_nops >= config.params.batch_write) { + txn_restart(false, false); + txn_nops = 0; + } + + // удаляем вторую запись + log_trace("uphill: delete-b %" PRIu64, b_serial); + rc = mdbx_del(txn_guard.get(), dbi, &b_key->value, &b_data->value); + if (unlikely(rc != MDB_SUCCESS)) + failure_perror("mdbx_del(b)", rc); + + if (++txn_nops >= config.params.batch_write) { + txn_restart(false, false); + txn_nops = 0; + } + + report(1); + if (!keyvalue_maker.increment(serial_count, 1)) { + // дошли до границы пространства ключей + serial_count = a_serial; + goto overflow; + } + } + + while (serial_count > 0) { + if (unlikely(!keyvalue_maker.increment(serial_count, -2))) + failure("downhill: unexpected key-space underflow"); + + overflow: + const keygen::serial_t a_serial = serial_count; + const keygen::serial_t b_serial = a_serial + 1; + assert(b_serial > a_serial); + + // обновляем первую запись из пары + const keygen::serial_t age_shift = UINT64_C(1) << (a_serial % 31); + log_trace("downhill: update-a (age 0->%" PRIu64 ") %" PRIu64, age_shift, + a_serial); + generate_pair(a_serial, a_key, a_data_0, 0); + generate_pair(a_serial, a_key, a_data_1, age_shift); + if (a_serial == 808) + log_trace("!!!"); + int rc = mdbx_replace(txn_guard.get(), dbi, &a_key->value, &a_data_1->value, + &a_data_0->value, update_flags); + if (unlikely(rc != MDB_SUCCESS)) + failure_perror("mdbx_put(update-a: 0->1)", rc); + + if (++txn_nops >= config.params.batch_write) { + txn_restart(false, false); + txn_nops = 0; + } + + // создаем вторую запись из пары + log_trace("downhill: insert-b %" PRIu64, b_serial); + generate_pair(b_serial, b_key, b_data, 0); + rc = mdbx_put(txn_guard.get(), dbi, &b_key->value, &b_data->value, + insert_flags); + if (unlikely(rc != MDB_SUCCESS)) + failure_perror("mdbx_put(insert-b)", rc); + + if (++txn_nops >= config.params.batch_write) { + txn_restart(false, false); + txn_nops = 0; + } + + // удаляем первую запись + log_trace("downhill: delete-a (age %" PRIu64 ") %" PRIu64, age_shift, + a_serial); + rc = mdbx_del(txn_guard.get(), dbi, &a_key->value, &a_data_1->value); + if (unlikely(rc != MDB_SUCCESS)) + failure_perror("mdbx_del(a)", rc); + + if (++txn_nops >= config.params.batch_write) { + txn_restart(false, false); + txn_nops = 0; + } + + // удаляем вторую запись + log_trace("downhill: delete-b %" PRIu64, b_serial); + rc = mdbx_del(txn_guard.get(), dbi, &b_key->value, &b_data->value); + if (unlikely(rc != MDB_SUCCESS)) + failure_perror("mdbx_del(b)", rc); + + if (++txn_nops >= config.params.batch_write) { + txn_restart(false, false); + txn_nops = 0; + } + + report(1); + } + + if (txn_guard) + txn_end(false); + + if (dbi) { + if (config.params.drop_table && !mode_readonly()) { + txn_begin(false); + db_table_drop(dbi); + txn_end(false); + } else + db_table_close(dbi); + } return true; } diff --git a/test/keygen.cc b/test/keygen.cc index 20c80a2a..0d7c0409 100644 --- a/test/keygen.cc +++ b/test/keygen.cc @@ -16,18 +16,179 @@ namespace keygen { -size_t ffs_fallback(serial_t serial) { - size_t bit = sizeof(serial_t) * 8 - 1; - auto mask = (serial_t)1u << bit; - do { - if (serial & mask) - return bit; - --bit; - } while (mask >>= 1); - return 0; +static inline __pure_function serial_t mask(unsigned bits) { + assert(bits > 0 && bits <= serial_maxwith); + return serial_allones >> (serial_maxwith - bits); } -void __hot make(const serial_t serial, const params_t ¶ms, result_t &out) { +/* LY: https://en.wikipedia.org/wiki/Injective_function */ +serial_t injective(const serial_t serial, + const unsigned bits /* at least serial_minwith (8) */, + const serial_t salt) { + assert(bits > serial_minwith && bits <= serial_maxwith); + + /* LY: All these "magic" prime numbers were found + * and verified with a bit of brute force. */ + + static const uint64_t m[64 - serial_minwith] = { + /* 8 - 24 */ + 113, 157, 397, 653, 1753, 5641, 9697, 23873, 25693, 80833, 105953, 316937, + 309277, 834497, 1499933, 4373441, 10184137, + /* 25 - 64 */ + 10184137, 17279209, 33990377, 67295161, 284404553, 1075238767, 6346721573, + 6924051577, 19204053433, 45840188887, 53625693977, 73447827913, + 141638870249, 745683604649, 1283334050489, 1100828289853, 2201656586197, + 5871903036137, 11238507001417, 45264020802263, 105008404482889, + 81921776907059, 199987980256399, 307207457507641, 946769023178273, + 2420886491930041, 3601632139991929, 11984491914483833, 21805846439714153, + 23171543400565993, 53353226456762893, 155627817337932409, + 227827205384840249, 816509268558278821, 576933057762605689, + 2623957345935638441, 5048241705479929949, 4634245581946485653}; + static const uint8_t s[64 - serial_minwith] = { + /* 8 - 24 */ + 2, 3, 4, 4, 2, 4, 3, 3, 7, 3, 3, 4, 8, 3, 10, 3, 11, + /* 25 - 64 */ + 11, 9, 9, 9, 11, 10, 5, 14, 11, 16, 14, 12, 13, 16, 19, 10, 10, 21, 7, 20, + 10, 14, 22, 19, 3, 21, 18, 19, 26, 24, 2, 21, 25, 29, 24, 10, 11, 14}; + + serial_t result = serial * m[bits - 8]; + if (salt) { + const unsigned left = bits / 2; + const unsigned right = bits - left; + result = (result << left) | ((result & mask(bits)) >> right); + result = (result ^ salt) * m[bits - 8]; + } + + result ^= result << s[bits - 8]; + result &= mask(bits); + log_trace("keygen-injective: serial %" PRIu64 " into %" PRIu64, serial, + result); + return result; +} + +void __hot maker::pair(serial_t serial, const buffer &key, buffer &value, + serial_t value_age) { + assert(mapping.width >= serial_minwith && mapping.width <= serial_maxwith); + assert(mapping.split <= mapping.width); + assert(mapping.mesh <= mapping.width); + assert(mapping.rotate <= mapping.width); + assert(mapping.offset <= mask(mapping.width)); + assert(!(key_essentials.flags & (MDB_INTEGERDUP | MDB_REVERSEDUP))); + assert(!(value_essentials.flags & (MDB_INTEGERKEY | MDB_REVERSEKEY))); + + log_trace("keygen-pair: serial %" PRIu64 ", data-age %" PRIu64, serial, + value_age); + + if (mapping.mesh >= serial_minwith) { + serial = + (serial & ~mask(mapping.mesh)) | injective(serial, mapping.mesh, salt); + log_trace("keygen-pair: mesh %" PRIu64, serial); + } + + if (mapping.rotate) { + const unsigned right = mapping.rotate; + const unsigned left = mapping.width - right; + serial = (serial << left) | ((serial & mask(mapping.width)) >> right); + log_trace("keygen-pair: rotate %" PRIu64 ", 0x%" PRIx64, serial, serial); + } + + serial = (serial + mapping.offset) & mask(mapping.width); + log_trace("keygen-pair: offset %" PRIu64, serial); + serial += base; + + serial_t key_serial = serial; + serial_t value_serial = value_age; + if (mapping.split) { + key_serial = serial >> mapping.split; + value_serial = + (serial & mask(mapping.split)) | (value_age << mapping.split); + } + + log_trace("keygen-pair: key %" PRIu64 ", value %" PRIu64, key_serial, + value_serial); + + mk(key_serial, key_essentials, *key); + mk(value_serial, value_essentials, *value); + + if (log_enabled(logging::trace)) { + char dump_key[128], dump_value[128]; + log_trace("keygen-pair: key %s, value %s", + mdbx_dkey(&key->value, dump_key, sizeof(dump_key)), + mdbx_dkey(&value->value, dump_value, sizeof(dump_value))); + } +} + +void maker::setup(const config::actor_params_pod &actor, + unsigned thread_number) { + key_essentials.flags = actor.table_flags & (MDB_INTEGERKEY | MDB_REVERSEKEY); + key_essentials.minlen = actor.keylen_min; + key_essentials.maxlen = actor.keylen_max; + + value_essentials.flags = + actor.table_flags & (MDB_INTEGERDUP | MDB_REVERSEDUP); + value_essentials.minlen = actor.datalen_min; + value_essentials.maxlen = actor.datalen_max; + + assert(thread_number < 2); + (void)thread_number; + mapping = actor.keygen; + salt = actor.keygen.seed * UINT64_C(14653293970879851569); + + // FIXME: TODO + base = 0; +} + +bool maker::increment(serial_t &serial, int delta) { + if (serial > mask(mapping.width)) { + log_extra("keygen-increment: %" PRIu64 " > %" PRIu64 ", overflow", serial, + mask(mapping.width)); + return false; + } + + serial_t target = serial + (int64_t)delta; + if (target > mask(mapping.width)) { + log_extra("keygen-increment: %" PRIu64 "%-d => %" PRIu64 ", overflow", + serial, delta, target); + return false; + } + + log_extra("keygen-increment: %" PRIu64 "%-d => %" PRIu64 ", continue", serial, + delta, target); + serial = target; + return true; +} + +//----------------------------------------------------------------------------- + +size_t length(serial_t serial) { + size_t n = 0; + if (serial > UINT32_MAX) { + n = 4; + serial >>= 32; + } + if (serial > UINT16_MAX) { + n += 2; + serial >>= 16; + } + if (serial > UINT8_MAX) { + n += 1; + serial >>= 8; + } + return (serial > 0) ? n + 1 : n; +} + +buffer alloc(size_t limit) { + result *ptr = (result *)malloc(sizeof(result) + limit); + if (unlikely(ptr == nullptr)) + failure_perror("malloc(keyvalue_buffer)", errno); + ptr->value.iov_base = ptr->bytes; + ptr->value.iov_len = 0; + ptr->limit = limit; + return buffer(ptr); +} + +void __hot maker::mk(const serial_t serial, const essentials ¶ms, + result &out) { assert(out.limit >= params.maxlen); assert(params.maxlen >= params.minlen); assert(params.maxlen >= length(serial)); diff --git a/test/keygen.h b/test/keygen.h index 58db2633..e6eeb194 100644 --- a/test/keygen.h +++ b/test/keygen.h @@ -15,6 +15,7 @@ #pragma once #include "base.h" +#include "config.h" #include "log.h" #include "utils.h" @@ -42,25 +43,41 @@ namespace keygen { * - частотное распределение по алфавиту; * - абсолютное значение ключей или разность между отдельными значениями; * - * Соответственно, схема генерации следующая: - * - для ключей вводится плоская одномерная "координата" uint64_t; - * - все преобразования (назначение диапазонов, переупорядочивание, - * коррекция распределения) выполняются только над "координатой"; + * Соответственно, в общих чертах, схема генерации следующая: + * - вводится плоская одномерная "координата" uint64_t; + * - генерация специфических паттернов (последовательностей) + * реализуется посредством соответствующих преобразований "координат", при + * этом все подобные преобразования выполняются только над "координатой"; * - итоговая "координата" преобразуется в 8-байтное суррогатное значение - * ключа, при этом опционально суррогат может усекаться до ненулевых байт; - * - для получения ключей длиной более 8 байт суррогат дополняется - * фиксированной последовательностью; + * ключа; + * - для получения ключей длиной МЕНЕЕ 8 байт суррогат может усекаться + * до ненулевых байт, в том числе до нулевой длины; + * - для получения ключей длиной БОЛЕЕ 8 байт суррогат дополняется + * нулями или псевдослучайной последовательностью; + * + * Механизм генерации паттернов: + * - реализованный механизм является компромиссом между скоростью/простотой + * и гибкостью, необходимой для получения последовательностей, которых + * будет достаточно для проверки сценариев разделения и слияния страниц + * с данными внутри mdbx; + * - псевдо-случайные паттерны реализуются посредством набора инъективных + * отображающих функций; + * - не-псевдо-случайные паттерны реализуются посредством параметризируемого + * трех-этапного преобразования: + * 1) смещение (сложение) по модулю; + * 2) циклический сдвиг; + * 3) добавление абсолютного смещения (базы); */ typedef uint64_t serial_t; -struct params_t { - uint8_t minlen; - uint8_t flags; - uint16_t maxlen; +enum { + serial_minwith = 8, + serial_maxwith = sizeof(serial_t) * 8, + serial_allones = ~(serial_t)0 }; -struct result_t { +struct result { MDB_val value; size_t limit; union { @@ -70,54 +87,39 @@ struct result_t { }; }; -void make(const serial_t serial, const params_t ¶ms, result_t &out); +//----------------------------------------------------------------------------- -static __inline void make(const serial_t serial, const params_t ¶ms, - result_t &out, size_t limit) { - out.limit = limit; - make(serial, params, out); -} +struct buffer_deleter : public std::unary_function { + void operator()(result *buffer) const { free(buffer); } +}; -size_t ffs_fallback(serial_t serial); +typedef std::unique_ptr buffer; -static __inline size_t ffs(serial_t serial) { - size_t rc; -#ifdef __GNUC__ - if (sizeof(serial) <= sizeof(int)) - rc = __builtin_ffs((int)serial); - else if (sizeof(serial) == sizeof(long)) - rc = __builtin_ffsl((long)serial); - else if (sizeof(serial) == sizeof(long long)) - rc = __builtin_ffsll((long long)serial); - else - return ffs_fallback(serial); -#elif defined(_MSC_VER) - unsigned long index; - if (sizeof(serial) <= sizeof(unsigned long)) - rc = _BitScanReverse(&index, (unsigned long)serial) ? index : 0; - else if (sizeof(serial) <= sizeof(unsigned __int64)) { -#if defined(_M_ARM64) || defined(_M_X64) - rc = _BitScanReverse64(&index, (unsigned __int64)serial) ? index : 0; -#else - size_t base = 0; - unsigned long value = (unsigned long)serial; - if ((unsigned __int64)serial > ULONG_MAX) { - base = 32; - value = (unsigned long)(serial >> 32); - } - rc = (_BitScanReverse(&index, value) ? index : 0) + base; -#endif /* _M_ARM64 || _M_X64 */ - } else - return ffs_fallback(serial); -#else - return ffs_fallback(serial); -#endif - assert(rc == ffs_fallback(serial)); - return rc; -} +buffer alloc(size_t limit); -static __inline size_t length(const serial_t serial) { - return (ffs(serial) + 7) >> 3; -} +class maker { + config::keygen_params_pod mapping; + serial_t base; + serial_t salt; + + struct essentials { + uint8_t minlen; + uint8_t flags; + uint16_t maxlen; + } key_essentials, value_essentials; + + static void mk(const serial_t serial, const essentials ¶ms, result &out); + +public: + maker() { memset(this, 0, sizeof(*this)); } + + void pair(serial_t serial, const buffer &key, buffer &value, + serial_t value_age); + void setup(const config::actor_params_pod &actor, unsigned thread_number); + + bool increment(serial_t &serial, int delta); +}; + +size_t length(serial_t serial); } /* namespace keygen */ diff --git a/test/main.cc b/test/main.cc index 14805366..8ef6f2f2 100644 --- a/test/main.cc +++ b/test/main.cc @@ -41,7 +41,14 @@ void actor_params::set_defaults(void) { MDB_NOMEMINIT | MDBX_COALESCE | MDBX_LIFORECLAIM; table_flags = MDB_DUPSORT; size = 1024 * 1024; - seed = 1; + + keygen.seed = 1; + keygen.keycase = kc_random; + keygen.width = 32; + keygen.mesh = 32; + keygen.split = keygen.width / 2; + keygen.rotate = 0; + keygen.offset = 0; test_duration = 0; test_nops = 1000; @@ -129,13 +136,13 @@ int main(int argc, char *const argv[]) { params.set_defaults(); global::config::dump_config = true; logging::setup((logging::loglevel)params.loglevel, "main"); - unsigned lastid = 0; + unsigned last_space_id = 0; for (int narg = 1; narg < argc; ++narg) { const char *value = nullptr; if (config::parse_option(argc, argv, narg, "case", &value)) { - testcase_setup(value, params, lastid); + testcase_setup(value, params, last_space_id); continue; } if (config::parse_option(argc, argv, narg, "pathname", params.pathname_db)) @@ -149,9 +156,30 @@ int main(int argc, char *const argv[]) { if (config::parse_option(argc, argv, narg, "size", params.size, config::binary, 4096 * 4)) continue; - if (config::parse_option(argc, argv, narg, "seed", params.seed, - config::no_scale)) + + if (config::parse_option(argc, argv, narg, "keygen.width", + params.keygen.width, 1, 64)) continue; + if (config::parse_option(argc, argv, narg, "keygen.mesh", + params.keygen.mesh, 1, 64)) + continue; + if (config::parse_option(argc, argv, narg, "keygen.seed", + params.keygen.seed, config::no_scale)) + continue; + if (config::parse_option(argc, argv, narg, "keygen.split", + params.keygen.split, 1, 64)) + continue; + if (config::parse_option(argc, argv, narg, "keygen.rotate", + params.keygen.rotate, 1, 64)) + continue; + if (config::parse_option(argc, argv, narg, "keygen.offset", + params.keygen.offset, config::binary)) + continue; + if (config::parse_option(argc, argv, narg, "keygen.case", &value)) { + keycase_setup(value, params); + continue; + } + if (config::parse_option(argc, argv, narg, "repeat", params.nrepeat, config::no_scale)) continue; @@ -225,20 +253,20 @@ int main(int argc, char *const argv[]) { params.test_duration = 0; continue; } - if (config::parse_option(argc, argv, narg, "hill", &value)) { - configure_actor(lastid, ac_hill, value, params); + if (config::parse_option(argc, argv, narg, "hill", &value, "auto")) { + configure_actor(last_space_id, ac_hill, value, params); continue; } if (config::parse_option(argc, argv, narg, "jitter", nullptr)) { - configure_actor(lastid, ac_jitter, value, params); + configure_actor(last_space_id, ac_jitter, value, params); continue; } if (config::parse_option(argc, argv, narg, "dead.reader", nullptr)) { - configure_actor(lastid, ac_deadread, value, params); + configure_actor(last_space_id, ac_deadread, value, params); continue; } if (config::parse_option(argc, argv, narg, "dead.writer", nullptr)) { - configure_actor(lastid, ac_deadwrite, value, params); + configure_actor(last_space_id, ac_deadwrite, value, params); continue; } if (config::parse_option(argc, argv, narg, "failfast", @@ -246,7 +274,7 @@ int main(int argc, char *const argv[]) { continue; if (*argv[narg] != '-') - testcase_setup(argv[narg], params, lastid); + testcase_setup(argv[narg], params, last_space_id); else failure("Unknown option '%s'\n", argv[narg]); } diff --git a/test/test.cc b/test/test.cc index 4e8052e7..ad82fd39 100644 --- a/test/test.cc +++ b/test/test.cc @@ -17,6 +17,7 @@ const char *testcase2str(const actor_testcase testcase) { switch (testcase) { default: + assert(false); return "?!"; case ac_none: return "none"; @@ -49,6 +50,20 @@ const char *status2str(actor_status status) { } } +const char *keygencase2str(const keygen_case keycase) { + switch (keycase) { + default: + assert(false); + return "?!"; + case kc_random: + return "random"; + case kc_dashes: + return "dashes"; + case kc_custom: + return "custom"; + } +} + //----------------------------------------------------------------------------- static void mdbx_debug_logger(int type, const char *function, int line, @@ -67,7 +82,9 @@ static void mdbx_debug_logger(int type, const char *function, int line, level = logging::failure; } - if (logging::output(level, "mdbx: %s: ", function)) + if (logging::output(level, strncmp(function, "mdbx_", 5) == 0 ? "%s: " + : "mdbx: %s: ", + function)) logging::feed(msg, args); if (type & MDBX_DBG_ASSERT) abort(); @@ -87,26 +104,26 @@ void testcase::db_prepare() { MDB_env *env = nullptr; rc = mdbx_env_create(&env); - if (rc != MDB_SUCCESS) + if (unlikely(rc != MDB_SUCCESS)) failure_perror("mdbx_env_create()", rc); assert(env != nullptr); db_guard.reset(env); rc = mdbx_env_set_userctx(env, this); - if (rc != MDB_SUCCESS) + if (unlikely(rc != MDB_SUCCESS)) failure_perror("mdbx_env_set_userctx()", rc); rc = mdbx_env_set_maxreaders(env, config.params.max_readers); - if (rc != MDB_SUCCESS) + if (unlikely(rc != MDB_SUCCESS)) failure_perror("mdbx_env_set_maxreaders()", rc); rc = mdbx_env_set_maxdbs(env, config.params.max_tables); - if (rc != MDB_SUCCESS) + if (unlikely(rc != MDB_SUCCESS)) failure_perror("mdbx_env_set_maxdbs()", rc); rc = mdbx_env_set_mapsize(env, (size_t)config.params.size); - if (rc != MDB_SUCCESS) + if (unlikely(rc != MDB_SUCCESS)) failure_perror("mdbx_env_set_mapsize()", rc); log_trace("<< db_prepare"); @@ -119,7 +136,7 @@ void testcase::db_open() { db_prepare(); int rc = mdbx_env_open(db_guard.get(), config.params.pathname_db.c_str(), (unsigned)config.params.mode_flags, 0640); - if (rc != MDB_SUCCESS) + if (unlikely(rc != MDB_SUCCESS)) failure_perror("mdbx_env_open()", rc); log_trace("<< db_open"); @@ -140,7 +157,7 @@ void testcase::txn_begin(bool readonly) { MDB_txn *txn = nullptr; int rc = mdbx_txn_begin(db_guard.get(), nullptr, readonly ? MDB_RDONLY : 0, &txn); - if (rc != MDB_SUCCESS) + if (unlikely(rc != MDB_SUCCESS)) failure_perror("mdbx_txn_begin()", rc); txn_guard.reset(txn); @@ -154,17 +171,23 @@ void testcase::txn_end(bool abort) { MDB_txn *txn = txn_guard.release(); if (abort) { int rc = mdbx_txn_abort(txn); - if (rc != MDB_SUCCESS) + if (unlikely(rc != MDB_SUCCESS)) failure_perror("mdbx_txn_abort()", rc); } else { int rc = mdbx_txn_commit(txn); - if (rc != MDB_SUCCESS) + if (unlikely(rc != MDB_SUCCESS)) failure_perror("mdbx_txn_commit()", rc); } log_trace("<< txn_end(%s)", abort ? "abort" : "commit"); } +void testcase::txn_restart(bool abort, bool readonly) { + if (txn_guard) + txn_end(abort); + txn_begin(readonly); +} + bool testcase::wait4start() { if (config.wait4id) { log_trace(">> wait4start(%u)", config.wait4id); @@ -257,7 +280,7 @@ void testcase::fetch_canary() { log_trace(">> fetch_canary"); int rc = mdbx_canary_get(txn_guard.get(), &canary_now); - if (rc != MDB_SUCCESS) + if (unlikely(rc != MDB_SUCCESS)) failure_perror("mdbx_canary_get()", rc); if (canary_now.v < last.canary.v) @@ -283,10 +306,57 @@ void testcase::update_canary(uint64_t increment) { canary_now.y += increment; int rc = mdbx_canary_put(txn_guard.get(), &canary_now); - if (rc != MDB_SUCCESS) + if (unlikely(rc != MDB_SUCCESS)) failure_perror("mdbx_canary_put()", rc); - log_trace(">> update_canary: sequence = %" PRIu64, canary_now.y); + log_trace("<< update_canary: sequence = %" PRIu64, canary_now.y); +} + +MDB_dbi testcase::db_table_open(bool create) { + log_trace(">> testcase::db_table_create"); + + char tablename_buf[16]; + const char *tablename = nullptr; + if (config.space_id) { + int rc = snprintf(tablename_buf, sizeof(tablename_buf), "TBL%04u", + config.space_id); + if (rc < 4 || rc >= (int)sizeof(tablename_buf) - 1) + failure("snprintf(tablename): %d", rc); + tablename = tablename_buf; + } + log_verbose("use %s table", tablename ? tablename : "MAINDB"); + + MDB_dbi handle = 0; + int rc = mdbx_dbi_open(txn_guard.get(), tablename, + (create ? MDB_CREATE : 0) | config.params.table_flags, + &handle); + if (unlikely(rc != MDB_SUCCESS)) + failure_perror("mdbx_dbi_open()", rc); + + log_trace("<< testcase::db_table_create, handle %u", handle); + return handle; +} + +void testcase::db_table_drop(MDB_dbi handle) { + log_trace(">> testcase::db_table_drop, handle %u", handle); + + if (config.params.drop_table) { + int rc = mdbx_drop(txn_guard.get(), handle, true); + if (unlikely(rc != MDB_SUCCESS)) + failure_perror("mdbx_drop()", rc); + log_trace("<< testcase::db_table_drop"); + } else { + log_trace("<< testcase::db_table_drop: not needed"); + } +} + +void testcase::db_table_close(MDB_dbi handle) { + log_trace(">> testcase::db_table_close, handle %u", handle); + assert(!txn_guard); + int rc = mdbx_dbi_close(db_guard.get(), handle); + if (unlikely(rc != MDB_SUCCESS)) + failure_perror("mdbx_dbi_close()", rc); + log_trace("<< testcase::db_table_close"); } //----------------------------------------------------------------------------- diff --git a/test/test.h b/test/test.h index f1a039f7..07e4a094 100644 --- a/test/test.h +++ b/test/test.h @@ -25,9 +25,10 @@ bool test_execute(const actor_config &config); std::string thunk_param(const actor_config &config); void testcase_setup(const char *casename, actor_params ¶ms, - unsigned &lastid); -void configure_actor(unsigned &lastid, const actor_testcase testcase, - const char *id_cstr, const actor_params ¶ms); + unsigned &last_space_id); +void configure_actor(unsigned &last_space_id, const actor_testcase testcase, + const char *space_id_cstr, const actor_params ¶ms); +void keycase_setup(const char *casename, actor_params ¶ms); namespace global { @@ -87,6 +88,9 @@ protected: size_t nops_completed; chrono::time start_timestamp; + keygen::buffer key; + keygen::buffer data; + keygen::maker keyvalue_maker; struct { mdbx_canary canary; @@ -97,14 +101,29 @@ protected: void db_close(); void txn_begin(bool readonly); void txn_end(bool abort); + void txn_restart(bool abort, bool readonly); void fetch_canary(); void update_canary(uint64_t increment); + MDB_dbi db_table_open(bool create); + void db_table_drop(MDB_dbi handle); + void db_table_close(MDB_dbi handle); + bool wait4start(); void report(size_t nops_done); void signal(); bool should_continue() const; + void generate_pair(const keygen::serial_t serial, keygen::buffer &key, + keygen::buffer &value, keygen::serial_t data_age = 0) { + keyvalue_maker.pair(serial, key, value, data_age); + } + + void generate_pair(const keygen::serial_t serial, + keygen::serial_t data_age = 0) { + generate_pair(serial, key, data, data_age); + } + bool mode_readonly() const { return (config.params.mode_flags & MDB_RDONLY) ? true : false; } diff --git a/test/utils.cc b/test/utils.cc index 2c7e6d0b..ae58311f 100644 --- a/test/utils.cc +++ b/test/utils.cc @@ -14,7 +14,7 @@ #include "test.h" #include -#ifndef _MSC_VER +#ifdef HAVE_IEEE754_H #include #endif @@ -190,14 +190,11 @@ uint64_t entropy_ticks(void) { //----------------------------------------------------------------------------- static __inline uint64_t bleach64(uint64_t dirty) { - dirty = mul_64x64_high(bswap64(dirty), UINT64_C(17048867929148541611)); - return dirty; + return mul_64x64_high(bswap64(dirty), UINT64_C(17048867929148541611)); } static __inline uint32_t bleach32(uint32_t dirty) { - return (uint32_t)( - (bswap32(dirty) * UINT64_C(/*3080105489, 4267077937 */ 2175734609)) >> - 32); + return (uint32_t)((bswap32(dirty) * UINT64_C(2175734609)) >> 32); } uint64_t prng64_careless(uint64_t &state) { @@ -214,6 +211,39 @@ uint32_t prng32(uint64_t &state) { return (uint32_t)(prng64_careless(state) >> 32); } +void prng_fill(uint64_t &state, void *ptr, size_t bytes) { + while (bytes >= 4) { + *((uint32_t *)ptr) = prng32(state); + ptr = (uint32_t *)ptr + 1; + bytes -= 4; + } + + switch (bytes & 3) { + case 3: { + uint32_t u32 = prng32(state); + memcpy(ptr, &u32, 3); + } break; + case 2: + *((uint16_t *)ptr) = (uint16_t)prng32(state); + break; + case 1: + *((uint8_t *)ptr) = (uint8_t)prng32(state); + break; + case 0: + break; + } +} + +static __thread uint64_t prng_state; + +void prng_seed(uint64_t seed) { prng_state = bleach64(seed); } + +uint32_t prng32(void) { return prng32(prng_state); } + +uint64_t prng64(void) { return prng64_white(prng_state); } + +void prng_fill(void *ptr, size_t bytes) { prng_fill(prng_state, ptr, bytes); } + uint64_t entropy_white() { return bleach64(entropy_ticks()); } double double_from_lower(uint64_t salt) { diff --git a/test/utils.h b/test/utils.h index b4c88834..624a204c 100644 --- a/test/utils.h +++ b/test/utils.h @@ -101,12 +101,12 @@ #define bswap64(v) __bswap_64(v) #else static __inline uint64_t bswap64(uint64_t v) { - return v << 56 | v >> 56 | ((v << 40) & 0x00ff000000000000ull) | - ((v << 24) & 0x0000ff0000000000ull) | - ((v << 8) & 0x000000ff00000000ull) | - ((v >> 8) & 0x00000000ff000000ull) | - ((v >> 24) & 0x0000000000ff0000ull) | - ((v >> 40) & 0x000000000000ff00ull); + return v << 56 | v >> 56 | ((v << 40) & UINT64_C(0x00ff000000000000)) | + ((v << 24) & UINT64_C(0x0000ff0000000000)) | + ((v << 8) & UINT64_C(0x000000ff00000000)) | + ((v >> 8) & UINT64_C(0x00000000ff0000000)) | + ((v >> 24) & UINT64_C(0x0000000000ff0000)) | + ((v >> 40) & UINT64_C(0x000000000000ff00)); } #endif #endif /* bswap64 */ @@ -116,7 +116,8 @@ static __inline uint64_t bswap64(uint64_t v) { #define bswap32(v) __bswap_32(v) #else static __inline uint32_t bswap32(uint32_t v) { - return v << 24 | v >> 24 | ((v << 8) & 0x00ff0000) | ((v >> 8) & 0x0000ff00); + return v << 24 | v >> 24 | ((v << 8) & UINT32_C(0x00ff0000)) | + ((v >> 8) & UINT32_C(0x0000ff00)); } #endif #endif /* bswap32 */ @@ -360,6 +361,12 @@ uint64_t entropy_white(void); uint64_t prng64_careless(uint64_t &state); uint64_t prng64_white(uint64_t &state); uint32_t prng32(uint64_t &state); +void prng_fill(uint64_t &state, void *ptr, size_t bytes); + +void prng_seed(uint64_t seed); +uint32_t prng32(void); +uint64_t prng64(void); +void prng_fill(void *ptr, size_t bytes); bool flipcoin(); bool jitter(unsigned probability_percent);