Skip to content

Commit 2a9afd0

Browse files
committed
Add prepared statement cache and benchmarks implementation plans
Plan 5: Prepared statement cache with LRU, deep-copy AST, bindings-aware emitter Plan 6: Google Benchmark performance tests for all parser operations
1 parent b5dcbc7 commit 2a9afd0

2 files changed

Lines changed: 1065 additions & 0 deletions

File tree

Lines changed: 372 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
1+
# Performance Benchmarks Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** Add Google Benchmark-based performance tests to validate the parser meets its latency targets and catch performance regressions.
6+
7+
**Architecture:** Benchmarks use Google Benchmark (vendored alongside Google Test). Each benchmark measures a specific operation in isolation: Tier 2 classification, Tier 1 SET parse, Tier 1 SELECT parse (simple and complex), query reconstruction (round-trip), and arena reset. A single parser instance is reused across iterations (matching ProxySQL's per-thread usage pattern).
8+
9+
**Tech Stack:** C++17, Google Benchmark
10+
11+
**Spec:** `docs/superpowers/specs/2026-03-24-sql-parser-design.md` (Performance Targets section)
12+
13+
---
14+
15+
## Scope
16+
17+
1. Vendor Google Benchmark
18+
2. Benchmark targets matching the spec:
19+
- Tier 2 classification: <100ns
20+
- Tier 1 SET parse: <300ns
21+
- Tier 1 SELECT parse (simple): <500ns
22+
- Tier 1 SELECT parse (complex): <2us
23+
- Query reconstruction: <500ns
24+
- Arena reset: <10ns
25+
3. Makefile.new `bench` target
26+
27+
**Not in scope:** CI integration for benchmarks (too noisy in CI), optimization work.
28+
29+
---
30+
31+
## File Structure
32+
33+
```
34+
bench/
35+
bench_main.cpp — Google Benchmark main
36+
bench_parser.cpp — All parser benchmarks
37+
38+
Makefile.new — (modify) Add bench target
39+
```
40+
41+
---
42+
43+
### Task 1: Benchmark Setup and All Benchmarks
44+
45+
**Files:**
46+
- Create: `bench/bench_main.cpp`
47+
- Create: `bench/bench_parser.cpp`
48+
- Modify: `Makefile.new`
49+
50+
- [ ] **Step 1: Vendor Google Benchmark**
51+
52+
```bash
53+
git clone --depth 1 --branch v1.9.1 https://github.com/google/benchmark.git third_party/benchmark
54+
```
55+
56+
- [ ] **Step 2: Create bench_main.cpp**
57+
58+
Create `bench/bench_main.cpp`:
59+
```cpp
60+
#include <benchmark/benchmark.h>
61+
62+
BENCHMARK_MAIN();
63+
```
64+
65+
- [ ] **Step 3: Create bench_parser.cpp**
66+
67+
Create `bench/bench_parser.cpp`:
68+
```cpp
69+
#include <benchmark/benchmark.h>
70+
#include "sql_parser/parser.h"
71+
#include "sql_parser/emitter.h"
72+
73+
using namespace sql_parser;
74+
75+
// ========== Tier 2: Classification ==========
76+
// Target: <100ns
77+
78+
static void BM_Classify_Insert(benchmark::State& state) {
79+
Parser<Dialect::MySQL> parser;
80+
const char* sql = "INSERT INTO users VALUES (1, 'name', 'email')";
81+
size_t len = strlen(sql);
82+
for (auto _ : state) {
83+
auto r = parser.parse(sql, len);
84+
benchmark::DoNotOptimize(r.stmt_type);
85+
}
86+
}
87+
BENCHMARK(BM_Classify_Insert);
88+
89+
static void BM_Classify_Update(benchmark::State& state) {
90+
Parser<Dialect::MySQL> parser;
91+
const char* sql = "UPDATE users SET name = 'x' WHERE id = 1";
92+
size_t len = strlen(sql);
93+
for (auto _ : state) {
94+
auto r = parser.parse(sql, len);
95+
benchmark::DoNotOptimize(r.stmt_type);
96+
}
97+
}
98+
BENCHMARK(BM_Classify_Update);
99+
100+
static void BM_Classify_Delete(benchmark::State& state) {
101+
Parser<Dialect::MySQL> parser;
102+
const char* sql = "DELETE FROM users WHERE id = 1";
103+
size_t len = strlen(sql);
104+
for (auto _ : state) {
105+
auto r = parser.parse(sql, len);
106+
benchmark::DoNotOptimize(r.stmt_type);
107+
}
108+
}
109+
BENCHMARK(BM_Classify_Delete);
110+
111+
static void BM_Classify_Begin(benchmark::State& state) {
112+
Parser<Dialect::MySQL> parser;
113+
const char* sql = "BEGIN";
114+
size_t len = strlen(sql);
115+
for (auto _ : state) {
116+
auto r = parser.parse(sql, len);
117+
benchmark::DoNotOptimize(r.stmt_type);
118+
}
119+
}
120+
BENCHMARK(BM_Classify_Begin);
121+
122+
// ========== Tier 1: SET parse ==========
123+
// Target: <300ns
124+
125+
static void BM_Set_Simple(benchmark::State& state) {
126+
Parser<Dialect::MySQL> parser;
127+
const char* sql = "SET @@session.wait_timeout = 600";
128+
size_t len = strlen(sql);
129+
for (auto _ : state) {
130+
auto r = parser.parse(sql, len);
131+
benchmark::DoNotOptimize(r.ast);
132+
}
133+
}
134+
BENCHMARK(BM_Set_Simple);
135+
136+
static void BM_Set_Names(benchmark::State& state) {
137+
Parser<Dialect::MySQL> parser;
138+
const char* sql = "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci";
139+
size_t len = strlen(sql);
140+
for (auto _ : state) {
141+
auto r = parser.parse(sql, len);
142+
benchmark::DoNotOptimize(r.ast);
143+
}
144+
}
145+
BENCHMARK(BM_Set_Names);
146+
147+
static void BM_Set_MultiVar(benchmark::State& state) {
148+
Parser<Dialect::MySQL> parser;
149+
const char* sql = "SET autocommit = 1, wait_timeout = 28800, sql_mode = 'STRICT_TRANS_TABLES'";
150+
size_t len = strlen(sql);
151+
for (auto _ : state) {
152+
auto r = parser.parse(sql, len);
153+
benchmark::DoNotOptimize(r.ast);
154+
}
155+
}
156+
BENCHMARK(BM_Set_MultiVar);
157+
158+
static void BM_Set_FunctionRHS(benchmark::State& state) {
159+
Parser<Dialect::MySQL> parser;
160+
const char* sql = "SET sql_mode = CONCAT(@@sql_mode, ',STRICT_TRANS_TABLES')";
161+
size_t len = strlen(sql);
162+
for (auto _ : state) {
163+
auto r = parser.parse(sql, len);
164+
benchmark::DoNotOptimize(r.ast);
165+
}
166+
}
167+
BENCHMARK(BM_Set_FunctionRHS);
168+
169+
// ========== Tier 1: SELECT parse ==========
170+
// Target: <500ns simple, <2us complex
171+
172+
static void BM_Select_Simple(benchmark::State& state) {
173+
Parser<Dialect::MySQL> parser;
174+
const char* sql = "SELECT col FROM t WHERE id = 1";
175+
size_t len = strlen(sql);
176+
for (auto _ : state) {
177+
auto r = parser.parse(sql, len);
178+
benchmark::DoNotOptimize(r.ast);
179+
}
180+
}
181+
BENCHMARK(BM_Select_Simple);
182+
183+
static void BM_Select_MultiColumn(benchmark::State& state) {
184+
Parser<Dialect::MySQL> parser;
185+
const char* sql = "SELECT id, name, email, status FROM users WHERE active = 1 ORDER BY name LIMIT 100";
186+
size_t len = strlen(sql);
187+
for (auto _ : state) {
188+
auto r = parser.parse(sql, len);
189+
benchmark::DoNotOptimize(r.ast);
190+
}
191+
}
192+
BENCHMARK(BM_Select_MultiColumn);
193+
194+
static void BM_Select_Join(benchmark::State& state) {
195+
Parser<Dialect::MySQL> parser;
196+
const char* sql = "SELECT u.id, o.total FROM users u JOIN orders o ON u.id = o.user_id WHERE o.status = 'active'";
197+
size_t len = strlen(sql);
198+
for (auto _ : state) {
199+
auto r = parser.parse(sql, len);
200+
benchmark::DoNotOptimize(r.ast);
201+
}
202+
}
203+
BENCHMARK(BM_Select_Join);
204+
205+
static void BM_Select_Complex(benchmark::State& state) {
206+
Parser<Dialect::MySQL> parser;
207+
const char* sql =
208+
"SELECT u.id, u.name, COUNT(o.id) AS order_count "
209+
"FROM users u "
210+
"LEFT JOIN orders o ON u.id = o.user_id "
211+
"WHERE u.status = 'active' AND u.created_at > '2024-01-01' "
212+
"GROUP BY u.id, u.name "
213+
"HAVING COUNT(o.id) > 5 "
214+
"ORDER BY order_count DESC "
215+
"LIMIT 50 OFFSET 10";
216+
size_t len = strlen(sql);
217+
for (auto _ : state) {
218+
auto r = parser.parse(sql, len);
219+
benchmark::DoNotOptimize(r.ast);
220+
}
221+
}
222+
BENCHMARK(BM_Select_Complex);
223+
224+
static void BM_Select_MultiJoin(benchmark::State& state) {
225+
Parser<Dialect::MySQL> parser;
226+
const char* sql =
227+
"SELECT a.id, b.name, c.value, d.total "
228+
"FROM t1 a "
229+
"JOIN t2 b ON a.id = b.a_id "
230+
"LEFT JOIN t3 c ON b.id = c.b_id "
231+
"JOIN t4 d ON c.id = d.c_id "
232+
"WHERE a.status = 1 AND d.total > 100 "
233+
"ORDER BY d.total DESC "
234+
"LIMIT 20";
235+
size_t len = strlen(sql);
236+
for (auto _ : state) {
237+
auto r = parser.parse(sql, len);
238+
benchmark::DoNotOptimize(r.ast);
239+
}
240+
}
241+
BENCHMARK(BM_Select_MultiJoin);
242+
243+
// ========== Query Reconstruction (round-trip) ==========
244+
// Target: <500ns
245+
246+
static void BM_Emit_SetSimple(benchmark::State& state) {
247+
Parser<Dialect::MySQL> parser;
248+
const char* sql = "SET autocommit = 1";
249+
size_t len = strlen(sql);
250+
for (auto _ : state) {
251+
auto r = parser.parse(sql, len);
252+
Emitter<Dialect::MySQL> emitter(parser.arena());
253+
emitter.emit(r.ast);
254+
benchmark::DoNotOptimize(emitter.result());
255+
}
256+
}
257+
BENCHMARK(BM_Emit_SetSimple);
258+
259+
static void BM_Emit_SelectSimple(benchmark::State& state) {
260+
Parser<Dialect::MySQL> parser;
261+
const char* sql = "SELECT * FROM users WHERE id = 1";
262+
size_t len = strlen(sql);
263+
for (auto _ : state) {
264+
auto r = parser.parse(sql, len);
265+
Emitter<Dialect::MySQL> emitter(parser.arena());
266+
emitter.emit(r.ast);
267+
benchmark::DoNotOptimize(emitter.result());
268+
}
269+
}
270+
BENCHMARK(BM_Emit_SelectSimple);
271+
272+
// ========== Arena reset ==========
273+
// Target: <10ns
274+
275+
static void BM_ArenaReset(benchmark::State& state) {
276+
Arena arena(65536);
277+
for (auto _ : state) {
278+
arena.allocate(256); // allocate something
279+
arena.reset();
280+
benchmark::DoNotOptimize(arena.bytes_used());
281+
}
282+
}
283+
BENCHMARK(BM_ArenaReset);
284+
285+
// ========== PostgreSQL ==========
286+
287+
static void BM_PgSQL_Select_Simple(benchmark::State& state) {
288+
Parser<Dialect::PostgreSQL> parser;
289+
const char* sql = "SELECT col FROM t WHERE id = 1";
290+
size_t len = strlen(sql);
291+
for (auto _ : state) {
292+
auto r = parser.parse(sql, len);
293+
benchmark::DoNotOptimize(r.ast);
294+
}
295+
}
296+
BENCHMARK(BM_PgSQL_Select_Simple);
297+
298+
static void BM_PgSQL_Set_Simple(benchmark::State& state) {
299+
Parser<Dialect::PostgreSQL> parser;
300+
const char* sql = "SET work_mem = '256MB'";
301+
size_t len = strlen(sql);
302+
for (auto _ : state) {
303+
auto r = parser.parse(sql, len);
304+
benchmark::DoNotOptimize(r.ast);
305+
}
306+
}
307+
BENCHMARK(BM_PgSQL_Set_Simple);
308+
```
309+
310+
- [ ] **Step 4: Update Makefile.new**
311+
312+
Add benchmark build rules to `Makefile.new`:
313+
314+
```makefile
315+
# Google Benchmark
316+
GBENCH_DIR = $(PROJECT_ROOT)/third_party/benchmark
317+
### NOTE: After cloning Google Benchmark v1.9.1, verify the actual source files:
318+
### ls third_party/benchmark/src/*.cc
319+
### Then set GBENCH_SRCS to match. The following is a common set:
320+
GBENCH_SRCS = $(wildcard $(GBENCH_DIR)/src/*.cc)
321+
GBENCH_OBJS = $(GBENCH_SRCS:.cc=.o)
322+
GBENCH_CPPFLAGS = -I$(GBENCH_DIR)/include -I$(GBENCH_DIR)/src -DHAVE_STD_REGEX -DHAVE_STEADY_CLOCK
323+
324+
BENCH_DIR = $(PROJECT_ROOT)/bench
325+
BENCH_SRCS = $(BENCH_DIR)/bench_main.cpp $(BENCH_DIR)/bench_parser.cpp
326+
BENCH_OBJS = $(BENCH_SRCS:.cpp=.o)
327+
BENCH_TARGET = $(PROJECT_ROOT)/run_bench
328+
329+
# Benchmark objects
330+
$(GBENCH_DIR)/src/%.o: $(GBENCH_DIR)/src/%.cc
331+
$(CXX) $(CXXFLAGS) $(GBENCH_CPPFLAGS) -c $< -o $@
332+
333+
$(BENCH_DIR)/%.o: $(BENCH_DIR)/%.cpp
334+
$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(GBENCH_CPPFLAGS) -c $< -o $@
335+
336+
bench: $(BENCH_TARGET)
337+
./$(BENCH_TARGET) --benchmark_format=console
338+
339+
$(BENCH_TARGET): $(BENCH_OBJS) $(GBENCH_OBJS) $(LIB_TARGET)
340+
$(CXX) $(CXXFLAGS) -o $@ $(BENCH_OBJS) $(GBENCH_OBJS) -L$(PROJECT_ROOT) -lsqlparser -lpthread
341+
```
342+
343+
Add `bench` to `.PHONY` and update `clean` to include bench artifacts:
344+
```makefile
345+
.PHONY: all lib test bench clean
346+
```
347+
348+
Add to clean:
349+
```makefile
350+
rm -f $(BENCH_OBJS) $(GBENCH_OBJS) $(BENCH_TARGET)
351+
```
352+
353+
- [ ] **Step 5: Create bench directory and build**
354+
355+
```bash
356+
mkdir -p bench
357+
make -f Makefile.new clean && make -f Makefile.new lib && make -f Makefile.new test && make -f Makefile.new bench
358+
```
359+
360+
- [ ] **Step 6: Update .gitignore**
361+
362+
Add to `.gitignore`:
363+
```
364+
run_bench
365+
```
366+
367+
- [ ] **Step 7: Commit**
368+
369+
```bash
370+
git add bench/ third_party/benchmark Makefile.new .gitignore
371+
git commit -m "feat: add Google Benchmark performance tests for parser operations"
372+
```

0 commit comments

Comments
 (0)