Skip to content

Commit

Permalink
Add support for evaluation cost (#43)
Browse files Browse the repository at this point in the history
* Update wasm_exec to 1.21.3

Signed-off-by: Kevin Conner <kev.conner@gmail.com>

* Add support for displaying the cost of the evaluation

Signed-off-by: Kevin Conner <kev.conner@gmail.com>

---------

Signed-off-by: Kevin Conner <kev.conner@gmail.com>
  • Loading branch information
knrc authored Nov 23, 2023
1 parent a015ab6 commit 8687777
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 32 deletions.
41 changes: 36 additions & 5 deletions eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,22 @@
package eval

import (
"encoding/json"
"fmt"
"reflect"

"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/ext"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/structpb"
k8s "k8s.io/apiserver/pkg/cel/library"
)

type EvalResponse struct {
Result any `json:"result"`
Cost *uint64 `json:"cost, omitempty"`
}

var celEnvOptions = []cel.EnvOption{
cel.EagerlyValidateDeclarations(true),
cel.DefaultUTCTimeZone(true),
Expand Down Expand Up @@ -59,14 +65,39 @@ func Eval(exp string, input map[string]any) (string, error) {
if err != nil {
return "", fmt.Errorf("failed to instantiate CEL program: %w", err)
}
val, _, err := prog.Eval(input)
val, costTracker, err := prog.Eval(input)
if err != nil {
return "", fmt.Errorf("failed to evaluate: %w", err)
}
jsonData, err := val.ConvertToNative(reflect.TypeOf(&structpb.Value{}))

response, err := generateResponse(val, costTracker)
if err != nil {
return "", fmt.Errorf("failed to generate the response: %w", err)
}

out, err := json.Marshal(response)
if err != nil {
return "", fmt.Errorf("failed to marshal the output: %w", err)
}
out := protojson.Format(jsonData.(*structpb.Value))
return out, nil
return string(out), nil
}

func getResults(val *ref.Val) (any, error) {
if value, err := (*val).ConvertToNative(reflect.TypeOf(&structpb.Value{})); err != nil {
return nil, err
} else {
return value, nil
}
}

func generateResponse(val ref.Val, costTracker *cel.EvalDetails) (*EvalResponse, error) {
result, evalError := getResults(&val)
if evalError != nil {
return nil, evalError
}
cost := costTracker.ActualCost()
return &EvalResponse{
Result: result,
Cost: cost,
}, nil
}
50 changes: 29 additions & 21 deletions eval/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
package eval

import (
"strings"
"encoding/json"
"reflect"
"testing"
)

Expand All @@ -34,13 +35,13 @@ func TestEval(t *testing.T) {
tests := []struct {
name string
exp string
want string
want any
wantErr bool
}{
{
name: "lte",
exp: "object.replicas <= 5",
want: "true",
want: true,
},
{
name: "error",
Expand All @@ -50,66 +51,73 @@ func TestEval(t *testing.T) {
{
name: "url",
exp: "isURL(object.href) && url(object.href).getScheme() == 'https' && url(object.href).getEscapedPath() == '/path'",
want: "true",
want: true,
},
{
name: "query",
exp: "url(object.href).getQuery()",
want: `{"query": ["val"]}`,
want: map[string]any{
"query": []any{"val"},
},
},
{
name: "regex",
exp: "object.image.find('v[0-9]+.[0-9]+.[0-9]*$')",
want: `"v0.0.0"`,
want: "v0.0.0",
},
{
name: "list",
exp: "object.items.isSorted() && object.items.sum() == 6 && object.items.max() == 3 && object.items.indexOf(1) == 0",
want: "true",
want: true,
},
{
name: "optional",
exp: `object.?foo.orValue("fallback")`,
want: `"fallback"`,
want: "fallback",
},
{
name: "strings",
exp: "object.abc.join(', ')",
want: `"a, b, c"`,
want: "a, b, c",
},
{
name: "cross type numeric comparisons",
exp: "object.replicas > 1.4",
want: "true",
want: true,
},
{
name: "split",
exp: "object.image.split(':').size() == 2",
want: "true",
want: true,
},
{
name: "quantity",
exp: `isQuantity(object.memory) && quantity(object.memory).add(quantity("700M")).sub(1).isLessThan(quantity("2G"))`,
want: "true",
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Eval(tt.exp, input)

if (err != nil) != tt.wantErr {
t.Errorf("Eval() error = %v, wantErr %v", err, tt.wantErr)
return
}
if stripWhitespace(got) != stripWhitespace(tt.want) {
t.Errorf("Eval() got = %v, want %v", got, tt.want)

if !tt.wantErr {
evalResponse := EvalResponse{}
if err := json.Unmarshal([]byte(got), &evalResponse); err != nil {
t.Errorf("Eval() error = %v", err)
}

if !reflect.DeepEqual(tt.want, evalResponse.Result) {
t.Errorf("Expected %v\n, received %v", tt.want, evalResponse.Result)
}
if evalResponse.Cost == nil || *evalResponse.Cost <= 0 {
t.Errorf("Expected Cost, returned %v", evalResponse.Cost)
}
}
})
}
}

func stripWhitespace(a string) string {
a = strings.ReplaceAll(a, " ", "")
a = strings.ReplaceAll(a, "\n", "")
a = strings.ReplaceAll(a, "\t", "")
return strings.ReplaceAll(a, "\r", "")
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/undistro/cel-playground

go 1.20
go 1.21

require (
github.com/google/cel-go v0.16.0
Expand Down
5 changes: 5 additions & 0 deletions web/assets/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -598,3 +598,8 @@ footer .langdef {
background: #e3e3e3;
border-radius: 4px;
}

.cost__header {
width: 25%;
text-align: left;
}
18 changes: 16 additions & 2 deletions web/assets/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,29 @@ if (!WebAssembly.instantiateStreaming) {

const celEditor = new AceEditor("cel-input");
const dataEditor = new AceEditor("data-input");
const output = document.getElementById("output");
const costElem = document.getElementById("cost");

function setCost(cost) {
costElem.innerText = costElem.textContent = cost;
}

function run() {
const data = dataEditor.getValue();
const expression = celEditor.getValue();
const output = document.getElementById("output");
const cost = document.getElementById("cost");

output.value = "Evaluating...";
setCost("")

const result = eval(expression, data);

const { output: resultOutput, isError } = result;
output.value = `${resultOutput}`;
var response = JSON.parse(resultOutput);

output.value = JSON.stringify(response.result);
setCost(response.cost);

output.style.color = isError ? "red" : "white";
}

Expand Down Expand Up @@ -304,6 +316,8 @@ fetch("../assets/data.json")
);
celEditor.setValue(example.cel, -1);
dataEditor.setValue(example.data, -1);
setCost("");
output.value = "";
});
})
.catch((err) => {
Expand Down
Binary file modified web/assets/main.wasm.gz
Binary file not shown.
11 changes: 9 additions & 2 deletions web/dist/wasm_exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
}

const setInt32 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
}

const getInt64 = (addr) => {
const low = this.mem.getUint32(addr + 0, true);
const high = this.mem.getInt32(addr + 4, true);
Expand Down Expand Up @@ -206,7 +210,10 @@

const timeOrigin = Date.now() - performance.now();
this.importObject = {
go: {
_gotest: {
add: (a, b) => a + b,
},
gojs: {
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
Expand Down Expand Up @@ -269,7 +276,7 @@
this._resume();
}
},
getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early
getInt64(sp + 8),
));
this.mem.setInt32(sp + 16, id, true);
},
Expand Down
10 changes: 9 additions & 1 deletion web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,15 @@
</div>
<div class="editor editor--output">
<div class="editor__header">
Output
<span>
Output
</span>
<span class="cost__header">
<span>
Cost:
</span>
<span id="cost"></span>
</span>
</div>
<textarea id="output" class="editor__output"
placeholder="Loading Wasm…" disabled></textarea>
Expand Down

0 comments on commit 8687777

Please sign in to comment.