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