Skip to content

Commit 7716c20

Browse files
committed
evict LRU stmt when stmt cache is full
1 parent 58e032d commit 7716c20

3 files changed

Lines changed: 293 additions & 29 deletions

File tree

sqlite3.go

Lines changed: 53 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,13 @@ type SQLiteConn struct {
451451
txlock string
452452
funcs []*functionInfo
453453
aggregators []*aggInfo
454-
stmtCache map[string][]*SQLiteStmt
454+
// Prepared-statement cache. stmtCacheBuf is a preallocated slice of
455+
// length stmtCacheSize holding up to stmtCacheCount live entries at
456+
// indices [0, stmtCacheCount). Ordering is LRU-first: index 0 is the
457+
// oldest (next to be evicted), index stmtCacheCount-1 is the most
458+
// recently put. put at the tail is O(1) when not full; eviction shifts
459+
// the remaining entries left by one.
460+
stmtCacheBuf []*SQLiteStmt
455461
stmtCacheSize int
456462
stmtCacheCount int
457463
}
@@ -1613,7 +1619,7 @@ func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) {
16131619
// Create connection to SQLite
16141620
conn := &SQLiteConn{db: db, loc: loc, txlock: txlock, stmtCacheSize: stmtCacheSize}
16151621
if stmtCacheSize > 0 {
1616-
conn.stmtCache = make(map[string][]*SQLiteStmt)
1622+
conn.stmtCacheBuf = make([]*SQLiteStmt, stmtCacheSize)
16171623
}
16181624

16191625
// Password Cipher has to be registered before authentication
@@ -1917,55 +1923,73 @@ func (c *SQLiteConn) takeCachedStmt(query string) *SQLiteStmt {
19171923
if c.db == nil {
19181924
return nil
19191925
}
1920-
stmts := c.stmtCache[query]
1921-
if len(stmts) == 0 {
1922-
return nil
1923-
}
1924-
s := stmts[len(stmts)-1]
1925-
if len(stmts) == 1 {
1926-
delete(c.stmtCache, query)
1927-
} else {
1928-
c.stmtCache[query] = stmts[:len(stmts)-1]
1926+
// Scan from the MRU end (tail) so that a stmt put just before is
1927+
// found immediately.
1928+
for i := c.stmtCacheCount - 1; i >= 0; i-- {
1929+
s := c.stmtCacheBuf[i]
1930+
if s.cacheKey != query {
1931+
continue
1932+
}
1933+
// Remove s from the buffer by shifting subsequent entries left.
1934+
if i != c.stmtCacheCount-1 {
1935+
copy(c.stmtCacheBuf[i:c.stmtCacheCount-1], c.stmtCacheBuf[i+1:c.stmtCacheCount])
1936+
}
1937+
c.stmtCacheCount--
1938+
c.stmtCacheBuf[c.stmtCacheCount] = nil
1939+
s.closed = false
1940+
s.cls = false
1941+
s.t = ""
1942+
return s
19291943
}
1930-
c.stmtCacheCount--
1931-
s.closed = false
1932-
s.cls = false
1933-
s.t = ""
1934-
return s
1944+
return nil
19351945
}
19361946

19371947
func (c *SQLiteConn) putCachedStmt(s *SQLiteStmt) bool {
1938-
if c == nil || s == nil || s.s == nil || s.cacheKey == "" {
1948+
if c == nil || s == nil || s.s == nil || s.cacheKey == "" || c.stmtCacheSize <= 0 {
19391949
return false
19401950
}
19411951

19421952
c.mu.Lock()
19431953
defer c.mu.Unlock()
19441954

1945-
if c.db == nil || c.stmtCacheCount >= c.stmtCacheSize {
1955+
if c.db == nil {
19461956
return false
19471957
}
19481958
rv := C._sqlite3_reset_clear(s.s)
19491959
if rv != C.SQLITE_ROW && rv != C.SQLITE_OK && rv != C.SQLITE_DONE {
19501960
return false
19511961
}
1952-
c.stmtCache[s.cacheKey] = append(c.stmtCache[s.cacheKey], s)
1962+
// If full, finalize the least-recently-used entry at index 0 and
1963+
// compact the remaining entries left by one.
1964+
if c.stmtCacheCount == c.stmtCacheSize {
1965+
victim := c.stmtCacheBuf[0]
1966+
runtime.SetFinalizer(victim, nil)
1967+
if victim.s != nil {
1968+
C.sqlite3_finalize(victim.s)
1969+
victim.s = nil
1970+
}
1971+
victim.c = nil
1972+
victim.closed = true
1973+
copy(c.stmtCacheBuf[0:c.stmtCacheCount-1], c.stmtCacheBuf[1:c.stmtCacheCount])
1974+
c.stmtCacheCount--
1975+
}
1976+
// Append at the MRU tail.
1977+
c.stmtCacheBuf[c.stmtCacheCount] = s
19531978
c.stmtCacheCount++
19541979
return true
19551980
}
19561981

19571982
func (c *SQLiteConn) closeCachedStmtsLocked() {
1958-
for key, stmts := range c.stmtCache {
1959-
for _, s := range stmts {
1960-
if s == nil || s.s == nil {
1961-
continue
1962-
}
1963-
runtime.SetFinalizer(s, nil)
1964-
C.sqlite3_finalize(s.s)
1965-
s.s = nil
1966-
s.c = nil
1983+
for i := 0; i < c.stmtCacheCount; i++ {
1984+
s := c.stmtCacheBuf[i]
1985+
c.stmtCacheBuf[i] = nil
1986+
if s == nil || s.s == nil {
1987+
continue
19671988
}
1968-
delete(c.stmtCache, key)
1989+
runtime.SetFinalizer(s, nil)
1990+
C.sqlite3_finalize(s.s)
1991+
s.s = nil
1992+
s.c = nil
19691993
}
19701994
c.stmtCacheCount = 0
19711995
}

sqlite3_stmt_cache_bench_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
2+
//
3+
// Use of this source code is governed by an MIT-style
4+
// license that can be found in the LICENSE file.
5+
6+
//go:build cgo
7+
// +build cgo
8+
9+
package sqlite3
10+
11+
import (
12+
"context"
13+
"database/sql/driver"
14+
"fmt"
15+
"testing"
16+
)
17+
18+
// BenchmarkStmtCache measures the stmt cache hit / miss / eviction paths by
19+
// cycling through a fixed set of queries under various cache sizes. It is
20+
// intended for comparing cache behavior changes, not for absolute numbers.
21+
func BenchmarkStmtCache(b *testing.B) {
22+
cases := []struct {
23+
name string
24+
cacheSize int
25+
keyCount int
26+
}{
27+
{"off", 0, 1}, // baseline: no cache
28+
{"size4_keys1_hit", 4, 1}, // trivial hit path
29+
{"size4_keys4_hit", 4, 4}, // all queries fit, always hit
30+
{"size4_keys8_evict", 4, 8}, // working set > cache: miss + eviction
31+
{"size16_keys8_hit", 16, 8}, // all queries fit in larger cache
32+
{"size16_keys32_evict", 16, 32}, // working set >> cache
33+
}
34+
for _, tc := range cases {
35+
b.Run(tc.name, func(b *testing.B) {
36+
dsn := ":memory:"
37+
if tc.cacheSize > 0 {
38+
dsn = fmt.Sprintf(":memory:?_stmt_cache_size=%d", tc.cacheSize)
39+
}
40+
d := SQLiteDriver{}
41+
conn, err := d.Open(dsn)
42+
if err != nil {
43+
b.Fatal(err)
44+
}
45+
defer conn.Close()
46+
c := conn.(*SQLiteConn)
47+
48+
queries := make([]string, tc.keyCount)
49+
for i := range queries {
50+
// Distinct literal forces a distinct prepared statement.
51+
queries[i] = fmt.Sprintf("SELECT %d", i+1)
52+
}
53+
54+
ctx := context.Background()
55+
// Warm up: exercise each query at least once so the cache (if any)
56+
// reaches steady state before timing begins.
57+
for _, q := range queries {
58+
rows, err := c.query(ctx, q, nil)
59+
if err != nil {
60+
b.Fatal(err)
61+
}
62+
drainRows(b, rows)
63+
}
64+
65+
b.ReportAllocs()
66+
b.ResetTimer()
67+
for i := 0; i < b.N; i++ {
68+
q := queries[i%len(queries)]
69+
rows, err := c.query(ctx, q, nil)
70+
if err != nil {
71+
b.Fatal(err)
72+
}
73+
drainRows(b, rows)
74+
}
75+
})
76+
}
77+
}
78+
79+
func drainRows(b *testing.B, rows driver.Rows) {
80+
b.Helper()
81+
dest := make([]driver.Value, len(rows.Columns()))
82+
for {
83+
if err := rows.Next(dest); err != nil {
84+
break
85+
}
86+
}
87+
rows.Close()
88+
}

sqlite3_stmt_cache_test.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
2+
//
3+
// Use of this source code is governed by an MIT-style
4+
// license that can be found in the LICENSE file.
5+
6+
//go:build cgo
7+
// +build cgo
8+
9+
package sqlite3
10+
11+
import (
12+
"context"
13+
"testing"
14+
)
15+
16+
// TestStmtCacheLRUEviction verifies that when the prepared-statement cache is
17+
// full, the least-recently-used entry is evicted to make room for a new one.
18+
// Without eviction, the first N queries to enter the cache would squat on
19+
// every slot forever and any subsequently-prepared query (even a hot one)
20+
// would never benefit from caching.
21+
func TestStmtCacheLRUEviction(t *testing.T) {
22+
d := SQLiteDriver{}
23+
conn, err := d.Open(":memory:?_stmt_cache_size=2")
24+
if err != nil {
25+
t.Fatal(err)
26+
}
27+
defer conn.Close()
28+
29+
c := conn.(*SQLiteConn)
30+
ctx := context.Background()
31+
32+
prepareAndClose := func(q string) {
33+
t.Helper()
34+
stmt, err := c.prepareWithCache(ctx, q)
35+
if err != nil {
36+
t.Fatalf("prepareWithCache(%q): %v", q, err)
37+
}
38+
if err := stmt.Close(); err != nil {
39+
t.Fatalf("Close(%q): %v", q, err)
40+
}
41+
}
42+
43+
q1 := "SELECT 1"
44+
q2 := "SELECT 2"
45+
q3 := "SELECT 3"
46+
47+
// Fill the cache with q1 and q2.
48+
prepareAndClose(q1)
49+
prepareAndClose(q2)
50+
if got, want := c.stmtCacheCount, 2; got != want {
51+
t.Fatalf("after filling: stmtCacheCount = %d, want %d", got, want)
52+
}
53+
if cacheCount(c, q1) != 1 || cacheCount(c, q2) != 1 {
54+
t.Fatalf("after filling: expected q1 and q2 cached, got %#v", cacheKeys(c))
55+
}
56+
57+
// Insert q3. q1 is the oldest entry and should be evicted.
58+
prepareAndClose(q3)
59+
if got, want := c.stmtCacheCount, 2; got != want {
60+
t.Fatalf("after q3: stmtCacheCount = %d, want %d", got, want)
61+
}
62+
if cacheCount(c, q1) != 0 {
63+
t.Fatalf("after q3: q1 should have been evicted, cache=%#v", cacheKeys(c))
64+
}
65+
if cacheCount(c, q2) != 1 || cacheCount(c, q3) != 1 {
66+
t.Fatalf("after q3: expected q2 and q3 cached, got %#v", cacheKeys(c))
67+
}
68+
69+
// Touching q2 should make q3 the oldest (the entry at buf[0]).
70+
prepareAndClose(q2)
71+
if c.stmtCacheCount == 0 || c.stmtCacheBuf[0].cacheKey != q3 {
72+
var head string
73+
if c.stmtCacheCount > 0 {
74+
head = c.stmtCacheBuf[0].cacheKey
75+
}
76+
t.Fatalf("after touching q2: expected q3 at buf[0] (LRU), got %q", head)
77+
}
78+
79+
// Insert q1 again. Now q3 should be evicted (q2 is newer).
80+
prepareAndClose(q1)
81+
if cacheCount(c, q3) != 0 {
82+
t.Fatalf("after reinserting q1: q3 should have been evicted, cache=%#v", cacheKeys(c))
83+
}
84+
if cacheCount(c, q1) != 1 || cacheCount(c, q2) != 1 {
85+
t.Fatalf("after reinserting q1: expected q1 and q2 cached, got %#v", cacheKeys(c))
86+
}
87+
if got, want := c.stmtCacheCount, 2; got != want {
88+
t.Fatalf("after reinserting q1: stmtCacheCount = %d, want %d", got, want)
89+
}
90+
91+
// Sanity-check: no dangling entries past stmtCacheCount.
92+
for i := c.stmtCacheCount; i < len(c.stmtCacheBuf); i++ {
93+
if c.stmtCacheBuf[i] != nil {
94+
t.Fatalf("stmtCacheBuf[%d] = %p, expected nil tail slot", i, c.stmtCacheBuf[i])
95+
}
96+
}
97+
}
98+
99+
// TestStmtCacheReuseReturnsSameHandle verifies that a cached prepare reuses
100+
// the underlying sqlite3_stmt rather than preparing a fresh one.
101+
func TestStmtCacheReuseReturnsSameHandle(t *testing.T) {
102+
d := SQLiteDriver{}
103+
conn, err := d.Open(":memory:?_stmt_cache_size=4")
104+
if err != nil {
105+
t.Fatal(err)
106+
}
107+
defer conn.Close()
108+
109+
c := conn.(*SQLiteConn)
110+
ctx := context.Background()
111+
112+
const q = "SELECT 42"
113+
stmt1, err := c.prepareWithCache(ctx, q)
114+
if err != nil {
115+
t.Fatal(err)
116+
}
117+
h1 := stmt1.(*SQLiteStmt).s
118+
if err := stmt1.Close(); err != nil {
119+
t.Fatal(err)
120+
}
121+
122+
stmt2, err := c.prepareWithCache(ctx, q)
123+
if err != nil {
124+
t.Fatal(err)
125+
}
126+
h2 := stmt2.(*SQLiteStmt).s
127+
if err := stmt2.Close(); err != nil {
128+
t.Fatal(err)
129+
}
130+
131+
if h1 != h2 {
132+
t.Fatalf("expected cached prepare to reuse sqlite3_stmt handle, got %p vs %p", h1, h2)
133+
}
134+
}
135+
136+
func cacheKeys(c *SQLiteConn) map[string]int {
137+
out := make(map[string]int)
138+
for i := 0; i < c.stmtCacheCount; i++ {
139+
out[c.stmtCacheBuf[i].cacheKey]++
140+
}
141+
return out
142+
}
143+
144+
func cacheCount(c *SQLiteConn, q string) int {
145+
n := 0
146+
for i := 0; i < c.stmtCacheCount; i++ {
147+
if c.stmtCacheBuf[i].cacheKey == q {
148+
n++
149+
}
150+
}
151+
return n
152+
}

0 commit comments

Comments
 (0)