From 5a99429f8cf230214736d4a0a3dbb566b42fe053 Mon Sep 17 00:00:00 2001 From: Andrey Saranchin <Andrey22102001@gmail.com> Date: Wed, 16 Mar 2022 23:12:07 +0300 Subject: [PATCH] txm: introduce memtx mvcc memory monitoring This patch introduces memtx_tx_region and memtx_tx_mempool: engineers must use only these proxies to collect statistics. Also this patch introduces box.stat.memtx.mvcc - the way to get memtx mvcc memory statistics. Closes #6150 @TarantoolBot document Title: Memtx MVCC memory monitoring Introduce memtx MVCC memory monitoring. One can get it with box.stat.memtx.tx() method or use index to access a particular statistic. The statistics format: txn: statements: max: 0 avg: 0 total: 0 user: max: 0 avg: 0 total: 0 system: max: 0 avg: 0 total: 0 mvcc: trackers: max: 0 avg: 0 total: 0 conflicts: max: 0 avg: 0 total: 0 tuples: tracking: stories: count: 0 total: 0 retained: count: 0 total: 0 used: stories: count: 0 total: 0 retained: count: 0 total: 0 read_view: stories: count: 0 total: 0 retained: count: 0 total: 0 --- .../gh-6150-memtx-mvcc-memory-monitoring.md | 3 + src/box/lua/stat.c | 156 ++++++ src/box/memtx_tx.c | 524 +++++++++++++++--- src/box/memtx_tx.h | 91 ++- src/box/txn.c | 6 +- src/box/txn.h | 4 + ...h_6150_memtx_tx_memory_monitoring_test.lua | 436 +++++++++++++++ 7 files changed, 1142 insertions(+), 78 deletions(-) create mode 100644 changelogs/unreleased/gh-6150-memtx-mvcc-memory-monitoring.md create mode 100644 test/box-luatest/gh_6150_memtx_tx_memory_monitoring_test.lua diff --git a/changelogs/unreleased/gh-6150-memtx-mvcc-memory-monitoring.md b/changelogs/unreleased/gh-6150-memtx-mvcc-memory-monitoring.md new file mode 100644 index 0000000000..0df29dbd8c --- /dev/null +++ b/changelogs/unreleased/gh-6150-memtx-mvcc-memory-monitoring.md @@ -0,0 +1,3 @@ +## feature/memtx + +* Introduced memtx mvcc memory monitoring (gh-6150). diff --git a/src/box/lua/stat.c b/src/box/lua/stat.c index 16a9df6766..60a3d26fb0 100644 --- a/src/box/lua/stat.c +++ b/src/box/lua/stat.c @@ -43,6 +43,7 @@ #include "box/engine.h" #include "box/vinyl.h" #include "box/sql.h" +#include "box/memtx_tx.h" #include "info/info.h" #include "lua/info.h" #include "lua/utils.h" @@ -279,6 +280,143 @@ lbox_stat_sql(struct lua_State *L) return 1; } +/** + * Push total, max and avg table onto lua stack. + */ +static void +fill_memtx_mvcc_alloc_stat_item(struct lua_State *L, uint64_t total, + uint64_t max, uint64_t avg) +{ + lua_pushstring(L, "total"); + lua_pushnumber(L, total); + lua_settable(L, -3); + + lua_pushstring(L, "max"); + lua_pushnumber(L, max); + lua_settable(L, -3); + + lua_pushstring(L, "avg"); + lua_pushnumber(L, avg); + lua_settable(L, -3); +} + +/** + * Push table name with subtables total, max and avg onto the lua stack. + */ +static void +set_memtx_mvcc_alloc_stat_item(struct lua_State *L, const char *name, + uint64_t total, uint64_t max, uint64_t avg) +{ + lua_pushstring(L, name); + lua_newtable(L); + + fill_memtx_mvcc_alloc_stat_item(L, total, max, avg); + + lua_settable(L, -3); +} + +void +lbox_stat_memtx_mvcc_set_txn_item(struct lua_State *L, + const struct memtx_tx_statistics *stats) +{ + lua_pushstring(L, "txn"); + lua_newtable(L); + for (size_t i = 0; i < TX_ALLOC_TYPE_MAX; ++i) { + size_t avg = 0; + if (stats->txn_count != 0) + avg = stats->tx_total[i] / stats->txn_count; + set_memtx_mvcc_alloc_stat_item(L, tx_alloc_type_strs[i], + stats->tx_total[i], + stats->tx_max[i], avg); + } + lua_settable(L, -3); +} + +void +lbox_stat_memtx_mvcc_set_mvcc_item(struct lua_State *L, + const struct memtx_tx_statistics *stats) +{ + lua_pushstring(L, "mvcc"); + lua_newtable(L); + for (size_t i = 0; i < MEMTX_TX_ALLOC_TYPE_MAX; ++i) { + size_t avg = 0; + if (stats->txn_count != 0) + avg = stats->memtx_tx_total[i] / stats->txn_count; + set_memtx_mvcc_alloc_stat_item(L, memtx_tx_alloc_type_strs[i], + stats->memtx_tx_total[i], + stats->memtx_tx_max[i], avg); + } + + lua_pushstring(L, "tuples"); + lua_newtable(L); + for (size_t i = 0; i < MEMTX_TX_STORY_STATUS_MAX; ++i) { + lua_pushstring(L, memtx_tx_story_status_strs[i]); + lua_newtable(L); + + lua_pushstring(L, "stories"); + lua_newtable(L); + lua_pushstring(L, "total"); + lua_pushnumber(L, stats->stories[i].total); + lua_settable(L, -3); + + lua_pushstring(L, "count"); + lua_pushnumber(L, stats->stories[i].count); + lua_settable(L, -3); + lua_settable(L, -3); + + lua_pushstring(L, "retained"); + lua_newtable(L); + lua_pushstring(L, "total"); + lua_pushnumber(L, stats->retained_tuples[i].total); + lua_settable(L, -3); + + lua_pushstring(L, "count"); + lua_pushnumber(L, stats->retained_tuples[i].count); + lua_settable(L, -3); + lua_settable(L, -3); + + lua_settable(L, -3); + } + lua_settable(L, -3); +} + +/** + * Memtx MVCC stats table's __call method. + */ +static int +lbox_stat_memtx_mvcc_call(struct lua_State *L) +{ + struct memtx_tx_statistics stats; + memtx_tx_statistics_collect(&stats); + lbox_stat_memtx_mvcc_set_txn_item(L, &stats); + lbox_stat_memtx_mvcc_set_mvcc_item(L, &stats); + lua_settable(L, -3); + + return 1; +} + +/** + * Memtx MVCC stats table's __index method. + */ +static int +lbox_stat_memtx_mvcc_index(struct lua_State *L) +{ + const char *key = luaL_checkstring(L, -1); + struct memtx_tx_statistics stats; + memtx_tx_statistics_collect(&stats); + lua_newtable(L); + if (strcmp("txn", key) == 0) { + lbox_stat_memtx_mvcc_set_txn_item(L, &stats); + return 1; + } + if (strcmp("mvcc", key) == 0) { + lbox_stat_memtx_mvcc_set_mvcc_item(L, &stats); + return 1; + } + lua_pop(L, -1); + return 0; +} + static const struct luaL_Reg lbox_stat_meta [] = { {"__index", lbox_stat_index}, {"__call", lbox_stat_call}, @@ -297,6 +435,12 @@ static const struct luaL_Reg lbox_stat_net_thread_meta [] = { {NULL, NULL} }; +static const struct luaL_Reg lbox_stat_memtx_mvcc_meta[] = { + {"__index", lbox_stat_memtx_mvcc_index }, + {"__call", lbox_stat_memtx_mvcc_call }, + {NULL, NULL} +}; + /** Initialize box.stat package. */ void box_lua_stat_init(struct lua_State *L) @@ -332,5 +476,17 @@ box_lua_stat_init(struct lua_State *L) luaL_register(L, NULL, lbox_stat_net_thread_meta); lua_setmetatable(L, -2); lua_pop(L, 1); /* stat net module */ + + static const struct luaL_Reg memtx_mvcc_statlib[] = { + {NULL, NULL} + }; + + luaL_register_module(L, "box.stat.memtx.tx", memtx_mvcc_statlib); + + lua_newtable(L); + luaL_register(L, NULL, lbox_stat_memtx_mvcc_meta); + lua_setmetatable(L, -2); + lua_pop(L, 1); /* stat tx module */ + } diff --git a/src/box/memtx_tx.c b/src/box/memtx_tx.c index dd7a9b6d24..b0156b6cef 100644 --- a/src/box/memtx_tx.c +++ b/src/box/memtx_tx.c @@ -35,7 +35,6 @@ #include <stddef.h> #include <stdint.h> -#include "txn.h" #include "schema_def.h" #include "small/mempool.h" @@ -183,6 +182,198 @@ point_hole_storage_key_equal(const struct point_hole_key *key, #define MH_SOURCE #include "salad/mhash.h" +/** + * Temporary (allocated on region) struct that stores a conflicting TX. + */ +struct memtx_tx_conflict { + /* The transaction that will conflict us upon commit. */ + struct txn *breaker; + /* The transaction that will conflicted by upon commit. */ + struct txn *victim; + /* Link in single-linked list. */ + struct memtx_tx_conflict *next; +}; + +/** + * Collect an allocation to memtx_tx_stats. + */ +static inline void +memtx_tx_stats_collect(struct memtx_tx_stats *stats, size_t size) +{ + stats->count++; + stats->total += size; +} + +/** + * Discard an allocation collected by memtx_tx_stats. + */ +static inline void +memtx_tx_stats_discard(struct memtx_tx_stats *stats, size_t size) +{ + assert(stats->count > 0); + assert(stats->total >= size); + + stats->count--; + stats->total -= size; +} + +/** + * Collect allocation statistics. + */ +static inline void +memtx_tx_track_allocation(struct txn *txn, size_t size, + enum memtx_tx_alloc_type alloc_type) +{ + assert(alloc_type < MEMTX_TX_ALLOC_TYPE_MAX); + txn->memtx_tx_alloc_stats[alloc_type] += size; +} + +/** + * Collect deallocation statistics. + */ +static inline void +memtx_tx_track_deallocation(struct txn *txn, size_t size, + enum memtx_tx_alloc_type alloc_type) +{ + assert(alloc_type < MEMTX_TX_ALLOC_TYPE_MAX); + assert(txn->memtx_tx_alloc_stats[alloc_type] >= size); + txn->memtx_tx_alloc_stats[alloc_type] -= size; +} + +/** + * A wrapper over mempool. + * Use it instead of mempool to track allocations! + */ +struct memtx_tx_mempool { + /** + * Wrapped mempool. + */ + struct mempool pool; + /** + * Each allocation is accounted with this type. + */ + enum memtx_tx_alloc_type alloc_type; +}; + +static inline void +memtx_tx_mempool_create(struct memtx_tx_mempool *mempool, uint32_t objsize, + enum memtx_tx_alloc_type alloc_type) +{ + mempool_create(&mempool->pool, cord_slab_cache(), objsize); + mempool->alloc_type = alloc_type; +} + +static inline void +memtx_tx_mempool_destroy(struct memtx_tx_mempool *mempool) +{ + mempool_destroy(&mempool->pool); + mempool->alloc_type = MEMTX_TX_ALLOC_TYPE_MAX; +} + +void * +memtx_tx_mempool_alloc(struct txn *txn, struct memtx_tx_mempool *mempool) +{ + void *allocation = mempool_alloc(&mempool->pool); + if (allocation != NULL) { + uint32_t size = mempool->pool.objsize; + memtx_tx_track_allocation(txn, size, mempool->alloc_type); + } + return allocation; +} + +void +memtx_tx_mempool_free(struct txn *txn, struct memtx_tx_mempool *mempool, void *ptr) +{ + uint32_t size = mempool->pool.objsize; + memtx_tx_track_deallocation(txn, size, mempool->alloc_type); + mempool_free(&mempool->pool, ptr); +} + +/** + * Choose memtx_tx_alloc_type for alloc_obj. + */ +static inline enum memtx_tx_alloc_type +memtx_tx_region_object_to_type(enum memtx_tx_alloc_object alloc_obj) +{ + enum memtx_tx_alloc_type alloc_type = MEMTX_TX_ALLOC_TYPE_MAX; + switch (alloc_obj) { + case MEMTX_TX_OBJECT_CONFLICT: + alloc_type = MEMTX_TX_ALLOC_CONFLICT; + break; + + case MEMTX_TX_OBJECT_CONFLICT_TRACKER: + case MEMTX_TX_OBJECT_READ_TRACKER: + alloc_type = MEMTX_TX_ALLOC_TRACKER; + break; + default: + unreachable(); + }; + assert(alloc_type < MEMTX_TX_ALLOC_TYPE_MAX); + return alloc_type; +} + +/** + * Alloc object on region. Pass object as enum memtx_tx_alloc_object. + * Use this method to track txn's allocations! + */ +static inline void * +memtx_tx_region_alloc_object(struct txn *txn, + enum memtx_tx_alloc_object alloc_obj) +{ + size_t size = 0; + void *alloc = NULL; + enum memtx_tx_alloc_type alloc_type = + memtx_tx_region_object_to_type(alloc_obj); + switch (alloc_obj) { + case MEMTX_TX_OBJECT_CONFLICT: + alloc = region_alloc_object(&txn->region, + struct memtx_tx_conflict, &size); + break; + case MEMTX_TX_OBJECT_CONFLICT_TRACKER: + alloc = region_alloc_object(&txn->region, + struct tx_conflict_tracker, &size); + break; + case MEMTX_TX_OBJECT_READ_TRACKER: + alloc = region_alloc_object(&txn->region, + struct tx_read_tracker, &size); + break; + default: + unreachable(); + } + assert(alloc_type < MEMTX_TX_ALLOC_TYPE_MAX); + if (alloc != NULL) + memtx_tx_track_allocation(txn, size, alloc_type); + return alloc; +} + +/** + * Tx_region method for allocations of arbitrary size. + * You must pass allocation type explicitly to categorize an allocation. + * Use this method to track allocations! + */ +static inline void * +memtx_tx_region_alloc(struct txn *txn, size_t size, + enum memtx_tx_alloc_type alloc_type) +{ + void *allocation = region_alloc(&txn->region, size); + if (allocation != NULL) + memtx_tx_track_allocation(txn, size, alloc_type); + return allocation; +} + +/** String representation of enum memtx_tx_alloc_type. */ +const char *memtx_tx_alloc_type_strs[MEMTX_TX_ALLOC_TYPE_MAX] = { + "trackers", + "conflicts", +}; + +/** String representation of enum memtx_tx_story_status. */ +const char *memtx_tx_story_status_strs[MEMTX_TX_STORY_STATUS_MAX] = { + "used", + "read_view", + "tracking", +}; + struct tx_manager { /** @@ -191,22 +382,28 @@ struct tx_manager * so the list is ordered by rv_psn. */ struct rlist read_view_txs; - /** Mempools for tx_story objects with different index count. */ + /** + * Mempools for tx_story objects with different index count. + * It's the only case when we use bare mempool in memtx_tx because + * we cannot account story allocation to any particular txn. + */ struct mempool memtx_tx_story_pool[BOX_INDEX_MAX]; /** Hash table tuple -> memtx_story of that tuple. */ struct mh_history_t *history; /** Mempool for point_hole_item objects. */ - struct mempool point_hole_item_pool; + struct memtx_tx_mempool point_hole_item_pool; /** Hash table that hold point selects with empty result. */ struct mh_point_holes_t *point_holes; /** Count of elements in point_holes table. */ size_t point_holes_size; /** Mempool for gap_item objects. */ - struct mempool gap_item_mempoool; + struct memtx_tx_mempool gap_item_mempoool; /** Mempool for full_scan_item objects. */ - struct mempool full_scan_item_mempool; + struct memtx_tx_mempool full_scan_item_mempool; /** List of all memtx_story objects. */ struct rlist all_stories; + struct memtx_tx_stats story_stats[MEMTX_TX_STORY_STATUS_MAX]; + struct memtx_tx_stats retained_tuple_stats[MEMTX_TX_STORY_STATUS_MAX]; /** Iterator that sequentially traverses all memtx_story objects. */ struct rlist *traverse_all_stories; /** The list containing all transactions. */ @@ -241,18 +438,22 @@ memtx_tx_manager_init() cord_slab_cache(), item_size); } txm.history = mh_history_new(); - mempool_create(&txm.point_hole_item_pool, - cord_slab_cache(), sizeof(struct point_hole_item)); + memtx_tx_mempool_create(&txm.point_hole_item_pool, + sizeof(struct point_hole_item), + MEMTX_TX_ALLOC_TRACKER); txm.point_holes = mh_point_holes_new(); - mempool_create(&txm.gap_item_mempoool, - cord_slab_cache(), sizeof(struct gap_item)); - mempool_create(&txm.full_scan_item_mempool, - cord_slab_cache(), sizeof(struct full_scan_item)); + memtx_tx_mempool_create(&txm.gap_item_mempoool, + sizeof(struct gap_item), + MEMTX_TX_ALLOC_TRACKER); + memtx_tx_mempool_create(&txm.full_scan_item_mempool, + sizeof(struct full_scan_item), + MEMTX_TX_ALLOC_TRACKER); txm.point_holes_size = 0; rlist_create(&txm.all_stories); rlist_create(&txm.all_txs); txm.traverse_all_stories = &txm.all_stories; txm.must_do_gc_steps = 0; + memset(&txm.story_stats, 0, sizeof(txm.story_stats)); } void @@ -261,16 +462,57 @@ memtx_tx_manager_free() for (size_t i = 0; i < BOX_INDEX_MAX; i++) mempool_destroy(&txm.memtx_tx_story_pool[i]); mh_history_delete(txm.history); - mempool_destroy(&txm.point_hole_item_pool); + memtx_tx_mempool_destroy(&txm.point_hole_item_pool); mh_point_holes_delete(txm.point_holes); - mempool_destroy(&txm.gap_item_mempoool); - mempool_destroy(&txm.full_scan_item_mempool); + memtx_tx_mempool_destroy(&txm.gap_item_mempoool); + memtx_tx_mempool_destroy(&txm.full_scan_item_mempool); } void +memtx_tx_statistics_collect(struct memtx_tx_statistics *stats) +{ + memset(stats, 0, sizeof(*stats)); + for (size_t i = 0; i < MEMTX_TX_STORY_STATUS_MAX; ++i) { + stats->stories[i] = txm.story_stats[i]; + stats->retained_tuples[i] = txm.retained_tuple_stats[i]; + } + if (rlist_empty(&txm.all_txs)) { + return; + } + struct txn *txn; + size_t txn_count = 0; + rlist_foreach_entry(txn, &txm.all_txs, in_all_txs) { + txn_count++; + for (size_t i = 0; i < MEMTX_TX_ALLOC_TYPE_MAX; ++i) { + size_t txn_stat = txn->memtx_tx_alloc_stats[i]; + stats->memtx_tx_total[i] += txn_stat; + if (txn_stat > stats->memtx_tx_max[i]) + stats->memtx_tx_max[i] = txn_stat; + } + for (size_t i = 0; i < TX_ALLOC_TYPE_MAX; ++i) { + size_t txn_stat = txn->alloc_stats[i]; + stats->tx_total[i] += txn_stat; + if (txn_stat > stats->tx_max[i]) + stats->tx_max[i] = txn_stat; + } + } + stats->txn_count = txn_count; +} + +int memtx_tx_register_tx(struct txn *tx) { + int size = 0; + tx->memtx_tx_alloc_stats = + region_alloc_array(&tx->region, + typeof(*tx->memtx_tx_alloc_stats), + MEMTX_TX_ALLOC_TYPE_MAX, &size); + if (tx->memtx_tx_alloc_stats == NULL) + return -1; + memset(tx->memtx_tx_alloc_stats, 0, + sizeof(*tx->memtx_tx_alloc_stats) * MEMTX_TX_ALLOC_TYPE_MAX); rlist_add_tail(&txm.all_txs, &tx->in_all_txs); + return 0; } void @@ -328,12 +570,11 @@ memtx_tx_cause_conflict(struct txn *breaker, struct txn *victim) rlist_del(&tracker->in_conflict_list); rlist_del(&tracker->in_conflicted_by_list); } else { - size_t size; - tracker = region_alloc_object(&victim->region, - struct tx_conflict_tracker, - &size); + tracker = + memtx_tx_region_alloc_object( + victim, MEMTX_TX_OBJECT_CONFLICT_TRACKER); if (tracker == NULL) { - diag_set(OutOfMemory, size, "tx region", + diag_set(OutOfMemory, sizeof(*tracker), "tx region", "conflict_tracker"); return -1; } @@ -416,12 +657,108 @@ memtx_tx_handle_conflict(struct txn *breaker, struct txn *victim) } } +/** + * Calculate size of story with its links. + */ +static inline size_t +memtx_story_size(struct memtx_story *story) +{ + struct mempool *pool = &txm.memtx_tx_story_pool[story->index_count]; + return pool->objsize; +} + +/** + * Notify memory manager that a tuple referenced by @a story + * was replaced from primary key and that is why @a story + * is the only reason why the tuple cannot be deleted. + */ +static inline void +memtx_tx_story_track_retained_tuple(struct memtx_story *story) +{ + assert(!story->tuple_is_retained); + assert(story->status < MEMTX_TX_STORY_STATUS_MAX); + + story->tuple_is_retained = true; + struct memtx_tx_stats *stats = &txm.retained_tuple_stats[story->status]; + size_t tuplesize = tuple_size(story->tuple); + memtx_tx_stats_collect(stats, tuplesize); +} + +/** + * Notify memory manager that a tuple referenced by @a story + * was placed to primary key. + */ +static inline void +memtx_tx_story_untrack_retained_tuple(struct memtx_story *story) +{ + assert(story->tuple_is_retained); + assert(story->status < MEMTX_TX_STORY_STATUS_MAX); + + story->tuple_is_retained = false; + struct memtx_tx_stats *stats = &txm.retained_tuple_stats[story->status]; + size_t tuplesize = tuple_size(story->tuple); + memtx_tx_stats_discard(stats, tuplesize); +} + +/** Set status of story (see memtx_tx_story_status) */ +static inline void +memtx_tx_story_set_status(struct memtx_story *story, + enum memtx_tx_story_status new_status) +{ + assert(story->status < MEMTX_TX_STORY_STATUS_MAX); + enum memtx_tx_story_status old_status = story->status; + if (old_status == new_status) + return; + story->status = new_status; + struct memtx_tx_stats *old_story_stats = &txm.story_stats[old_status]; + struct memtx_tx_stats *new_story_stats = &txm.story_stats[new_status]; + size_t story_size = memtx_story_size(story); + memtx_tx_stats_discard(old_story_stats, story_size); + memtx_tx_stats_collect(new_story_stats, story_size); + if (story->tuple_is_retained) { + size_t tuplesize = tuple_size(story->tuple); + struct memtx_tx_stats *old = + &txm.retained_tuple_stats[old_status]; + struct memtx_tx_stats *new = + &txm.retained_tuple_stats[new_status]; + memtx_tx_stats_discard(old, tuplesize); + memtx_tx_stats_collect(new, tuplesize); + } +} + +/** + * Use this method to ref tuple that belongs to @a story + * by primary index. Do not use bare tuple_ref!!! + */ +static inline void +memtx_tx_ref_to_primary(struct memtx_story *story) +{ + assert(story != NULL); + assert(story->tuple_is_retained); + tuple_ref(story->tuple); + memtx_tx_story_untrack_retained_tuple(story); +} + +/** + * Use this method to unref tuple that belongs to @a story + * from primary index. Do not use bare tuple_unref!!! + */ +static inline void +memtx_tx_unref_from_primary(struct memtx_story *story) +{ + assert(story != NULL); + tuple_unref(story->tuple); + if (!story->tuple_is_retained) + memtx_tx_story_track_retained_tuple(story); +} + /** * Create a new story and link it with the @a tuple. * @return story on success, NULL on error (diag is set). */ static struct memtx_story * -memtx_tx_story_new(struct space *space, struct tuple *tuple) +memtx_tx_story_new(struct space *space, struct tuple *tuple, + bool tuple_is_referenced_to_pk) { txm.must_do_gc_steps += TX_MANAGER_GC_STEPS_SIZE; assert(!tuple->is_dirty); @@ -429,10 +766,8 @@ memtx_tx_story_new(struct space *space, struct tuple *tuple) assert(index_count < BOX_INDEX_MAX); struct mempool *pool = &txm.memtx_tx_story_pool[index_count]; struct memtx_story *story = (struct memtx_story *) mempool_alloc(pool); + size_t item_size = pool->objsize; if (story == NULL) { - size_t item_size = sizeof(struct memtx_story) + - index_count * - sizeof(struct memtx_story_link); diag_set(OutOfMemory, item_size, "mempool_alloc", "story"); return NULL; } @@ -444,7 +779,12 @@ memtx_tx_story_new(struct space *space, struct tuple *tuple) mh_history_put(txm.history, put_story, &empty, 0); tuple->is_dirty = true; tuple_ref(tuple); - + story->status = MEMTX_TX_STORY_USED; + struct memtx_tx_stats *stats = &txm.story_stats[story->status]; + memtx_tx_stats_collect(stats, item_size); + story->tuple_is_retained = false; + if (!tuple_is_referenced_to_pk) + memtx_tx_story_track_retained_tuple(story); story->space = space; story->index_count = index_count; story->add_stmt = NULL; @@ -465,6 +805,11 @@ memtx_tx_story_new(struct space *space, struct tuple *tuple) static void memtx_tx_story_delete(struct memtx_story *story) { + memtx_tx_stats_discard(&txm.story_stats[story->status], + memtx_story_size(story)); + if (story->tuple_is_retained) + memtx_tx_story_untrack_retained_tuple(story); + if (story->add_stmt != NULL) { assert(story->add_stmt->add_story == story); story->add_stmt->add_story = NULL; @@ -690,8 +1035,8 @@ memtx_tx_story_link_top(struct memtx_story *new_top, * index and dereference the tuple that was removed from it. */ if (idx == 0) { - tuple_ref(new_top->tuple); - tuple_unref(old_top->tuple); + memtx_tx_ref_to_primary(new_top); + memtx_tx_unref_from_primary(old_top); } } @@ -772,8 +1117,8 @@ memtx_tx_story_unlink_top(struct memtx_story *story, uint32_t idx) */ if (idx == 0) { if (old_story != NULL) - tuple_ref(old_story->tuple); - tuple_unref(story->tuple); + memtx_tx_ref_to_primary(old_story); + memtx_tx_unref_from_primary(story); } memtx_tx_story_unlink_top_light(story, idx); @@ -895,7 +1240,7 @@ memtx_tx_story_full_unlink(struct memtx_story *story) * Once removed it must be unreferenced. */ if (i == 0) - tuple_unref(story->tuple); + memtx_tx_unref_from_primary(story); } memtx_tx_story_unlink(story, link->older_story, i); @@ -944,18 +1289,26 @@ memtx_tx_story_gc_step() in_all_stories); txm.traverse_all_stories = txm.traverse_all_stories->next; + /** + * The order in which conditions are checked is important, + * see description of enum memtx_tx_story_status. + */ if (story->add_stmt != NULL || story->del_stmt != NULL || !rlist_empty(&story->reader_list)) { + memtx_tx_story_set_status(story, MEMTX_TX_STORY_USED); /* The story is used directly by some transactions. */ return; } if (story->add_psn >= lowest_rv_psn || story->del_psn >= lowest_rv_psn) { + memtx_tx_story_set_status(story, MEMTX_TX_STORY_READ_VIEW); /* The story can be used by a read view. */ return; } for (uint32_t i = 0; i < story->index_count; i++) { if (!rlist_empty(&story->link[i].nearby_gaps)) { + memtx_tx_story_set_status(story, + MEMTX_TX_STORY_TRACK_GAP); /* The story is used for gap tracking. */ return; } @@ -1043,19 +1396,6 @@ memtx_tx_story_is_visible(struct memtx_story *story, struct txn *txn, return false; } -/** - * Temporary (allocated on region) struct that stores a conflicting TX. - */ -struct memtx_tx_conflict -{ - /* The transaction that will conflict us upon commit. */ - struct txn *breaker; - /* The transaction that will conflicted by upon commit. */ - struct txn *victim; - /* Link in single-linked list. */ - struct memtx_tx_conflict *next; -}; - /** * Save @a breaker in list with head @a conflicts_head. New list node is * allocated on a region of @a breaker. @@ -1065,12 +1405,12 @@ static int memtx_tx_save_conflict(struct txn *breaker, struct txn *victim, struct memtx_tx_conflict **conflicts_head) { - size_t size; struct memtx_tx_conflict *next_conflict; - next_conflict = region_alloc_object(&breaker->region, - struct memtx_tx_conflict, &size); + next_conflict = memtx_tx_region_alloc_object(breaker, + MEMTX_TX_OBJECT_CONFLICT); if (next_conflict == NULL) { - diag_set(OutOfMemory, size, "txn_region", "txn conflict"); + diag_set(OutOfMemory, sizeof(*next_conflict), "txn_region", + "txn conflict"); return -1; } next_conflict->breaker = breaker; @@ -1497,14 +1837,22 @@ memtx_tx_history_add_insert_stmt(struct txn_stmt *stmt, if (rc != 0) goto fail; - /* Create add_story and replaced_story if necessary. */ - add_story = memtx_tx_story_new(space, new_tuple); + /* + * Create add_story and replaced_story if necessary. + * Note that despite the tuple is already in pk, + * it is not referenced to it, so we pass false as the last argument. + */ + add_story = memtx_tx_story_new(space, new_tuple, false); if (add_story == NULL) goto fail; memtx_tx_story_link_added_by(add_story, stmt); if (replaced != NULL && !replaced->is_dirty) { - created_story = memtx_tx_story_new(space, replaced); + /* + * Note that despite the tuple is not in pk, + * it is referenced to it, so we pass true as the last argument. + */ + created_story = memtx_tx_story_new(space, replaced, true); if (created_story == NULL) goto fail; replaced_story = created_story; @@ -1564,9 +1912,14 @@ memtx_tx_history_add_insert_stmt(struct txn_stmt *stmt, * in primary index must be referenced (a replaces tuple must * be dereferenced). */ - tuple_ref(new_tuple); - if (directly_replaced[0] != NULL) - tuple_unref(directly_replaced[0]); + assert(add_story == memtx_tx_story_get(new_tuple)); + + memtx_tx_ref_to_primary(add_story); + if (directly_replaced[0] != NULL) { + assert(replaced_story == + memtx_tx_story_get(directly_replaced[0])); + memtx_tx_unref_from_primary(replaced_story); + } } *result = old_tuple; @@ -1624,10 +1977,17 @@ memtx_tx_history_add_delete_stmt(struct txn_stmt *stmt, if (old_tuple->is_dirty) { del_story = memtx_tx_story_get(old_tuple); } else { - del_story = memtx_tx_story_new(space, old_tuple); + assert(stmt->txn != NULL); + /* + * The tuple is not dirty therefore it is placed in pk + * and is referenced to it. + */ + del_story = memtx_tx_story_new(space, old_tuple, true); if (del_story == NULL) return -1; } + if (!del_story->tuple_is_retained) + memtx_tx_story_track_retained_tuple(del_story); memtx_tx_story_link_deleted_by(del_story, stmt); *result = old_tuple; @@ -2071,7 +2431,7 @@ memtx_tx_delete_gap(struct gap_item *item) { rlist_del(&item->in_gap_list); rlist_del(&item->in_nearby_gaps); - mempool_free(&txm.gap_item_mempoool, item); + memtx_tx_mempool_free(item->txn, &txm.gap_item_mempoool, item); } static void @@ -2079,7 +2439,7 @@ memtx_tx_full_scan_item_delete(struct full_scan_item *item) { rlist_del(&item->in_full_scan_list); rlist_del(&item->in_full_scans); - mempool_free(&txm.full_scan_item_mempool, item); + memtx_tx_mempool_free(item->txn, &txm.full_scan_item_mempool, item); } void @@ -2134,12 +2494,12 @@ static struct tx_read_tracker * tx_read_tracker_new(struct txn *reader, struct memtx_story *story, uint64_t index_mask) { - size_t sz; struct tx_read_tracker *tracker; - tracker = region_alloc_object(&reader->region, - struct tx_read_tracker, &sz); + tracker = memtx_tx_region_alloc_object(reader, + MEMTX_TX_OBJECT_READ_TRACKER); if (tracker == NULL) { - diag_set(OutOfMemory, sz, "tx region", "read_tracker"); + diag_set(OutOfMemory, sizeof(*tracker), "tx region", + "read_tracker"); return NULL; } tracker->reader = reader; @@ -2217,7 +2577,12 @@ memtx_tx_track_read(struct txn *txn, struct space *space, struct tuple *tuple) struct memtx_story *story = memtx_tx_story_get(tuple); return memtx_tx_track_read_story(txn, space, story, UINT64_MAX); } else { - struct memtx_story *story = memtx_tx_story_new(space, tuple); + /* + * The tuple is not dirty therefore it is placed in pk + * and is referenced to it. + */ + struct memtx_story *story = + memtx_tx_story_new(space, tuple, true); if (story == NULL) return -1; struct tx_read_tracker *tracker; @@ -2239,9 +2604,8 @@ static int point_hole_storage_new(struct index *index, const char *key, size_t key_len, struct txn *txn) { - struct mempool *pool = &txm.point_hole_item_pool; - struct point_hole_item *object = - (struct point_hole_item *) mempool_alloc(pool); + struct memtx_tx_mempool *pool = &txm.point_hole_item_pool; + struct point_hole_item *object = memtx_tx_mempool_alloc(txn, pool); if (object == NULL) { diag_set(OutOfMemory, sizeof(*object), "mempool_alloc", "point_hole_item"); @@ -2255,9 +2619,10 @@ point_hole_storage_new(struct index *index, const char *key, if (key_len <= sizeof(object->short_key)) { object->key = object->short_key; } else { - object->key = (char *)region_alloc(&txn->region, key_len); + object->key = memtx_tx_region_alloc(txn, key_len, + MEMTX_TX_ALLOC_TRACKER); if (object->key == NULL) { - mempool_free(pool, object); + memtx_tx_mempool_free(txn, pool, object); diag_set(OutOfMemory, key_len, "tx region", "point key"); return -1; @@ -2333,8 +2698,8 @@ point_hole_storage_delete(struct point_hole_item *object) txm.point_holes_size--; } rlist_del(&object->in_point_holes_list); - struct mempool *pool = &txm.point_hole_item_pool; - mempool_free(pool, object); + struct memtx_tx_mempool *pool = &txm.point_hole_item_pool; + memtx_tx_mempool_free(object->txn, pool, object); } /** @@ -2362,8 +2727,8 @@ static struct gap_item * memtx_tx_gap_item_new(struct txn *txn, enum iterator_type type, const char *key, uint32_t part_count) { - struct gap_item *item = (struct gap_item *) - mempool_alloc(&txm.gap_item_mempoool); + struct gap_item *item = + memtx_tx_mempool_alloc(txn, &txm.gap_item_mempoool); if (item == NULL) { diag_set(OutOfMemory, sizeof(*item), "mempool_alloc", "gap"); return NULL; @@ -2381,9 +2746,11 @@ memtx_tx_gap_item_new(struct txn *txn, enum iterator_type type, } else if (item->key_len <= sizeof(item->short_key)) { item->key = item->short_key; } else { - item->key = (char *)region_alloc(&txn->region, item->key_len); + item->key = memtx_tx_region_alloc(txn, item->key_len, + MEMTX_TX_ALLOC_TRACKER); if (item->key == NULL) { - mempool_free(&txm.gap_item_mempoool, item); + memtx_tx_mempool_free(txn, + &txm.gap_item_mempoool, item); diag_set(OutOfMemory, item->key_len, "tx region", "point key"); return NULL; @@ -2421,9 +2788,15 @@ memtx_tx_track_gap_slow(struct txn *txn, struct space *space, struct index *inde if (successor->is_dirty) { story = memtx_tx_story_get(successor); } else { - story = memtx_tx_story_new(space, successor); + /* + * The tuple is not dirty therefore it is placed in pk + * and is referenced to it. + */ + story = memtx_tx_story_new(space, successor, true); if (story == NULL) { - mempool_free(&txm.gap_item_mempoool, item); + memtx_tx_mempool_free(txn, + &txm.gap_item_mempoool, + item); return -1; } } @@ -2440,8 +2813,9 @@ memtx_tx_track_gap_slow(struct txn *txn, struct space *space, struct index *inde static struct full_scan_item * memtx_tx_full_scan_item_new(struct txn *txn) { - struct full_scan_item *item = (struct full_scan_item *) - mempool_alloc(&txm.full_scan_item_mempool); + struct full_scan_item *item = + (struct full_scan_item *)memtx_tx_mempool_alloc(txn, + &txm.full_scan_item_mempool); if (item == NULL) { diag_set(OutOfMemory, sizeof(*item), "mempool_alloc", "full_scan_item"); diff --git a/src/box/memtx_tx.h b/src/box/memtx_tx.h index d8ceac31cf..dc06fcc997 100644 --- a/src/box/memtx_tx.h +++ b/src/box/memtx_tx.h @@ -34,8 +34,7 @@ #include "index.h" #include "tuple.h" #include "space.h" - -#include "small/rlist.h" +#include "txn.h" #if defined(__cplusplus) extern "C" { @@ -48,6 +47,56 @@ extern "C" { */ extern bool memtx_tx_manager_use_mvcc_engine; +enum memtx_tx_alloc_type { + MEMTX_TX_ALLOC_TRACKER = 0, + MEMTX_TX_ALLOC_CONFLICT = 1, + MEMTX_TX_ALLOC_TYPE_MAX = 2, +}; + +extern const char *memtx_tx_alloc_type_strs[]; + +/** + * Memtx_tx allocation objects for memtx_tx_region and memtx_tx_mempool. + */ +enum memtx_tx_alloc_object { + /** + * Object of type struct memtx_tx_conflict. + */ + MEMTX_TX_OBJECT_CONFLICT = 0, + /** + * Object of type struct tx_conflict_tracker. + */ + MEMTX_TX_OBJECT_CONFLICT_TRACKER = 1, + /** + * Object of type struct tx_read_tracker. + */ + MEMTX_TX_OBJECT_READ_TRACKER = 2, + MEMTX_TX_OBJECT_MAX = 3, +}; + +/** + * Status of story. Describes the reason why it is not deleted. + * In the case when story fits several statuses at once, status with + * least value is chosen. + */ +enum memtx_tx_story_status { + /** + * The story is used directly by some transactions. + */ + MEMTX_TX_STORY_USED = 0, + /** + * The story can be used by a read view. + */ + MEMTX_TX_STORY_READ_VIEW = 1, + /** + * The story is used for gap tracking. + */ + MEMTX_TX_STORY_TRACK_GAP = 2, + MEMTX_TX_STORY_STATUS_MAX = 3, +}; + +extern const char *memtx_tx_story_status_strs[]; + /** * Record that links two transactions, breaker and victim. * See memtx_tx_cause_conflict for details. @@ -151,6 +200,17 @@ struct memtx_story { * Number of indexes in this space - and the count of link[]. */ uint32_t index_count; + /** + * Status of story, describes the reason why story cannot be deleted. + * It is initialized in memtx_story constructor and is changed only in + * memtx_tx_story_gc. + */ + enum memtx_tx_story_status status; + /** + * Flag is set when @a tuple is not placed in primary key and + * the story is the only reason why @a tuple cannot be deleted. + */ + bool tuple_is_retained; /** * Link with older and newer stories (and just tuples) for each * index respectively. @@ -167,7 +227,34 @@ struct memtx_tx_snapshot_cleaner { struct mh_snapshot_cleaner_t *ht; }; +/** + * Cell of stats with total and count statistics. + */ +struct memtx_tx_stats { + /* Total over all measurements. */ + size_t total; + /* Number of measured objects. */ + size_t count; +}; + +/** + * Memory statistics of memtx mvcc engine. + */ +struct memtx_tx_statistics { + struct memtx_tx_stats stories[MEMTX_TX_STORY_STATUS_MAX]; + struct memtx_tx_stats retained_tuples[MEMTX_TX_STORY_STATUS_MAX]; + size_t memtx_tx_total[MEMTX_TX_ALLOC_TYPE_MAX]; + size_t memtx_tx_max[MEMTX_TX_ALLOC_TYPE_MAX]; + size_t tx_total[TX_ALLOC_TYPE_MAX]; + size_t tx_max[TX_ALLOC_TYPE_MAX]; + /* Number of txns registered in memtx transaction manager. */ + size_t txn_count; +}; + void +memtx_tx_statistics_collect(struct memtx_tx_statistics *stats); + +int memtx_tx_register_tx(struct txn *tx); /** diff --git a/src/box/txn.c b/src/box/txn.c index 999cf08aea..f5060c69eb 100644 --- a/src/box/txn.c +++ b/src/box/txn.c @@ -544,7 +544,6 @@ txn_begin(void) txn->fk_deferred_count = 0; txn->is_schema_changed = false; rlist_create(&txn->savepoints); - memtx_tx_register_tx(txn); txn->fiber = NULL; txn->timeout = TIMEOUT_INFINITY; txn->rollback_timer = NULL; @@ -559,6 +558,11 @@ txn_begin(void) * if they are not supported. */ txn_set_flags(txn, TXN_CAN_YIELD); + int rc = memtx_tx_register_tx(txn); + if (rc == -1) { + txn_free(txn); + return NULL; + } return txn; } diff --git a/src/box/txn.h b/src/box/txn.h index cc78abe67b..a0fb38953f 100644 --- a/src/box/txn.h +++ b/src/box/txn.h @@ -418,6 +418,10 @@ struct txn { * Allocation statistics. */ uint32_t alloc_stats[TX_ALLOC_TYPE_MAX]; + /** + * Memtx tx allocation statistics. + */ + uint32_t *memtx_tx_alloc_stats; /** * A sequentially growing transaction id, assigned when * a transaction is initiated. Used to identify diff --git a/test/box-luatest/gh_6150_memtx_tx_memory_monitoring_test.lua b/test/box-luatest/gh_6150_memtx_tx_memory_monitoring_test.lua new file mode 100644 index 0000000000..df404d54c4 --- /dev/null +++ b/test/box-luatest/gh_6150_memtx_tx_memory_monitoring_test.lua @@ -0,0 +1,436 @@ +local server = require('test.luatest_helpers.server') +local t = require('luatest') +local g = t.group() + +-- Sizes of objects from transaction manager. +-- Please update them, if you changed the relevant structures. +local SIZE_OF_STMT = 136 +-- Size of story with one link (for spaces with 1 index). +local SIZE_OF_STORY = 152 +-- Size of tuple with 2 number fields +local SIZE_OF_TUPLE = 9 +-- Size of xrow for tuples with 2 number fields +local SIZE_OF_XROW = 147 +local SIZE_OF_CONFLICT = 24 +-- Tracker can allocate additional memory, be careful! +local SIZE_OF_READ_TRACKER = 56 +local SIZE_OF_CONFLICT_TRACKER = 48 +local SIZE_OF_POINT_TRACKER = 88 +local SIZE_OF_GAP_TRACKER = 80 + +local current_stat = {} + +local function table_apply_change(table, related_changes) + for k, v in pairs(related_changes) do + if type(v) ~= 'table' then + table[k] = table[k] + v + else + table_apply_change(table[k], v) + end + end +end + +local function table_values_are_zeros(table) + for _, v in pairs(table) do + if type(v) ~= 'table' then + if v ~= 0 then + return false + end + else + if not table_values_are_zeros(v) then + return false + end + end + end + return true +end + +local function tx_gc(server, steps, related_changes) + server:eval('box.internal.memtx_tx_gc(' .. steps .. ')') + if related_changes then + table_apply_change(current_stat, related_changes) + end + assert(table.equals(current_stat, server:eval('return box.stat.memtx.tx()'))) +end + +local function tx_step(server, txn_name, op, related_changes) + server:eval(txn_name .. '("' .. op .. '")') + if related_changes then + table_apply_change(current_stat, related_changes) + end + assert(table.equals(current_stat, server:eval('return box.stat.memtx.tx()'))) +end + +g.before_each(function() + g.server = server:new{ + alias = 'default', + box_cfg = {memtx_use_mvcc_engine = true} + } + g.server:start() + + g.server:eval('txn_proxy = require("test.box.lua.txn_proxy")') + g.server:eval('s = box.schema.space.create("test")') + g.server:eval('s:create_index("pk")') + -- Clear txm before test + g.server:eval('box.internal.memtx_tx_gc(100)') + -- CREATING CURRENT STAT + current_stat = g.server:eval('return box.stat.memtx.tx()') + -- Check if txm use no memory + assert(table_values_are_zeros(current_stat)) +end) + +g.after_each(function() + -- Check if there is no memory occupied by txm + g.server:drop() +end) + +g.test_simple = function() + g.server:eval('tx1 = txn_proxy.new()') + g.server:eval('tx2 = txn_proxy.new()') + g.server:eval('tx1:begin()') + g.server:eval('tx2:begin()') + local diff = { + ["txn"] = { + ["statements"] = { + ["avg"] = math.floor(SIZE_OF_STMT / 2), + ["total"] = SIZE_OF_STMT, + ["max"] = SIZE_OF_STMT, + }, + ["system"] = { + ["total"] = SIZE_OF_XROW, + ["max"] = SIZE_OF_XROW, + ["avg"] = math.floor(SIZE_OF_XROW / 2), + } + }, + ["mvcc"] = { + ["tuples"] = { + ["used"] = { + ["stories"] = { + ["total"] = SIZE_OF_STORY, + ["count"] = 1, + } + } + } + } + } + tx_step(g.server, 'tx1', "s:replace{1, 1}", diff) + diff = { + ["txn"] = { + ["statements"] = { + ["avg"] = math.floor(SIZE_OF_STMT / 2), + ["total"] = SIZE_OF_STMT, + }, + ["system"] = { + ["total"] = SIZE_OF_XROW, + ["avg"] = math.floor(SIZE_OF_XROW / 2) + 1, + } + }, + ["mvcc"] = { + ["tuples"] = { + ["used"] = { + ["stories"] = { + ["total"] = SIZE_OF_STORY, + ["count"] = 1, + }, + ["retained"] = { + ["total"] = SIZE_OF_TUPLE, + ["count"] = 1, + }, + } + } + } + } + tx_step(g.server, 'tx2', "s:replace{1, 2}", diff) + diff = { + ["txn"] = { + ["statements"] = { + ["total"] = SIZE_OF_STMT, + ["avg"] = math.floor(SIZE_OF_STMT / 2), + ["max"] = SIZE_OF_STMT, + }, + ["system"] = { + ["total"] = SIZE_OF_XROW, + ["avg"] = math.floor(SIZE_OF_XROW / 2), + ["max"] = SIZE_OF_XROW, + } + }, + ["mvcc"] = { + ["tuples"] = { + ["used"] = { + ["stories"] = { + ["total"] = SIZE_OF_STORY, + ["count"] = 1, + }, + } + } + } + } + tx_step(g.server, 'tx2', "s:replace{2, 2}", diff) + tx_gc(g.server, 100, nil) + local err = g.server:eval('return tx2:commit()') + assert(not err[1]) + diff = { + ["txn"] = { + ["statements"] = { + ["total"] = -2 * SIZE_OF_STMT, + ["avg"] = -1 * math.floor(SIZE_OF_STMT / 2), + ["max"] = -1 * SIZE_OF_STMT, + }, + ["system"] = { + ["total"] = -2 * SIZE_OF_XROW, + ["avg"] = -1 * math.floor(SIZE_OF_XROW / 2), + ["max"] = -1 * SIZE_OF_XROW, + } + }, + ["mvcc"] = { + ["tuples"] = { + ["used"] = { + ["stories"] = { + ["total"] = -1 * SIZE_OF_STORY, + ["count"] = -1, + }, + } + } + } + } + tx_gc(g.server, 10, diff) + err = g.server:eval('return tx1:commit()') + assert(not err[1]) + diff = { + ["txn"] = { + ["statements"] = { + ["total"] = -1 * SIZE_OF_STMT, + ["avg"] = -1 * SIZE_OF_STMT, + ["max"] = -1 * SIZE_OF_STMT, + }, + ["system"] = { + ["total"] = -1 * SIZE_OF_XROW, + ["avg"] = -1 * SIZE_OF_XROW, + ["max"] = -1 * SIZE_OF_XROW, + }, + }, + ["mvcc"] = { + ["tuples"] = { + ["used"] = { + ["stories"] = { + ["total"] = -2 * SIZE_OF_STORY, + ["count"] = -2, + }, + ["retained"] = { + ["total"] = -1 * SIZE_OF_TUPLE, + ["count"] = -1, + }, + } + } + } + } + tx_gc(g.server, 100, diff) + assert(table_values_are_zeros(current_stat)) +end + +g.test_read_view = function() + g.server:eval('tx1 = txn_proxy.new()') + g.server:eval('tx2 = txn_proxy.new()') + g.server:eval('tx1:begin()') + g.server:eval('tx2:begin()') + g.server:eval('s:replace{1, 1}') + g.server:eval('s:replace{2, 1}') + g.server:eval('box.internal.memtx_tx_gc(10)') + assert(table_values_are_zeros(g.server:eval('return box.stat.memtx.tx()'))) + g.server:eval('tx1("s:get(1)")') + g.server:eval('tx2("s:replace{1, 2}")') + g.server:eval('tx2("s:replace{2, 2}")') + g.server:eval('tx2:commit()') + local diff = { + ["mvcc"] = { + ["trackers"] = { + ["max"] = SIZE_OF_READ_TRACKER, + ["avg"] = SIZE_OF_READ_TRACKER, + ["total"] = SIZE_OF_READ_TRACKER, + }, + ["tuples"] = { + ["read_view"] = { + ["stories"] = { + ["total"] = 3 * SIZE_OF_STORY, + ["count"] = 3, + }, + ["retained"] = { + ["total"] = SIZE_OF_TUPLE, + ["count"] = 1, + }, + }, + ["used"] = { + ["stories"] = { + ["total"] = SIZE_OF_STORY, + ["count"] = 1, + }, + ["retained"] = { + ["total"] = SIZE_OF_TUPLE, + ["count"] = 1, + }, + }, + }, + } + } + tx_gc(g.server, 10, diff) +end + +g.test_read_view_with_empty_space = function() + g.server:eval('tx1 = txn_proxy.new()') + g.server:eval('tx2 = txn_proxy.new()') + g.server:eval('tx1:begin()') + g.server:eval('tx2:begin()') + g.server:eval('s:replace{1, 1}') + g.server:eval('s:replace{2, 1}') + g.server:eval('box.internal.memtx_tx_gc(10)') + assert(table_values_are_zeros(g.server:eval('return box.stat.memtx.tx()'))) + g.server:eval('tx1("s:get(1)")') + g.server:eval('tx2("s:delete(1)")') + g.server:eval('tx2("s:delete(2)")') + g.server:eval('tx2:commit()') + local diff = { + ["mvcc"] = { + ["trackers"] = { + ["max"] = SIZE_OF_READ_TRACKER, + ["avg"] = SIZE_OF_READ_TRACKER, + ["total"] = SIZE_OF_READ_TRACKER, + }, + ["tuples"] = { + ["read_view"] = { + ["stories"] = { + ["total"] = SIZE_OF_STORY, + ["count"] = 1, + }, + ["retained"] = { + ["total"] = SIZE_OF_TUPLE, + ["count"] = 1, + }, + }, + ["used"] = { + ["stories"] = { + ["total"] = SIZE_OF_STORY, + ["count"] = 1, + }, + ["retained"] = { + ["total"] = SIZE_OF_TUPLE, + ["count"] = 1, + }, + }, + }, + } + } + tx_gc(g.server, 10, diff) +end + +g.test_tracker = function() + g.server:eval('s.index.pk:alter({parts={{field = 1, type = "unsigned"}, {field = 2, type = "unsigned"}}})') + g.server:eval('s:replace{1, 0}') + g.server:eval('s:replace{3, 2}') + g.server:eval('s:replace{2, 0}') + g.server:eval('tx1 = txn_proxy.new()') + g.server:eval('tx1:begin()') + g.server:eval('tx1("s:select{2}")') + local trackers_used = 2 * SIZE_OF_GAP_TRACKER + SIZE_OF_READ_TRACKER + local diff = { + ["mvcc"] = { + ["trackers"] = { + ["max"] = trackers_used, + ["avg"] = trackers_used, + ["total"] = trackers_used, + }, + ["tuples"] = { + ["used"] = { + ["stories"] = { + ["total"] = SIZE_OF_STORY, + ["count"] = 1, + }, + }, + ["tracking"] = { + ["stories"] = { + ["total"] = SIZE_OF_STORY, + ["count"] = 1, + }, + }, + }, + }, + } + tx_gc(g.server, 10, diff) +end + +g.test_conflict = function() + g.server:eval('tx1 = txn_proxy.new()') + g.server:eval('tx2 = txn_proxy.new()') + g.server:eval('box.internal.memtx_tx_gc(10)') + assert(table_values_are_zeros(g.server:eval('return box.stat.memtx.tx()'))) + g.server:eval('tx1:begin()') + g.server:eval('tx2:begin()') + g.server:eval('tx1("s:get(1)")') + g.server:eval('tx2("s:replace{1, 2}")') + g.server:eval("box.internal.memtx_tx_gc(10)") + local trackers_used = SIZE_OF_CONFLICT_TRACKER + SIZE_OF_POINT_TRACKER + local diff = { + ["txn"] = { + ["statements"] = { + ["max"] = SIZE_OF_STMT, + ["avg"] = math.floor(SIZE_OF_STMT / 2), + ["total"] = SIZE_OF_STMT, + }, + ["system"] = { + ["max"] = SIZE_OF_XROW, + ["avg"] = math.floor(SIZE_OF_XROW / 2), + ["total"] = SIZE_OF_XROW, + }, + }, + ["mvcc"] = { + ["trackers"] = { + ["max"] = trackers_used, + ["avg"] = math.floor(trackers_used / 2), + ["total"] = trackers_used, + }, + ["conflicts"] = { + ["max"] = SIZE_OF_CONFLICT, + ["avg"] = math.floor(SIZE_OF_CONFLICT / 2), + ["total"] = SIZE_OF_CONFLICT, + }, + ["tuples"] = { + ["used"] = { + ["stories"] = { + ["total"] = SIZE_OF_STORY, + ["count"] = 1, + }, + }, + }, + }, + } + tx_gc(g.server, 10, diff) +end + +g.test_user_data = function() + g.server:eval('ffi = require("ffi")') + g.server:eval('ffi.cdef("void *box_txn_alloc(size_t size);")') + g.server:eval('tx = txn_proxy.new()') + g.server:eval('tx:begin()') + local alloc_size = 100 + local diff = { + ["txn"] = { + ["user"] = { + ["total"] = alloc_size, + ["avg"] = alloc_size, + ["max"] = alloc_size, + }, + }, + } + tx_step(g.server, 'tx', 'ffi.C.box_txn_alloc(' .. alloc_size .. ')', diff) + local err = g.server:eval('return tx:commit()') + assert(not err[1]) + diff = { + ["txn"] = { + ["user"] = { + ["total"] = -1 * alloc_size, + ["avg"] = -1 * alloc_size, + ["max"] = -1 * alloc_size, + }, + }, + } + tx_gc(g.server, 1, diff) +end -- GitLab