Skip to content

Commit

Permalink
Merge pull request #3172 from onflow/supun/computation-metering
Browse files Browse the repository at this point in the history
Meter computation for standard-library functions
  • Loading branch information
SupunS authored Mar 13, 2024
2 parents aa14837 + 757b187 commit b4475c4
Show file tree
Hide file tree
Showing 2 changed files with 173 additions and 4 deletions.
29 changes: 25 additions & 4 deletions runtime/interpreter/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down
148 changes: 148 additions & 0 deletions runtime/tests/interpreter/metering_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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])
})
}

0 comments on commit b4475c4

Please sign in to comment.