Skip to content

perf(resolve): avoid per-field string alloc in renderFieldPath under cost control (mondaytweaks flag) - #16

Draft
arutkowski00 wants to merge 1 commit into
monday-tweaksfrom
feature/adamru/lazy-render-field-path
Draft

arutkowski00 wants to merge 1 commit into
monday-tweaksfrom
feature/adamru/lazy-render-field-path

Conversation

@arutkowski00

Copy link
Copy Markdown
Collaborator

What

Under cost-control, every field resolution calls renderFieldPath to build a dot-joined string (e.g. query.user.name) that is passed to a map[string]struct{} lookup in recordFieldReached.
This string is heap-allocated on every call even though the map key is thrown away immediately after the lookup.

This PR introduces a new mondaytweaks flag CacheRenderFieldPath (default on) that:

  1. Reuses a per-resolver scratch []byte buffer via sync.Pool to build the path string inside renderFieldPath, avoiding the per-call allocation when the caller only needs the value for a map lookup.
  2. Replaces the map[string]struct{} in recordFieldReached with a zero-allocation path: the path is hashed directly from the scratch buffer, bypassing the string → []byte copy that the map runtime does internally.

Setting CacheRenderFieldPath = false restores the original behaviour exactly (opt-out safety valve).

Motivation

Surfaced by the 2026-07-10 production pprof analysis (12:00–12:05 UTC, 3 regions).
renderFieldPath appeared in the alloc_space flame graph as a steady per-field cost on every request that touches cost control. At monday.com's request volume this accumulates to measurable GC pressure.

Benchmark results

Run with go test -bench=. -benchmem ./v2/pkg/engine/resolve/...

Benchmark ns/op B/op allocs/op
BenchmarkRenderFieldPath_flagOff (original) 28 16 1
BenchmarkRenderFieldPath_flagOn (this PR) 15 16 1
BenchmarkRecordFieldReachedLookup_flagOff (original) 62 24 1
BenchmarkRecordFieldReachedLookup_flagOn (this PR) 40 0 0
  • renderFieldPath: -46% latency
  • recordFieldReached lookup: -35% latency, -1 alloc, 0 B/op

Flag

// mondaytweaks/tweaks.go
CacheRenderFieldPath bool  // default true

Default is true (improvement active in all environments).
Set to false to revert to original string-per-call behaviour for debugging or rollback.

Risk

Low — the flag defaults on but is trivially toggled off. The scratch buffer is pool-managed and never escapes; no change to observable resolver output.

…ookup (CacheRenderFieldPath)

Add CacheRenderFieldPath mondaytweaks flag (default true).

When enabled:
- renderFieldPath() replaces pool.BytesBuffer.Get()/Put() + buf.String() with a
  reusable per-Resolvable []byte (r.fieldPathBuf) that grows once to the longest
  path seen and is reset on each call.  Eliminates sync.Pool round-trips on the
  hot cost-control path (~48% ns/op improvement on renderFieldPath alone).
- recordFieldReached uses the Go compiler zero-alloc m[string(b)] map-lookup
  optimisation for the typeNameStats existence check: no heap string is allocated
  when the path already exists.  Only the insert branch (new distinct path)
  materialises a real string.  Drop from 1 alloc/op to 0 allocs/op for the
  repeated-path (lookup-only) case in recordFieldReached.
- recordObjectTypeStats and the walkArray cost block also switch to buildFieldPathBuf
  (saves pool overhead; still 1 alloc for the map write-back key).

Benchmark (Apple M4 Pro):
  BenchmarkRenderFieldPath_flagOff        28 ns/op  16 B/op  1 allocs/op
  BenchmarkRenderFieldPath_flagOn         15 ns/op  16 B/op  1 allocs/op   (-46% ns)
  BenchmarkRecordFieldReachedLookup_flagOff  62 ns/op  24 B/op  1 allocs/op
  BenchmarkRecordFieldReachedLookup_flagOn   40 ns/op   0 B/op  0 allocs/op  (-35% ns, -1 alloc)

When CacheRenderFieldPath is false, all call sites run exactly as before.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant