should be it

This commit is contained in:
2025-10-24 19:21:19 -05:00
parent a4b23fc57c
commit f09560c7b1
14047 changed files with 3161551 additions and 1 deletions

View File

@@ -0,0 +1,5 @@
add_library_unity(test_memory_leak OBJECT test_appender.cpp
test_temporary_tables.cpp)
set(ALL_OBJECT_FILES
${ALL_OBJECT_FILES} $<TARGET_OBJECTS:test_memory_leak>
PARENT_SCOPE)

View File

@@ -0,0 +1,32 @@
## Usage
Run all memory leak tests:
```bash
python3 test/memoryleak/test_memory_leaks.py
```
Run a specific test:
```bash
python3 test/memoryleak/test_memory_leaks.py --test="Test temporary table leaks (#5501)"
```
Running tests in the debugger requires passing the `--memory-leak-tests` flag:
```bash
build/debug/test/unittest "Test temporary table leaks (#5501)" --memory-leak-tests
```
## Description
The memory leak folder contains memory leak tests. These are tests that run forever (i.e. they contain a `while true` loop). The tests are disabled unless the `--memory-leak-tests` flag is passed to the test runner.
The core idea of the tests is that they perform operations in a loop that should not increase memory of the system (e.g. they might create tables and then drop them again, or create connections and then destroy them).
A separate Python script is used to run these tests (`test/memoryleak/test_memory_leaks.py`). The Python script measures the resident set size of the unittest using the `ps` system call.
* The script measures memory usage of the test - if the memory usage does not stabilize within the timeout the test is considered a failure.
* Stabilized memory usage means that the trend of memory usage has not been going up in the past 10 seconds
* The exact threshold of what "going up" means is determined by `--threshold-percentage` and `--threshold-absolute`
* The timeout is determined by `--timeout`

View File

@@ -0,0 +1,87 @@
#include "catch.hpp"
#include "test_helpers.hpp"
#include "duckdb/main/config.hpp"
#include "test_config.hpp"
using namespace duckdb;
using namespace std;
void rand_str(char *dest, idx_t length) {
char charset[] = "0123456789"
"abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
while (length-- > 0) {
idx_t index = (double)rand() / RAND_MAX * (sizeof charset - 1);
*dest++ = charset[index];
}
*dest = '\0';
}
TEST_CASE("Test repeated appending small chunks to a table", "[memoryleak]") {
if (!TestConfiguration::TestMemoryLeaks()) {
return;
}
duckdb_database db;
duckdb_connection con;
duckdb_state state;
auto db_path = TestCreatePath("appender_leak_test.db");
TestDeleteFile(db_path);
if (duckdb_open(db_path.c_str(), &db) == DuckDBError) {
// handle error
FAIL("Failed to open");
}
if (duckdb_connect(db, &con) == DuckDBError) {
// handle error
FAIL("Failed to connect");
}
state =
duckdb_query(con, "create table test(col1 varchar, col2 varchar, col3 bigint, col4 bigint, col5 double)", NULL);
if (state == DuckDBError) {
FAIL("Failed to create table");
}
state = duckdb_query(con, "set memory_limit='100mb'", NULL);
if (state == DuckDBError) {
FAIL("Failed to set memory limit");
}
long n1 = 0;
double d1 = 0.5;
for (int i = 0; i < 100000; i++) {
duckdb_appender appender;
if (duckdb_appender_create(con, NULL, "test", &appender) == DuckDBError) {
FAIL("Failed to create appender");
}
for (int j = 0; j < 1000; j++) {
char str[41];
rand_str(str, sizeof(str) - 1);
duckdb_append_varchar(appender, str);
duckdb_append_varchar(appender, "hello");
duckdb_append_int64(appender, n1++);
duckdb_append_int64(appender, n1++);
duckdb_append_double(appender, d1);
d1 += 1.25;
duckdb_appender_end_row(appender);
}
state = duckdb_appender_close(appender);
if (state == DuckDBError) {
FAIL("Failed to close appender");
}
state = duckdb_appender_destroy(&appender);
if (state == DuckDBError) {
fprintf(stderr, "err: %d", state);
FAIL("Failed to destroy appender");
}
if (i % 500 == 0) {
printf("completed %d\n", i);
duckdb_query(con, "checkpoint", NULL);
}
}
// cleanup
duckdb_disconnect(&con);
duckdb_close(&db);
REQUIRE(1 == 1);
}

View File

@@ -0,0 +1,160 @@
import subprocess
import time
import argparse
import os
parser = argparse.ArgumentParser(description='Runs the memory leak tests')
parser.add_argument(
'--unittest',
dest='unittest',
action='store',
help='Path to unittest executable',
default='build/release/test/unittest',
)
parser.add_argument('--test', dest='test', action='store', help='The name of the tests to run (* is all)', default='*')
parser.add_argument(
'--timeout',
dest='timeout',
action='store',
help='The maximum time to run the test and measure memory usage (in seconds)',
default=60,
)
parser.add_argument(
'--threshold-percentage',
dest='threshold_percentage',
action='store',
help='The percentage threshold before we consider an increase a regression',
default=0.01,
)
parser.add_argument(
'--threshold-absolute',
dest='threshold_absolute',
action='store',
help='The absolute threshold before we consider an increase a regression',
default=1000,
)
parser.add_argument('--verbose', dest='verbose', action='store', help='Verbose output', default=True)
args = parser.parse_args()
unittest_program = args.unittest
test_filter = args.test
test_time = int(args.timeout)
verbose = args.verbose
measurements_per_second = 1.0
# get a list of all unittests
proc = subprocess.Popen([unittest_program, '-l', '[memoryleak]'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout = proc.stdout.read().decode('utf8')
stderr = proc.stderr.read().decode('utf8')
if proc.returncode is not None and proc.returncode != 0:
print("Failed to run program " + unittest_program)
print(proc.returncode)
print(stdout)
print(stderr)
exit(1)
test_cases = []
first_line = True
for line in stdout.splitlines():
if first_line:
first_line = False
continue
if len(line.strip()) == 0:
continue
splits = line.rsplit('\t', 1)
if test_filter == '*' or test_filter in splits[0]:
test_cases.append(splits[0])
if len(test_cases) == 0:
print(f"No tests matching filter \"{test_filter}\" found")
exit(0)
def sizeof_fmt(num, suffix="B"):
for unit in ["", "K", "M", "G", "T", "P", "E", "Z"]:
if abs(num) < 1000.0:
return f"{num:3.1f}{unit}{suffix}"
num /= 1000.0
return f"{num:.1f}Yi{suffix}"
def run_test(test_case):
# launch the unittest program
proc = subprocess.Popen(
[unittest_program, test_case, '--memory-leak-tests'], stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
pid = proc.pid
# capture the memory output for the duration of the program running
leak = True
rss = []
for i in range(int(test_time * measurements_per_second)):
time.sleep(1.0 / measurements_per_second)
if proc.poll() is not None:
print("------------------------------------------------")
print(" FAILURE ")
print("------------------------------------------------")
print(" stdout: ")
print("------------------------------------------------")
print(proc.stdout.read().decode('utf8'))
print("------------------------------------------------")
print(" stderr: ")
print("------------------------------------------------")
print(proc.stderr.read().decode('utf8'))
exit(1)
ps_proc = subprocess.Popen(f'ps -o rss= -p {pid}'.split(' '), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
res = ps_proc.stdout.read().decode('utf8').strip()
memory_usage_in_bytes = int(res) * 1024
if verbose:
print(f"{i / measurements_per_second}: {sizeof_fmt(memory_usage_in_bytes)}")
rss.append(memory_usage_in_bytes)
if not has_memory_leak(rss):
leak = False
break
proc.terminate()
if leak:
print("------------------------------------------------")
print(" ERROR ")
print("------------------------------------------------")
print(f"Memory leak detected in test case \"{test_case}\"")
print("------------------------------------------------")
elif verbose:
print("------------------------------------------------")
print(" Success! ")
print("------------------------------------------------")
print("------------------------------------------------")
print(f"No memory leaks detected in test case \"{test_case}\"")
print("------------------------------------------------")
if verbose or leak:
print("Observed memory usages")
print("------------------------------------------------")
for i in range(len(rss)):
memory = rss[i]
print(f"{i / measurements_per_second}: {sizeof_fmt(memory)}")
if leak:
exit(1)
def has_memory_leak(rss):
measurement_count = int(measurements_per_second * 10)
if len(rss) <= measurement_count:
# not enough measurements yet
return True
differences = []
for i in range(1, len(rss)):
differences.append(rss[i] - rss[i - 1])
max_memory = max(rss)
sum_differences = sum(differences[-measurement_count:])
return sum_differences > (max_memory * args.threshold_percentage + args.threshold_absolute)
try:
for index, test in enumerate(test_cases):
print(f"[{index}/{len(test_cases)}] {test}")
run_test(test)
finally:
os.system('killall -9 unittest')

View File

@@ -0,0 +1,79 @@
#include "catch.hpp"
#include "duckdb/common/file_system.hpp"
#include "duckdb/storage/buffer_manager.hpp"
#include "duckdb/storage/storage_info.hpp"
#include "test_helpers.hpp"
#include "duckdb/main/client_context.hpp"
#include "duckdb/main/config.hpp"
#include "test_config.hpp"
using namespace duckdb;
using namespace std;
TEST_CASE("Test in-memory database scanning from tables", "[memoryleak]") {
if (!TestConfiguration::TestMemoryLeaks()) {
return;
}
DuckDB db;
Connection con(db);
REQUIRE_NO_FAIL(
con.Query("create table t1 as select i, concat('thisisalongstring', i) s from range(1000000) t(i);"));
while (true) {
REQUIRE_NO_FAIL(con.Query("SELECT * FROM t1"));
}
}
TEST_CASE("Rollback create table", "[memoryleak]") {
if (!TestConfiguration::TestMemoryLeaks()) {
return;
}
DBConfig config;
config.options.load_extensions = false;
DuckDB db(":memory:", &config);
Connection con(db);
while (true) {
REQUIRE_NO_FAIL(con.Query("BEGIN"));
REQUIRE_NO_FAIL(con.Query("CREATE TABLE t2(i INT);"));
REQUIRE_NO_FAIL(con.Query("ROLLBACK"));
}
}
TEST_CASE("DB temporary table insertion", "[memoryleak]") {
if (!TestConfiguration::TestMemoryLeaks()) {
return;
}
auto db_path = TestCreatePath("memory_leak_temp_table.db");
DeleteDatabase(db_path);
DuckDB db(db_path);
{
Connection con(db);
REQUIRE_NO_FAIL(con.Query("SET threads=8;"));
REQUIRE_NO_FAIL(con.Query("SET preserve_insertion_order=false;"));
REQUIRE_NO_FAIL(con.Query("SET force_compression='uncompressed';"));
REQUIRE_NO_FAIL(con.Query("create table t1 as select i from range(1000000) t(i);"));
}
Connection con(db);
while (true) {
REQUIRE_NO_FAIL(con.Query("BEGIN"));
REQUIRE_NO_FAIL(con.Query("CREATE OR REPLACE TEMPORARY TABLE t2(i int)"));
REQUIRE_NO_FAIL(con.Query("insert into t2 SELECT * FROM t1;"));
REQUIRE_NO_FAIL(con.Query("ROLLBACk"));
}
}
// FIXME: broken right now - we need to flush/merge deletes to fix this
// TEST_CASE("Insert and delete data repeatedly", "[memoryleak]") {
// if (!TestMemoryLeaks()) {
// return;
// }
// DBConfig config;
// config.options.load_extensions = false;
// DuckDB db(":memory:", &config);
// Connection con(db);
// REQUIRE_NO_FAIL(con.Query("CREATE TABLE t1(i INT);"));
// while (true) {
// REQUIRE_NO_FAIL(con.Query("INSERT INTO t1 SELECT * FROM range(100000)"));
// REQUIRE_NO_FAIL(con.Query("DELETE FROM t1"));
// }
//}