Skip to content
This repository was archived by the owner on Mar 29, 2026. It is now read-only.

Commit e40e88c

Browse files
committed
Add memory leak tests for open/close cycles in C API and engine
1 parent c90816a commit e40e88c

6 files changed

Lines changed: 244 additions & 0 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import os
2+
import psutil
3+
import gc
4+
import pytest
5+
import decentdb
6+
7+
def test_engine_open_close_memory_leak(tmp_path):
8+
db_path = str(tmp_path / "test_py_leak.ddb")
9+
process = psutil.Process(os.getpid())
10+
11+
db = decentdb.connect(db_path)
12+
cur = db.cursor()
13+
cur.execute("CREATE TABLE leak_test(id int, data text)")
14+
for i in range(1000):
15+
# 1000 rows with 1KB text each ~ 1MB data
16+
cur.execute("INSERT INTO leak_test VALUES (?, ?)", (i, "a" * 1000))
17+
db.commit()
18+
db.close()
19+
20+
gc.collect()
21+
mem_before = process.memory_info().rss
22+
23+
for _ in range(500):
24+
db = decentdb.connect(db_path)
25+
cur = db.cursor()
26+
cur.execute("SELECT * FROM leak_test")
27+
cur.fetchall()
28+
db.close()
29+
30+
gc.collect()
31+
mem_after = process.memory_info().rss
32+
diff = mem_after - mem_before
33+
34+
print(f"Memory before: {mem_before}, after: {mem_after}, diff: {diff}")
35+
assert diff < 10000000, f"Memory leak detected! Diff: {diff} bytes"

src/c_api.nim

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,12 @@ proc decentdb_open*(path: cstring, options: cstring): pointer {.exportc, cdecl,
501501
GC_ref(handle)
502502
return cast[pointer](handle)
503503

504+
when defined(linux) and not defined(android):
505+
import dynlib
506+
var
507+
mallocTrimCached: proc(pad: csize_t): cint {.cdecl.}
508+
mallocTrimChecked: bool = false
509+
504510
proc decentdb_close*(p: pointer): cint {.exportc, cdecl, dynlib.} =
505511
if p == nil: return 0
506512
let handle = cast[DbHandle](p)
@@ -509,6 +515,19 @@ proc decentdb_close*(p: pointer): cint {.exportc, cdecl, dynlib.} =
509515
discard closeDb(handle.db)
510516

511517
GC_unref(handle)
518+
519+
when defined(linux) and not defined(android):
520+
if not mallocTrimChecked:
521+
mallocTrimChecked = true
522+
let lib = loadLib("libc.so.6")
523+
if lib != nil:
524+
let sym = symAddr(lib, "malloc_trim")
525+
if sym != nil:
526+
mallocTrimCached = cast[proc(pad: csize_t): cint {.cdecl.}](sym)
527+
528+
if mallocTrimCached != nil:
529+
discard mallocTrimCached(0)
530+
512531
return 0
513532

514533
proc decentdb_checkpoint*(p: pointer): cint {.exportc, cdecl, dynlib.} =
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import unittest
2+
import os
3+
import ../../src/c_api
4+
5+
proc makeTempDb(name: string): string =
6+
let path = getTempDir() / name
7+
if fileExists(path):
8+
removeFile(path)
9+
if fileExists(path & ".wal"):
10+
removeFile(path & ".wal")
11+
path
12+
13+
suite "C API open/close memory leak":
14+
test "opening and closing via C API should not leak memory":
15+
let path = makeTempDb("decentdb_capi_leak_test.db")
16+
17+
# Warm up
18+
for i in 1..5:
19+
let db = decentdb_open(path.cstring, "".cstring)
20+
require(db != nil)
21+
discard decentdb_close(db)
22+
23+
GC_fullCollect()
24+
let initMem = getOccupiedMem()
25+
26+
for i in 1..1000:
27+
let db = decentdb_open(path.cstring, "".cstring)
28+
doAssert(db != nil)
29+
discard decentdb_close(db)
30+
31+
GC_fullCollect()
32+
let finalMem = getOccupiedMem()
33+
34+
let diff = int(finalMem) - int(initMem)
35+
echo "C API Initial Mem: ", initMem, " Final Mem: ", finalMem, " Diff: ", diff
36+
require(diff < 1000000)
37+
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import unittest
2+
import os
3+
import ../../src/engine
4+
import ../../src/c_api
5+
import ../../src/errors
6+
7+
proc makeTempDb(name: string): string =
8+
let path = getTempDir() / name
9+
if fileExists(path): removeFile(path)
10+
if fileExists(path & ".wal"): removeFile(path & ".wal")
11+
path
12+
13+
suite "Memory leak tests for open/close cycles":
14+
test "opening and closing db connections many times via core engine should not leak memory":
15+
let path = makeTempDb("decentdb_engine_leak_test.db")
16+
17+
# Warm up
18+
block:
19+
for i in 1..5:
20+
let dbRes = openDb(path)
21+
require(dbRes.ok)
22+
discard dbRes.value.closeDb()
23+
24+
GC_fullCollect()
25+
let initMem = getOccupiedMem()
26+
27+
block:
28+
for i in 1..2000:
29+
let dbRes = openDb(path)
30+
doAssert(dbRes.ok)
31+
discard dbRes.value.closeDb()
32+
33+
GC_fullCollect()
34+
let finalMem = getOccupiedMem()
35+
let diff = int(finalMem) - int(initMem)
36+
echo "Engine Initial Mem: ", initMem, " Final Mem: ", finalMem, " Diff: ", diff
37+
require(diff < 200000)
38+
39+
test "opening and closing db connections many times via C API should not leak memory":
40+
let path = makeTempDb("decentdb_capi_leak_test.db")
41+
42+
# Warm up
43+
block:
44+
for i in 1..5:
45+
let db = decentdb_open(path.cstring, "".cstring)
46+
require(db != nil)
47+
discard decentdb_close(db)
48+
49+
GC_fullCollect()
50+
let initMem = getOccupiedMem()
51+
52+
block:
53+
for i in 1..2000:
54+
let db = decentdb_open(path.cstring, "".cstring)
55+
doAssert(db != nil)
56+
discard decentdb_close(db)
57+
58+
GC_fullCollect()
59+
let finalMem = getOccupiedMem()
60+
let diff = int(finalMem) - int(initMem)
61+
echo "C API Initial Mem: ", initMem, " Final Mem: ", finalMem, " Diff: ", diff
62+
require(diff < 200000)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import unittest
2+
import os
3+
import ../../src/engine
4+
import ../../src/c_api
5+
import ../../src/errors
6+
7+
proc makeTempDb(name: string): string =
8+
let path = getTempDir() / name
9+
if fileExists(path): removeFile(path)
10+
if fileExists(path & ".wal"): removeFile(path & ".wal")
11+
path
12+
13+
suite "Memory leak tests for open/close cycles":
14+
test "opening and closing db connections many times via core engine should not leak memory":
15+
let path = makeTempDb("decentdb_engine_leak_test.db")
16+
17+
# Warm up / initialize anything that might do lazy init
18+
for i in 1..5:
19+
let dbRes = openDb(path)
20+
require(dbRes.ok)
21+
discard dbRes.value.closeDb()
22+
23+
GC_fullCollect()
24+
let initMem = getOccupiedMem()
25+
26+
for i in 1..2000:
27+
let dbRes = openDb(path)
28+
doAssert(dbRes.ok)
29+
discard dbRes.value.closeDb()
30+
31+
GC_fullCollect()
32+
let finalMem = getOccupiedMem()
33+
34+
let diff = int(finalMem) - int(initMem)
35+
echo "Engine Initial Mem: ", initMem, " Final Mem: ", finalMem, " Diff: ", diff
36+
# A tiny bit of fluctuation might happen, restrict it strictly
37+
require(diff < 200000)
38+
39+
test "opening and closing db connections many times via C API should not leak memory":
40+
let path = makeTempDb("decentdb_capi_leak_test.db")
41+
42+
# Warm up
43+
for i in 1..5:
44+
let db = decentdb_open(path.cstring, "".cstring)
45+
require(db != nil)
46+
discard decentdb_close(db)
47+
48+
GC_fullCollect()
49+
let initMem = getOccupiedMem()
50+
51+
for i in 1..2000:
52+
let db = decentdb_open(path.cstring, "".cstring)
53+
doAssert(db != nil)
54+
discard decentdb_close(db)
55+
56+
GC_fullCollect()
57+
let finalMem = getOccupiedMem()
58+
59+
let diff = int(finalMem) - int(initMem)
60+
echo "C API Initial Mem: ", initMem, " Final Mem: ", finalMem, " Diff: ", diff
61+
require(diff < 200000)
62+

tests/repro_py_leak.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import os
2+
import psutil
3+
import gc
4+
import decentdb
5+
6+
db_path = "test_py_leak.ddb"
7+
if os.path.exists(db_path):
8+
os.remove(db_path)
9+
if os.path.exists(db_path + ".wal"):
10+
os.remove(db_path + ".wal")
11+
12+
process = psutil.Process(os.getpid())
13+
14+
# warm up
15+
for i in range(5):
16+
db = decentdb.connect(db_path)
17+
db.close()
18+
19+
gc.collect()
20+
mem_before = process.memory_info().rss
21+
22+
for i in range(10000):
23+
db = decentdb.connect(db_path)
24+
db.close()
25+
26+
gc.collect()
27+
mem_after = process.memory_info().rss
28+
29+
print(f"Memory before: {mem_before}, after: {mem_after}, diff: {mem_after - mem_before}")

0 commit comments

Comments
 (0)