From 6606d587596e13dec4db1812ffb702f3c345c081 Mon Sep 17 00:00:00 2001
From: Vladimir Davydov <vdavydov@tarantool.org>
Date: Tue, 29 Aug 2023 11:59:37 +0300
Subject: [PATCH] vinyl: add statistic for total size of memory occupied by
 tuples

Vinyl tuples returned to the user are allocated with malloc. They may be
pinned by Lua indefinitely. Currently, there's no way to figure out how
much memory is occupied by these tuples. This commit adds a statistic to
box.stat.vinyl() that accounts them.

Closes #8485

@TarantoolBot document
Title: Document `memory.tuple` statistic of `box.stat.vinyl()`

The new statistic shows the total size of memory in bytes occupied by
Vinyl tuples. It includes cached tuples and tuples pinned by the Lua
world.
---
 .../unreleased/gh-8485-vy-tuple-stat.md       |  5 ++
 src/box/vinyl.c                               |  1 +
 src/box/vy_stmt.c                             | 17 +++++-
 src/box/vy_stmt.h                             |  7 +++
 .../vinyl-luatest/gh_8485_tuple_stat_test.lua | 61 +++++++++++++++++++
 test/vinyl/stat.result                        | 13 +++-
 test/vinyl/stat.test.lua                      |  4 ++
 7 files changed, 103 insertions(+), 5 deletions(-)
 create mode 100644 changelogs/unreleased/gh-8485-vy-tuple-stat.md
 create mode 100644 test/vinyl-luatest/gh_8485_tuple_stat_test.lua

diff --git a/changelogs/unreleased/gh-8485-vy-tuple-stat.md b/changelogs/unreleased/gh-8485-vy-tuple-stat.md
new file mode 100644
index 0000000000..eeaff6c89a
--- /dev/null
+++ b/changelogs/unreleased/gh-8485-vy-tuple-stat.md
@@ -0,0 +1,5 @@
+## feature/vinyl
+
+* Introduced the `memory.tuple` statistic for `box.stat.vinyl()` that shows
+  the total size of memory occupied by all tuples allocated by the Vinyl engine
+  (gh-8485).
diff --git a/src/box/vinyl.c b/src/box/vinyl.c
index 2cb41a222d..fc3201042d 100644
--- a/src/box/vinyl.c
+++ b/src/box/vinyl.c
@@ -296,6 +296,7 @@ vy_info_append_memory(struct vy_env *env, struct info_handler *h)
 	info_table_begin(h, "memory");
 	info_append_int(h, "tx", vy_tx_manager_mem_used(env->xm));
 	info_append_int(h, "level0", lsregion_used(&env->mem_env.allocator));
+	info_append_int(h, "tuple", env->stmt_env.sum_tuple_size);
 	info_append_int(h, "tuple_cache", env->cache_env.mem_used);
 	info_append_int(h, "page_index", env->lsm_env.page_index_size);
 	info_append_int(h, "bloom_filter", env->lsm_env.bloom_size);
diff --git a/src/box/vy_stmt.c b/src/box/vy_stmt.c
index 4105b82d03..a6659082b4 100644
--- a/src/box/vy_stmt.c
+++ b/src/box/vy_stmt.c
@@ -98,6 +98,8 @@ vy_tuple_new(struct tuple_format *format, const char *data, const char *end)
 static void
 vy_tuple_delete(struct tuple_format *format, struct tuple *tuple)
 {
+	struct vy_stmt_env *env = format->engine;
+	size_t size = tuple_size(tuple);
 	say_debug("%s(%p)", __func__, tuple);
 	assert(tuple_is_unreferenced(tuple));
 	/*
@@ -105,10 +107,15 @@ vy_tuple_delete(struct tuple_format *format, struct tuple *tuple)
 	 * multithread unsafe modifications of a reference
 	 * counter.
 	 */
-	if (cord_is_main())
+	if (cord_is_main()) {
+		if (format != env->key_format) {
+			assert(env->sum_tuple_size >= size);
+			env->sum_tuple_size -= size;
+		}
 		tuple_format_unref(format);
+	}
 #ifndef NDEBUG
-	memset(tuple, '#', tuple_size(tuple)); /* fail early */
+	memset(tuple, '#', size); /* fail early */
 #endif
 	free(tuple);
 }
@@ -119,6 +126,7 @@ vy_stmt_env_create(struct vy_stmt_env *env)
 	env->tuple_format_vtab.tuple_new = vy_tuple_new;
 	env->tuple_format_vtab.tuple_delete = vy_tuple_delete;
 	env->max_tuple_size = 1024 * 1024;
+	env->sum_tuple_size = 0;
 	env->key_format = vy_simple_stmt_format_new(env, NULL, 0);
 	if (env->key_format == NULL)
 		panic("failed to create vinyl key format");
@@ -185,8 +193,11 @@ vy_stmt_alloc(struct tuple_format *format, uint32_t data_offset, uint32_t bsize)
 		  format->id, data_offset, bsize, tuple);
 	tuple_create(tuple, 1, tuple_format_id(format),
 		     data_offset, bsize, false);
-	if (cord_is_main())
+	if (cord_is_main()) {
+		if (format != env->key_format)
+			env->sum_tuple_size += total_size;
 		tuple_format_ref(format);
+	}
 	vy_stmt_set_lsn(tuple, 0);
 	vy_stmt_set_type(tuple, 0);
 	vy_stmt_set_flags(tuple, 0);
diff --git a/src/box/vy_stmt.h b/src/box/vy_stmt.h
index 3938c5a2af..771f26bbce 100644
--- a/src/box/vy_stmt.h
+++ b/src/box/vy_stmt.h
@@ -74,6 +74,13 @@ struct vy_stmt_env {
 	 * @see box.cfg.vinyl_max_tuple_size
 	 */
 	size_t max_tuple_size;
+	/**
+	 * Size of memory occupied by all vinyl tuples allocated
+	 * in the main thread. Note, this doesn't include keys,
+	 * which should be fine because keys shouldn't stay in
+	 * memory for long.
+	 */
+	size_t sum_tuple_size;
 	/**
 	 * Tuple format used for creating key statements (e.g.
 	 * statements read from secondary index runs). It doesn't
diff --git a/test/vinyl-luatest/gh_8485_tuple_stat_test.lua b/test/vinyl-luatest/gh_8485_tuple_stat_test.lua
new file mode 100644
index 0000000000..4df1b7a22c
--- /dev/null
+++ b/test/vinyl-luatest/gh_8485_tuple_stat_test.lua
@@ -0,0 +1,61 @@
+local server = require('luatest.server')
+local t = require('luatest')
+
+local g = t.group()
+
+g.before_all(function(cg)
+    cg.server = server:new()
+    cg.server:start()
+    cg.server:exec(function()
+        box.schema.create_space('test', {engine = 'vinyl'})
+        box.space.test:create_index('primary', {
+            parts = {1, 'unsigned'},
+        })
+        box.space.test:create_index('secondary', {
+            parts = {2, 'string'}, unique = false,
+        })
+        for i = 1, 10 do
+            box.space.test:insert({i, string.rep('x', 1000)})
+            if i == 5 then
+                box.snapshot()
+            end
+        end
+        collectgarbage()
+    end)
+end)
+
+g.after_all(function(cg)
+    cg.server:drop()
+end)
+
+g.test_tuple_stat = function(cg)
+    cg.server:exec(function()
+        local function gc()
+            box.tuple.new() -- drop blessed tuple ref
+            collectgarbage()
+        end
+        -- Initial value is 0.
+        gc()
+        t.assert_equals(box.stat.vinyl().memory.tuple, 0)
+
+        -- Tuples pinned by Lua.
+        box.cfg({vinyl_cache = 0})
+        local ret = box.space.test:select()
+        t.assert_equals(#ret, 10)
+        t.assert_almost_equals(box.stat.vinyl().memory.tuple, 10 * 1000, 1000)
+        ret = nil -- luacheck: ignore
+        gc()
+        t.assert_equals(box.stat.vinyl().memory.tuple, 0)
+
+        -- Tuples pinned by cache.
+        box.cfg({vinyl_cache = 100 * 1000})
+        ret = box.space.test:select()
+        t.assert_equals(#ret, 10)
+        t.assert_almost_equals(box.stat.vinyl().memory.tuple, 10 * 1000, 1000)
+        ret = nil -- luacheck: ignore
+        gc()
+        t.assert_almost_equals(box.stat.vinyl().memory.tuple, 10 * 1000, 1000)
+        box.cfg({vinyl_cache = 0})
+        t.assert_equals(box.stat.vinyl().memory.tuple, 0)
+    end)
+end
diff --git a/test/vinyl/stat.result b/test/vinyl/stat.result
index c4dc9af394..909b6039c0 100644
--- a/test/vinyl/stat.result
+++ b/test/vinyl/stat.result
@@ -256,8 +256,9 @@ gstat()
     tuple_cache: 0
     tx: 0
     level0: 0
-    page_index: 0
     bloom_filter: 0
+    page_index: 0
+    tuple: 0
   disk:
     data_compacted: 0
     data: 0
@@ -1007,6 +1008,13 @@ stat_diff(gstat(), st, 'tx')
 ---
 - commit: 1
 ...
+-- free tuples pinned by Lua
+_ = nil
+---
+...
+_ = collectgarbage()
+---
+...
 -- box.stat.reset
 box.stat.reset()
 ---
@@ -1140,8 +1148,9 @@ gstat()
     tuple_cache: 14313
     tx: 0
     level0: 261562
-    page_index: 1250
     bloom_filter: 140
+    page_index: 1250
+    tuple: 13689
   disk:
     data_compacted: 104299
     data: 104299
diff --git a/test/vinyl/stat.test.lua b/test/vinyl/stat.test.lua
index a8657ccf49..83c595509c 100644
--- a/test/vinyl/stat.test.lua
+++ b/test/vinyl/stat.test.lua
@@ -323,6 +323,10 @@ stat_diff(gstat(), st, 'tx')
 box.commit()
 stat_diff(gstat(), st, 'tx')
 
+-- free tuples pinned by Lua
+_ = nil
+_ = collectgarbage()
+
 -- box.stat.reset
 box.stat.reset()
 istat()
-- 
GitLab