You've already forked opentelemetry-go
							
							
				mirror of
				https://github.com/open-telemetry/opentelemetry-go.git
				synced 2025-10-31 00:07:40 +02:00 
			
		
		
		
	improve tracestate performance (#4722)
* improve tracestate performance * use string.Builder to directly construct the result * reduce the redundant copying during Insert * avoid using regex * fix lint * revert changelog * update comment * refine code * fix lint * fix unittest * Update trace/tracestate.go Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> --------- Co-authored-by: Chester Cheung <cheung.zhy.csu@gmail.com> Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>
This commit is contained in:
		| @@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm | ||||
|  | ||||
| ### Changed | ||||
|  | ||||
| - Improve `go.opentelemetry.io/otel/trace.TraceState`'s performance. (#4722) | ||||
| - Improve `go.opentelemetry.io/otel/propagation.TraceContext`'s performance. (#4721) | ||||
|  | ||||
| ### Added | ||||
|   | ||||
| @@ -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" | ||||
| @@ -39,43 +33,128 @@ 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 | ||||
| } | ||||
|  | ||||
| // according to (chr = %x20 / (nblk-char = %x21-2B / %x2D-3C / %x3E-7E) ) | ||||
| // means (chr = %x20-2B / %x2D-3C / %x3E-7E) . | ||||
| func checkValueChar(v byte) bool { | ||||
| 	return v >= '\x20' && v <= '\x7e' && v != '\x2c' && v != '\x3d' | ||||
| } | ||||
|  | ||||
| // according to (nblk-chr = %x21-2B / %x2D-3C / %x3E-7E) . | ||||
| func checkValueLast(v byte) bool { | ||||
| 	return v >= '\x21' && v <= '\x7e' && v != '\x2c' && v != '\x3d' | ||||
| } | ||||
|  | ||||
| // based on the W3C Trace Context specification | ||||
| // | ||||
| //	value    = (0*255(chr)) nblk-chr | ||||
| //	nblk-chr = %x21-2B / %x2D-3C / %x3E-7E | ||||
| //	chr      = %x20 / nblk-chr | ||||
| // | ||||
| // see https://www.w3.org/TR/trace-context-1/#value | ||||
| func checkValue(val string) bool { | ||||
| 	n := len(val) | ||||
| 	if n == 0 || n > 256 { | ||||
| 		return false | ||||
| 	} | ||||
| 	for i := 0; i < n-1; i++ { | ||||
| 		if !checkValueChar(val[i]) { | ||||
| 			return false | ||||
| 		} | ||||
| 	} | ||||
| 	return checkValueLast(val[n-1]) | ||||
| } | ||||
|  | ||||
| func checkKeyRemain(key string) bool { | ||||
| 	// ( lcalpha / DIGIT / "_" / "-"/ "*" / "/" ) | ||||
| 	for _, v := range key { | ||||
| 		if isAlphaNum(byte(v)) { | ||||
| 			continue | ||||
| 		} | ||||
| 		switch v { | ||||
| 		case '_', '-', '*', '/': | ||||
| 			continue | ||||
| 		} | ||||
| 		return false | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| // according to | ||||
| // | ||||
| //	simple-key = lcalpha (0*255( lcalpha / DIGIT / "_" / "-"/ "*" / "/" )) | ||||
| //	system-id = lcalpha (0*13( lcalpha / DIGIT / "_" / "-"/ "*" / "/" )) | ||||
| // | ||||
| // param n is remain part length, should be 255 in simple-key or 13 in system-id. | ||||
| func checkKeyPart(key string, n int) bool { | ||||
| 	if len(key) == 0 { | ||||
| 		return false | ||||
| 	} | ||||
| 	first := key[0] // key's first char | ||||
| 	ret := len(key[1:]) <= n | ||||
| 	ret = ret && first >= 'a' && first <= 'z' | ||||
| 	return ret && checkKeyRemain(key[1:]) | ||||
| } | ||||
|  | ||||
| func isAlphaNum(c byte) bool { | ||||
| 	if c >= 'a' && c <= 'z' { | ||||
| 		return true | ||||
| 	} | ||||
| 	return c >= '0' && c <= '9' | ||||
| } | ||||
|  | ||||
| // according to | ||||
| // | ||||
| //	tenant-id = ( lcalpha / DIGIT ) 0*240( lcalpha / DIGIT / "_" / "-"/ "*" / "/" ) | ||||
| // | ||||
| // param n is remain part length, should be 240 exactly. | ||||
| func checkKeyTenant(key string, n int) bool { | ||||
| 	if len(key) == 0 { | ||||
| 		return false | ||||
| 	} | ||||
| 	return isAlphaNum(key[0]) && len(key[1:]) <= n && checkKeyRemain(key[1:]) | ||||
| } | ||||
|  | ||||
| // based on the W3C Trace Context specification | ||||
| // | ||||
| //	key = simple-key / multi-tenant-key | ||||
| //	simple-key = lcalpha (0*255( lcalpha / DIGIT / "_" / "-"/ "*" / "/" )) | ||||
| //	multi-tenant-key = tenant-id "@" system-id | ||||
| //	tenant-id = ( lcalpha / DIGIT ) (0*240( lcalpha / DIGIT / "_" / "-"/ "*" / "/" )) | ||||
| //	system-id = lcalpha (0*13( lcalpha / DIGIT / "_" / "-"/ "*" / "/" )) | ||||
| //	lcalpha    = %x61-7A ; a-z | ||||
| // | ||||
| // see https://www.w3.org/TR/trace-context-1/#tracestate-header. | ||||
| func checkKey(key string) bool { | ||||
| 	tenant, system, ok := strings.Cut(key, "@") | ||||
| 	if !ok { | ||||
| 		return checkKeyPart(key, 255) | ||||
| 	} | ||||
| 	return checkKeyTenant(tenant, 240) && checkKeyPart(system, 13) | ||||
| } | ||||
|  | ||||
| func newMember(key, value string) (member, error) { | ||||
| 	if len(key) > 256 { | ||||
| 		return member{}, fmt.Errorf("%w: %s", errInvalidKey, key) | ||||
| 	if !checkKey(key) { | ||||
| 		return member{}, errInvalidKey | ||||
| 	} | ||||
| 	if !noTenantKeyRe.MatchString(key) { | ||||
| 		if !withTenantKeyRe.MatchString(key) { | ||||
| 			return member{}, fmt.Errorf("%w: %s", errInvalidKey, key) | ||||
| 		} | ||||
| 		atIndex := strings.LastIndex(key, "@") | ||||
| 		if atIndex > 241 || len(key)-1-atIndex > 14 { | ||||
| 			return member{}, fmt.Errorf("%w: %s", errInvalidKey, key) | ||||
| 		} | ||||
| 	} | ||||
| 	if len(value) > 256 || !valueRe.MatchString(value) { | ||||
| 		return member{}, fmt.Errorf("%w: %s", errInvalidValue, value) | ||||
| 	if !checkValue(value) { | ||||
| 		return member{}, errInvalidValue | ||||
| 	} | ||||
| 	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) | ||||
| 	} | ||||
| @@ -85,7 +164,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 | ||||
| } | ||||
|  | ||||
| // TraceState provides additional vendor-specific trace identification | ||||
| @@ -109,8 +188,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 | ||||
| 	} | ||||
|  | ||||
| @@ -120,7 +199,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 | ||||
| 		} | ||||
| @@ -153,11 +234,29 @@ 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 n int | ||||
| 	n += len(ts.list)     // member delimiters: '=' | ||||
| 	n += len(ts.list) - 1 // list delimiters: ',' | ||||
| 	for _, mem := range ts.list { | ||||
| 		n += len(mem.Key) | ||||
| 		n += len(mem.Value) | ||||
| 	} | ||||
|  | ||||
| 	var sb strings.Builder | ||||
| 	sb.Grow(n) | ||||
| 	_, _ = 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 | ||||
| @@ -189,15 +288,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 | ||||
| } | ||||
|  | ||||
|   | ||||
							
								
								
									
										58
									
								
								trace/tracestate_benchkmark_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								trace/tracestate_benchkmark_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| // 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) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| 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() | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func BenchmarkTraceStateInsert(b *testing.B) { | ||||
| 	for _, test := range insertTestcase { | ||||
| 		b.Run(test.name, func(b *testing.B) { | ||||
| 			b.ResetTimer() | ||||
| 			b.ReportAllocs() | ||||
| 			for i := 0; i < b.N; i++ { | ||||
| 				_, _ = test.tracestate.Insert(test.key, test.value) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| @@ -420,85 +420,85 @@ func TestTraceStateDelete(t *testing.T) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| var insertTS = TraceState{list: []member{ | ||||
| 	{Key: "key1", Value: "val1"}, | ||||
| 	{Key: "key2", Value: "val2"}, | ||||
| 	{Key: "key3", Value: "val3"}, | ||||
| }} | ||||
|  | ||||
| var insertTestcase = []struct { | ||||
| 	name       string | ||||
| 	tracestate TraceState | ||||
| 	key, value string | ||||
| 	expected   TraceState | ||||
| 	err        error | ||||
| }{ | ||||
| 	{ | ||||
| 		name:       "add new", | ||||
| 		tracestate: insertTS, | ||||
| 		key:        "key4@vendor", | ||||
| 		value:      "val4", | ||||
| 		expected: TraceState{list: []member{ | ||||
| 			{Key: "key4@vendor", Value: "val4"}, | ||||
| 			{Key: "key1", Value: "val1"}, | ||||
| 			{Key: "key2", Value: "val2"}, | ||||
| 			{Key: "key3", Value: "val3"}, | ||||
| 		}}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		name:       "replace", | ||||
| 		tracestate: insertTS, | ||||
| 		key:        "key2", | ||||
| 		value:      "valX", | ||||
| 		expected: TraceState{list: []member{ | ||||
| 			{Key: "key2", Value: "valX"}, | ||||
| 			{Key: "key1", Value: "val1"}, | ||||
| 			{Key: "key3", Value: "val3"}, | ||||
| 		}}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		name:       "invalid key", | ||||
| 		tracestate: insertTS, | ||||
| 		key:        "key!", | ||||
| 		value:      "val", | ||||
| 		expected:   insertTS, | ||||
| 		err:        errInvalidKey, | ||||
| 	}, | ||||
| 	{ | ||||
| 		name:       "invalid value", | ||||
| 		tracestate: insertTS, | ||||
| 		key:        "key", | ||||
| 		value:      "v=l", | ||||
| 		expected:   insertTS, | ||||
| 		err:        errInvalidValue, | ||||
| 	}, | ||||
| 	{ | ||||
| 		name:       "invalid key/value", | ||||
| 		tracestate: insertTS, | ||||
| 		key:        "key!", | ||||
| 		value:      "v=l", | ||||
| 		expected:   insertTS, | ||||
| 		err:        errInvalidKey, | ||||
| 	}, | ||||
| 	{ | ||||
| 		name:       "drop the right-most member(oldest) in queue", | ||||
| 		tracestate: maxMembers, | ||||
| 		key:        "keyx", | ||||
| 		value:      "valx", | ||||
| 		expected: func() TraceState { | ||||
| 			// Prepend the new element and remove the oldest one, which is over capacity. | ||||
| 			return TraceState{ | ||||
| 				list: append( | ||||
| 					[]member{{Key: "keyx", Value: "valx"}}, | ||||
| 					maxMembers.list[:len(maxMembers.list)-1]..., | ||||
| 				), | ||||
| 			} | ||||
| 		}(), | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| func TestTraceStateInsert(t *testing.T) { | ||||
| 	ts := TraceState{list: []member{ | ||||
| 		{Key: "key1", Value: "val1"}, | ||||
| 		{Key: "key2", Value: "val2"}, | ||||
| 		{Key: "key3", Value: "val3"}, | ||||
| 	}} | ||||
|  | ||||
| 	testCases := []struct { | ||||
| 		name       string | ||||
| 		tracestate TraceState | ||||
| 		key, value string | ||||
| 		expected   TraceState | ||||
| 		err        error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:       "add new", | ||||
| 			tracestate: ts, | ||||
| 			key:        "key4@vendor", | ||||
| 			value:      "val4", | ||||
| 			expected: TraceState{list: []member{ | ||||
| 				{Key: "key4@vendor", Value: "val4"}, | ||||
| 				{Key: "key1", Value: "val1"}, | ||||
| 				{Key: "key2", Value: "val2"}, | ||||
| 				{Key: "key3", Value: "val3"}, | ||||
| 			}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "replace", | ||||
| 			tracestate: ts, | ||||
| 			key:        "key2", | ||||
| 			value:      "valX", | ||||
| 			expected: TraceState{list: []member{ | ||||
| 				{Key: "key2", Value: "valX"}, | ||||
| 				{Key: "key1", Value: "val1"}, | ||||
| 				{Key: "key3", Value: "val3"}, | ||||
| 			}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "invalid key", | ||||
| 			tracestate: ts, | ||||
| 			key:        "key!", | ||||
| 			value:      "val", | ||||
| 			expected:   ts, | ||||
| 			err:        errInvalidKey, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "invalid value", | ||||
| 			tracestate: ts, | ||||
| 			key:        "key", | ||||
| 			value:      "v=l", | ||||
| 			expected:   ts, | ||||
| 			err:        errInvalidValue, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "invalid key/value", | ||||
| 			tracestate: ts, | ||||
| 			key:        "key!", | ||||
| 			value:      "v=l", | ||||
| 			expected:   ts, | ||||
| 			err:        errInvalidKey, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "drop the right-most member(oldest) in queue", | ||||
| 			tracestate: maxMembers, | ||||
| 			key:        "keyx", | ||||
| 			value:      "valx", | ||||
| 			expected: func() TraceState { | ||||
| 				// Prepend the new element and remove the oldest one, which is over capacity. | ||||
| 				return TraceState{ | ||||
| 					list: append( | ||||
| 						[]member{{Key: "keyx", Value: "valx"}}, | ||||
| 						maxMembers.list[:len(maxMembers.list)-1]..., | ||||
| 					), | ||||
| 				} | ||||
| 			}(), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tc := range testCases { | ||||
| 	for _, tc := range insertTestcase { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			actual, err := tc.tracestate.Insert(tc.key, tc.value) | ||||
| 			assert.ErrorIs(t, err, tc.err, tc.name) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user