Skip to content

Commit

Permalink
improve tracestate performance
Browse files Browse the repository at this point in the history
* use string.Builder to directly construct the result

* reduce the redundant copying during Insert

* avoid using regex
  • Loading branch information
xiehuc committed Nov 16, 2023
1 parent d968509 commit 93ce500
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 43 deletions.
154 changes: 111 additions & 43 deletions trace/tracestate.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,14 @@ package trace // import "go.opentelemetry.io/otel/trace"
import (
"encoding/json"
"fmt"
"regexp"
"strings"
)

const (
maxListMembers = 32

listDelimiter = ","

// based on the W3C Trace Context specification, see
// https://www.w3.org/TR/trace-context-1/#tracestate-header
noTenantKeyFormat = `[a-z][_0-9a-z\-\*\/]*`
withTenantKeyFormat = `[a-z0-9][_0-9a-z\-\*\/]*@[a-z][_0-9a-z\-\*\/]*`
valueFormat = `[\x20-\x2b\x2d-\x3c\x3e-\x7e]*[\x21-\x2b\x2d-\x3c\x3e-\x7e]`
listDelimiters = ","
memberDelimiter = "="

errInvalidKey errorConst = "invalid tracestate key"
errInvalidValue errorConst = "invalid tracestate value"
Expand All @@ -39,43 +33,96 @@ const (
errDuplicate errorConst = "duplicate list-member in tracestate"
)

var (
noTenantKeyRe = regexp.MustCompile(`^` + noTenantKeyFormat + `$`)
withTenantKeyRe = regexp.MustCompile(`^` + withTenantKeyFormat + `$`)
valueRe = regexp.MustCompile(`^` + valueFormat + `$`)
memberRe = regexp.MustCompile(`^\s*((?:` + noTenantKeyFormat + `)|(?:` + withTenantKeyFormat + `))=(` + valueFormat + `)\s*$`)
)

type member struct {
Key string
Value string
}

func newMember(key, value string) (member, error) {
if len(key) > 256 {
return member{}, fmt.Errorf("%w: %s", errInvalidKey, key)
// [\x20-\x2b\x2d-\x3c\x3e-\x7e]*
func checkValueChar(v byte) bool {
return v >= '\x20' && v <= '\x7e' && v != '\x2c' && v != '\x3d'
}

// [\x21-\x2b\x2d-\x3c\x3e-\x7e]
func checkValueLast(v byte) bool {
return v >= '\x21' && v <= '\x7e' && v != '\x2c' && v != '\x3d'
}

func checkValue(val string) bool {
n := len(val)
if n == 0 || n > 256 {
return false
}
if !noTenantKeyRe.MatchString(key) {
if !withTenantKeyRe.MatchString(key) {
return member{}, fmt.Errorf("%w: %s", errInvalidKey, key)
// valueFormat = `[\x20-\x2b\x2d-\x3c\x3e-\x7e]{0,255}[\x21-\x2b\x2d-\x3c\x3e-\x7e]`
for i := 0; i < n-1; i++ {
if !checkValueChar(val[i]) {
return false
}
atIndex := strings.LastIndex(key, "@")
if atIndex > 241 || len(key)-1-atIndex > 14 {
return member{}, fmt.Errorf("%w: %s", errInvalidKey, key)
}
return checkValueLast(val[n-1])
}

// [_0-9a-z\-\*\/]*
func checkKeyRemain(key string) bool {
for _, v := range key {
if (v >= '0' && v <= '9') || (v >= 'a' && v <= 'z') {
continue
}
switch v {
case '_', '-', '*', '/':
continue
}
return false
}
return true
}

func checkKeyPart(key string, n int, tenant bool) bool {
if len(key) == 0 {
return false
}
if len(value) > 256 || !valueRe.MatchString(value) {
first := key[0] // key first char
ret := len(key[1:]) <= n
if tenant {
// [a-z0-9]
ret = ret && ((first >= 'a' && first <= 'z') || (first >= '0' && first <= '9'))
} else {
// [a-z]
ret = ret && first >= 'a' && first <= 'z'
}
return ret && checkKeyRemain(key[1:])
}

func checkKey(key string) bool {
// noTenantKeyFormat = `[a-z][_0-9a-z\-\*\/]{0,255}`
// withTenantKeyFormat = `[a-z0-9][_0-9a-z\-\*\/]{0,240}@[a-z][_0-9a-z\-\*\/]{0,13}`
tenant, system, ok := strings.Cut(key, "@")
if !ok {
return checkKeyPart(key, 255, false)
}
return checkKeyPart(tenant, 240, true) && checkKeyPart(system, 13, false)
}

// based on the W3C Trace Context specification, see
// https://www.w3.org/TR/trace-context-1/#tracestate-header
func newMember(key, value string) (member, error) {
if !checkKey(key) {
return member{}, fmt.Errorf("%w: %s", errInvalidKey, key)
}
if !checkValue(value) {
return member{}, fmt.Errorf("%w: %s", errInvalidValue, value)
}
return member{Key: key, Value: value}, nil
}

func parseMember(m string) (member, error) {
matches := memberRe.FindStringSubmatch(m)
if len(matches) != 3 {
key, val, ok := strings.Cut(m, memberDelimiter)
if !ok {
return member{}, fmt.Errorf("%w: %s", errInvalidMember, m)
}
result, e := newMember(matches[1], matches[2])
key = strings.TrimLeft(key, " \t")
val = strings.TrimRight(val, " \t")
result, e := newMember(key, val)
if e != nil {
return member{}, fmt.Errorf("%w: %s", errInvalidMember, m)
}
Expand All @@ -85,7 +132,7 @@ func parseMember(m string) (member, error) {
// String encodes member into a string compliant with the W3C Trace Context
// specification.
func (m member) String() string {
return fmt.Sprintf("%s=%s", m.Key, m.Value)
return m.Key + "=" + m.Value

Check warning on line 135 in trace/tracestate.go

View check run for this annotation

Codecov / codecov/patch

trace/tracestate.go#L135

Added line #L135 was not covered by tests
}

// TraceState provides additional vendor-specific trace identification
Expand All @@ -109,8 +156,8 @@ var _ json.Marshaler = TraceState{}
// ParseTraceState attempts to decode a TraceState from the passed
// string. It returns an error if the input is invalid according to the W3C
// Trace Context specification.
func ParseTraceState(tracestate string) (TraceState, error) {
if tracestate == "" {
func ParseTraceState(ts string) (TraceState, error) {
if ts == "" {
return TraceState{}, nil
}

Expand All @@ -120,7 +167,9 @@ func ParseTraceState(tracestate string) (TraceState, error) {

var members []member
found := make(map[string]struct{})
for _, memberStr := range strings.Split(tracestate, listDelimiter) {
for ts != "" {
var memberStr string
memberStr, ts, _ = strings.Cut(ts, listDelimiters)
if len(memberStr) == 0 {
continue
}
Expand Down Expand Up @@ -153,11 +202,20 @@ func (ts TraceState) MarshalJSON() ([]byte, error) {
// Trace Context specification. The returned string will be invalid if the
// TraceState contains any invalid members.
func (ts TraceState) String() string {
members := make([]string, len(ts.list))
for i, m := range ts.list {
members[i] = m.String()
if len(ts.list) == 0 {
return ""
}
return strings.Join(members, listDelimiter)
var sb strings.Builder
sb.WriteString(ts.list[0].Key)
sb.WriteByte('=')
sb.WriteString(ts.list[0].Value)
for i := 1; i < len(ts.list); i++ {
sb.WriteByte(listDelimiters[0])
sb.WriteString(ts.list[i].Key)
sb.WriteByte('=')
sb.WriteString(ts.list[i].Value)
}
return sb.String()
}

// Get returns the value paired with key from the corresponding TraceState
Expand Down Expand Up @@ -189,15 +247,25 @@ func (ts TraceState) Insert(key, value string) (TraceState, error) {
if err != nil {
return ts, err
}

cTS := ts.Delete(key)
if cTS.Len()+1 <= maxListMembers {
cTS.list = append(cTS.list, member{})
n := len(ts.list)
found := n
for i := range ts.list {
if ts.list[i].Key == key {
found = i
}
}
cTS := TraceState{}
if found == n && n < maxListMembers {
cTS.list = make([]member, n+1)
} else {
cTS.list = make([]member, n)
}
// When the number of members exceeds capacity, drop the "right-most".
copy(cTS.list[1:], cTS.list)
cTS.list[0] = m

// When the number of members exceeds capacity, drop the "right-most".
copy(cTS.list[1:], ts.list[0:found])
if found < n {
copy(cTS.list[1+found:], ts.list[found+1:])
}
return cTS, nil
}

Expand Down
48 changes: 48 additions & 0 deletions trace/tracestate_benchkmark_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package trace

import (
"testing"
)

func BenchmarkTraceStateParse(b *testing.B) {
for _, test := range testcases {
b.Run(test.name, func(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
ParseTraceState(test.in)
}
})
}
}

// Insert 因为语义发生变化了,我们没有做正则校验,所以一定是比 otel 的实现快的,并且因为不平等,没必要再做 benchmark 了

func BenchmarkTraceStateString(b *testing.B) {
for _, test := range testcases {
if len(test.tracestate.list) == 0 {
continue
}
b.Run(test.name, func(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = test.tracestate.String()
}
})
}
}

0 comments on commit 93ce500

Please sign in to comment.