refactor(cache): Phase B language-advantage pass — dichotomy wave
One pooled *sql.DB per Env (lazily created, schema verified once — the oracle re-ran the PRAGMA/DDL block on every call; identical observable behavior since IF NOT EXISTS is idempotent and WAL persists in the file). ContentHash hex-encodes only the 8 bytes it returns; Compress builds via strings.Builder. Benchmarks added (hash 140ns/op, compress 6.5µs, recordHit 0.9µs, getCache-hit 23.7µs, putCache 42.6µs). Full ladder re-run after the rewrite: frozen battery green, gofmt/vet/ build clean, live parity 100% (16/16), freeze --verify 78/78, Python battery 12 passed both pins.
This commit is contained in:
1 parent
f944d7df38
commit
e0fd14409e
10 files changed
+101
-22
No files matched your search
Vendored
+24
-18
@@ -48,10 +48,9 @@ var invalidationMap = map[string][]string{
|
||||
// Env — the module-global state of cache.py (DB path, clock, the
|
||||
// in-memory hit/miss accumulators) behind injectable seams.
|
||||
type Env struct {
|
||||
MemoryDir string
|
||||
clockFn func() time.Time
|
||||
monotonic func() float64
|
||||
dbPathOnce string
|
||||
MemoryDir string
|
||||
clockFn func() time.Time
|
||||
monotonic func() float64
|
||||
|
||||
mu sync.Mutex
|
||||
hitCounts map[string]int
|
||||
@@ -60,6 +59,10 @@ type Env struct {
|
||||
lastMisses map[string]string
|
||||
flushCounter int
|
||||
lastFlushT float64
|
||||
|
||||
dbOnce sync.Once
|
||||
db *sql.DB
|
||||
dbErr error
|
||||
}
|
||||
|
||||
// NewEnv — module globals at import time (fresh accumulators, empty
|
||||
@@ -169,6 +172,16 @@ func (e *Env) connect() (*sql.DB, error) {
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// openDB — Phase B: one pooled *sql.DB per Env (lazily created, schema
|
||||
// verified once). The oracle opens a fresh sqlite3 connection AND
|
||||
// re-runs the PRAGMA/DDL block on every call; the pool removes the
|
||||
// per-call DDL walk with identical observable behavior (IF NOT EXISTS
|
||||
// is idempotent; WAL mode persists in the database file).
|
||||
func (e *Env) openDB() (*sql.DB, error) {
|
||||
e.dbOnce.Do(func() { e.db, e.dbErr = e.connect() })
|
||||
return e.db, e.dbErr
|
||||
}
|
||||
|
||||
// recordHit — Python _record_hit: accumulate a hit in memory; flush to
|
||||
// SQLite when the threshold is reached.
|
||||
func (e *Env) recordHit(key string) {
|
||||
@@ -236,11 +249,10 @@ func (e *Env) flushStats() {
|
||||
return
|
||||
}
|
||||
|
||||
db, err := e.connect()
|
||||
db, err := e.openDB()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
for key, count := range hits {
|
||||
db.Exec("UPDATE cache SET hit_count=hit_count+?, last_hit=? WHERE key=?",
|
||||
count, lastOrNow(lh, key, e), key)
|
||||
@@ -262,11 +274,10 @@ func lastOrNow(m map[string]string, key string, e *Env) string {
|
||||
// memory (no SQLite write on the hot path). The pending count is
|
||||
// captured BEFORE recording (the oracle's double-count audit fix).
|
||||
func (e *Env) getCache(key string) *OrderedMap {
|
||||
db, err := e.connect()
|
||||
db, err := e.openDB()
|
||||
if err != nil {
|
||||
return NewOrderedMap(KV{"hit", false}, KV{"key", key})
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var compressed, full, contentHash, scope, createdAt sql.NullString
|
||||
var hitCount int
|
||||
@@ -310,7 +321,7 @@ func nullStr(ns sql.NullString) any {
|
||||
func (e *Env) putCache(key, value string, compressed *string, scope, sourceHash string) *OrderedMap {
|
||||
e.flushStats()
|
||||
|
||||
db, err := e.connect()
|
||||
db, err := e.openDB()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -329,7 +340,6 @@ func (e *Env) putCache(key, value string, compressed *string, scope, sourceHash
|
||||
created_at = excluded.created_at,
|
||||
invalidated_at = NULL`,
|
||||
key, scope, contentHash, value, comp, sourceHash, e.now())
|
||||
db.Close()
|
||||
return NewOrderedMap(KV{"stored", key}, KV{"content_hash", contentHash}, KV{"scope", scope})
|
||||
}
|
||||
|
||||
@@ -343,10 +353,9 @@ func ContentCompressed(compressed *string, value string) string {
|
||||
|
||||
// invalidate — Python invalidate: invalidate a specific entry.
|
||||
func (e *Env) invalidate(key, reason string) *OrderedMap {
|
||||
db, err := e.connect()
|
||||
db, err := e.openDB()
|
||||
if err == nil {
|
||||
db.Exec("UPDATE cache SET invalidated_at=? WHERE key=? AND invalidated_at IS NULL", e.now(), key)
|
||||
db.Close()
|
||||
}
|
||||
return NewOrderedMap(KV{"invalidated", key}, KV{"reason", reason})
|
||||
}
|
||||
@@ -355,7 +364,7 @@ func (e *Env) invalidate(key, reason string) *OrderedMap {
|
||||
// a scope.
|
||||
func (e *Env) invalidateScope(scope, reason string) *OrderedMap {
|
||||
count := 0
|
||||
db, err := e.connect()
|
||||
db, err := e.openDB()
|
||||
if err == nil {
|
||||
if res, err := db.Exec(
|
||||
"UPDATE cache SET invalidated_at=? WHERE scope=? AND invalidated_at IS NULL", e.now(), scope); err == nil {
|
||||
@@ -363,7 +372,6 @@ func (e *Env) invalidateScope(scope, reason string) *OrderedMap {
|
||||
count = int(n)
|
||||
}
|
||||
}
|
||||
db.Close()
|
||||
}
|
||||
return NewOrderedMap(KV{"invalidated_scope", scope}, KV{"count", count}, KV{"reason", reason})
|
||||
}
|
||||
@@ -371,7 +379,7 @@ func (e *Env) invalidateScope(scope, reason string) *OrderedMap {
|
||||
// invalidateAll — Python invalidate_all: nuclear option.
|
||||
func (e *Env) invalidateAll(reason string) *OrderedMap {
|
||||
count := 0
|
||||
db, err := e.connect()
|
||||
db, err := e.openDB()
|
||||
if err == nil {
|
||||
if res, err := db.Exec(
|
||||
"UPDATE cache SET invalidated_at=? WHERE invalidated_at IS NULL", e.now()); err == nil {
|
||||
@@ -379,7 +387,6 @@ func (e *Env) invalidateAll(reason string) *OrderedMap {
|
||||
count = int(n)
|
||||
}
|
||||
}
|
||||
db.Close()
|
||||
}
|
||||
return NewOrderedMap(KV{"invalidated_all", count}, KV{"reason", reason})
|
||||
}
|
||||
@@ -389,14 +396,13 @@ func (e *Env) invalidateAll(reason string) *OrderedMap {
|
||||
func (e *Env) cacheStats() *OrderedMap {
|
||||
e.flushStats()
|
||||
|
||||
db, err := e.connect()
|
||||
db, err := e.openDB()
|
||||
if err != nil {
|
||||
return NewOrderedMap(
|
||||
KV{"total", 0}, KV{"active", 0}, KV{"invalidated", 0},
|
||||
KV{"hits", 0}, KV{"misses", 0}, KV{"hit_rate", "0%"},
|
||||
KV{"scopes", NewOrderedMap()})
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var total, active int
|
||||
var hits, misses sql.NullInt64
|
||||
|
||||
Vendored
+3
-1
@@ -9,7 +9,9 @@ import (
|
||||
)
|
||||
|
||||
// ContentHash — sha256[:16] hex (the cache's change-detection hash).
|
||||
// Phase B: hex-encode only the first 8 bytes (16 hex chars) instead of
|
||||
// encoding all 32 and slicing — identical output, half the work.
|
||||
func ContentHash(text string) string {
|
||||
sum := sha256.Sum256([]byte(text))
|
||||
return hex.EncodeToString(sum[:])[:16]
|
||||
return hex.EncodeToString(sum[:8])
|
||||
}
|
||||
Vendored
+48
@@ -14,6 +14,7 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -304,6 +305,53 @@ func TestPluginManifest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase B benchmarks ────────────────────────────────────────────────
|
||||
|
||||
var benchSink string
|
||||
|
||||
func BenchmarkContentHash(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
benchSink = ContentHash("benchmark input line for hashing")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCompress(b *testing.B) {
|
||||
text := strings.Repeat("# comment\n====\n(_source_: cite)\nsubstantive benchmark line number here\n\n", 40)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
benchSink = Compress(text)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRecordHitPending(b *testing.B) {
|
||||
e := NewEnv(b.TempDir())
|
||||
e.monotonic = func() float64 { return 0 } // interval never fires
|
||||
e.putCache("k", "v", nil, "general", "")
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
e.recordHit("k")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGetCacheHit(b *testing.B) {
|
||||
e := NewEnv(b.TempDir())
|
||||
e.monotonic = func() float64 { return 0 }
|
||||
e.putCache("k", "value line", nil, "general", "")
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
benchSink = encodePythonJSON(e.getCache("k"), -1)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPutCache(b *testing.B) {
|
||||
e := NewEnv(b.TempDir())
|
||||
e.monotonic = func() float64 { return 0 }
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
e.putCache("k", "value line", nil, "general", "")
|
||||
}
|
||||
}
|
||||
|
||||
// ── CLI live parity vs the running oracle ──────────────────────────────
|
||||
|
||||
var (
|
||||
|
||||
Vendored
+6
-3
@@ -18,7 +18,7 @@ func Compress(text string) string {
|
||||
return ""
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(text), "\n")
|
||||
var compressed []string
|
||||
var sb strings.Builder
|
||||
for _, line := range lines {
|
||||
stripped := strings.TrimSpace(line)
|
||||
if stripped == "" {
|
||||
@@ -43,9 +43,12 @@ func Compress(text string) string {
|
||||
if len(stripped) < 5 {
|
||||
continue
|
||||
}
|
||||
compressed = append(compressed, stripped)
|
||||
if sb.Len() > 0 {
|
||||
sb.WriteByte('\n')
|
||||
}
|
||||
sb.WriteString(stripped)
|
||||
}
|
||||
result := strings.Join(compressed, "\n")
|
||||
result := sb.String()
|
||||
if len(result) > 800 {
|
||||
result = result[:797] + "..."
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -54,3 +54,18 @@ Oracle: src/rune/cache.py (492 lines, 17 fns).
|
||||
- Flush-timing is invisible in every observable (row+pending invariant
|
||||
flushed at put/stats): first-record flush behavior under either
|
||||
monotonic epoch cannot diverge in outputs.
|
||||
|
||||
## Phase B (commit 2) re-verification
|
||||
- One pooled *sql.DB per Env (lazily created; the oracle re-ran the
|
||||
PRAGMA/DDL block on EVERY call — the pool drops that per-call walk
|
||||
with identical observable behavior: IF NOT EXISTS is idempotent,
|
||||
WAL persists in the file); ContentHash hex-encodes only the 8 bytes
|
||||
it needs; Compress builds via strings.Builder.
|
||||
- Ladder re-run after Phase B: battery green (cache-phaseb-gotest),
|
||||
gofmt/vet/build clean (cache-phaseb-goladder), live parity 100%
|
||||
16/16 again (cache-phaseb-live-parity), freeze --verify 78/78
|
||||
(cache-phaseb-freeze-verify), Python battery 12 passed
|
||||
(cache-phaseb-pytest).
|
||||
- Benchmarks (200x): ContentHash 139.9 ns/op, Compress 6.5 µs/op,
|
||||
recordHit-pending 0.9 µs/op, getCache-hit 23.7 µs/op, putCache
|
||||
42.6 µs/op.
|
||||
Reference in new issue
Block a user