Change Set internals and make (extreme) performance improvements
Changing the Set internals makes a lot of sense as it saves doing conversions in multiple places and gives a central place to alter the key when a item is computed. This will have no side effects other then that the ordering is now based on strings instead on integers, so the order will be different. This will however have no effect on existing configs as these will use the individual codes/keys and not the ordering to determine if there is a diff or not. Lastly (but I think also most importantly) there is a fix in this PR that makes diffing sets extremely more performand. Before a full diff required reading the complete Set for every single parameter/attribute you wanted to diff, while now it only gets that specific parameter. We have a use case where we have a Set that has 18 parameters and the set consist of about 600 items (don't ask 😉). So when doing a diff it would take 100% CPU of all cores and stay that way for almost an hour before being able to complete the diff. Debugging this we learned that for retrieving every single parameter it made over 52.000 calls to `func (c *ResourceConfig) get(..)`. In this function a slice is created and used only for the duration of the call, so the time needed to create all needed slices and on the other hand the time the garbage collector needed to clean them up again caused the system to cripple itself. Next to that there are also some expensive reflect calls in this function which also claimed a fair amount of CPU time. After this fix the number of calls needed to get a single parameter dropped from 52.000+ to only 2! 😃
This commit is contained in:
parent
774ed1ded8
commit
ef4726bd50
|
@ -18,10 +18,12 @@ type ConfigFieldReader struct {
|
||||||
Config *terraform.ResourceConfig
|
Config *terraform.ResourceConfig
|
||||||
Schema map[string]*Schema
|
Schema map[string]*Schema
|
||||||
|
|
||||||
lock sync.Mutex
|
indexMaps map[string]map[string]int
|
||||||
|
once sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ConfigFieldReader) ReadField(address []string) (FieldReadResult, error) {
|
func (r *ConfigFieldReader) ReadField(address []string) (FieldReadResult, error) {
|
||||||
|
r.once.Do(func() { r.indexMaps = make(map[string]map[string]int) })
|
||||||
return r.readField(address, false)
|
return r.readField(address, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,20 +57,18 @@ func (r *ConfigFieldReader) readField(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the code
|
indexMap, ok := r.indexMaps[strings.Join(address[:i+1], ".")]
|
||||||
code, err := strconv.ParseInt(address[i+1], 0, 0)
|
if !ok {
|
||||||
if err != nil {
|
|
||||||
return FieldReadResult{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the set so we can get the index map that tells us the
|
// Get the set so we can get the index map that tells us the
|
||||||
// mapping of the hash code to the list index
|
// mapping of the hash code to the list index
|
||||||
_, indexMap, err := r.readSet(address[:i+1], v)
|
_, err := r.readSet(address[:i+1], v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return FieldReadResult{}, err
|
return FieldReadResult{}, err
|
||||||
}
|
}
|
||||||
|
indexMap = r.indexMaps[strings.Join(address[:i+1], ".")]
|
||||||
|
}
|
||||||
|
|
||||||
index, ok := indexMap[int(code)]
|
index, ok := indexMap[address[i+1]]
|
||||||
if !ok {
|
if !ok {
|
||||||
return FieldReadResult{}, nil
|
return FieldReadResult{}, nil
|
||||||
}
|
}
|
||||||
|
@ -87,8 +87,7 @@ func (r *ConfigFieldReader) readField(
|
||||||
case TypeMap:
|
case TypeMap:
|
||||||
return r.readMap(k)
|
return r.readMap(k)
|
||||||
case TypeSet:
|
case TypeSet:
|
||||||
result, _, err := r.readSet(address, schema)
|
return r.readSet(address, schema)
|
||||||
return result, err
|
|
||||||
case typeObject:
|
case typeObject:
|
||||||
return readObjectField(
|
return readObjectField(
|
||||||
&nestedConfigFieldReader{r},
|
&nestedConfigFieldReader{r},
|
||||||
|
@ -112,7 +111,7 @@ func (r *ConfigFieldReader) readMap(k string) (FieldReadResult, error) {
|
||||||
switch m := mraw.(type) {
|
switch m := mraw.(type) {
|
||||||
case []interface{}:
|
case []interface{}:
|
||||||
for i, innerRaw := range m {
|
for i, innerRaw := range m {
|
||||||
for ik, _ := range innerRaw.(map[string]interface{}) {
|
for ik := range innerRaw.(map[string]interface{}) {
|
||||||
key := fmt.Sprintf("%s.%d.%s", k, i, ik)
|
key := fmt.Sprintf("%s.%d.%s", k, i, ik)
|
||||||
if r.Config.IsComputed(key) {
|
if r.Config.IsComputed(key) {
|
||||||
computed = true
|
computed = true
|
||||||
|
@ -125,7 +124,7 @@ func (r *ConfigFieldReader) readMap(k string) (FieldReadResult, error) {
|
||||||
}
|
}
|
||||||
case []map[string]interface{}:
|
case []map[string]interface{}:
|
||||||
for i, innerRaw := range m {
|
for i, innerRaw := range m {
|
||||||
for ik, _ := range innerRaw {
|
for ik := range innerRaw {
|
||||||
key := fmt.Sprintf("%s.%d.%s", k, i, ik)
|
key := fmt.Sprintf("%s.%d.%s", k, i, ik)
|
||||||
if r.Config.IsComputed(key) {
|
if r.Config.IsComputed(key) {
|
||||||
computed = true
|
computed = true
|
||||||
|
@ -137,7 +136,7 @@ func (r *ConfigFieldReader) readMap(k string) (FieldReadResult, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
for ik, _ := range m {
|
for ik := range m {
|
||||||
key := fmt.Sprintf("%s.%s", k, ik)
|
key := fmt.Sprintf("%s.%s", k, ik)
|
||||||
if r.Config.IsComputed(key) {
|
if r.Config.IsComputed(key) {
|
||||||
computed = true
|
computed = true
|
||||||
|
@ -198,17 +197,17 @@ func (r *ConfigFieldReader) readPrimitive(
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ConfigFieldReader) readSet(
|
func (r *ConfigFieldReader) readSet(
|
||||||
address []string, schema *Schema) (FieldReadResult, map[int]int, error) {
|
address []string, schema *Schema) (FieldReadResult, error) {
|
||||||
indexMap := make(map[int]int)
|
indexMap := make(map[string]int)
|
||||||
// Create the set that will be our result
|
// Create the set that will be our result
|
||||||
set := schema.ZeroValue().(*Set)
|
set := schema.ZeroValue().(*Set)
|
||||||
|
|
||||||
raw, err := readListField(&nestedConfigFieldReader{r}, address, schema)
|
raw, err := readListField(&nestedConfigFieldReader{r}, address, schema)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return FieldReadResult{}, indexMap, err
|
return FieldReadResult{}, err
|
||||||
}
|
}
|
||||||
if !raw.Exists {
|
if !raw.Exists {
|
||||||
return FieldReadResult{Value: set}, indexMap, nil
|
return FieldReadResult{Value: set}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the list is computed, the set is necessarilly computed
|
// If the list is computed, the set is necessarilly computed
|
||||||
|
@ -217,7 +216,7 @@ func (r *ConfigFieldReader) readSet(
|
||||||
Value: set,
|
Value: set,
|
||||||
Exists: true,
|
Exists: true,
|
||||||
Computed: raw.Computed,
|
Computed: raw.Computed,
|
||||||
}, indexMap, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build up the set from the list elements
|
// Build up the set from the list elements
|
||||||
|
@ -226,19 +225,16 @@ func (r *ConfigFieldReader) readSet(
|
||||||
computed := r.hasComputedSubKeys(
|
computed := r.hasComputedSubKeys(
|
||||||
fmt.Sprintf("%s.%d", strings.Join(address, "."), i), schema)
|
fmt.Sprintf("%s.%d", strings.Join(address, "."), i), schema)
|
||||||
|
|
||||||
code := set.add(v)
|
code := set.add(v, computed)
|
||||||
indexMap[code] = i
|
indexMap[code] = i
|
||||||
if computed {
|
|
||||||
set.m[-code] = set.m[code]
|
|
||||||
delete(set.m, code)
|
|
||||||
code = -code
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.indexMaps[strings.Join(address, ".")] = indexMap
|
||||||
|
|
||||||
return FieldReadResult{
|
return FieldReadResult{
|
||||||
Value: set,
|
Value: set,
|
||||||
Exists: true,
|
Exists: true,
|
||||||
}, indexMap, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasComputedSubKeys walks through a schema and returns whether or not the
|
// hasComputedSubKeys walks through a schema and returns whether or not the
|
||||||
|
|
|
@ -228,8 +228,8 @@ func TestConfigFieldReader_ComputedSet(t *testing.T) {
|
||||||
"set, normal": {
|
"set, normal": {
|
||||||
[]string{"strSet"},
|
[]string{"strSet"},
|
||||||
FieldReadResult{
|
FieldReadResult{
|
||||||
Value: map[int]interface{}{
|
Value: map[string]interface{}{
|
||||||
2356372769: "foo",
|
"2356372769": "foo",
|
||||||
},
|
},
|
||||||
Exists: true,
|
Exists: true,
|
||||||
Computed: false,
|
Computed: false,
|
||||||
|
|
|
@ -298,8 +298,7 @@ func (w *MapFieldWriter) setSet(
|
||||||
}
|
}
|
||||||
|
|
||||||
for code, elem := range value.(*Set).m {
|
for code, elem := range value.(*Set).m {
|
||||||
codeStr := strconv.FormatInt(int64(code), 10)
|
if err := w.set(append(addrCopy, code), elem); err != nil {
|
||||||
if err := w.set(append(addrCopy, codeStr), elem); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -228,7 +228,7 @@ func (d *ResourceData) State() *terraform.InstanceState {
|
||||||
// attribute set as a map[string]interface{}, write it to a MapFieldWriter,
|
// attribute set as a map[string]interface{}, write it to a MapFieldWriter,
|
||||||
// and then use that map.
|
// and then use that map.
|
||||||
rawMap := make(map[string]interface{})
|
rawMap := make(map[string]interface{})
|
||||||
for k, _ := range d.schema {
|
for k := range d.schema {
|
||||||
source := getSourceSet
|
source := getSourceSet
|
||||||
if d.partial {
|
if d.partial {
|
||||||
source = getSourceState
|
source = getSourceState
|
||||||
|
@ -343,13 +343,13 @@ func (d *ResourceData) diffChange(
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *ResourceData) getChange(
|
func (d *ResourceData) getChange(
|
||||||
key string,
|
k string,
|
||||||
oldLevel getSource,
|
oldLevel getSource,
|
||||||
newLevel getSource) (getResult, getResult) {
|
newLevel getSource) (getResult, getResult) {
|
||||||
var parts, parts2 []string
|
var parts, parts2 []string
|
||||||
if key != "" {
|
if k != "" {
|
||||||
parts = strings.Split(key, ".")
|
parts = strings.Split(k, ".")
|
||||||
parts2 = strings.Split(key, ".")
|
parts2 = strings.Split(k, ".")
|
||||||
}
|
}
|
||||||
|
|
||||||
o := d.get(parts, oldLevel)
|
o := d.get(parts, oldLevel)
|
||||||
|
@ -374,13 +374,6 @@ func (d *ResourceData) get(addr []string, source getSource) getResult {
|
||||||
level = "state"
|
level = "state"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the address of the key we're looking for and ask the FieldReader
|
|
||||||
for i, v := range addr {
|
|
||||||
if v[0] == '~' {
|
|
||||||
addr[i] = v[1:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var result FieldReadResult
|
var result FieldReadResult
|
||||||
var err error
|
var err error
|
||||||
if exact {
|
if exact {
|
||||||
|
|
|
@ -1509,9 +1509,9 @@ func TestResourceDataSet(t *testing.T) {
|
||||||
|
|
||||||
Key: "ports",
|
Key: "ports",
|
||||||
Value: &Set{
|
Value: &Set{
|
||||||
m: map[int]interface{}{
|
m: map[string]interface{}{
|
||||||
1: 1,
|
"1": 1,
|
||||||
2: 2,
|
"2": 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1546,7 +1546,7 @@ func TestResourceDataSet(t *testing.T) {
|
||||||
Err: true,
|
Err: true,
|
||||||
|
|
||||||
GetKey: "ports",
|
GetKey: "ports",
|
||||||
GetValue: []interface{}{80, 100},
|
GetValue: []interface{}{100, 80},
|
||||||
},
|
},
|
||||||
|
|
||||||
// #11: Set with nested set
|
// #11: Set with nested set
|
||||||
|
|
|
@ -866,23 +866,16 @@ func (m schemaMap) diffSet(
|
||||||
|
|
||||||
// Build the list of codes that will make up our set. This is the
|
// Build the list of codes that will make up our set. This is the
|
||||||
// removed codes as well as all the codes in the new codes.
|
// removed codes as well as all the codes in the new codes.
|
||||||
codes := make([][]int, 2)
|
codes := make([][]string, 2)
|
||||||
codes[0] = os.Difference(ns).listCode()
|
codes[0] = os.Difference(ns).listCode()
|
||||||
codes[1] = ns.listCode()
|
codes[1] = ns.listCode()
|
||||||
for _, list := range codes {
|
for _, list := range codes {
|
||||||
for _, code := range list {
|
for _, code := range list {
|
||||||
// If the code is negative (first character is -) then
|
|
||||||
// replace it with "~" for our computed set stuff.
|
|
||||||
codeStr := strconv.Itoa(code)
|
|
||||||
if codeStr[0] == '-' {
|
|
||||||
codeStr = string('~') + codeStr[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
switch t := schema.Elem.(type) {
|
switch t := schema.Elem.(type) {
|
||||||
case *Resource:
|
case *Resource:
|
||||||
// This is a complex resource
|
// This is a complex resource
|
||||||
for k2, schema := range t.Schema {
|
for k2, schema := range t.Schema {
|
||||||
subK := fmt.Sprintf("%s.%s.%s", k, codeStr, k2)
|
subK := fmt.Sprintf("%s.%s.%s", k, code, k2)
|
||||||
err := m.diff(subK, schema, diff, d, true)
|
err := m.diff(subK, schema, diff, d, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -896,7 +889,7 @@ func (m schemaMap) diffSet(
|
||||||
|
|
||||||
// This is just a primitive element, so go through each and
|
// This is just a primitive element, so go through each and
|
||||||
// just diff each.
|
// just diff each.
|
||||||
subK := fmt.Sprintf("%s.%s", k, codeStr)
|
subK := fmt.Sprintf("%s.%s", k, code)
|
||||||
err := m.diff(subK, &t2, diff, d, true)
|
err := m.diff(subK, &t2, diff, d, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/helper/hashcode"
|
"github.com/hashicorp/terraform/helper/hashcode"
|
||||||
|
@ -43,7 +44,7 @@ func HashSchema(schema *Schema) SchemaSetFunc {
|
||||||
type Set struct {
|
type Set struct {
|
||||||
F SchemaSetFunc
|
F SchemaSetFunc
|
||||||
|
|
||||||
m map[int]interface{}
|
m map[string]interface{}
|
||||||
once sync.Once
|
once sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,7 +66,7 @@ func CopySet(otherSet *Set) *Set {
|
||||||
|
|
||||||
// Add adds an item to the set if it isn't already in the set.
|
// Add adds an item to the set if it isn't already in the set.
|
||||||
func (s *Set) Add(item interface{}) {
|
func (s *Set) Add(item interface{}) {
|
||||||
s.add(item)
|
s.add(item, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove removes an item if it's already in the set. Idempotent.
|
// Remove removes an item if it's already in the set. Idempotent.
|
||||||
|
@ -157,13 +158,17 @@ func (s *Set) GoString() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Set) init() {
|
func (s *Set) init() {
|
||||||
s.m = make(map[int]interface{})
|
s.m = make(map[string]interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Set) add(item interface{}) int {
|
func (s *Set) add(item interface{}, computed bool) string {
|
||||||
s.once.Do(s.init)
|
s.once.Do(s.init)
|
||||||
|
|
||||||
code := s.hash(item)
|
code := s.hash(item)
|
||||||
|
if computed {
|
||||||
|
code = "~" + code
|
||||||
|
}
|
||||||
|
|
||||||
if _, ok := s.m[code]; !ok {
|
if _, ok := s.m[code]; !ok {
|
||||||
s.m[code] = item
|
s.m[code] = item
|
||||||
}
|
}
|
||||||
|
@ -171,34 +176,34 @@ func (s *Set) add(item interface{}) int {
|
||||||
return code
|
return code
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Set) hash(item interface{}) int {
|
func (s *Set) hash(item interface{}) string {
|
||||||
code := s.F(item)
|
code := s.F(item)
|
||||||
// Always return a nonnegative hashcode.
|
// Always return a nonnegative hashcode.
|
||||||
if code < 0 {
|
if code < 0 {
|
||||||
return -code
|
code = -code
|
||||||
}
|
}
|
||||||
return code
|
return strconv.Itoa(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Set) remove(item interface{}) int {
|
func (s *Set) remove(item interface{}) string {
|
||||||
s.once.Do(s.init)
|
s.once.Do(s.init)
|
||||||
|
|
||||||
code := s.F(item)
|
code := s.hash(item)
|
||||||
delete(s.m, code)
|
delete(s.m, code)
|
||||||
|
|
||||||
return code
|
return code
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Set) index(item interface{}) int {
|
func (s *Set) index(item interface{}) int {
|
||||||
return sort.SearchInts(s.listCode(), s.hash(item))
|
return sort.SearchStrings(s.listCode(), s.hash(item))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Set) listCode() []int {
|
func (s *Set) listCode() []string {
|
||||||
// Sort the hash codes so the order of the list is deterministic
|
// Sort the hash codes so the order of the list is deterministic
|
||||||
keys := make([]int, 0, len(s.m))
|
keys := make([]string, 0, len(s.m))
|
||||||
for k, _ := range s.m {
|
for k := range s.m {
|
||||||
keys = append(keys, k)
|
keys = append(keys, k)
|
||||||
}
|
}
|
||||||
sort.Sort(sort.IntSlice(keys))
|
sort.Sort(sort.StringSlice(keys))
|
||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ func TestSetAdd(t *testing.T) {
|
||||||
s.Add(5)
|
s.Add(5)
|
||||||
s.Add(25)
|
s.Add(25)
|
||||||
|
|
||||||
expected := []interface{}{1, 5, 25}
|
expected := []interface{}{1, 25, 5}
|
||||||
actual := s.List()
|
actual := s.List()
|
||||||
if !reflect.DeepEqual(actual, expected) {
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
t.Fatalf("bad: %#v", actual)
|
t.Fatalf("bad: %#v", actual)
|
||||||
|
@ -101,7 +101,7 @@ func TestSetUnion(t *testing.T) {
|
||||||
union := s1.Union(s2)
|
union := s1.Union(s2)
|
||||||
union.Add(2)
|
union.Add(2)
|
||||||
|
|
||||||
expected := []interface{}{1, 2, 5, 25}
|
expected := []interface{}{1, 2, 25, 5}
|
||||||
actual := union.List()
|
actual := union.List()
|
||||||
if !reflect.DeepEqual(actual, expected) {
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
t.Fatalf("bad: %#v", actual)
|
t.Fatalf("bad: %#v", actual)
|
||||||
|
|
Loading…
Reference in New Issue