Files
releases/v1/util/json.go
T
Anders Eknert 48f76fd2aa Improve performance of storage operations (#7957)
And the default inmem storage implementation(s) in particular.

- Avoid creating expensive references for data which isn't in need of that, like strings or booleans.
- Avoid expensive roundtrips of data which doesn't need it, for the same reasons.
- Never roundtrip AST store items being written — unknown values will be roundtripped by `ast.InterfaceToValue` already
- Convert numbers to json.Number directly and without a roundtrip
- Avoid AST -> Go type conversion which is done for triggers... when no triggers have been registered
- Greatly reduce cost of not found lookups by using a static NotFound error that does not include the path — the path is already known by the caller so that information adds nothing they can't add themselves.
- Improve interning to make it easier to get interned `Value`'s without a *Term
- Use interned numbers when possible in `ast.InterfaceToValue`
- Much improved benchmarking framework for inmem storage, and many new benchmarks added.
- A few general improvements of error handling

**Benchstat**
```
goos: darwin
goarch: arm64
pkg: github.com/open-policy-agent/opa/v1/storage/inmem
cpu: Apple M4 Max
                                                             │    v1.9.0    │                 now                 │
                                                             │    sec/op    │   sec/op     vs base                │
NewTransaction/write-16                                         44.60n ± 1%   17.12n ± 1%  -61.61% (p=0.000 n=10)
NewTransaction/read-16                                          44.62n ± 0%   17.04n ± 0%  -61.80% (p=0.000 n=10)
ReadOne/Go-16                                                  126.95n ± 0%   98.93n ± 0%  -22.07% (p=0.000 n=10)
ReadOne/Go_(roundtrip)-16                                      127.65n ± 0%   99.04n ± 0%  -22.41% (p=0.000 n=10)
ReadOne/AST-16                                                  151.1n ± 0%   121.7n ± 0%  -19.40% (p=0.000 n=10)
ReadOneNested/Go-16                                             138.0n ± 0%   109.5n ± 0%  -20.69% (p=0.000 n=10)
ReadOneNested/Go_(roundtrip)-16                                 139.1n ± 0%   110.1n ± 1%  -20.85% (p=0.000 n=10)
ReadOneNested/AST-16                                            207.6n ± 1%   177.2n ± 0%  -14.64% (p=0.000 n=10)
ReadNotFound/Go-16                                             127.70n ± 0%   73.22n ± 1%  -42.67% (p=0.000 n=10)
ReadNotFound/Go_(roundtrip)-16                                 128.00n ± 0%   73.35n ± 1%  -42.70% (p=0.000 n=10)
ReadNotFound/AST-16                                            148.90n ± 0%   99.38n ± 0%  -33.26% (p=0.000 n=10)
WriteOneString/Go-16                                            240.1n ± 0%   184.5n ± 0%  -23.16% (p=0.000 n=10)
WriteOneString/Go_(roundtrip)-16                                655.2n ± 0%   185.3n ± 0%  -71.72% (p=0.000 n=10)
WriteOneString/AST-16                                           803.4n ± 0%   325.4n ± 0%  -59.49% (p=0.000 n=10)
WriteSameValue/Go-16                                           138.75n ± 1%   77.56n ± 2%  -44.10% (p=0.000 n=10)
WriteSameValue/Go_(roundtrip)-16                               557.95n ± 1%   77.86n ± 2%  -86.05% (p=0.000 n=10)
WriteSameValue/AST-16                                          615.65n ± 1%   79.25n ± 1%  -87.13% (p=0.000 n=10)
WriteIncrementingValueSamePath/Go-16                            6.312µ ± 0%   6.462µ ± 0%   +2.38% (p=0.000 n=10)
WriteIncrementingValueSamePath/Go_(roundtrip)-16               48.825µ ± 1%   7.860µ ± 0%  -83.90% (p=0.000 n=10)
WriteIncrementingValueSamePath/AST-16                          53.723µ ± 1%   8.718µ ± 1%  -83.77% (p=0.000 n=10)
WriteIncrementingValueSamePathInterned/Go-16                    6.351µ ± 1%   6.511µ ± 1%   +2.52% (p=0.000 n=10)
WriteIncrementingValueSamePathInterned/Go_(roundtrip)-16       49.804µ ± 1%   7.907µ ± 0%  -84.12% (p=0.000 n=10)
WriteIncrementingValueSamePathInterned/AST-16                  51.806µ ± 2%   6.663µ ± 1%  -87.14% (p=0.000 n=10)
PathAndValueInternedAndNoRoundtripRequired/Go-16                96.23µ ± 4%   43.83µ ± 4%  -54.45% (p=0.000 n=10)
PathAndValueInternedAndNoRoundtripRequired/Go_(roundtrip)-16   135.08µ ± 1%   43.01µ ± 3%  -68.16% (p=0.000 n=10)
PathAndValueInternedAndNoRoundtripRequired/AST-16              145.67µ ± 1%   43.19µ ± 3%  -70.35% (p=0.000 n=10)
WriteCollection/Go-16                                           136.8n ± 1%   137.0n ± 1%        ~ (p=0.643 n=10)
WriteCollection/Go_(roundtrip)-16                               1.304µ ± 0%   1.304µ ± 2%        ~ (p=0.629 n=10)
WriteCollection/AST-16                                         2034.0n ± 0%   772.5n ± 2%  -62.02% (p=0.000 n=10)
WriteAndCommit/Go-16                                            54.00µ ± 3%   47.63µ ± 1%  -11.80% (p=0.000 n=10)
WriteAndCommit/Go_(roundtrip)-16                                94.63µ ± 1%   47.19µ ± 3%  -50.13% (p=0.000 n=10)
WriteAndCommit/AST-16                                           239.6µ ± 1%   192.7µ ± 1%  -19.59% (p=0.000 n=10)
WriteAndCommitWithTriggers/Go-16                                54.08µ ± 1%   47.87µ ± 2%  -11.47% (p=0.000 n=10)
WriteAndCommitWithTriggers/Go_(roundtrip)-16                    94.75µ ± 1%   47.41µ ± 2%  -49.97% (p=0.000 n=10)
WriteAndCommitWithTriggers/AST-16                               247.2µ ± 0%   192.2µ ± 1%  -22.27% (p=0.000 n=10)

                                                             │      v1.9.0      │                    now                    │
                                                             │       B/op       │     B/op      vs base                     │
NewTransaction/write-16                                           144.00 ± 0%       48.00 ± 0%   -66.67% (p=0.000 n=10)
NewTransaction/read-16                                            144.00 ± 0%       48.00 ± 0%   -66.67% (p=0.000 n=10)
ReadOne/Go-16                                                     144.00 ± 0%       48.00 ± 0%   -66.67% (p=0.000 n=10)
ReadOne/Go_(roundtrip)-16                                         144.00 ± 0%       48.00 ± 0%   -66.67% (p=0.000 n=10)
ReadOne/AST-16                                                    160.00 ± 0%       64.00 ± 0%   -60.00% (p=0.000 n=10)
ReadOneNested/Go-16                                               144.00 ± 0%       48.00 ± 0%   -66.67% (p=0.000 n=10)
ReadOneNested/Go_(roundtrip)-16                                   144.00 ± 0%       48.00 ± 0%   -66.67% (p=0.000 n=10)
ReadOneNested/AST-16                                              192.00 ± 0%       96.00 ± 0%   -50.00% (p=0.000 n=10)
ReadNotFound/Go-16                                                 72.00 ± 0%        0.00 ± 0%  -100.00% (p=0.000 n=10)
ReadNotFound/Go_(roundtrip)-16                                     72.00 ± 0%        0.00 ± 0%  -100.00% (p=0.000 n=10)
ReadNotFound/AST-16                                                88.00 ± 0%       16.00 ± 0%   -81.82% (p=0.000 n=10)
WriteOneString/Go-16                                               368.0 ± 0%       240.0 ± 0%   -34.78% (p=0.000 n=10)
WriteOneString/Go_(roundtrip)-16                                  2818.0 ± 0%       240.0 ± 0%   -91.48% (p=0.000 n=10)
WriteOneString/AST-16                                             2938.0 ± 0%       360.0 ± 0%   -87.75% (p=0.000 n=10)
WriteSameValue/Go-16                                               128.0 ± 0%         0.0 ± 0%  -100.00% (p=0.000 n=10)
WriteSameValue/Go_(roundtrip)-16                                 2.518Ki ± 0%     0.000Ki ± 0%  -100.00% (p=0.000 n=10)
WriteSameValue/AST-16                                            2.572Ki ± 0%     0.000Ki ± 0%  -100.00% (p=0.000 n=10)
WriteIncrementingValueSamePath/Go-16                             12.50Ki ± 0%     12.50Ki ± 0%         ~ (p=1.000 n=10) ¹
WriteIncrementingValueSamePath/Go_(roundtrip)-16                251.73Ki ± 0%     14.06Ki ± 0%   -94.41% (p=0.000 n=10)
WriteIncrementingValueSamePath/AST-16                           255.63Ki ± 0%     13.28Ki ± 0%   -94.80% (p=0.000 n=10)
WriteIncrementingValueSamePathInterned/Go-16                     12.50Ki ± 0%     12.50Ki ± 0%         ~ (p=1.000 n=10) ¹
WriteIncrementingValueSamePathInterned/Go_(roundtrip)-16        251.73Ki ± 0%     14.06Ki ± 0%   -94.41% (p=0.000 n=10)
WriteIncrementingValueSamePathInterned/AST-16                  251.722Ki ± 0%     9.375Ki ± 0%   -96.28% (p=0.000 n=10)
PathAndValueInternedAndNoRoundtripRequired/Go-16                14.062Ki ± 0%     1.562Ki ± 0%   -88.89% (p=0.000 n=10)
PathAndValueInternedAndNoRoundtripRequired/Go_(roundtrip)-16   253.267Ki ± 0%     1.562Ki ± 0%   -99.38% (p=0.000 n=10)
PathAndValueInternedAndNoRoundtripRequired/AST-16              254.870Ki ± 0%     1.562Ki ± 0%   -99.39% (p=0.000 n=10)
WriteCollection/Go-16                                              128.0 ± 0%       128.0 ± 0%         ~ (p=1.000 n=10) ¹
WriteCollection/Go_(roundtrip)-16                                2.243Ki ± 0%     2.243Ki ± 0%         ~ (p=1.000 n=10) ¹
WriteCollection/AST-16                                           3.472Ki ± 0%     1.320Ki ± 0%   -61.97% (p=0.000 n=10)
WriteAndCommit/Go-16                                             29.58Ki ± 0%     11.08Ki ± 0%   -62.55% (p=0.000 n=10)
WriteAndCommit/Go_(roundtrip)-16                                268.80Ki ± 0%     11.08Ki ± 0%   -95.88% (p=0.000 n=10)
WriteAndCommit/AST-16                                           272.77Ki ± 0%     15.06Ki ± 0%   -94.48% (p=0.000 n=10)
WriteAndCommitWithTriggers/Go-16                                 29.58Ki ± 0%     15.83Ki ± 0%   -46.49% (p=0.000 n=10)
WriteAndCommitWithTriggers/Go_(roundtrip)-16                    268.80Ki ± 0%     15.83Ki ± 0%   -94.11% (p=0.000 n=10)
WriteAndCommitWithTriggers/AST-16                               279.09Ki ± 0%     26.12Ki ± 0%   -90.64% (p=0.000 n=10)

                                                             │    v1.9.0     │                   now                   │
                                                             │   allocs/op   │ allocs/op   vs base                     │
NewTransaction/write-16                                         3.000 ± 0%     1.000 ± 0%   -66.67% (p=0.000 n=10)
NewTransaction/read-16                                          3.000 ± 0%     1.000 ± 0%   -66.67% (p=0.000 n=10)
ReadOne/Go-16                                                   3.000 ± 0%     1.000 ± 0%   -66.67% (p=0.000 n=10)
ReadOne/Go_(roundtrip)-16                                       3.000 ± 0%     1.000 ± 0%   -66.67% (p=0.000 n=10)
ReadOne/AST-16                                                  4.000 ± 0%     2.000 ± 0%   -50.00% (p=0.000 n=10)
ReadOneNested/Go-16                                             3.000 ± 0%     1.000 ± 0%   -66.67% (p=0.000 n=10)
ReadOneNested/Go_(roundtrip)-16                                 3.000 ± 0%     1.000 ± 0%   -66.67% (p=0.000 n=10)
ReadOneNested/AST-16                                            6.000 ± 0%     4.000 ± 0%   -33.33% (p=0.000 n=10)
ReadNotFound/Go-16                                              3.000 ± 0%     0.000 ± 0%  -100.00% (p=0.000 n=10)
ReadNotFound/Go_(roundtrip)-16                                  3.000 ± 0%     0.000 ± 0%  -100.00% (p=0.000 n=10)
ReadNotFound/AST-16                                             4.000 ± 0%     1.000 ± 0%   -75.00% (p=0.000 n=10)
WriteOneString/Go-16                                            9.000 ± 0%     5.000 ± 0%   -44.44% (p=0.000 n=10)
WriteOneString/Go_(roundtrip)-16                               16.000 ± 0%     5.000 ± 0%   -68.75% (p=0.000 n=10)
WriteOneString/AST-16                                           22.00 ± 0%     11.00 ± 0%   -50.00% (p=0.000 n=10)
WriteSameValue/Go-16                                            4.000 ± 0%     0.000 ± 0%  -100.00% (p=0.000 n=10)
WriteSameValue/Go_(roundtrip)-16                                11.00 ± 0%      0.00 ± 0%  -100.00% (p=0.000 n=10)
WriteSameValue/AST-16                                           14.00 ± 0%      0.00 ± 0%  -100.00% (p=0.000 n=10)
WriteIncrementingValueSamePath/Go-16                            400.0 ± 0%     400.0 ± 0%         ~ (p=1.000 n=10) ¹
WriteIncrementingValueSamePath/Go_(roundtrip)-16               1090.0 ± 0%     500.0 ± 0%   -54.13% (p=0.000 n=10)
WriteIncrementingValueSamePath/AST-16                          1290.0 ± 0%     400.0 ± 0%   -68.99% (p=0.000 n=10)
WriteIncrementingValueSamePathInterned/Go-16                    400.0 ± 0%     400.0 ± 0%         ~ (p=1.000 n=10) ¹
WriteIncrementingValueSamePathInterned/Go_(roundtrip)-16       1090.0 ± 0%     500.0 ± 0%   -54.13% (p=0.000 n=10)
WriteIncrementingValueSamePathInterned/AST-16                  1090.0 ± 0%     200.0 ± 0%   -81.65% (p=0.000 n=10)
PathAndValueInternedAndNoRoundtripRequired/Go-16                500.0 ± 0%     100.0 ± 0%   -80.00% (p=0.000 n=10)
PathAndValueInternedAndNoRoundtripRequired/Go_(roundtrip)-16   1190.0 ± 0%     100.0 ± 0%   -91.60% (p=0.000 n=10)
PathAndValueInternedAndNoRoundtripRequired/AST-16              1292.0 ± 0%     100.0 ± 0%   -92.26% (p=0.000 n=10)
WriteCollection/Go-16                                           4.000 ± 0%     4.000 ± 0%         ~ (p=1.000 n=10) ¹
WriteCollection/Go_(roundtrip)-16                               35.00 ± 0%     35.00 ± 0%         ~ (p=1.000 n=10) ¹
WriteCollection/AST-16                                          63.00 ± 0%     30.00 ± 0%   -52.38% (p=0.000 n=10)
WriteAndCommit/Go-16                                            512.0 ± 0%     303.0 ± 0%   -40.82% (p=0.000 n=10)
WriteAndCommit/Go_(roundtrip)-16                               1202.0 ± 0%     303.0 ± 0%   -74.79% (p=0.000 n=10)
WriteAndCommit/AST-16                                          1406.0 ± 0%     507.0 ± 0%   -63.94% (p=0.000 n=10)
WriteAndCommitWithTriggers/Go-16                                512.0 ± 0%     304.0 ± 0%   -40.62% (p=0.000 n=10)
WriteAndCommitWithTriggers/Go_(roundtrip)-16                   1202.0 ± 0%     304.0 ± 0%   -74.71% (p=0.000 n=10)
WriteAndCommitWithTriggers/AST-16                              1507.0 ± 0%     609.0 ± 0%   -59.59% (p=0.000 n=10)
```

Signed-off-by: Anders Eknert <anders@eknert.com>
2025-10-02 14:54:14 +02:00

196 lines
5.2 KiB
Go

// Copyright 2016 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 util
import (
"bytes"
"encoding/json"
"fmt"
"io"
"reflect"
"strconv"
"sigs.k8s.io/yaml"
"github.com/open-policy-agent/opa/v1/loader/extension"
)
// UnmarshalJSON parses the JSON encoded data and stores the result in the value
// pointed to by x.
//
// This function is intended to be used in place of the standard [json.Marshal]
// function when [json.Number] is required.
func UnmarshalJSON(bs []byte, x any) error {
return unmarshalJSON(bs, x, true)
}
func unmarshalJSON(bs []byte, x any, ext bool) error {
decoder := NewJSONDecoder(bytes.NewBuffer(bs))
if err := decoder.Decode(x); err != nil {
if handler := extension.FindExtension(".json"); handler != nil && ext {
return handler(bs, x)
}
return err
}
// Since decoder.Decode validates only the first json structure in bytes,
// check if decoder has more bytes to consume to validate whole input bytes.
tok, err := decoder.Token()
if tok != nil {
return fmt.Errorf("error: invalid character '%s' after top-level value", tok)
}
if err != nil && err != io.EOF {
return err
}
return nil
}
// NewJSONDecoder returns a new decoder that reads from r.
//
// This function is intended to be used in place of the standard [json.NewDecoder]
// when [json.Number] is required.
func NewJSONDecoder(r io.Reader) *json.Decoder {
decoder := json.NewDecoder(r)
decoder.UseNumber()
return decoder
}
// MustUnmarshalJSON parse the JSON encoded data and returns the result.
//
// If the data cannot be decoded, this function will panic. This function is for
// test purposes.
func MustUnmarshalJSON(bs []byte) any {
var x any
if err := UnmarshalJSON(bs, &x); err != nil {
panic(err)
}
return x
}
// MustMarshalJSON returns the JSON encoding of x
//
// If the data cannot be encoded, this function will panic. This function is for
// test purposes.
func MustMarshalJSON(x any) []byte {
bs, err := json.Marshal(x)
if err != nil {
panic(err)
}
return bs
}
// RoundTrip encodes to JSON, and decodes the result again.
//
// Thereby, it is converting its argument to the representation expected by
// rego.Input and inmem's Write operations. Works with both references and
// values.
func RoundTrip(x *any) error {
// Avoid round-tripping types that won't change as a result of
// marshalling/unmarshalling, as even for those values, round-tripping
// comes with a significant cost.
if x == nil || !NeedsRoundTrip(*x) {
return nil
}
// For number types, we can write the json.Number representation
// directly into x without marshalling to bytes and back.
a := *x
switch v := a.(type) {
case int:
*x = json.Number(strconv.Itoa(v))
return nil
case int8:
*x = json.Number(strconv.FormatInt(int64(v), 10))
return nil
case int16:
*x = json.Number(strconv.FormatInt(int64(v), 10))
return nil
case int32:
*x = json.Number(strconv.FormatInt(int64(v), 10))
return nil
case int64:
*x = json.Number(strconv.FormatInt(v, 10))
return nil
case uint:
*x = json.Number(strconv.FormatUint(uint64(v), 10))
return nil
case uint8:
*x = json.Number(strconv.FormatUint(uint64(v), 10))
return nil
case uint16:
*x = json.Number(strconv.FormatUint(uint64(v), 10))
return nil
case uint32:
*x = json.Number(strconv.FormatUint(uint64(v), 10))
return nil
case uint64:
*x = json.Number(strconv.FormatUint(v, 10))
return nil
case float32:
*x = json.Number(strconv.FormatFloat(float64(v), 'f', -1, 32))
return nil
case float64:
*x = json.Number(strconv.FormatFloat(v, 'f', -1, 64))
return nil
}
bs, err := json.Marshal(x)
if err != nil {
return err
}
return UnmarshalJSON(bs, x)
}
// NeedsRoundTrip returns true if the value won't change as a result of
// a marshalling/unmarshalling round-trip. Since [RoundTrip] itself calls
// this you normally don't need to call this function directly, unless you
// want to make decisions based on the round-tripability of a value without
// actually doing the round-trip.
func NeedsRoundTrip(x any) bool {
switch x.(type) {
case nil, bool, string, json.Number:
return false
}
return true
}
// Reference returns a pointer to its argument unless the argument already is
// a pointer. If the argument is **t, or ***t, etc, it will return *t.
//
// Used for preparing Go types (including pointers to structs) into values to be
// put through [RoundTrip].
func Reference(x any) *any {
var y any
rv := reflect.ValueOf(x)
if rv.Kind() == reflect.Pointer {
return Reference(rv.Elem().Interface())
}
if rv.Kind() != reflect.Invalid {
y = rv.Interface()
return &y
}
return &x
}
// Unmarshal decodes a YAML, JSON or JSON extension value into the specified type.
func Unmarshal(bs []byte, v any) error {
if len(bs) > 2 && bs[0] == 0xef && bs[1] == 0xbb && bs[2] == 0xbf {
bs = bs[3:] // Strip UTF-8 BOM, see https://www.rfc-editor.org/rfc/rfc8259#section-8.1
}
if json.Valid(bs) {
return unmarshalJSON(bs, v, false)
}
nbs, err := yaml.YAMLToJSON(bs)
if err == nil {
return unmarshalJSON(nbs, v, false)
}
// not json or yaml: try extensions
if handler := extension.FindExtension(".json"); handler != nil {
return handler(bs, v)
}
return err
}