Skip to content

Commit 2478b98

Browse files
vishrclaude
andcommitted
perf(router): cache-friendly static child lookup + dedupe leaf check
findStaticChild scanned []*node and dereferenced every child to read its one-byte label — a pointer chase per static step. Maintain a parallel scLabels []byte (kept in sync in addStaticChild, newNode, the insert split, and Remove) and scan that contiguous slice instead, indexing into staticChildren only on a hit. benchstat (n=6): RouterStaticRoutes-14 8.49µ -> 8.01µ -5.67% (p=0.002) RouterGitHubAPI-14 16.93µ -> 16.45µ -2.86% (p=0.002) RouterParseAPI / GooglePlusAPI: neutral (param-heavy) allocations: 0, unchanged Also fold the is-leaf recomputation (duplicated across 5 sites, with the Remove copy subtly using len()==0 vs == nil) into a single refreshLeaf() helper that uses len() so it is correct whether staticChildren is nil or an emptied-but-non-nil slice left after a removal. Split out of #3023 so the router and JSON changes can be reviewed independently (requested by @aldas). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 78c3d95 commit 2478b98

1 file changed

Lines changed: 39 additions & 14 deletions

File tree

router.go

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,15 @@ type node struct {
121121
prefix string
122122
originalPath string
123123
staticChildren children
124-
paramsCount int
125-
kind kind
126-
label byte
127-
isLeaf bool
128-
isHandler bool
124+
// scLabels holds the first byte (label) of each staticChildren entry in the
125+
// same order. Scanning this contiguous byte slice during routing is more
126+
// cache-friendly than dereferencing each *node to read its label.
127+
scLabels []byte
128+
paramsCount int
129+
kind kind
130+
label byte
131+
isLeaf bool
132+
isHandler bool
129133
}
130134

131135
type kind uint8
@@ -406,13 +410,14 @@ func (r *DefaultRouter) Remove(method string, path string) error {
406410
}
407411
}
408412
parent.staticChildren = append(parent.staticChildren[:index], parent.staticChildren[index+1:]...)
413+
parent.scLabels = append(parent.scLabels[:index], parent.scLabels[index+1:]...)
409414
case paramKind:
410415
parent.paramChild = nil
411416
case anyKind:
412417
parent.anyChild = nil
413418
}
414419

415-
parent.isLeaf = parent.anyChild == nil && parent.paramChild == nil && len(parent.staticChildren) == 0
420+
parent.refreshLeaf()
416421
if !parent.isLeaf || parent.isHandler {
417422
break
418423
}
@@ -572,7 +577,7 @@ func (r *DefaultRouter) insert(t kind, path string, method string, ri routeMetho
572577
currentNode.paramsCount = len(ri.Parameters)
573578
currentNode.originalPath = ri.Path
574579
}
575-
currentNode.isLeaf = currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil
580+
currentNode.refreshLeaf()
576581
} else if lcpLen < prefixLen {
577582
// Split node into two before we insert new node.
578583
// This happens when we are inserting path that is submatch of any existing inserted paths.
@@ -607,6 +612,7 @@ func (r *DefaultRouter) insert(t kind, path string, method string, ri routeMetho
607612
currentNode.label = currentNode.prefix[0]
608613
currentNode.prefix = currentNode.prefix[:lcpLen]
609614
currentNode.staticChildren = nil
615+
currentNode.scLabels = nil
610616
currentNode.methods = new(routeMethods)
611617
currentNode.originalPath = ""
612618
currentNode.paramsCount = 0
@@ -636,7 +642,7 @@ func (r *DefaultRouter) insert(t kind, path string, method string, ri routeMetho
636642
// Only Static children could reach here
637643
currentNode.addStaticChild(n)
638644
}
639-
currentNode.isLeaf = currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil
645+
currentNode.refreshLeaf()
640646
} else if lcpLen < searchLen {
641647
search = search[lcpLen:]
642648
c := currentNode.findChildWithLabel(search[0])
@@ -659,7 +665,7 @@ func (r *DefaultRouter) insert(t kind, path string, method string, ri routeMetho
659665
case anyKind:
660666
currentNode.anyChild = n
661667
}
662-
currentNode.isLeaf = currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil
668+
currentNode.refreshLeaf()
663669
} else {
664670
// Node already exists
665671
if ri.handler != nil {
@@ -683,30 +689,49 @@ func newNode(
683689
paramChildren,
684690
anyChildren *node,
685691
) *node {
692+
var scLabels []byte
693+
if len(sc) > 0 {
694+
scLabels = make([]byte, len(sc))
695+
for i, c := range sc {
696+
scLabels[i] = c.label
697+
}
698+
}
686699
return &node{
687700
kind: t,
688701
label: pre[0],
689702
prefix: pre,
690703
parent: p,
691704
staticChildren: sc,
705+
scLabels: scLabels,
692706
originalPath: ppath,
693707
paramsCount: paramsCount,
694708
methods: mh,
695709
paramChild: paramChildren,
696710
anyChild: anyChildren,
697-
isLeaf: sc == nil && paramChildren == nil && anyChildren == nil,
698-
isHandler: mh.isHandler(),
711+
// len() (not == nil) so an empty-but-non-nil sc — e.g. a slice from a
712+
// prior Remove that was spliced down to length 0 — is still treated as
713+
// no children, matching refreshLeaf.
714+
isLeaf: len(sc) == 0 && paramChildren == nil && anyChildren == nil,
715+
isHandler: mh.isHandler(),
699716
}
700717
}
701718

719+
// refreshLeaf recomputes whether the node is a leaf (has no children of any
720+
// kind). len() is used for staticChildren so it stays correct whether the slice
721+
// is nil or an emptied-but-non-nil slice left behind after a removal.
722+
func (n *node) refreshLeaf() {
723+
n.isLeaf = len(n.staticChildren) == 0 && n.paramChild == nil && n.anyChild == nil
724+
}
725+
702726
func (n *node) addStaticChild(c *node) {
703727
n.staticChildren = append(n.staticChildren, c)
728+
n.scLabels = append(n.scLabels, c.label)
704729
}
705730

706731
func (n *node) findStaticChild(l byte) *node {
707-
for _, c := range n.staticChildren {
708-
if c.label == l {
709-
return c
732+
for i, cl := range n.scLabels {
733+
if cl == l {
734+
return n.staticChildren[i]
710735
}
711736
}
712737
return nil

0 commit comments

Comments
 (0)