From 757b187a2980b6e5afa1533f93d79be5bffc0ab5 Mon Sep 17 00:00:00 2001 From: Supun Setunga Date: Wed, 13 Mar 2024 12:28:23 -0700 Subject: [PATCH] Meter computation for standard-library functions --- runtime/interpreter/value.go | 29 +++- runtime/tests/interpreter/metering_test.go | 148 +++++++++++++++++++++ 2 files changed, 173 insertions(+), 4 deletions(-) diff --git a/runtime/interpreter/value.go b/runtime/interpreter/value.go index adbe758703..dfdb162416 100644 --- a/runtime/interpreter/value.go +++ b/runtime/interpreter/value.go @@ -1198,6 +1198,10 @@ func (v *StringValue) Concat(interpreter *Interpreter, other *StringValue, locat memoryUsage := common.NewStringMemoryUsage(newLength) + // Meter computation as if the two strings were iterated. + length := len(v.Str) + len(other.Str) + interpreter.ReportComputation(common.ComputationKindLoop, uint(length)) + return NewStringValue( interpreter, memoryUsage, @@ -1430,6 +1434,9 @@ func (v *StringValue) Length() int { func (v *StringValue) ToLower(interpreter *Interpreter) *StringValue { + // Meter computation as if the string was iterated. + interpreter.ReportComputation(common.ComputationKindLoop, uint(len(v.Str))) + // Over-estimate resulting string length, // as an uppercase character may be converted to several lower-case characters, e.g İ => [i, ̇] // see https://stackoverflow.com/questions/28683805/is-there-a-unicode-string-which-gets-longer-when-converted-to-lowercase @@ -1454,7 +1461,12 @@ func (v *StringValue) ToLower(interpreter *Interpreter) *StringValue { ) } -func (v *StringValue) Split(inter *Interpreter, locationRange LocationRange, separator string) Value { +func (v *StringValue) Split(inter *Interpreter, _ LocationRange, separator string) Value { + + // Meter computation as if the string was iterated. + // i.e: linear search to find the split points. This is an estimate. + inter.ReportComputation(common.ComputationKindLoop, uint(len(v.Str))) + split := strings.Split(v.Str, separator) var index int @@ -1483,14 +1495,18 @@ func (v *StringValue) Split(inter *Interpreter, locationRange LocationRange, sep ) } -func (v *StringValue) ReplaceAll(inter *Interpreter, locationRange LocationRange, of string, with string) *StringValue { +func (v *StringValue) ReplaceAll(inter *Interpreter, _ LocationRange, of string, with string) *StringValue { // Over-estimate the resulting string length. // In the worst case, `of` can be empty in which case, `with` will be added at every index. // e.g. `of` = "", `v` = "ABC", `with` = "1": result = "1A1B1C1". - lengthOverEstimate := (2*len(v.Str) + 1) * len(with) + strLen := len(v.Str) + lengthOverEstimate := (2*strLen + 1) * len(with) memoryUsage := common.NewStringMemoryUsage(lengthOverEstimate) + // Meter computation as if the string was iterated. + inter.ReportComputation(common.ComputationKindLoop, uint(strLen)) + return NewStringValue( inter, memoryUsage, @@ -2046,6 +2062,9 @@ func (v *ArrayValue) Concat(interpreter *Interpreter, locationRange LocationRang v.array.Count()+other.array.Count(), func() Value { + // Meter computation for iterating the two arrays. + interpreter.ReportComputation(common.ComputationKindLoop, 1) + var value Value if first { @@ -3174,13 +3193,15 @@ func (v *ArrayValue) Slice( uint64(toIndex-fromIndex), func() Value { - var value Value + // Meter computation for iterating the array. + interpreter.ReportComputation(common.ComputationKindLoop, 1) atreeValue, err := iterator.Next() if err != nil { panic(errors.NewExternalError(err)) } + var value Value if atreeValue != nil { value = MustConvertStoredValue(interpreter, atreeValue) } diff --git a/runtime/tests/interpreter/metering_test.go b/runtime/tests/interpreter/metering_test.go index 40f996d756..40fbd09e16 100644 --- a/runtime/tests/interpreter/metering_test.go +++ b/runtime/tests/interpreter/metering_test.go @@ -482,6 +482,58 @@ func TestInterpretArrayFunctionsComputationMetering(t *testing.T) { assert.Equal(t, uint(6), computationMeteredValues[common.ComputationKindLoop]) }) + + t.Run("slice", func(t *testing.T) { + t.Parallel() + + computationMeteredValues := make(map[common.ComputationKind]uint) + inter, err := parseCheckAndInterpretWithOptions(t, ` + fun main() { + let x = [1, 2, 3, 4, 5, 6] + let y = x.slice(from: 1, upTo: 4) + }`, + ParseCheckAndInterpretOptions{ + Config: &interpreter.Config{ + OnMeterComputation: func(compKind common.ComputationKind, intensity uint) { + computationMeteredValues[compKind] += intensity + }, + }, + }, + ) + require.NoError(t, err) + + _, err = inter.Invoke("main") + require.NoError(t, err) + + assert.Equal(t, uint(4), computationMeteredValues[common.ComputationKindLoop]) + }) + + t.Run("concat", func(t *testing.T) { + t.Parallel() + + computationMeteredValues := make(map[common.ComputationKind]uint) + inter, err := parseCheckAndInterpretWithOptions(t, ` + fun main() { + let x = [1, 2, 3] + let y = x.concat([4, 5, 6]) + }`, + ParseCheckAndInterpretOptions{ + Config: &interpreter.Config{ + OnMeterComputation: func(compKind common.ComputationKind, intensity uint) { + computationMeteredValues[compKind] += intensity + }, + }, + }, + ) + require.NoError(t, err) + + _, err = inter.Invoke("main") + require.NoError(t, err) + + // Computation is (arrayLength +1). It's an overestimate. + // The last one is for checking the end of array. + assert.Equal(t, uint(7), computationMeteredValues[common.ComputationKindLoop]) + }) } func TestInterpretStdlibComputationMetering(t *testing.T) { @@ -511,4 +563,100 @@ func TestInterpretStdlibComputationMetering(t *testing.T) { assert.Equal(t, uint(4), computationMeteredValues[common.ComputationKindLoop]) }) + + t.Run("string concat", func(t *testing.T) { + t.Parallel() + + computationMeteredValues := make(map[common.ComputationKind]uint) + inter, err := parseCheckAndInterpretWithOptions(t, ` + fun main() { + let s = "a b c".concat("1 2 3") + }`, + ParseCheckAndInterpretOptions{ + Config: &interpreter.Config{ + OnMeterComputation: func(compKind common.ComputationKind, intensity uint) { + computationMeteredValues[compKind] += intensity + }, + }, + }, + ) + require.NoError(t, err) + + _, err = inter.Invoke("main") + require.NoError(t, err) + + assert.Equal(t, uint(10), computationMeteredValues[common.ComputationKindLoop]) + }) + + t.Run("string replace all", func(t *testing.T) { + t.Parallel() + + computationMeteredValues := make(map[common.ComputationKind]uint) + inter, err := parseCheckAndInterpretWithOptions(t, ` + fun main() { + let s = "abcadeaf".replaceAll(of: "a", with: "z") + }`, + ParseCheckAndInterpretOptions{ + Config: &interpreter.Config{ + OnMeterComputation: func(compKind common.ComputationKind, intensity uint) { + computationMeteredValues[compKind] += intensity + }, + }, + }, + ) + require.NoError(t, err) + + _, err = inter.Invoke("main") + require.NoError(t, err) + + assert.Equal(t, uint(8), computationMeteredValues[common.ComputationKindLoop]) + }) + + t.Run("string to lower", func(t *testing.T) { + t.Parallel() + + computationMeteredValues := make(map[common.ComputationKind]uint) + inter, err := parseCheckAndInterpretWithOptions(t, ` + fun main() { + let s = "ABCdef".toLower() + }`, + ParseCheckAndInterpretOptions{ + Config: &interpreter.Config{ + OnMeterComputation: func(compKind common.ComputationKind, intensity uint) { + computationMeteredValues[compKind] += intensity + }, + }, + }, + ) + require.NoError(t, err) + + _, err = inter.Invoke("main") + require.NoError(t, err) + + assert.Equal(t, uint(6), computationMeteredValues[common.ComputationKindLoop]) + }) + + t.Run("string split", func(t *testing.T) { + t.Parallel() + + computationMeteredValues := make(map[common.ComputationKind]uint) + inter, err := parseCheckAndInterpretWithOptions(t, ` + fun main() { + let s = "abc/d/ef//".split(separator: "/") + }`, + ParseCheckAndInterpretOptions{ + Config: &interpreter.Config{ + OnMeterComputation: func(compKind common.ComputationKind, intensity uint) { + computationMeteredValues[compKind] += intensity + }, + }, + }, + ) + require.NoError(t, err) + + _, err = inter.Invoke("main") + require.NoError(t, err) + + assert.Equal(t, uint(10), computationMeteredValues[common.ComputationKindLoop]) + }) }