diff --git a/configs/configschema/decoder_spec.go b/configs/configschema/decoder_spec.go index 2c21ca5e5..6dbdce21e 100644 --- a/configs/configschema/decoder_spec.go +++ b/configs/configschema/decoder_spec.go @@ -1,11 +1,68 @@ package configschema import ( + "runtime" + "sync" + "unsafe" + "github.com/hashicorp/hcl/v2/hcldec" ) var mapLabelNames = []string{"key"} +// specCache is a global cache of all the generated hcldec.Spec values for +// Blocks. This cache is used by the Block.DecoderSpec method to memoize calls +// and prevent unnecessary regeneration of the spec, especially when they are +// large and deeply nested. +// Caching these externally rather than within the struct is required because +// Blocks are used by value and copied when working with NestedBlocks, and the +// copying of the value prevents any safe synchronisation of the struct itself. +type specCache struct { + sync.Mutex + specs map[uintptr]hcldec.Spec +} + +var decoderSpecCache = specCache{ + specs: map[uintptr]hcldec.Spec{}, +} + +// get returns the Spec associated with eth given Block, or nil if non is +// found. +func (s *specCache) get(b *Block) hcldec.Spec { + s.Lock() + defer s.Unlock() + k := uintptr(unsafe.Pointer(b)) + return s.specs[k] +} + +// set stores the given Spec as being the result of b.DecoderSpec(). +func (s *specCache) set(b *Block, spec hcldec.Spec) { + s.Lock() + defer s.Unlock() + + // the uintptr value gets us a unique identifier for each block, without + // tying this to the block value itself. + k := uintptr(unsafe.Pointer(b)) + if _, ok := s.specs[k]; ok { + return + } + + s.specs[k] = spec + + // This must use a finalizer tied to the Block, otherwise we'll continue to + // build up Spec values as the Blocks are recycled. + runtime.SetFinalizer(b, s.delete) +} + +// delete removes the spec associated with the given Block. +func (s *specCache) delete(b *Block) { + s.Lock() + defer s.Unlock() + + k := uintptr(unsafe.Pointer(b)) + delete(s.specs, k) +} + // DecoderSpec returns a hcldec.Spec that can be used to decode a HCL Body // using the facilities in the hcldec package. // @@ -18,6 +75,10 @@ func (b *Block) DecoderSpec() hcldec.Spec { return ret } + if spec := decoderSpecCache.get(b); spec != nil { + return spec + } + for name, attrS := range b.Attributes { ret[name] = attrS.decoderSpec(name) } @@ -111,6 +172,7 @@ func (b *Block) DecoderSpec() hcldec.Spec { } } + decoderSpecCache.set(b, ret) return ret }