diff --git a/tsweb/tsweb.go b/tsweb/tsweb.go index 6e8d636dd..fcd907033 100644 --- a/tsweb/tsweb.go +++ b/tsweb/tsweb.go @@ -43,9 +43,11 @@ func init() { expvar.Publish("gauge_goroutines", expvar.Func(func() any { return runtime.NumGoroutine() })) } -const gaugePrefix = "gauge_" -const counterPrefix = "counter_" -const labelMapPrefix = "labelmap_" +const ( + gaugePrefix = "gauge_" + counterPrefix = "counter_" + labelMapPrefix = "labelmap_" +) // prefixesToTrim contains key prefixes to remove when exporting and sorting metrics. 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") } -// 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() - nameToIndex := map[string]int{} - sortedFields := make([]string, 0, t.NumField()) +// sortedStructField is metadata about a struct field used both for sorting once +// (by structTypeSortedFields) and at serving time (by +// foreachExportedStructField). +type sortedStructField struct { + Index int // index of struct field in struct + Name string // struct field name, or "json" name + 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++ { sf := t.Field(i) name := sf.Name @@ -662,28 +676,45 @@ func foreachExportedStructField(rv reflect.Value, f func(fieldOrJSONName, metric name = v } } - nameToIndex[name] = i - sortedFields = append(sortedFields, name) - } - sort.Slice(sortedFields, func(i, j int) bool { - left := sortedFields[i] - right := sortedFields[j] - for _, prefix := range prefixesToTrim { - left = strings.TrimPrefix(left, prefix) - right = strings.TrimPrefix(right, prefix) - } - return left < right + fields = append(fields, sortedStructField{ + Index: i, + Name: name, + SortName: removeTypePrefixes(name), + MetricType: sf.Tag.Get("metrictype"), + StructFieldType: &sf, + }) + } + sort.Slice(fields, func(i, j int) bool { + return fields[i].SortName < fields[j].SortName }) - for _, name := range sortedFields { - i := nameToIndex[name] - sf := t.Field(i) - metricType := sf.Tag.Get("metrictype") - if metricType != "" || sf.Type.Kind() == reflect.Struct { - f(name, metricType, rv.Field(i)) + structSortedFieldsCache.Store(t, fields) + 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 { - fv := rv.Field(i) + fv := rv.Field(ssf.Index) if !fv.IsNil() { - f(name, metricType, fv.Elem()) + f(ssf.Name, ssf.MetricType, fv.Elem()) } } } diff --git a/tsweb/tsweb_test.go b/tsweb/tsweb_test.go index 0beef9511..42bef16d3 100644 --- a/tsweb/tsweb_test.go +++ b/tsweb/tsweb_test.go @@ -14,6 +14,7 @@ import ( "net" "net/http" "net/http/httptest" + "reflect" "strings" "testing" "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) + } +}