Skip to content

Commit 32264e3

Browse files
bench: measure the path a request actually takes (#195)
The benchmark timed only `.toSQL()`, which recompiles per call — the right measurement for a query whose shape varies per request, and the wrong one for the shape a request usually has. select-all 1438ns -> 51ns 28x select-where-eq 3291ns -> 58ns 57x select-where-deep-and 10614ns -> 148ns 72x insert-values 3356ns -> 63ns 53x update-where 3481ns -> 59ns 59x Also records what a loaded machine does to the perf guard: 9, then 13, then 12 of 48 failing with a different set each run, every failure 0–5% under its floor. At load 2 it passes 48/48.
1 parent 099dc42 commit 32264e3

2 files changed

Lines changed: 150 additions & 1 deletion

File tree

bench/README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ Prisma is intentionally **not** included: Prisma is a code-gen + engine layer ra
1010

1111
```bash
1212
pnpm install
13-
pnpm vitest bench --run bench/compile.bench.ts
13+
pnpm vitest bench --run bench/compile.bench.ts # sumak vs kysely vs drizzle
14+
pnpm vitest bench --run bench/prepared.bench.ts # the two paths within sumak
1415
```
1516

1617
## Smoke test
@@ -95,6 +96,33 @@ wave (PRs #155, #156, #162, #164, #165, #166): `REGEXP_REPLACE`,
9596
AST node (extract-field, position-IN keyword form, inlined pattern /
9697
unit literals) and the competitors fall back to raw template literals.
9798

99+
## The two paths — `bench/prepared.bench.ts`
100+
101+
`compile.bench.ts` above times the whole pipeline, which is what `.toSQL()`
102+
runs on every call. That is the right measurement for a query whose shape
103+
genuinely varies per request, and the wrong one for the shape a request
104+
usually has, where the query was written once and only the values change.
105+
Nothing measured that until this file existed.
106+
107+
| scenario | `toSQL()` | `toCompiled()` | |
108+
| ----------------------- | --------: | -------------: | --: |
109+
| `select-all` | 1438ns | 51ns | 28× |
110+
| `select-where-eq` | 3291ns | 58ns | 57× |
111+
| `select-where-deep-and` | 10614ns | 148ns | 72× |
112+
| `insert-values` | 3356ns | 63ns | 53× |
113+
| `update-where` | 3481ns | 59ns | 59× |
114+
115+
Everything the pipeline does — plugin transforms, hooks, normalize, optimize,
116+
print — happens once, at definition. What is left is filling the parameter
117+
array.
118+
119+
Two things the table does not show. A compiled query's SQL text is fixed, so
120+
`sumak/drivers/pg` sends it as a named prepared statement and the server keeps
121+
the plan: 243µs against 303µs for the same query, measured with `PREPARE` /
122+
`EXECUTE` against pglite. And end to end a single query costs ~330µs either
123+
way, because the compile this removes is ~1% of it — the split pays on a cold
124+
start, under CPU pressure, and through that plan reuse.
125+
98126
## Plugin overhead microbench
99127

100128
`bench/plugin.bench.ts` measures the per-compile cost of each built-in

bench/prepared.bench.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { bench, describe } from "vitest"
2+
3+
import { placeholder } from "../src/builder/compiled.ts"
4+
import { and } from "../src/builder/eb.ts"
5+
import { pgDialect } from "../src/dialect/pg.ts"
6+
import { sumak } from "../src/sumak.ts"
7+
import { tables } from "./src/schema-sumak.ts"
8+
9+
/**
10+
* What a request pays, on each of the two paths.
11+
*
12+
* `compile.bench.ts` times the whole pipeline, which is what `.toSQL()` runs on
13+
* every call. That is the right measurement for a query whose shape genuinely
14+
* varies per request. It is the wrong one for the shape a request usually has,
15+
* where the query was written once and only the values change — and until this
16+
* file existed, nothing measured that at all.
17+
*
18+
* Run with:
19+
* pnpm vitest bench --run bench/prepared.bench.ts
20+
*/
21+
22+
const db = sumak({ dialect: pgDialect(), tables })
23+
24+
const scenarios = [
25+
{
26+
name: "select-all",
27+
live: () => db.selectFrom("users").selectAll().toSQL(),
28+
prepared: db.selectFrom("users").selectAll().toCompiled(),
29+
args: {},
30+
},
31+
{
32+
name: "select-where-eq",
33+
live: () =>
34+
db
35+
.selectFrom("users")
36+
.select("id", "name")
37+
.where(({ id }) => id.eq(1))
38+
.toSQL(),
39+
prepared: db
40+
.selectFrom("users")
41+
.select("id", "name")
42+
.where(({ id }) => id.eq(placeholder("id") as never))
43+
.toCompiled<{ id: number }>(),
44+
args: { id: 1 },
45+
},
46+
{
47+
name: "select-where-deep-and",
48+
live: () =>
49+
db
50+
.selectFrom("posts")
51+
.selectAll()
52+
.where(({ authorId, published, title, body, id }) =>
53+
and(authorId.eq(1), published.gt(0), title.neq(""), body.neq(""), id.gt(0)),
54+
)
55+
.toSQL(),
56+
prepared: db
57+
.selectFrom("posts")
58+
.selectAll()
59+
.where(({ authorId, published, title, body, id }) =>
60+
and(
61+
authorId.eq(placeholder("authorId") as never),
62+
published.gt(placeholder("published") as never),
63+
title.neq(placeholder("title") as never),
64+
body.neq(placeholder("body") as never),
65+
id.gt(placeholder("id") as never),
66+
),
67+
)
68+
.toCompiled<{
69+
authorId: number
70+
published: number
71+
title: string
72+
body: string
73+
id: number
74+
}>(),
75+
args: { authorId: 1, published: 0, title: "", body: "", id: 0 },
76+
},
77+
{
78+
name: "insert-values",
79+
live: () =>
80+
db
81+
.insertInto("users")
82+
.values({ id: 1, name: "a", email: "a@x.io", createdAt: new Date(0) })
83+
.toSQL(),
84+
prepared: db
85+
.insertInto("users")
86+
.values({
87+
id: placeholder("id") as never,
88+
name: placeholder("name") as never,
89+
email: placeholder("email") as never,
90+
createdAt: placeholder("createdAt") as never,
91+
})
92+
.toCompiled<{ id: number; name: string; email: string; createdAt: Date }>(),
93+
args: { id: 1, name: "a", email: "a@x.io", createdAt: new Date(0) },
94+
},
95+
{
96+
name: "update-where",
97+
live: () =>
98+
db
99+
.update("users")
100+
.set({ name: "a" })
101+
.where(({ id }) => id.eq(1))
102+
.toSQL(),
103+
prepared: db
104+
.update("users")
105+
.set({ name: placeholder("name") as never })
106+
.where(({ id }) => id.eq(placeholder("id") as never))
107+
.toCompiled<{ name: string; id: number }>(),
108+
args: { name: "a", id: 1 },
109+
},
110+
] as const
111+
112+
for (const sc of scenarios) {
113+
describe(sc.name, () => {
114+
bench("toSQL — recompiled per call", () => {
115+
sc.live()
116+
})
117+
bench("toCompiled — parameters only", () => {
118+
;(sc.prepared as (args: unknown) => unknown)(sc.args)
119+
})
120+
})
121+
}

0 commit comments

Comments
 (0)