Skip to content

Commit 08abf5d

Browse files
authored
Merge pull request #26577 from hashicorp/jbardin/decoder-spec
Memoize Block.DecoderSpec
2 parents dc48450 + e27ecba commit 08abf5d

File tree

2 files changed

+165
-0
lines changed

2 files changed

+165
-0
lines changed

configs/configschema/decoder_spec.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,74 @@
11
package configschema
22

33
import (
4+
"runtime"
5+
"sync"
6+
"unsafe"
7+
48
"github.com/hashicorp/hcl/v2/hcldec"
59
)
610

711
var mapLabelNames = []string{"key"}
812

13+
// specCache is a global cache of all the generated hcldec.Spec values for
14+
// Blocks. This cache is used by the Block.DecoderSpec method to memoize calls
15+
// and prevent unnecessary regeneration of the spec, especially when they are
16+
// large and deeply nested.
17+
// Caching these externally rather than within the struct is required because
18+
// Blocks are used by value and copied when working with NestedBlocks, and the
19+
// copying of the value prevents any safe synchronisation of the struct itself.
20+
//
21+
// While we are using the *Block pointer as the cache key, and the Block
22+
// contents are mutable, once a Block is created it is treated as immutable for
23+
// the duration of its life. Because a Block is a representation of a logical
24+
// schema, which cannot change while it's being used, any modifications to the
25+
// schema during execution would be an error.
26+
type specCache struct {
27+
sync.Mutex
28+
specs map[uintptr]hcldec.Spec
29+
}
30+
31+
var decoderSpecCache = specCache{
32+
specs: map[uintptr]hcldec.Spec{},
33+
}
34+
35+
// get returns the Spec associated with eth given Block, or nil if non is
36+
// found.
37+
func (s *specCache) get(b *Block) hcldec.Spec {
38+
s.Lock()
39+
defer s.Unlock()
40+
k := uintptr(unsafe.Pointer(b))
41+
return s.specs[k]
42+
}
43+
44+
// set stores the given Spec as being the result of b.DecoderSpec().
45+
func (s *specCache) set(b *Block, spec hcldec.Spec) {
46+
s.Lock()
47+
defer s.Unlock()
48+
49+
// the uintptr value gets us a unique identifier for each block, without
50+
// tying this to the block value itself.
51+
k := uintptr(unsafe.Pointer(b))
52+
if _, ok := s.specs[k]; ok {
53+
return
54+
}
55+
56+
s.specs[k] = spec
57+
58+
// This must use a finalizer tied to the Block, otherwise we'll continue to
59+
// build up Spec values as the Blocks are recycled.
60+
runtime.SetFinalizer(b, s.delete)
61+
}
62+
63+
// delete removes the spec associated with the given Block.
64+
func (s *specCache) delete(b *Block) {
65+
s.Lock()
66+
defer s.Unlock()
67+
68+
k := uintptr(unsafe.Pointer(b))
69+
delete(s.specs, k)
70+
}
71+
972
// DecoderSpec returns a hcldec.Spec that can be used to decode a HCL Body
1073
// using the facilities in the hcldec package.
1174
//
@@ -18,6 +81,10 @@ func (b *Block) DecoderSpec() hcldec.Spec {
1881
return ret
1982
}
2083

84+
if spec := decoderSpecCache.get(b); spec != nil {
85+
return spec
86+
}
87+
2188
for name, attrS := range b.Attributes {
2289
ret[name] = attrS.decoderSpec(name)
2390
}
@@ -111,6 +178,7 @@ func (b *Block) DecoderSpec() hcldec.Spec {
111178
}
112179
}
113180

181+
decoderSpecCache.set(b, ret)
114182
return ret
115183
}
116184

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package blocktoattr
2+
3+
import (
4+
"testing"
5+
6+
"github.com/hashicorp/hcl/v2"
7+
"github.com/hashicorp/hcl/v2/hcldec"
8+
"github.com/hashicorp/hcl/v2/hclsyntax"
9+
"github.com/hashicorp/terraform/configs/configschema"
10+
"github.com/zclconf/go-cty/cty"
11+
)
12+
13+
func ambiguousNestedBlock(nesting int) *configschema.NestedBlock {
14+
ret := &configschema.NestedBlock{
15+
Nesting: configschema.NestingList,
16+
Block: configschema.Block{
17+
Attributes: map[string]*configschema.Attribute{
18+
"a": {Type: cty.String, Required: true},
19+
"b": {Type: cty.String, Optional: true},
20+
},
21+
},
22+
}
23+
if nesting > 0 {
24+
ret.BlockTypes = map[string]*configschema.NestedBlock{
25+
"nested0": ambiguousNestedBlock(nesting - 1),
26+
"nested1": ambiguousNestedBlock(nesting - 1),
27+
"nested2": ambiguousNestedBlock(nesting - 1),
28+
"nested3": ambiguousNestedBlock(nesting - 1),
29+
"nested4": ambiguousNestedBlock(nesting - 1),
30+
"nested5": ambiguousNestedBlock(nesting - 1),
31+
"nested6": ambiguousNestedBlock(nesting - 1),
32+
"nested7": ambiguousNestedBlock(nesting - 1),
33+
"nested8": ambiguousNestedBlock(nesting - 1),
34+
"nested9": ambiguousNestedBlock(nesting - 1),
35+
}
36+
}
37+
return ret
38+
}
39+
40+
func schemaWithAmbiguousNestedBlock(nesting int) *configschema.Block {
41+
return &configschema.Block{
42+
BlockTypes: map[string]*configschema.NestedBlock{
43+
"maybe_block": ambiguousNestedBlock(nesting),
44+
},
45+
}
46+
}
47+
48+
const configForFixupBlockAttrsBenchmark = `
49+
maybe_block {
50+
a = "hello"
51+
b = "world"
52+
nested0 {
53+
a = "the"
54+
nested1 {
55+
a = "deeper"
56+
nested2 {
57+
a = "we"
58+
nested3 {
59+
a = "go"
60+
b = "inside"
61+
}
62+
}
63+
}
64+
}
65+
}
66+
`
67+
68+
func configBodyForFixupBlockAttrsBenchmark() hcl.Body {
69+
f, diags := hclsyntax.ParseConfig([]byte(configForFixupBlockAttrsBenchmark), "", hcl.Pos{Line: 1, Column: 1})
70+
if diags.HasErrors() {
71+
panic("test configuration is invalid")
72+
}
73+
return f.Body
74+
}
75+
76+
func BenchmarkFixUpBlockAttrs(b *testing.B) {
77+
for i := 0; i < b.N; i++ {
78+
b.StopTimer()
79+
body := configBodyForFixupBlockAttrsBenchmark()
80+
schema := schemaWithAmbiguousNestedBlock(5)
81+
b.StartTimer()
82+
83+
spec := schema.DecoderSpec()
84+
fixedBody := FixUpBlockAttrs(body, schema)
85+
val, diags := hcldec.Decode(fixedBody, spec, nil)
86+
if diags.HasErrors() {
87+
b.Fatal("diagnostics during decoding", diags)
88+
}
89+
if !val.Type().IsObjectType() {
90+
b.Fatal("result is not an object")
91+
}
92+
blockVal := val.GetAttr("maybe_block")
93+
if !blockVal.Type().IsListType() || blockVal.LengthInt() != 1 {
94+
b.Fatal("result has wrong value for 'maybe_block'")
95+
}
96+
}
97+
}

0 commit comments

Comments
 (0)