mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-26 18:25:06 -06:00
Lazy obj performance (#6031)
Adding entry cache to `lazyObj` for storing already type converted entries for subsequent reuse. Before this change, `.Find()` invocations on `lazyObj` instances where the searched for ref contains multiple variables would cause multiple type conversions for the same object entry; which can be very costly for large objects. Fixes: #6009 Signed-off-by: Johan Fylling <johan.dev@fylling.se>
This commit is contained in:
+24
-5
@@ -1840,17 +1840,22 @@ func ObjectTerm(o ...[2]*Term) *Term {
|
||||
}
|
||||
|
||||
func LazyObject(blob map[string]interface{}) Object {
|
||||
return &lazyObj{native: blob}
|
||||
return &lazyObj{native: blob, cache: map[string]Value{}}
|
||||
}
|
||||
|
||||
type lazyObj struct {
|
||||
strict Object
|
||||
cache map[string]Value
|
||||
native map[string]interface{}
|
||||
}
|
||||
|
||||
func (l *lazyObj) force() Object {
|
||||
if l.strict == nil {
|
||||
l.strict = MustInterfaceToValue(l.native).(Object)
|
||||
// NOTE(jf): a possible performance improvement here would be to check how many
|
||||
// entries have been realized to AST in the cache, and if some threshold compared to the
|
||||
// total number of keys is exceeded, realize the remaining entries and set l.strict to l.cache.
|
||||
l.cache = map[string]Value{} // We don't need the cache anymore; drop it to free up memory.
|
||||
}
|
||||
return l.strict
|
||||
}
|
||||
@@ -1924,13 +1929,20 @@ func (l *lazyObj) Get(k *Term) *Term {
|
||||
return l.strict.Get(k)
|
||||
}
|
||||
if s, ok := k.Value.(String); ok {
|
||||
if v, ok := l.cache[string(s)]; ok {
|
||||
return NewTerm(v)
|
||||
}
|
||||
|
||||
if val, ok := l.native[string(s)]; ok {
|
||||
var converted Value
|
||||
switch val := val.(type) {
|
||||
case map[string]interface{}:
|
||||
return NewTerm(&lazyObj{native: val})
|
||||
converted = LazyObject(val)
|
||||
default:
|
||||
return NewTerm(MustInterfaceToValue(val))
|
||||
converted = MustInterfaceToValue(val)
|
||||
}
|
||||
l.cache[string(s)] = converted
|
||||
return NewTerm(converted)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -1985,13 +1997,20 @@ func (l *lazyObj) Find(path Ref) (Value, error) {
|
||||
return l, nil
|
||||
}
|
||||
if p0, ok := path[0].Value.(String); ok {
|
||||
if v, ok := l.cache[string(p0)]; ok {
|
||||
return v.Find(path[1:])
|
||||
}
|
||||
|
||||
if v, ok := l.native[string(p0)]; ok {
|
||||
var converted Value
|
||||
switch v := v.(type) {
|
||||
case map[string]interface{}:
|
||||
return (&lazyObj{native: v}).Find(path[1:])
|
||||
converted = LazyObject(v)
|
||||
default:
|
||||
return MustInterfaceToValue(v).Find(path[1:])
|
||||
converted = MustInterfaceToValue(v)
|
||||
}
|
||||
l.cache[string(p0)] = converted
|
||||
return converted.Find(path[1:])
|
||||
}
|
||||
}
|
||||
return nil, errFindNotFound
|
||||
|
||||
@@ -32,6 +32,35 @@ func BenchmarkObjectLookup(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkObjectFind(b *testing.B) {
|
||||
sizes := []int{5, 50, 500, 5000}
|
||||
for _, n := range sizes {
|
||||
for _, m := range sizes {
|
||||
b.Run(fmt.Sprintf("%d_%d", n, m), func(b *testing.B) {
|
||||
obj := NewObject()
|
||||
for i := 0; i < n; i++ {
|
||||
arr := NewArray()
|
||||
for j := 0; j < m; j++ {
|
||||
arr = arr.Append(IntNumberTerm(j))
|
||||
}
|
||||
obj.Insert(StringTerm(fmt.Sprint(i)), NewTerm(arr))
|
||||
}
|
||||
key := Ref{StringTerm(fmt.Sprint(n - 1)), IntNumberTerm(m - 1)}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
value, err := obj.Find(key)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if value == nil {
|
||||
b.Fatal("expected hit")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkObjectCreationAndLookup(b *testing.B) {
|
||||
sizes := []int{5, 50, 500, 5000, 50000, 500000}
|
||||
for _, n := range sizes {
|
||||
@@ -51,6 +80,57 @@ func BenchmarkObjectCreationAndLookup(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLazyObjectLookup(b *testing.B) {
|
||||
sizes := []int{5, 50, 500, 5000}
|
||||
for _, n := range sizes {
|
||||
b.Run(fmt.Sprint(n), func(b *testing.B) {
|
||||
data := make(map[string]interface{}, n)
|
||||
for i := 0; i < n; i++ {
|
||||
data[fmt.Sprint(i)] = i
|
||||
}
|
||||
obj := LazyObject(data)
|
||||
key := StringTerm(fmt.Sprint(n - 1))
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
value := obj.Get(key)
|
||||
if value == nil {
|
||||
b.Fatal("expected hit")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLazyObjectFind(b *testing.B) {
|
||||
sizes := []int{5, 50, 500, 5000}
|
||||
for _, n := range sizes {
|
||||
for _, m := range sizes {
|
||||
b.Run(fmt.Sprintf("%d_%d", n, m), func(b *testing.B) {
|
||||
data := make(map[string]interface{}, n)
|
||||
for i := 0; i < n; i++ {
|
||||
arr := make([]string, 0, m)
|
||||
for j := 0; j < m; j++ {
|
||||
arr = append(arr, fmt.Sprint(j))
|
||||
}
|
||||
data[fmt.Sprint(i)] = arr
|
||||
}
|
||||
obj := LazyObject(data)
|
||||
key := Ref{StringTerm(fmt.Sprint(n - 1)), IntNumberTerm(m - 1)}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
value, err := obj.Find(key)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if value == nil {
|
||||
b.Fatal("expected hit")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSetCreationAndLookup(b *testing.B) {
|
||||
sizes := []int{5, 50, 500, 5000, 50000, 500000}
|
||||
for _, n := range sizes {
|
||||
|
||||
@@ -1221,6 +1221,47 @@ func TestLazyObjectGet(t *testing.T) {
|
||||
assertForced(t, x, false)
|
||||
}
|
||||
|
||||
func TestLazyObjectGetCache(t *testing.T) {
|
||||
x := LazyObject(map[string]interface{}{
|
||||
"a": true,
|
||||
"b": false,
|
||||
"d": map[string]interface{}{
|
||||
"e": "f",
|
||||
"f": "g",
|
||||
},
|
||||
})
|
||||
|
||||
// Assert that non-objects are cached
|
||||
|
||||
y := x.Get(StringTerm("a"))
|
||||
|
||||
if x.(*lazyObj).cache["a"].Compare(y.Value) != 0 {
|
||||
t.Errorf("expected cache to be populated with retreived value")
|
||||
}
|
||||
|
||||
if x.(*lazyObj).cache["b"] != nil {
|
||||
t.Errorf("expected cache to not be populated with non-retrieved value")
|
||||
}
|
||||
|
||||
// Assert that objects are cached as lazy objects
|
||||
|
||||
y = x.Get(StringTerm("d"))
|
||||
|
||||
expected := NewObject(Item(StringTerm("e"), StringTerm("f")), Item(StringTerm("f"), StringTerm("g")))
|
||||
if y.Value.Compare(expected) != 0 {
|
||||
t.Errorf("expected returned value to be %v, got %v", expected, y)
|
||||
}
|
||||
|
||||
d := x.(*lazyObj).cache["d"]
|
||||
ld, ok := d.(*lazyObj)
|
||||
if !ok {
|
||||
t.Errorf("expected cache to be populated with lazy object, got %v", d)
|
||||
}
|
||||
if ld.Compare(expected) != 0 {
|
||||
t.Errorf("expected cached intermediate value to be %v, got %v", expected, y)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLazyObjectFind(t *testing.T) {
|
||||
x := LazyObject(map[string]interface{}{
|
||||
"a": map[string]interface{}{
|
||||
@@ -1252,6 +1293,64 @@ func TestLazyObjectFind(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLazyObjectFindCache(t *testing.T) {
|
||||
x := LazyObject(map[string]interface{}{
|
||||
"a": []string{
|
||||
"b", "c", "d",
|
||||
},
|
||||
"c": []string{
|
||||
"d", "e", "f",
|
||||
},
|
||||
"d": map[string]interface{}{
|
||||
"e": "f",
|
||||
"f": "g",
|
||||
},
|
||||
})
|
||||
|
||||
// Assert that non-objects are cached
|
||||
|
||||
y, err := x.Find(Ref{StringTerm("a"), IntNumberTerm(1)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if y.Compare(String("c")) != 0 {
|
||||
t.Errorf("expected returned value to be 'c', got %v", y)
|
||||
}
|
||||
|
||||
expected := NewArray(StringTerm("b"), StringTerm("c"), StringTerm("d"))
|
||||
if x.(*lazyObj).cache["a"].Compare(expected) != 0 {
|
||||
t.Errorf("expected cache to be populated with type-converted intermediate value, got %v",
|
||||
x.(*lazyObj).cache["a"])
|
||||
}
|
||||
|
||||
if x.(*lazyObj).cache["b"] != nil {
|
||||
t.Errorf("expected cache to not be populated non-retrieved intermediate value, got %v",
|
||||
x.(*lazyObj).cache["b"])
|
||||
}
|
||||
|
||||
// Assert that objects are cached as lazy objects
|
||||
|
||||
y, err = x.Find(Ref{StringTerm("d"), StringTerm("e")})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if y.Compare(String("f")) != 0 {
|
||||
t.Errorf("expected returned value to be 'c', got %v", y)
|
||||
}
|
||||
|
||||
d := x.(*lazyObj).cache["d"]
|
||||
ld, ok := d.(*lazyObj)
|
||||
if !ok {
|
||||
t.Errorf("expected cache to be populated with lazy object, got %v", d)
|
||||
}
|
||||
if ld.cache["e"].Compare(String("f")) != 0 {
|
||||
t.Errorf("expected cache of intermediate lazyObj to be populated with type-converted intermediate value, got %v",
|
||||
ld.cache["e"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLazyObjectCopy(t *testing.T) {
|
||||
x := LazyObject(map[string]interface{}{
|
||||
"a": map[string]interface{}{
|
||||
|
||||
Reference in New Issue
Block a user