From 4989048d7f8f44d5e33140541731af73d483391b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 00:57:16 +0000 Subject: [PATCH 1/3] perf(runtime): allocate in the binding walks only where a binding can be found Co-Authored-By: jason.han --- .../unreleased/binding-walk-allocs.fixed.md | 7 ++ internal/exec/runtime/binding.go | 7 +- internal/exec/runtime/instance.go | 72 ++++++++++--------- internal/exec/runtime/invoke_calc.go | 45 ++++++++++-- internal/exec/runtime/shape.go | 2 +- internal/exec/runtime/subsetting.go | 68 +++++++++++++++--- 6 files changed, 152 insertions(+), 49 deletions(-) create mode 100644 changes/unreleased/binding-walk-allocs.fixed.md diff --git a/changes/unreleased/binding-walk-allocs.fixed.md b/changes/unreleased/binding-walk-allocs.fixed.md new file mode 100644 index 0000000000..032e75c6cb --- /dev/null +++ b/changes/unreleased/binding-walk-allocs.fixed.md @@ -0,0 +1,7 @@ +The runtime's binding walks allocate only where a binding can be found: a feature read +that climbs to its owners spells out the feature path only while an owner declares a +binding, the witness draw builds its pool index on the first candidate, the sibling +subsetters' set is made on the first value, and a body is scanned for declared features +without listing its members first. `GRPCVerifyConstraint` allocates 14 887 times per +constraint instead of 16 301, recovering most of what #1508 and #1512 added over +0.11.0's 14 091; the remaining 796 are the reads those fixes made (no parity claim). diff --git a/internal/exec/runtime/binding.go b/internal/exec/runtime/binding.go index 8540478e61..c300acafd2 100644 --- a/internal/exec/runtime/binding.go +++ b/internal/exec/runtime/binding.go @@ -220,7 +220,10 @@ func (ctx *Context) resolveBindings(inst *Instance, target *FeatureValue, name s var unmet []lower.Binding path := name for current := inst; current != nil; { - bindings := ctx.bindingsOf(current, path) + var bindings []lower.Binding + if ctx.declaresBindings(current) { + bindings = ctx.bindingsOf(current, path) + } if len(bindings) != 0 { ctx.resolvingBindings[key] = true result, err := ctx.resolveBindingSet(current, inst, target, path, bindings, key) @@ -237,7 +240,7 @@ func (ctx *Context) resolveBindings(inst *Instance, target *FeatureValue, name s } } owner, ownerFeature := current.Owner() - if owner == nil || ownerFeature == "" { + if owner == nil || ownerFeature == "" || !ctx.ownersDeclareBindings(owner) { break } path = ownerFeature + "." + path diff --git a/internal/exec/runtime/instance.go b/internal/exec/runtime/instance.go index 26567204a6..485b9f24c7 100644 --- a/internal/exec/runtime/instance.go +++ b/internal/exec/runtime/instance.go @@ -1674,44 +1674,52 @@ func isReferenceUsage(sym *symbols.Symbol) bool { // declaresFeatures reports whether a usage's own body restates or adds features, // which the object it materializes has to carry. func declaresFeatures(sym *symbols.Symbol) bool { - return declaresAFeature(unwrappedDeclMembers(sym.Decl)) + for member := range eachUnwrappedDeclMember(sym.Decl) { + if memberDeclaresFeature(member) { + return true + } + } + return false } // declaresAFeature reports whether one of a body's members declares a feature: a // named or specializing usage, or a state body's named substate or entry, do or // exit action, each a feature of the state declaring it (SysML v2 §7.18.2). func declaresAFeature(members []ast.Node) bool { - for _, member := range members { - switch m := member.(type) { - case *ast.Usage: - if m.Ident.Name != "" || m.Ident.ShortName != "" || len(m.Relationships) > 0 { - return true - } - case *ast.SubjectMember: - // A subject the body values or defaults (`subject = x;`) restates the inherited parameter. - if m.BindingExpr != nil { - return true - } - case *ast.SubstateMember: - if m.Name != "" { - return true - } - case *ast.StateNode: - if m.Name != "" { - return true - } - case *ast.EntryMember: - if declaresAFeature(m.Actions) { - return true - } - case *ast.DoMember: - if declaresAFeature(m.Actions) { - return true - } - case *ast.ExitMember: - if declaresAFeature(m.Actions) { - return true - } + return slices.ContainsFunc(members, memberDeclaresFeature) +} + +// memberDeclaresFeature reports whether one body member declares a feature (see declaresAFeature). +func memberDeclaresFeature(member ast.Node) bool { + switch m := member.(type) { + case *ast.Usage: + if m.Ident.Name != "" || m.Ident.ShortName != "" || len(m.Relationships) > 0 { + return true + } + case *ast.SubjectMember: + // A subject the body values or defaults (`subject = x;`) restates the inherited parameter. + if m.BindingExpr != nil { + return true + } + case *ast.SubstateMember: + if m.Name != "" { + return true + } + case *ast.StateNode: + if m.Name != "" { + return true + } + case *ast.EntryMember: + if declaresAFeature(m.Actions) { + return true + } + case *ast.DoMember: + if declaresAFeature(m.Actions) { + return true + } + case *ast.ExitMember: + if declaresAFeature(m.Actions) { + return true } } return false diff --git a/internal/exec/runtime/invoke_calc.go b/internal/exec/runtime/invoke_calc.go index a28054694c..c194f30701 100644 --- a/internal/exec/runtime/invoke_calc.go +++ b/internal/exec/runtime/invoke_calc.go @@ -2,6 +2,7 @@ package runtime import ( "fmt" + "iter" "slices" "sort" "strings" @@ -1622,15 +1623,51 @@ func unwrappedDeclMembers(decl ast.Node) []ast.Node { } out := make([]ast.Node, 0, len(members)) - for _, member := range members { - if membership, ok := member.(*ast.Membership); ok { - member = membership.Member - } + for member := range eachUnwrappedDeclMember(decl) { out = append(out, member) } return out } +// eachUnwrappedDeclMember yields what unwrappedDeclMembers lists without building the list, +// for the readers that stop at the first member of interest. +func eachUnwrappedDeclMember(decl ast.Node) iter.Seq[ast.Node] { + return func(yield func(ast.Node) bool) { + if oc, ok := ast.OwnedConstraintOf(decl); ok { + for _, member := range oc.Body { + if !yield(member) { + return + } + } + return + } + switch m := decl.(type) { + case *ast.AssumeMember: + for _, member := range m.Body { + if !yield(member) { + return + } + } + return + case *ast.RequireMember: + for _, member := range m.Body { + if !yield(member) { + return + } + } + return + } + for _, member := range ast.DeclMembers(decl) { + if membership, ok := member.(*ast.Membership); ok { + member = membership.Member + } + if !yield(member) { + return + } + } + } +} + // QualifiedName renders a symbol's qualified name, falling back to its // local name when no index is reachable. func (ctx *Context) QualifiedName(sym *symbols.Symbol) string { diff --git a/internal/exec/runtime/shape.go b/internal/exec/runtime/shape.go index 9361674eb5..83cb2ccb37 100644 --- a/internal/exec/runtime/shape.go +++ b/internal/exec/runtime/shape.go @@ -543,7 +543,7 @@ func (ctx *Context) roleRedefiningDefault(sym, typeSym *symbols.Symbol) (ast.Nod return nil, nil } scope := bodyScope(typeSym, typeSym.OwnerScope) - for _, node := range unwrappedDeclMembers(typeSym.Decl) { + for node := range eachUnwrappedDeclMember(typeSym.Decl) { member := memberSymbol(scope, node) if member == nil || member == sym || member.Name != "" { continue diff --git a/internal/exec/runtime/subsetting.go b/internal/exec/runtime/subsetting.go index 6d1ac39532..02a9cf6690 100644 --- a/internal/exec/runtime/subsetting.go +++ b/internal/exec/runtime/subsetting.go @@ -1134,7 +1134,7 @@ func instancesAlsoIn(pool, els []Value) []Value { // siblingSubsetterValues is the objects the other features subsetting what fv subsets already // hold on inst: a value drawn for fv is one of them only where the subsetted feature has no other. func (ctx *Context) siblingSubsetterValues(inst *Instance, fv *FeatureValue, subsetted []string) map[int64]bool { - taken := map[int64]bool{} + var taken map[int64]bool for _, sub := range subsetted { for _, feat := range ctx.subsettingFeaturesOf(inst, sub) { sibling, ok := inst.FeatureValues[feat.Name] @@ -1143,6 +1143,9 @@ func (ctx *Context) siblingSubsetterValues(inst *Instance, fv *FeatureValue, sub } for _, v := range elementsOf(sibling.HeldValue()) { if v.Kind == ValInstance { + if taken == nil { + taken = map[int64]bool{} + } taken[v.Instance] = true } } @@ -1154,19 +1157,18 @@ func (ctx *Context) siblingSubsetterValues(inst *Instance, fv *FeatureValue, sub // bindingWitnesses draws, per binding constraining the named feature, as many of its other end's // values as the binding links, from pool where the subsetted features fix one (KerML 1.0 §7.4.6). func (ctx *Context) bindingWitnesses(inst *Instance, name string, pool []Value, pooled bool, taken map[int64]bool) ([]Value, map[int64]bool, error) { - draw := &witnessDraw{fv: inst.FeatureValues[name], pooled: pooled, inPool: make(map[int64]bool, len(pool)), taken: taken} - for _, v := range pool { - draw.inPool[v.Instance] = true - } + draw := witnessDraw{fv: inst.FeatureValues[name], pooled: pooled, pool: pool, taken: taken} // A binding of an owner naming the feature through its path constrains it as well. for current, path := inst, name; current != nil; { - for _, binding := range ctx.bindingsOf(current, path) { - if err := ctx.drawBindingWitnesses(draw, inst, current, binding, path); err != nil { - return nil, nil, err + if ctx.declaresBindings(current) { + for _, binding := range ctx.bindingsOf(current, path) { + if err := ctx.drawBindingWitnesses(&draw, inst, current, binding, path); err != nil { + return nil, nil, err + } } } owner, ownerFeature := current.Owner() - if owner == nil || ownerFeature == "" { + if owner == nil || ownerFeature == "" || !ctx.ownersDeclareBindings(owner) { break } path = ownerFeature + "." + path @@ -1175,17 +1177,63 @@ func (ctx *Context) bindingWitnesses(inst *Instance, name string, pool []Value, return draw.chosen, draw.seen, nil } +// ownersDeclareBindings reports whether inst or an owner of it declares a binding: where none +// does, a walk up from inst spells out no feature path, as no binding could name it. +func (ctx *Context) ownersDeclareBindings(inst *Instance) bool { + for current := inst; current != nil; { + if ctx.declaresBindings(current) { + return true + } + owner, ownerFeature := current.Owner() + if owner == nil || ownerFeature == "" { + return false + } + current = owner + } + return false +} + +// declaresBindings reports whether any type of inst declares a binding, so that a walk over +// its owners spells out a feature path only where a binding could name it. +func (ctx *Context) declaresBindings(inst *Instance) bool { + if inst.Type != nil && len(ctx.objectBindings(inst.Type)) != 0 { + return true + } + for _, typ := range inst.classifiers { + if len(ctx.objectBindings(typ)) != 0 { + return true + } + } + return false +} + // witnessDraw is one bindingWitnesses draw: the feature drawn for, the pool it may draw // from, what sibling subsetters hold, and what it has chosen so far. type witnessDraw struct { fv *FeatureValue pooled bool + pool []Value inPool map[int64]bool taken map[int64]bool chosen []Value seen map[int64]bool } +// admitsPooled reports whether a pooled draw may choose v: the pool index is built on the +// first candidate, so a walk that finds no binding never pays for it. +func (d *witnessDraw) admitsPooled(v Value) bool { + if !d.pooled { + return true + } + if d.inPool == nil { + d.inPool = make(map[int64]bool, len(d.pool)) + for _, p := range d.pool { + d.inPool[p.Instance] = true + } + } + return d.inPool[v.Instance] +} + // drawBindingWitnesses draws from the other end of binding, when it constrains path at // current, as many of that end's values as the binding links. func (ctx *Context) drawBindingWitnesses(draw *witnessDraw, inst, current *Instance, binding lower.Binding, path string) error { @@ -1231,7 +1279,7 @@ func (ctx *Context) drawBindingWitnesses(draw *witnessDraw, inst, current *Insta // pick draws up to links admitted candidates, those no sibling subsetter holds first. func (d *witnessDraw) pick(candidates []Value, links int64) { admits := func(v Value) bool { - return v.Kind == ValInstance && (!d.pooled || d.inPool[v.Instance]) && !d.seen[v.Instance] + return v.Kind == ValInstance && d.admitsPooled(v) && !d.seen[v.Instance] } for n := int64(0); n < links; n++ { pick := slices.IndexFunc(candidates, func(v Value) bool { return admits(v) && !d.taken[v.Instance] }) From 55cd6b727ae5ad301861ce7eb94f548aa919b1bf Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 06:35:45 +0000 Subject: [PATCH 2/3] docs(changes): note the warm instantiation row in the binding-walk fragment Co-Authored-By: jason.han --- changes/unreleased/binding-walk-allocs.fixed.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changes/unreleased/binding-walk-allocs.fixed.md b/changes/unreleased/binding-walk-allocs.fixed.md index 032e75c6cb..d870471e79 100644 --- a/changes/unreleased/binding-walk-allocs.fixed.md +++ b/changes/unreleased/binding-walk-allocs.fixed.md @@ -5,3 +5,5 @@ subsetters' set is made on the first value, and a body is scanned for declared f without listing its members first. `GRPCVerifyConstraint` allocates 14 887 times per constraint instead of 16 301, recovering most of what #1508 and #1512 added over 0.11.0's 14 091; the remaining 796 are the reads those fixes made (no parity claim). +The warm gRPC instantiation (`InstantiateWarmModel`) allocates 1 996 times instead of +2 155, against 1 962 on 0.11.0. From d9b102767d73b310592c6be5287f2581c314ac0b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 06:37:59 +0000 Subject: [PATCH 3/3] docs(changes): write the fragment as the list item the changelog check requires Co-Authored-By: jason.han --- changes/unreleased/binding-walk-allocs.fixed.md | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/changes/unreleased/binding-walk-allocs.fixed.md b/changes/unreleased/binding-walk-allocs.fixed.md index d870471e79..73b8054747 100644 --- a/changes/unreleased/binding-walk-allocs.fixed.md +++ b/changes/unreleased/binding-walk-allocs.fixed.md @@ -1,9 +1 @@ -The runtime's binding walks allocate only where a binding can be found: a feature read -that climbs to its owners spells out the feature path only while an owner declares a -binding, the witness draw builds its pool index on the first candidate, the sibling -subsetters' set is made on the first value, and a body is scanned for declared features -without listing its members first. `GRPCVerifyConstraint` allocates 14 887 times per -constraint instead of 16 301, recovering most of what #1508 and #1512 added over -0.11.0's 14 091; the remaining 796 are the reads those fixes made (no parity claim). -The warm gRPC instantiation (`InstantiateWarmModel`) allocates 1 996 times instead of -2 155, against 1 962 on 0.11.0. +- The runtime's binding walks allocate only where a binding can be found: a feature read that climbs to its owners spells out the feature path only while an owner declares a binding, the witness draw builds its pool index on the first candidate, the sibling subsetters' set is made on the first value, and a body is scanned for declared features without listing its members first. `GRPCVerifyConstraint` allocates 14 887 times per constraint instead of 16 301, recovering most of what #1508 and #1512 added over 0.11.0's 14 091; the remaining 796 are the reads those fixes made (no parity claim). The warm gRPC instantiation (`InstantiateWarmModel`) allocates 1 996 times instead of 2 155, against 1 962 on 0.11.0.