// PlanCache: a size-bounded LRU cache of auto-parameterized query plans.// Two layers share one lock. An exact-string fast path (L1) skips parse +// plan on a verbatim repeat; a template index (L2) lets queries differing// only in lifted constants share one compiled plan. L1 entries own the// plans; a template entry dies when its last L1 variant is evicted (the Go// refcount standing in for the Rust engine's Arc/Weak split).//// Both layers hold the TEMPLATE plan -- planned after auto-// parameterization, where every cost probe abstains on parameter slots --// so a cached plan is value-independent by construction. It may differ in// SHAPE (never in results) from the literal-probed plan an uncached Run// builds for the same text; this mirrors the Rust engine's query_cached// composition, trading the best-effort literal plan for zero double// planning and cross-literal sharing.//// Eviction is least-recently-used past a configurable byte budget. The// byte estimate counts each L1 entry's key, parameters, and a per-plan// estimate; a plan shared by several literal variants is charged once per// variant -- a deliberate over-count, so the true footprint stays under// the bound.
package gqlimport (chickpeas)// DefaultCacheBytes is the default budget: 128 MiB of approximate// resident plan + key + parameter bytes.constDefaultCacheBytes = 128 << 20// l1Overhead is the per-entry fixed bookkeeping charge.const l1Overhead = 96// cachedPlan is one template's compiled plan, shared across every L1// literal variant of that template.type cachedPlan struct { plan *plan.Plan mode ast.QueryMode key string estBytes int// refs counts the L1 variants sharing this plan; the template entry // is dropped when the last one evicts. refs int// flipped marks a template whose blind plan differs structurally from // value-sighted planning (detected once, when the template was first // planned): execution for such a template routes through the sighted // path instead of the cached tree, because a flipped plan's cost can // be arbitrarily wrong (see planflip.go). Conservative: a template // that flipped for its first caller's values re-plans sighted for all. flipped bool}// l1Entry is one verbatim query string's cache slot: the shared plan plus// this string's lifted constant values.type l1Entry struct { plan *cachedPlan params []value.Value bytes int tick uint64}// PlanCache is a size-bounded cache of auto-parameterized plans, safe for// concurrent use. Hold one per long-lived snapshot: a cached plan's cost// choices reflect the statistics of the snapshot its template was first// planned against (executing against another snapshot is correct, possibly// suboptimal).typePlanCachestruct { mu sync.Mutex byQuery map[string]*l1Entry byTemplate map[string]*cachedPlan bytes int tick uint64 maxBytes int hitsL1 uint64 hitsL2 uint64 misses uint64}// NewPlanCache is an empty cache bounded to maxBytes of approximate// resident memory (LRU eviction past the bound); maxBytes <= 0 means// DefaultCacheBytes.func ( int) *PlanCache {if <= 0 { = DefaultCacheBytes }return &PlanCache{byQuery: map[string]*l1Entry{},byTemplate: map[string]*cachedPlan{},maxBytes: , }}// Run executes a GQL query through the cache.func ( *PlanCache) ( *chickpeas.Snapshot, string) (*Rows, error) {return .RunWithParams(, , nil)}// RunWithParams executes a GQL query through the cache with explicit// $name parameter values. Results are identical to the uncached// RunWithParams.func ( *PlanCache) ( *chickpeas.Snapshot, string, map[string]value.Value) (*Rows, error) { := graph.New()// L1: a verbatim repeat skips parse + plan.if , , := .l1Lookup(); {if .flipped {returnRunUncachedWithParams(, , ) }return .execCached(, , , ) }// L2: parse, lift constants, and share a plan across templates. , := parseDesugar()if != nil {returnnil, } := semantics.AutoParameterize() := ast.Fingerprint() , := .l2Lookup()if ! {// Plan the template outside the lock; a concurrent duplicate is // reconciled by insert (the first plan wins, the second is // dropped). , := plan.Build(, )if != nil {returnnil, wrapStage() } = &cachedPlan{plan: , mode: .Mode, key: , estBytes: 2048 + len()*8}// Flip detection, once per template: compare the tree the cache // would execute (post adaptive choice for this call's values) // against sighted planning of the literal text. One extra plan // per template miss buys the do-no-harm guarantee. := &eval.Ctx{G: , Params: } .flipped = planFlipped(, chooseAdaptivePlan(, , ), ) } .insert(, , )if .flipped {returnRunUncachedWithParams(, , ) }return .execCached(, , , )}// execCached executes a cached plan with this call's lifted values and// named parameters (cached EXPLAIN/PROFILE renders show no planning time,// matching the Rust engine).func ( *PlanCache) ( *graph.SnapshotGraph, *cachedPlan, []value.Value, map[string]value.Value) (*Rows, error) { := &eval.Ctx{G: , Params: , Named: , ForceInterp: forceInterp}returnexecPlan(, chooseAdaptivePlan(.plan, , ), .mode, 0, )}// chooseAdaptivePlan resolves an auto-parameterized anchor tie now that the// parameters are bound: it scores the primary plan and its flipped sibling by// each anchor's REAL first-hop fan-out and runs whichever seeds from the// lower-degree end. Returns the primary unchanged when there is no sibling,// the query is a UNION, or either anchor cannot be scored cheaply (declining// keeps the per-execution probe bounded on a hot cached query). This is where// the value the planner could not see -- a hub vs a selective seek behind the// same param slot -- finally decides the plan, without baking the value into// the cache key.func chooseAdaptivePlan( *plan.Plan, *eval.Ctx, graph.Graph) *plan.Plan {if .Alt == nil || len(.Branches) != 1 {return } , := anchorFanout(, , ) , := anchorFanout(.Alt, , )if ! || ! {return }if < {adaptiveAltPicked++return .Alt }return}// adaptiveAltPicked counts sibling selections, so tests can assert a path// actually CONSULTED the adaptive choice (rows are identical either way,// making the wiring invisible to a result assertion).var adaptiveAltPicked int// anchorFanout is the summed real first-hop degree of a plan's parameter-seek// anchor over the nodes the bound parameter now resolves to. ok is false when// the plan's first stage isn't a property-seek anchor followed by an expand,// or the resolved anchor set exceeds 64 nodes (the probe runs on every// execution of a cached query, so it must stay O(1)-ish).func anchorFanout( *plan.Plan, *eval.Ctx, graph.Graph) (uint64, bool) { := firstMatchStage()if == nil || len(.Ops) < 2 {return0, false } , := .Ops[0], .Ops[1]if .Kind != plan.OpScan || .Source.Kind != plan.ScanProperty || .Kind != plan.OpExpand {return0, false } := .NodesWithProperty(.Source.Label, .Source.Key, eval.LitValue(, .Source.Value))if == nil {return0, true }if .Len() > 64 {return0, false }// The real first-hop fan-out is the TYPED degree over the hop's relation // types, not the node's untyped degree -- an anchor can carry many edges // of other types (a Country's IS_LOCATED_IN dwarf its IS_PART_OF) that the // first hop never traverses. Count is capped at a work budget so the // per-execution probe stays bounded even against a hub: reaching the cap // only means "at least this large", which is all the comparison needs.const = 4096varuint64for := range .Iter() {forrange .NeighborsByType(chickpeas.NodeID(), .Dir, .Types) { ++if >= {return , true } } }return , true}// firstMatchStage is a plan's first MatchStage in its single branch, or nil.func firstMatchStage( *plan.Plan) *plan.MatchStage {for , := range .Branches[0] {for , := range .Stages {if , := .(*plan.MatchStage); {return } } }returnnil}// l1Lookup returns the verbatim entry's shared plan and lifted values,// marking it most-recently-used.func ( *PlanCache) ( string) (*cachedPlan, []value.Value, bool) { .mu.Lock()defer .mu.Unlock() , := .byQuery[]if ! {returnnil, nil, false } .tick++ .tick = .tick .hitsL1++return .plan, .params, true}// l2Lookup returns an existing template's plan, counting the hit.func ( *PlanCache) ( string) (*cachedPlan, bool) { .mu.Lock()defer .mu.Unlock() , := .byTemplate[]if { .hitsL2++ } else { .misses++ }return , }// insert memoizes the exact query string (L1) against the template's// shared plan (L2), reconciling a concurrently inserted duplicate// template, then evicts LRU entries past the budget.func ( *PlanCache) ( string, *cachedPlan, []value.Value) { .mu.Lock()defer .mu.Unlock()if , := .byTemplate[.key]; { = } else { .byTemplate[.key] = } .tick++ := l1Overhead + len() + .estBytesfor , := range { += valueBytes() }// Take the new hold BEFORE releasing a replaced entry's: when the old // entry holds the same plan (two racing first-runs of one query // string), deref-first transiently zeroed the refcount and deleted // the template while its L1 holders lived on -- the Len()==0 flake. .refs++if , := .byQuery[]; { .bytes -= .bytes .deref(.plan) } .byQuery[] = &l1Entry{plan: , params: , bytes: , tick: .tick} .bytes += .evict()}// deref drops one L1 variant's hold; the template entry dies with the// last one.func ( *PlanCache) ( *cachedPlan) { .refs--if .refs <= 0 {delete(.byTemplate, .key) }}// evict removes least-recently-used L1 entries until resident bytes fall// to 90% of the budget (hysteresis: a steady insert stream doesn't evict// on every call). Caller holds the lock.func ( *PlanCache) () {if .bytes <= .maxBytes {return } := .maxBytes / 10 * 9typestruct {uint64string } := make([], 0, len(.byQuery))for , := range .byQuery { = append(, {.tick, }) }sort.Slice(, func(, int) bool { return []. < []. })for , := range {if .bytes <= {break } := .byQuery[.]delete(.byQuery, .) .bytes -= .bytes .deref(.plan) }}// valueBytes approximates one lifted value's heap footprint.func valueBytes( value.Value) int {switch .Kind() {casevalue.KindStr: , := .AsStr()returnlen() + 24casevalue.KindList: , := .AsList() := 24for , := range { += () }returncasevalue.KindPath: , , := .AsPath()return24 + len()*4 + len()*4default:return16 }}// Bytes is the approximate resident bytes currently charged.func ( *PlanCache) () int { .mu.Lock()defer .mu.Unlock()return .bytes}// MaxBytes is the current byte budget.func ( *PlanCache) () int { .mu.Lock()defer .mu.Unlock()return .maxBytes}// SetMaxBytes changes the budget, evicting immediately if over it.func ( *PlanCache) ( int) { .mu.Lock()defer .mu.Unlock() .maxBytes = .evict()}// Len is the number of distinct templates with a live plan.func ( *PlanCache) () int { .mu.Lock()defer .mu.Unlock()returnlen(.byTemplate)}// IsEmpty reports whether no live template plan is cached.func ( *PlanCache) () bool { return .Len() == 0 }// stats exposes the hit/miss counters to same-package tests.func ( *PlanCache) () (, , uint64) { .mu.Lock()defer .mu.Unlock()return .hitsL1, .hitsL2, .misses}
The pages are generated with Goldsv0.8.7. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.