Skip to content

Commit d91350b

Browse files
authored
fix: cache concatList.Size() to prevent O(N^2) evaluation time (#1291)
* fix: cache concatList.Size() to prevent O(N^2) evaluation time concatList.Size() recomputes the total size by recursively calling Size() on prevList and nextList. After N concatenations, Size() on each iteration produces O(N^2) total time, bypassing CostLimit. Cache the result in a sync.Once field so repeated calls return in O(1). * refactor: compute concatList size at construction time * refactor: move size checks into newConcatList, add regression test
1 parent 68bdd8c commit d91350b

2 files changed

Lines changed: 44 additions & 24 deletions

File tree

common/types/list.go

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -126,16 +126,7 @@ func (l *baseList) Add(other ref.Val) ref.Val {
126126
if !ok {
127127
return MaybeNoSuchOverloadErr(other)
128128
}
129-
if l.Size() == IntZero {
130-
return other
131-
}
132-
if otherList.Size() == IntZero {
133-
return l
134-
}
135-
return &concatList{
136-
Adapter: l.Adapter,
137-
prevList: l,
138-
nextList: otherList}
129+
return newConcatList(l.Adapter, l, otherList)
139130
}
140131

141132
// Contains implements the traits.Container interface method.
@@ -353,9 +344,27 @@ func (l *mutableList) ToImmutableList() traits.Lister {
353344
// The `Adapter` enables native type to CEL type conversions.
354345
type concatList struct {
355346
Adapter
356-
value any
357-
prevList traits.Lister
358-
nextList traits.Lister
347+
value any
348+
prevList traits.Lister
349+
nextList traits.Lister
350+
cachedSize ref.Val
351+
}
352+
353+
func newConcatList(adapter Adapter, prevList, nextList traits.Lister) ref.Val {
354+
prevSize := prevList.Size().(Int)
355+
nextSize := nextList.Size().(Int)
356+
if prevSize == IntZero {
357+
return nextList.(ref.Val)
358+
}
359+
if nextSize == IntZero {
360+
return prevList.(ref.Val)
361+
}
362+
return &concatList{
363+
Adapter: adapter,
364+
prevList: prevList,
365+
nextList: nextList,
366+
cachedSize: prevSize.Add(nextSize),
367+
}
359368
}
360369

361370
// Add implements the traits.Adder interface method.
@@ -364,16 +373,7 @@ func (l *concatList) Add(other ref.Val) ref.Val {
364373
if !ok {
365374
return MaybeNoSuchOverloadErr(other)
366375
}
367-
if l.Size() == IntZero {
368-
return other
369-
}
370-
if otherList.Size() == IntZero {
371-
return l
372-
}
373-
return &concatList{
374-
Adapter: l.Adapter,
375-
prevList: l,
376-
nextList: otherList}
376+
return newConcatList(l.Adapter, l, otherList)
377377
}
378378

379379
// Contains implements the traits.Container interface method.
@@ -477,7 +477,7 @@ func (l *concatList) Iterator() traits.Iterator {
477477

478478
// Size implements the traits.Sizer interface method.
479479
func (l *concatList) Size() ref.Val {
480-
return l.prevList.Size().(Int).Add(l.nextList.Size())
480+
return l.cachedSize
481481
}
482482

483483
// String converts the concatenated list to a human-readable string.

common/types/list_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -910,3 +910,23 @@ func validateIterator123(t *testing.T, list traits.Lister) {
910910
t.Errorf("Iterator did not iterate until last value")
911911
}
912912
}
913+
914+
func TestConcatListSizeCached(t *testing.T) {
915+
reg := newTestRegistry(t)
916+
// Build a deep chain of concat lists
917+
var list traits.Lister = NewDynamicList(reg, []int64{0})
918+
for i := 0; i < 200; i++ {
919+
list = list.Add(NewDynamicList(reg, []int64{0})).(traits.Lister)
920+
}
921+
// Size() should return instantly since it's precomputed
922+
size := list.Size()
923+
if size != Int(201) {
924+
t.Errorf("Expected size 201, got %v", size)
925+
}
926+
// Call Size() many times to confirm no quadratic behavior
927+
for i := 0; i < 1000; i++ {
928+
if list.Size() != Int(201) {
929+
t.Errorf("Size() returned inconsistent value on call %d", i)
930+
}
931+
}
932+
}

0 commit comments

Comments
 (0)