mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +00:00
tsweb: remove allocs introduced by earlier change
This removes the ~9 allocs added by #5869, while still keeping struct fields sorted (the previous commit's tests still pass). And add a test to lock it in that this shouldn't allocate. Updates #5778 Change-Id: I4c12b9e2a1334adc1ea5aba1777681cb9fc18fbf Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
529e893f70
commit
718914b697
@ -43,9 +43,11 @@ func init() {
|
|||||||
expvar.Publish("gauge_goroutines", expvar.Func(func() any { return runtime.NumGoroutine() }))
|
expvar.Publish("gauge_goroutines", expvar.Func(func() any { return runtime.NumGoroutine() }))
|
||||||
}
|
}
|
||||||
|
|
||||||
const gaugePrefix = "gauge_"
|
const (
|
||||||
const counterPrefix = "counter_"
|
gaugePrefix = "gauge_"
|
||||||
const labelMapPrefix = "labelmap_"
|
counterPrefix = "counter_"
|
||||||
|
labelMapPrefix = "labelmap_"
|
||||||
|
)
|
||||||
|
|
||||||
// prefixesToTrim contains key prefixes to remove when exporting and sorting metrics.
|
// prefixesToTrim contains key prefixes to remove when exporting and sorting metrics.
|
||||||
var prefixesToTrim = []string{gaugePrefix, counterPrefix, labelMapPrefix}
|
var prefixesToTrim = []string{gaugePrefix, counterPrefix, labelMapPrefix}
|
||||||
@ -642,13 +644,25 @@ func writeMemstats(w io.Writer, ms *runtime.MemStats) {
|
|||||||
c("num_gc", uint64(ms.NumGC), "number of completed GC cycles")
|
c("num_gc", uint64(ms.NumGC), "number of completed GC cycles")
|
||||||
}
|
}
|
||||||
|
|
||||||
// foreachExportedStructField iterates over the fields in sorted order of
|
// sortedStructField is metadata about a struct field used both for sorting once
|
||||||
// their name, after removing metric prefixes. This is not necessarily the
|
// (by structTypeSortedFields) and at serving time (by
|
||||||
// order they were declared in the struct
|
// foreachExportedStructField).
|
||||||
func foreachExportedStructField(rv reflect.Value, f func(fieldOrJSONName, metricType string, rv reflect.Value)) {
|
type sortedStructField struct {
|
||||||
t := rv.Type()
|
Index int // index of struct field in struct
|
||||||
nameToIndex := map[string]int{}
|
Name string // struct field name, or "json" name
|
||||||
sortedFields := make([]string, 0, t.NumField())
|
SortName string // Name with "foo_" type prefixes removed
|
||||||
|
MetricType string // the "metrictype" struct tag
|
||||||
|
StructFieldType *reflect.StructField
|
||||||
|
}
|
||||||
|
|
||||||
|
var structSortedFieldsCache sync.Map // reflect.Type => []sortedStructField
|
||||||
|
|
||||||
|
// structTypeSortedFields returns the sorted fields of t, caching as needed.
|
||||||
|
func structTypeSortedFields(t reflect.Type) []sortedStructField {
|
||||||
|
if v, ok := structSortedFieldsCache.Load(t); ok {
|
||||||
|
return v.([]sortedStructField)
|
||||||
|
}
|
||||||
|
fields := make([]sortedStructField, 0, t.NumField())
|
||||||
for i, n := 0, t.NumField(); i < n; i++ {
|
for i, n := 0, t.NumField(); i < n; i++ {
|
||||||
sf := t.Field(i)
|
sf := t.Field(i)
|
||||||
name := sf.Name
|
name := sf.Name
|
||||||
@ -662,28 +676,45 @@ func foreachExportedStructField(rv reflect.Value, f func(fieldOrJSONName, metric
|
|||||||
name = v
|
name = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nameToIndex[name] = i
|
fields = append(fields, sortedStructField{
|
||||||
sortedFields = append(sortedFields, name)
|
Index: i,
|
||||||
}
|
Name: name,
|
||||||
sort.Slice(sortedFields, func(i, j int) bool {
|
SortName: removeTypePrefixes(name),
|
||||||
left := sortedFields[i]
|
MetricType: sf.Tag.Get("metrictype"),
|
||||||
right := sortedFields[j]
|
StructFieldType: &sf,
|
||||||
for _, prefix := range prefixesToTrim {
|
|
||||||
left = strings.TrimPrefix(left, prefix)
|
|
||||||
right = strings.TrimPrefix(right, prefix)
|
|
||||||
}
|
|
||||||
return left < right
|
|
||||||
})
|
})
|
||||||
for _, name := range sortedFields {
|
}
|
||||||
i := nameToIndex[name]
|
sort.Slice(fields, func(i, j int) bool {
|
||||||
sf := t.Field(i)
|
return fields[i].SortName < fields[j].SortName
|
||||||
metricType := sf.Tag.Get("metrictype")
|
})
|
||||||
if metricType != "" || sf.Type.Kind() == reflect.Struct {
|
structSortedFieldsCache.Store(t, fields)
|
||||||
f(name, metricType, rv.Field(i))
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeTypePrefixes returns s with the first "foo_" prefix in prefixesToTrim
|
||||||
|
// removed.
|
||||||
|
func removeTypePrefixes(s string) string {
|
||||||
|
for _, prefix := range prefixesToTrim {
|
||||||
|
if trimmed := strings.TrimPrefix(s, prefix); trimmed != s {
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// foreachExportedStructField iterates over the fields in sorted order of
|
||||||
|
// their name, after removing metric prefixes. This is not necessarily the
|
||||||
|
// order they were declared in the struct
|
||||||
|
func foreachExportedStructField(rv reflect.Value, f func(fieldOrJSONName, metricType string, rv reflect.Value)) {
|
||||||
|
t := rv.Type()
|
||||||
|
for _, ssf := range structTypeSortedFields(t) {
|
||||||
|
sf := ssf.StructFieldType
|
||||||
|
if ssf.MetricType != "" || sf.Type.Kind() == reflect.Struct {
|
||||||
|
f(ssf.Name, ssf.MetricType, rv.Field(ssf.Index))
|
||||||
} else if sf.Type.Kind() == reflect.Ptr && sf.Type.Elem().Kind() == reflect.Struct {
|
} else if sf.Type.Kind() == reflect.Ptr && sf.Type.Elem().Kind() == reflect.Struct {
|
||||||
fv := rv.Field(i)
|
fv := rv.Field(ssf.Index)
|
||||||
if !fv.IsNil() {
|
if !fv.IsNil() {
|
||||||
f(name, metricType, fv.Elem())
|
f(ssf.Name, ssf.MetricType, fv.Elem())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -726,3 +727,19 @@ func TestPort80Handler(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSortedStructAllocs(t *testing.T) {
|
||||||
|
f := reflect.ValueOf(struct {
|
||||||
|
Foo int
|
||||||
|
Bar int
|
||||||
|
Baz int
|
||||||
|
}{})
|
||||||
|
n := testing.AllocsPerRun(1000, func() {
|
||||||
|
foreachExportedStructField(f, func(fieldOrJSONName, metricType string, rv reflect.Value) {
|
||||||
|
// Nothing.
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if n != 0 {
|
||||||
|
t.Errorf("allocs = %v; want 0", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user