diff --git a/database.go b/database.go index 372cc37821..ca087a6639 100644 --- a/database.go +++ b/database.go @@ -1308,18 +1308,37 @@ func (db *database) continuousQueryByName(name string) *ContinuousQuery { // used to convert the tag set to bytes for use as a lookup key func marshalTags(tags map[string]string) []byte { - s := make([]string, 0, len(tags)) - // pull out keys to sort - for k := range tags { - s = append(s, k) + // Empty maps marshal to empty bytes. + if len(tags) == 0 { + return nil } - sort.Strings(s) - // now append on the key values in key sorted order - for _, k := range s { - s = append(s, tags[k]) + // Extract keys and determine final size. + sz := (len(tags) * 2) - 1 // separators + keys := make([]string, 0, len(tags)) + for k, v := range tags { + keys = append(keys, k) + sz += len(k) + len(v) } - return []byte(strings.Join(s, "|")) + sort.Strings(keys) + + // Generate marshaled bytes. + b := make([]byte, sz) + buf := b + for _, k := range keys { + copy(buf, k) + buf[len(k)] = '|' + buf = buf[len(k)+1:] + } + for i, k := range keys { + v := tags[k] + copy(buf, v) + if i < len(keys)-1 { + buf[len(v)] = '|' + buf = buf[len(v)+1:] + } + } + return b } // timeBetweenInclusive returns true if t is between min and max, inclusive. diff --git a/internal_test.go b/internal_test.go index 6406f594bd..cb74408755 100644 --- a/internal_test.go +++ b/internal_test.go @@ -3,6 +3,8 @@ package influxdb // This file is run within the "influxdb" package and allows for internal unit tests. import ( + "bytes" + "fmt" "reflect" "testing" "time" @@ -310,6 +312,53 @@ func TestShardGroup_Contains(t *testing.T) { } } +// Ensure tags can be marshaled into a byte slice. +func TestMarshalTags(t *testing.T) { + for i, tt := range []struct { + tags map[string]string + result []byte + }{ + { + tags: nil, + result: nil, + }, + { + tags: map[string]string{"foo": "bar"}, + result: []byte(`foo|bar`), + }, + { + tags: map[string]string{"foo": "bar", "baz": "battttt"}, + result: []byte(`baz|foo|battttt|bar`), + }, + } { + result := marshalTags(tt.tags) + if !bytes.Equal(result, tt.result) { + t.Fatalf("%d. unexpected result: exp=%s, got=%s", i, tt.result, result) + } + } +} + +func BenchmarkMarshalTags_KeyN1(b *testing.B) { benchmarkMarshalTags(b, 1) } +func BenchmarkMarshalTags_KeyN3(b *testing.B) { benchmarkMarshalTags(b, 3) } +func BenchmarkMarshalTags_KeyN5(b *testing.B) { benchmarkMarshalTags(b, 5) } +func BenchmarkMarshalTags_KeyN10(b *testing.B) { benchmarkMarshalTags(b, 10) } + +func benchmarkMarshalTags(b *testing.B, keyN int) { + const keySize, valueSize = 8, 15 + + // Generate tag map. + tags := make(map[string]string) + for i := 0; i < keyN; i++ { + tags[fmt.Sprintf("%0*d", keySize, i)] = fmt.Sprintf("%0*d", valueSize, i) + } + + // Unmarshal map into byte slice. + b.ReportAllocs() + for i := 0; i < b.N; i++ { + marshalTags(tags) + } +} + // MustParseExpr parses an expression string and returns its AST representation. func MustParseExpr(s string) influxql.Expr { expr, err := influxql.ParseExpr(s)