Files
Anders Eknert 3e1af3bd6a perf: String() implementations using appenders (#8192)
Some work I did during the holidays as part of improving the performance
of interpolated strings. This change is however not isolated to those, but
updates the `String()` implementation of all AST node types (term values
and policy components). This change also lays the groundwork for migrating
OPA to the `json/v2` package once that's stable. The `json/v2` package
provides low-level functions for zero alloc marshalling via appenders — and
well, here they are. The appenders here should be usable for that purpose with
only a few tweaks needed for the few cases where our `String()` implementations
aren't also valid JSON.

Creating perfectly sized buffers requires knowing the expected length beforehand.
In order to do this, each component now implements not only `encoding.AppendText`
but a new custom `StringLengther` interface, which allows asking any AST node about
its `StringLength()` before `make`ing a buffer of that length.

We could definitely consider adding these to e.g. the `Value` or `Node` interfaces,
but I've left that out of this PR as it's an easy thing to do later should we want
to, and I guess there's always some concerns about changing public interfaces even
when they're not meant to be implemented by external code.

While no `Value` appenders allocate and almost none of the policy appenders do either,
one notable exception is `Module` when there are annotations present, as they are
a bit of a (YAML) special case. It's doable, but as serializing full modules isn't
on a hot path anywhere, I have chosen to defer that work to the future.

Signed-off-by: Anders Eknert <anders.eknert@apple.com>
2026-01-08 10:14:53 +01:00

100 lines
2.7 KiB
Go

// Copyright 2025 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package ast
import (
"encoding"
"strings"
"sync"
)
var builtinNamesByNumParts = sync.OnceValue(func() map[int][]string {
m := map[int][]string{}
for name := range BuiltinMap {
parts := strings.Count(name, ".") + 1
if parts > 1 {
m[parts] = append(m[parts], name)
}
}
return m
})
// BuiltinNameFromRef attempts to extract a known built-in function name from a ref,
// in the most efficient way possible. I.e. without allocating memory for a new string.
// If no built-in function name can be extracted, the second return value is false.
func BuiltinNameFromRef(ref Ref) (string, bool) {
reflen := len(ref)
if reflen == 0 {
return "", false
}
_var, ok := ref[0].Value.(Var)
if !ok {
return "", false
}
varName := string(_var)
if reflen == 1 {
if _, ok := BuiltinMap[varName]; ok {
return varName, true
}
return "", false
}
totalLen := len(varName)
for _, term := range ref[1:] {
if _, ok = term.Value.(String); !ok {
return "", false
}
totalLen += 1 + len(term.Value.(String)) // account for dot
}
matched, ok := builtinNamesByNumParts()[reflen]
if !ok {
return "", false
}
for _, name := range matched {
// This check saves us a huge amount of work, as only very few built-in
// names will have the exact same length as the ref we are checking.
if len(name) != totalLen {
continue
}
// Example: `name` is "io.jwt.decode" (and so is ref)
// The first part is varName, which have already been established to be 'io':
// io, jwt.decode io == io
if curr, remaining, _ := strings.Cut(name, "."); curr == varName {
// Loop over the remaining (now known to be string) terms in the ref, e.g. "jwt" and "decode"
for _, term := range ref[1:] {
ts := string(term.Value.(String))
// First iteration: jwt.decode != jwt, so we continue cutting
// Second iteration: remaining is "decode", and so is term
if remaining == ts {
return name, true
}
// Cutting remaining (e.g. jwt.decode), and we now get:
// jwt, decode, false || jwt != jwt
if curr, remaining, _ = strings.Cut(remaining, "."); remaining == "" || curr != ts {
break
}
}
}
}
return "", false
}
func AppendDelimeted[T encoding.TextAppender](buf []byte, appenders []T, delim string) ([]byte, error) {
for i, item := range appenders {
if i > 0 {
buf = append(buf, delim...)
}
var err error
if buf, err = item.AppendText(buf); err != nil {
return nil, err
}
}
return buf, nil
}