From e868b076525c720bfe8d4fc78776041cb9fa1b22 Mon Sep 17 00:00:00 2001 From: Shaun Patterson Date: Thu, 28 May 2026 14:16:18 -0400 Subject: [PATCH] perf(query): pre-size mergedList in merge() The @normalize merge appends exactly one node per (parent,child) pair, so the final length is len(parent)*len(child). Pre-sizing avoids grow-and-copy. The capacity is capped at x.Config.LimitNormalizeNode: the loop bails out once the node count exceeds that limit, so without the cap a large normalize query that is going to be rejected would still trigger a huge (or integer-overflowing) up-front allocation. BenchmarkMergeNormalize (64x64): allocs/op 25 -> 10 (-60%), time -24%. --- query/outputnode.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/query/outputnode.go b/query/outputnode.go index 88c6d9a36ec..17fff768d4c 100644 --- a/query/outputnode.go +++ b/query/outputnode.go @@ -888,8 +888,15 @@ func (enc *encoder) merge(parent, child []fastJsonNode) ([]fastJsonNode, error) return child, nil } - // Here we merge two slices of maps. - mergedList := make([]fastJsonNode, 0) + // Here we merge two slices of maps. The final size is len(parent) * len(child), + // but the loop below bails out once cnt exceeds LimitNormalizeNode, so cap the + // pre-allocation at that limit to avoid a huge (or overflowing) up-front alloc + // for queries that are going to be rejected anyway. + c := len(parent) * len(child) + if lim := x.Config.LimitNormalizeNode; lim > 0 && c > lim { + c = lim + } + mergedList := make([]fastJsonNode, 0, c) cnt := 0 for _, pa := range parent { for _, ca := range child {