should be it
This commit is contained in:
5
external/duckdb/test/memoryleak/CMakeLists.txt
vendored
Normal file
5
external/duckdb/test/memoryleak/CMakeLists.txt
vendored
Normal 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)
|
||||
32
external/duckdb/test/memoryleak/README.md
vendored
Normal file
32
external/duckdb/test/memoryleak/README.md
vendored
Normal 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`
|
||||
87
external/duckdb/test/memoryleak/test_appender.cpp
vendored
Normal file
87
external/duckdb/test/memoryleak/test_appender.cpp
vendored
Normal 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);
|
||||
}
|
||||
160
external/duckdb/test/memoryleak/test_memory_leaks.py
vendored
Normal file
160
external/duckdb/test/memoryleak/test_memory_leaks.py
vendored
Normal 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')
|
||||
79
external/duckdb/test/memoryleak/test_temporary_tables.cpp
vendored
Normal file
79
external/duckdb/test/memoryleak/test_temporary_tables.cpp
vendored
Normal 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"));
|
||||
// }
|
||||
//}
|
||||
Reference in New Issue
Block a user