Document and benchmark sibling inheritance
This commit is contained in:
88
internal/engine/inheritance_bench_test.go
Normal file
88
internal/engine/inheritance_bench_test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||
)
|
||||
|
||||
func BenchmarkInheritanceExpansion(b *testing.B) {
|
||||
for _, shape := range []string{"chain", "wide", "nested", "replacement"} {
|
||||
for _, size := range []int{16, 32, 64} {
|
||||
b.Run(shape+"/"+strconv.Itoa(size), func(b *testing.B) {
|
||||
input := inheritanceBenchmarkInput(shape, size)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
if _, diagnostics := expandInheritanceWithOrigins(input, nil, defaultProjectionLimits()); len(diagnostics) != 0 {
|
||||
b.Fatal(diagnostics)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkInheritanceInspectedLoad(b *testing.B) {
|
||||
type entry struct {
|
||||
Value string `yaml:"value"`
|
||||
}
|
||||
type configuration struct {
|
||||
Entries map[string]entry `yaml:"entries"`
|
||||
}
|
||||
for _, size := range []int{16, 32, 64} {
|
||||
b.Run(strconv.Itoa(size), func(b *testing.B) {
|
||||
var input strings.Builder
|
||||
input.WriteString("entries:\n item-0: {value: base}\n")
|
||||
for index := 1; index <= size; index++ {
|
||||
input.WriteString(" item-")
|
||||
input.WriteString(strconv.Itoa(index))
|
||||
input.WriteString(": {extends: item-0}\n")
|
||||
}
|
||||
source := Bytes("inheritance-benchmark", []byte(input.String()))
|
||||
query := Keys("entries", "item-"+strconv.Itoa(size), "value")
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
loaded, err := Load[configuration](source, testSecretBinding())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if _, found := loaded.Inspection.Explain(query); !found {
|
||||
b.Fatal("inherited value is not explainable")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func inheritanceBenchmarkInput(shape string, size int) *document.Node {
|
||||
value := func(text string) *document.Node { return &document.Node{Kind: document.String, Text: text} }
|
||||
mapping := func(entries ...document.Entry) *document.Node {
|
||||
return &document.Node{Kind: document.Mapping, Mapping: entries}
|
||||
}
|
||||
entry := func(key string, node *document.Node) document.Entry { return document.Entry{Key: key, Value: node} }
|
||||
base := mapping(entry("value", value("base")), entry("settings", mapping(entry("enabled", &document.Node{Kind: document.Boolean, Text: "false"}))))
|
||||
if shape == "nested" {
|
||||
base = mapping(entry("settings", mapping(entry("first", mapping(entry("value", value("base")))), entry("second", mapping(entry("extends", value("first")))))))
|
||||
}
|
||||
definitions := make([]document.Entry, 0, size+1)
|
||||
definitions = append(definitions, entry("item-0", base))
|
||||
for index := 1; index <= size; index++ {
|
||||
parent := "item-0"
|
||||
if shape == "chain" {
|
||||
parent = "item-" + strconv.Itoa(index-1)
|
||||
}
|
||||
payload := []document.Entry{entry("extends", value(parent))}
|
||||
if shape == "nested" {
|
||||
payload = append(payload, entry("settings", mapping(entry("first", mapping(entry("value", value("custom")))))))
|
||||
}
|
||||
if shape == "replacement" {
|
||||
payload = append(payload, entry("value", value("replacement")))
|
||||
}
|
||||
definitions = append(definitions, entry("item-"+strconv.Itoa(index), mapping(payload...)))
|
||||
}
|
||||
return mapping(entry("entries", mapping(definitions...)))
|
||||
}
|
||||
@@ -19,8 +19,7 @@ type inheritanceWalker struct {
|
||||
}
|
||||
|
||||
// expandInheritanceTree resolves each destination scope after its enclosing
|
||||
// relationships. It remains a private, test-only operation until the loading
|
||||
// lifecycle also carries its complete provenance and inspection evidence.
|
||||
// relationships. The loading lifecycle uses the origin-aware entry point.
|
||||
func expandInheritanceTree(input *document.Node, limits projectionLimits) (*inheritanceScope, []Diagnostic) {
|
||||
return expandInheritanceWithOrigins(input, nil, limits)
|
||||
}
|
||||
@@ -34,9 +33,13 @@ func expandInheritanceWithBudgets(input *document.Node, inputOrigins map[string]
|
||||
return nil, []Diagnostic{{Code: CodeInheritanceInvalid, Severity: SeverityError,
|
||||
Message: "inheritance requires a mapping document root"}}
|
||||
}
|
||||
root := input
|
||||
root := document.Clone(input)
|
||||
origins := cloneOriginIndex(inputOrigins)
|
||||
if inputOrigins == nil {
|
||||
origins = directInheritanceOrigins(input, Path{}, nil)
|
||||
}
|
||||
walker := inheritanceWalker{
|
||||
root: &root, limits: limits, origins: cloneOriginIndex(inputOrigins),
|
||||
root: &root, limits: limits, origins: origins,
|
||||
retained: newRetainedNodeCounter(historyLimit),
|
||||
}
|
||||
if problem := walker.visit(&root, Path{}, true); problem != nil {
|
||||
@@ -75,22 +78,24 @@ func (w *inheritanceWalker) visit(node **document.Node, path Path, unscoped bool
|
||||
*node = withoutInheritanceDirective(*node)
|
||||
}
|
||||
}
|
||||
resolved, diagnostics := expandSiblingDefinitionsWithOrigins(*node, path, w.limits, w.origins, w.retained)
|
||||
if len(diagnostics) != 0 {
|
||||
return &diagnostics[0]
|
||||
}
|
||||
*node = resolved.root
|
||||
deleteOriginSubtree(w.origins, path, original)
|
||||
for pointer, origin := range resolved.origins {
|
||||
w.origins[pointer] = origin
|
||||
}
|
||||
w.transfers = append(w.transfers, resolved.transfers...)
|
||||
w.controls = append(w.controls, resolved.controls...)
|
||||
w.replacements = append(w.replacements, resolved.replacements...)
|
||||
w.operations = append(w.operations, resolved.operations...)
|
||||
if !inheritanceLowerBoundWithin(*w.root, w.limits.nodes) {
|
||||
problem := inheritanceBudgetDiagnostic(path, (*node).Location)
|
||||
return &problem
|
||||
if scopeHasInheritanceDefinitions(*node) {
|
||||
resolved, diagnostics := expandSiblingDefinitionsWithOrigins(*node, path, w.limits, w.origins, w.retained)
|
||||
if len(diagnostics) != 0 {
|
||||
return &diagnostics[0]
|
||||
}
|
||||
*node = resolved.root
|
||||
deleteOriginSubtree(w.origins, path, original)
|
||||
for pointer, origin := range resolved.origins {
|
||||
w.origins[pointer] = origin
|
||||
}
|
||||
w.transfers = append(w.transfers, resolved.transfers...)
|
||||
w.controls = append(w.controls, resolved.controls...)
|
||||
w.replacements = append(w.replacements, resolved.replacements...)
|
||||
w.operations = append(w.operations, resolved.operations...)
|
||||
if !inheritanceLowerBoundWithin(*w.root, w.limits.nodes) {
|
||||
problem := inheritanceBudgetDiagnostic(path, (*node).Location)
|
||||
return &problem
|
||||
}
|
||||
}
|
||||
indices := make([]int, len((*node).Mapping))
|
||||
for index := range indices {
|
||||
@@ -116,6 +121,18 @@ func (w *inheritanceWalker) visit(node **document.Node, path Path, unscoped bool
|
||||
return nil
|
||||
}
|
||||
|
||||
func scopeHasInheritanceDefinitions(scope *document.Node) bool {
|
||||
for _, entry := range scope.Mapping {
|
||||
if entry.Value.Kind != document.Mapping {
|
||||
continue
|
||||
}
|
||||
if _, found := inheritanceDirective(entry.Value); found {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// A directive may disappear during later destination resolution, so only
|
||||
// non-directive nodes form a safe early lower bound on final output size.
|
||||
func inheritanceLowerBoundWithin(root *document.Node, limit int) bool {
|
||||
|
||||
@@ -38,7 +38,7 @@ func (m Metadata) IsExplicit(path Path) bool {
|
||||
return ok && info.Explicit
|
||||
}
|
||||
|
||||
// History returns detached definition and resolution records for path. A record's
|
||||
// History returns detached definition, inheritance, and resolution records for path. A record's
|
||||
// Path can be an ancestor when that container operation replaced or removed path.
|
||||
// Removed historical paths can have history even though Lookup returns false.
|
||||
// Unknown and invalid paths return nil.
|
||||
@@ -51,7 +51,7 @@ func (m Metadata) History(path Path) []Change {
|
||||
}
|
||||
|
||||
// HistoricalPaths returns the paths observed during loading, including effective
|
||||
// paths and paths removed by overlays or recorded resolution. It includes default
|
||||
// paths and paths removed by overlays, inheritance, or recorded resolution. It includes default
|
||||
// paths and the root, but not paths that never existed. The zero Metadata returns
|
||||
// an empty slice.
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user