Skip to content

Commit a1203bf

Browse files
authored
eval: performance improvements (#392)
* eval: performance improvements These changes improve evaluation performance by memoizing exported values and merged object keys and avoiding copies when building object schemas. This gives a tremendous improvement in execution time for evaluation-dominated scenarios, but results in little to no change for open- or load-dominated scenarios. Local benchmark results: goos: darwin goarch: arm64 pkg: github.com/pulumi/esc/eval cpu: Apple M1 Max BenchmarkEval-10 453 2566938 ns/op 2204129 B/op 18883 allocs/op BenchmarkEval-10 468 2586703 ns/op 2204146 B/op 18884 allocs/op BenchmarkEval-10 462 2588916 ns/op 2204202 B/op 18883 allocs/op BenchmarkEval-10 464 2586365 ns/op 2204228 B/op 18884 allocs/op BenchmarkEval-10 463 2577383 ns/op 2204362 B/op 18884 allocs/op BenchmarkEval-10 477 2537640 ns/op 2204389 B/op 18884 allocs/op BenchmarkEval-10 468 2582930 ns/op 2204594 B/op 18884 allocs/op BenchmarkEval-10 463 2582915 ns/op 2204246 B/op 18884 allocs/op BenchmarkEval-10 469 2608014 ns/op 2204382 B/op 18884 allocs/op BenchmarkEval-10 465 2554270 ns/op 2204313 B/op 18884 allocs/op BenchmarkEvalOpen-10 9 119163125 ns/op 2208651 B/op 18926 allocs/op BenchmarkEvalOpen-10 9 118168319 ns/op 2209928 B/op 18928 allocs/op BenchmarkEvalOpen-10 9 118805454 ns/op 2208294 B/op 18924 allocs/op BenchmarkEvalOpen-10 9 118506347 ns/op 2208712 B/op 18922 allocs/op BenchmarkEvalOpen-10 9 118898060 ns/op 2210256 B/op 18926 allocs/op BenchmarkEvalOpen-10 9 118450250 ns/op 2208210 B/op 18924 allocs/op BenchmarkEvalOpen-10 9 117723833 ns/op 2207528 B/op 18922 allocs/op BenchmarkEvalOpen-10 9 117134787 ns/op 2209227 B/op 18925 allocs/op BenchmarkEvalOpen-10 9 116210269 ns/op 2208843 B/op 18926 allocs/op BenchmarkEvalOpen-10 9 116987444 ns/op 2208736 B/op 18925 allocs/op BenchmarkEvalEnvLoad-10 4 298021334 ns/op 2216058 B/op 18951 allocs/op BenchmarkEvalEnvLoad-10 4 302557979 ns/op 2213974 B/op 18944 allocs/op BenchmarkEvalEnvLoad-10 4 293050229 ns/op 2212098 B/op 18945 allocs/op BenchmarkEvalEnvLoad-10 4 304410510 ns/op 2211322 B/op 18946 allocs/op BenchmarkEvalEnvLoad-10 4 301698562 ns/op 2212554 B/op 18947 allocs/op BenchmarkEvalEnvLoad-10 4 299588854 ns/op 2214102 B/op 18946 allocs/op BenchmarkEvalEnvLoad-10 4 295087740 ns/op 2211650 B/op 18944 allocs/op BenchmarkEvalEnvLoad-10 4 295875531 ns/op 2212638 B/op 18950 allocs/op BenchmarkEvalEnvLoad-10 4 294871781 ns/op 2212038 B/op 18945 allocs/op BenchmarkEvalEnvLoad-10 4 294592875 ns/op 2211682 B/op 18945 allocs/op BenchmarkEvalAll-10 3 405058722 ns/op 2215330 B/op 18976 allocs/op BenchmarkEvalAll-10 3 407002764 ns/op 2215688 B/op 18978 allocs/op BenchmarkEvalAll-10 3 409757153 ns/op 2214973 B/op 18976 allocs/op BenchmarkEvalAll-10 3 404553611 ns/op 2215261 B/op 18977 allocs/op BenchmarkEvalAll-10 3 402620945 ns/op 2216994 B/op 18980 allocs/op BenchmarkEvalAll-10 3 405302139 ns/op 2213112 B/op 18973 allocs/op BenchmarkEvalAll-10 3 404533556 ns/op 2215848 B/op 18978 allocs/op BenchmarkEvalAll-10 3 403431236 ns/op 2215896 B/op 18979 allocs/op BenchmarkEvalAll-10 3 402586597 ns/op 2217245 B/op 18983 allocs/op BenchmarkEvalAll-10 3 404775236 ns/op 2217122 B/op 18980 allocs/op * CL
1 parent 840f7c0 commit a1203bf

File tree

13 files changed

+102
-48
lines changed

13 files changed

+102
-48
lines changed

CHANGELOG_PENDING.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
### Improvements
22

3+
- Improve evaluation performance and memory footprint.
4+
[#392](https://github.com/pulumi/esc/pull/392)
5+
36
### Bug Fixes
47

58
### Breaking changes
9+
10+
- `schema`: `ObjectBuilder.Properties` and `Record` now take a `MapBuilder` in order to avoid copies.
11+
[#392](https://github.com/pulumi/esc/pull/392)

analysis/common_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,18 @@ import (
2727
)
2828

2929
var testProviderSchema = schema.Object().
30-
Properties(map[string]schema.Builder{
30+
Properties(schema.BuilderMap{
3131
"address": schema.String().
3232
Description("The URL of the Vault server. Must contain a scheme and hostname, but no path."),
3333
"jwt": schema.Object().
34-
Properties(map[string]schema.Builder{
34+
Properties(schema.BuilderMap{
3535
"mount": schema.String().Description("The name of the authentication engine mount."),
3636
"role": schema.String().Description("The name of the role to use for login."),
3737
}).
3838
Required("role").
3939
Description("Options for JWT login. JWT login uses an OIDC token issued by the Pulumi Cloud to generate an ephemeral token."),
4040
"token": schema.Object().
41-
Properties(map[string]schema.Builder{
41+
Properties(schema.BuilderMap{
4242
"displayName": schema.String().Description("The display name of the ephemeral token. Defaults to 'pulumi'."),
4343
"token": schema.String().Description("The parent token."),
4444
"maxTtl": schema.String().

ast/environment.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ func (d *MapDecl[T]) parse(name string, node syntax.Node) syntax.Diagnostics {
127127
kvp := obj.Index(i)
128128

129129
var v T
130-
vname := fmt.Sprintf("%s.%s", name, kvp.Key.Value())
130+
vname := name + "." + kvp.Key.Value()
131131
vdiags := parseNode(vname, &v, kvp.Value)
132132
diags.Extend(vdiags...)
133133

ast/expr.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ type SymbolExpr struct {
230230
func Symbol(accessors ...PropertyAccessor) *SymbolExpr {
231231
property := &PropertyAccess{Accessors: accessors}
232232
return &SymbolExpr{
233-
exprNode: expr(syntax.String(fmt.Sprintf("${%v}", property))),
233+
exprNode: expr(syntax.String("${" + property.String() + "}")),
234234
Property: property,
235235
}
236236
}

cmd/esc/cli/client/client_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,7 @@ func TestCheckYAMLEnvironment(t *testing.T) {
478478
expected := &esc.Environment{
479479
Exprs: map[string]esc.Expr{"foo": {Literal: "bar"}},
480480
Properties: map[string]esc.Value{"foo": esc.NewValue("bar")},
481-
Schema: schema.Record(map[string]schema.Builder{"foo": schema.String().Const("bar")}).Schema(),
481+
Schema: schema.Record(schema.BuilderMap{"foo": schema.String().Const("bar")}).Schema(),
482482
}
483483

484484
client := newTestClient(t, http.MethodPost, "/api/esc/environments/test-org/yaml/check", func(w http.ResponseWriter, r *http.Request) {
@@ -604,7 +604,7 @@ func TestGetOpenEnvironment(t *testing.T) {
604604
expected := &esc.Environment{
605605
Exprs: map[string]esc.Expr{"foo": {Literal: "bar"}},
606606
Properties: map[string]esc.Value{"foo": esc.NewValue("bar")},
607-
Schema: schema.Record(map[string]schema.Builder{"foo": schema.String().Const("bar")}).Schema(),
607+
Schema: schema.Record(schema.BuilderMap{"foo": schema.String().Const("bar")}).Schema(),
608608
}
609609

610610
client := newTestClient(t, http.MethodGet, "/api/esc/environments/test-org/test-project/test-env/open/session", func(w http.ResponseWriter, r *http.Request) {
@@ -638,7 +638,7 @@ func TestGetAnonymousOpenEnvironment(t *testing.T) {
638638
expected := &esc.Environment{
639639
Exprs: map[string]esc.Expr{"foo": {Literal: "bar"}},
640640
Properties: map[string]esc.Value{"foo": esc.NewValue("bar")},
641-
Schema: schema.Record(map[string]schema.Builder{"foo": schema.String().Const("bar")}).Schema(),
641+
Schema: schema.Record(schema.BuilderMap{"foo": schema.String().Const("bar")}).Schema(),
642642
}
643643

644644
client := newTestClient(t, http.MethodGet, "/api/esc/environments/test-org/yaml/open/session", func(w http.ResponseWriter, r *http.Request) {

eval/eval.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ func (e *evalContext) evaluate() (*value, syntax.Diagnostics) {
360360
// root.
361361
properties := make(map[string]*expr, len(e.env.Values.GetEntries()))
362362
e.root = &expr{
363-
path: fmt.Sprintf("<%v>", e.name),
363+
path: "<" + e.name + ">",
364364
repr: &objectExpr{
365365
node: ast.Object(),
366366
properties: properties,
@@ -402,7 +402,7 @@ func (e *evalContext) evaluateImports() {
402402
e.evaluateImport(myImports, entry)
403403
}
404404

405-
properties := make(map[string]schema.Builder, len(myImports))
405+
properties := make(schema.SchemaMap, len(myImports))
406406
for k, v := range myImports {
407407
properties[k] = v.schema
408408
}
@@ -582,7 +582,7 @@ func (e *evalContext) evaluateObject(x *expr, repr *objectExpr) *value {
582582
keys := maps.Keys(repr.properties)
583583
sort.Strings(keys)
584584

585-
object, properties := make(map[string]*value, len(keys)), make(map[string]schema.Builder, len(keys))
585+
object, properties := make(map[string]*value, len(keys)), make(schema.SchemaMap, len(keys))
586586
for _, k := range keys {
587587
pv := e.evaluateExpr(repr.properties[k])
588588
object[k], properties[k] = pv, pv.schema
@@ -921,7 +921,7 @@ func (e *evalContext) evaluateBuiltinOpen(x *expr, repr *openExpr) *value {
921921

922922
output, err := provider.Open(e.ctx, inputs.export("").Value.(map[string]esc.Value), e.execContext)
923923
if err != nil {
924-
e.errorf(repr.syntax(), err.Error())
924+
e.errorf(repr.syntax(), "%s", err.Error())
925925
v.unknown = true
926926
return v
927927
}

eval/eval_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func accept() bool {
4343
type errorProvider struct{}
4444

4545
func (errorProvider) Schema() (*schema.Schema, *schema.Schema) {
46-
return schema.Record(map[string]schema.Builder{"why": schema.String()}).Schema(), schema.Always()
46+
return schema.Record(schema.BuilderMap{"why": schema.String()}).Schema(), schema.Always()
4747
}
4848

4949
func (errorProvider) Open(ctx context.Context, inputs map[string]esc.Value, context esc.EnvExecContext) (esc.Value, error) {
@@ -54,12 +54,12 @@ type testSchemaProvider struct{}
5454

5555
func (testSchemaProvider) Schema() (*schema.Schema, *schema.Schema) {
5656
s := schema.Object().
57-
Defs(map[string]schema.Builder{
58-
"defRecord": schema.Record(map[string]schema.Builder{
57+
Defs(schema.BuilderMap{
58+
"defRecord": schema.Record(schema.BuilderMap{
5959
"baz": schema.String().Const("qux"),
6060
}),
6161
}).
62-
Properties(map[string]schema.Builder{
62+
Properties(schema.BuilderMap{
6363
"null": schema.Null(),
6464
"boolean": schema.Boolean(),
6565
"false": schema.Boolean().Const(false),
@@ -71,7 +71,7 @@ func (testSchemaProvider) Schema() (*schema.Schema, *schema.Schema) {
7171
"array": schema.Array().Items(schema.Always()),
7272
"tuple": schema.Tuple(schema.String().Const("hello"), schema.String().Const("world")),
7373
"map": schema.Object().AdditionalProperties(schema.Always()),
74-
"record": schema.Record(map[string]schema.Builder{
74+
"record": schema.Record(schema.BuilderMap{
7575
"foo": schema.String(),
7676
}),
7777
"anyOf": schema.AnyOf(schema.String(), schema.Number()),
@@ -87,7 +87,7 @@ func (testSchemaProvider) Schema() (*schema.Schema, *schema.Schema) {
8787
"double": schema.Tuple(schema.String(), schema.Number()),
8888
"triple": schema.Tuple(schema.String(), schema.Number(), schema.Boolean()),
8989
"dependentReq": schema.Object().
90-
Properties(map[string]schema.Builder{
90+
Properties(schema.BuilderMap{
9191
"foo": schema.String(),
9292
"bar": schema.Number(),
9393
}).DependentRequired(map[string][]string{"foo": {"bar"}}),

eval/expr.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,8 @@ func (x *expr) export(environment string) esc.Expr {
185185
ex.Builtin = &esc.BuiltinExpr{
186186
Name: name,
187187
NameRange: convertRange(repr.node.Name().Syntax().Syntax().Range(), environment),
188-
ArgSchema: schema.Record(map[string]schema.Builder{
189-
"provider": schema.String(),
188+
ArgSchema: schema.Record(schema.SchemaMap{
189+
"provider": schema.String().Schema(),
190190
"inputs": repr.inputSchema,
191191
}).Schema(),
192192
Arg: esc.Expr{

eval/value.go

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ type value struct {
3939
base *value // the base value, if any
4040
schema *schema.Schema // the value's schema
4141

42+
mergedKeys []string // the value's merged keys. computed lazily--use keys().
43+
exported *esc.Value // non-nil if this value has already been exported
44+
4245
// true if the value is unknown (e.g. because it did not evaluate successfully or is the result of an unevaluated
4346
// fn::open)
4447
unknown bool
@@ -149,20 +152,36 @@ func (v *value) combine(others ...*value) {
149152
// keys returns the value's keys if the value is an object. This method should be used instead of accessing the
150153
// underlying map[string]*value directly, as it takes JSON merge patch semantics into account.
151154
func (v *value) keys() []string {
152-
keySet := make(map[string]struct{})
153-
for v != nil {
155+
if v == nil {
156+
return nil
157+
}
158+
if v.mergedKeys == nil {
154159
m, ok := v.repr.(map[string]*value)
155160
if !ok {
156-
break
161+
return nil
157162
}
158-
for k := range m {
159-
keySet[k] = struct{}{}
163+
164+
baseKeys := v.base.keys()
165+
if len(baseKeys) == 0 {
166+
v.mergedKeys = maps.Keys(m)
167+
} else {
168+
l := len(baseKeys)
169+
if l < len(m) {
170+
l = len(m)
171+
}
172+
keySet := make(map[string]struct{}, l)
173+
174+
for _, k := range baseKeys {
175+
keySet[k] = struct{}{}
176+
}
177+
for k := range m {
178+
keySet[k] = struct{}{}
179+
}
180+
v.mergedKeys = maps.Keys(keySet)
160181
}
161-
v = v.base
182+
sort.Strings(v.mergedKeys)
162183
}
163-
keys := maps.Keys(keySet)
164-
sort.Strings(keys)
165-
return keys
184+
return v.mergedKeys
166185
}
167186

168187
// property returns the named property (if any) as per JSON merge patch semantics. If the receiver is unknown,
@@ -269,6 +288,10 @@ func (v *value) toString() (str string, unknown bool, secret bool) {
269288

270289
// export converts the value into its serializable representation.
271290
func (v *value) export(environment string) esc.Value {
291+
if v.exported != nil {
292+
return *v.exported
293+
}
294+
272295
var pv any
273296
switch repr := v.repr.(type) {
274297
case []*value:
@@ -295,7 +318,7 @@ func (v *value) export(environment string) esc.Value {
295318
base = &b
296319
}
297320

298-
return esc.Value{
321+
v.exported = &esc.Value{
299322
Value: pv,
300323
Secret: v.secret,
301324
Unknown: v.unknown,
@@ -304,6 +327,7 @@ func (v *value) export(environment string) esc.Value {
304327
Base: base,
305328
},
306329
}
330+
return *v.exported
307331
}
308332

309333
// unexport creates a value from a Value. This is used when interacting with providers, as the Provider API works on
@@ -327,7 +351,7 @@ func unexport(v esc.Value, x *expr) *value {
327351
}
328352
vv.repr, vv.schema = a, schema.Tuple(items...).Schema()
329353
case map[string]esc.Value:
330-
m, properties := make(map[string]*value, len(pv)), make(map[string]schema.Builder, len(pv))
354+
m, properties := make(map[string]*value, len(pv)), make(schema.SchemaMap, len(pv))
331355
for k, v := range pv {
332356
uv := unexport(v, x)
333357
m[k], properties[k] = uv, uv.schema
@@ -348,7 +372,12 @@ func mergedSchema(base, top *schema.Schema) *schema.Schema {
348372
return top
349373
}
350374

351-
record := make(map[string]schema.Builder)
375+
l := len(base.Properties)
376+
if l < len(top.Properties) {
377+
l = len(top.Properties)
378+
}
379+
380+
record := make(schema.SchemaMap, l)
352381
for k, base := range base.Properties {
353382
record[k] = base
354383
}

schema/objects.go

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,13 @@ func Object() *ObjectBuilder {
3030
return &ObjectBuilder{}
3131
}
3232

33-
func Record(m map[string]Builder) *ObjectBuilder {
34-
names := maps.Keys(m)
33+
func Record(m MapBuilder) *ObjectBuilder {
34+
props := m.Build()
35+
36+
names := maps.Keys(props)
3537
sort.Strings(names)
3638

37-
return Object().Properties(m).Required(names...)
39+
return Object().Properties(SchemaMap(props)).Required(names...)
3840
}
3941

4042
func (b *ObjectBuilder) Defs(defs map[string]Builder) *ObjectBuilder {
@@ -53,11 +55,8 @@ func (b *ObjectBuilder) OneOf(oneOf ...Builder) *ObjectBuilder {
5355
return buildOneOf(b, oneOf)
5456
}
5557

56-
func (b *ObjectBuilder) Properties(m map[string]Builder) *ObjectBuilder {
57-
b.s.Properties = make(map[string]*Schema, len(m))
58-
for k, v := range m {
59-
b.s.Properties[k] = v.Schema()
60-
}
58+
func (b *ObjectBuilder) Properties(m MapBuilder) *ObjectBuilder {
59+
b.s.Properties = m.Build()
6160
return b
6261
}
6362

0 commit comments

Comments
 (0)