Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/bpf/sqlite.bpf.c
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ enum ev_kind {
EV_BIND = 2,
EV_STEP = 3,
EV_EXEC = 4, // sqlite3_exec() one-shot: SQL + total latency + result code
EV_FINALIZE = 5, // sqlite3_finalize(): the stmt pointer is dead after this
};

// Bound-value type. REAL is captured as a *type only*: doubles arrive in an
Expand Down Expand Up @@ -320,6 +321,29 @@ int BPF_URETPROBE(exec_return, int rc)
return 0;
}

// ── finalize(stmt) — entry-only, no pairing needed ───────────────────────────
// A finalized stmt pointer returns to the allocator and WILL be reused for a
// future statement. Without eviction the reused pointer aliases the dead one:
// the `known` entry suppresses zSql recovery for the new statement, and
// userspace keeps attributing the old SQL to it. Emit the death so both sides
// forget the pointer.
SEC("uprobe")
int BPF_UPROBE(finalize_entry, void *stmt)
{
if (!stmt) {
return 0;
}
__u64 key = (__u64)stmt;
bpf_map_delete_elem(&known, &key);

struct sqlite_event *e = new_event(EV_FINALIZE);
if (e) {
e->stmt = key;
bpf_ringbuf_submit(e, 0);
}
return 0;
}

// ── bind_*(stmt, idx, value, …) — entry-only, no pairing needed ───────────────
static __always_inline void emit_bind(void *stmt, int idx, enum bind_type bt,
__s64 ival, const char *text)
Expand Down
18 changes: 16 additions & 2 deletions src/probes/sqlite.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { BpfObject, RingBuf } from "yeet:bpf";
import { from, signal } from "yeet:tui";

// ── constants shared with sqlite.bpf.c ──────────────────────────────────────
const EV = { PREPARE: 1, BIND: 2, STEP: 3, EXEC: 4 };
const EV = { PREPARE: 1, BIND: 2, STEP: 3, EXEC: 4, FINALIZE: 5 };
const BT = { NULL: 0, INT: 1, TEXT: 2, REAL: 3 };
// sqlite result codes we distinguish (see lib/format.js for names/colors).
const SQLITE_ROW = 100;
Expand Down Expand Up @@ -54,6 +54,7 @@ const ATTACH = [
["bind_text", up("sqlite3_bind_text")],
["bind_null", up("sqlite3_bind_null")],
["bind_double", up("sqlite3_bind_double")],
["finalize_entry", up("sqlite3_finalize")],
];

const load = () => {
Expand Down Expand Up @@ -203,6 +204,18 @@ export const statements = from((state) => {
return;
}

// The stmt pointer is dead — the allocator will hand it to a future
// statement, so forget everything tied to it. A lingering execution
// that did real work is complete by definition (nothing can step a
// finalized stmt); flush it rather than drop it.
if (ev.kind === EV.FINALIZE) {
const e = cur.get(stmtId);
if (e && (e.steps > 0 || e.params.size > 0)) finalize(e);
cur.delete(stmtId);
sqlByStmt.delete(stmtId);
return;
}

if (ev.kind === EV.BIND) {
// A bind after the previous execution already stepped is a
// reset+rebind — a new execution of a cached statement.
Expand Down Expand Up @@ -268,14 +281,15 @@ export const statements = from((state) => {
if (import.meta.main) {
const ctl = await load();
const rb = new RingBuf(ctl, "sql_events");
const kindName = { 1: "PREPARE", 2: "BIND", 3: "STEP", 4: "EXEC" };
const kindName = { 1: "PREPARE", 2: "BIND", 3: "STEP", 4: "EXEC", 5: "FINALIZE" };
console.log(`[sqlite] attached ${ATTACH.length} probes on ${SQLITE_LIB} — waiting…`);
rb.subscribe((w) => {
const e = unwrap(w);
const head = `[${kindName[e.kind]}] ${cstr(e.comm)}/${e.pid} stmt=0x${e.stmt.toString(16)}`;
if (e.kind === EV.PREPARE) console.log(`${head} rc=${e.rc} sql=${JSON.stringify(cstr(e.text))}`);
else if (e.kind === EV.EXEC) console.log(`${head} rc=${e.rc} latency=${Number(e.latency_ns)}ns sql=${JSON.stringify(cstr(e.text))}${e.rc ? ` err=${JSON.stringify(cstr(e.errmsg))}` : ""}`);
else if (e.kind === EV.BIND) console.log(`${head} bind #${e.param_idx} = ${JSON.stringify(bindValue(e).text)}`);
else if (e.kind === EV.FINALIZE) console.log(head);
else console.log(`${head} rc=${e.rc} latency=${Number(e.latency_ns)}ns`);
});
await new Promise(() => {});
Expand Down
Loading