diff --git a/pkg/prometheus/prometheus.go b/pkg/prometheus/prometheus.go index 024f5d9b4..270524698 100644 --- a/pkg/prometheus/prometheus.go +++ b/pkg/prometheus/prometheus.go @@ -19,6 +19,7 @@ package prometheus import ( "bufio" + "errors" "fmt" "io" "math" @@ -65,8 +66,24 @@ type Metric struct { // writeHeaderTo writes the metric comment header to the given writer. func (m *Metric) writeHeaderTo(w io.Writer, options SnapshotExportOptions) error { if m.Help != "" { - // Prometheus metric description escape rules: Only backslashes and line breaks need escaping. - if _, err := io.WriteString(w, fmt.Sprintf("# HELP %s%s %s\n", options.ExporterPrefix, m.Name, strings.ReplaceAll(strings.ReplaceAll(m.Help, "\\", "\\\\"), "\n", "\\n"))); err != nil { + // This writes each string component one by one (rather than using fmt.Sprintf) + // in order to avoid allocating strings for each metric. + if _, err := io.WriteString(w, "# HELP "); err != nil { + return err + } + if _, err := io.WriteString(w, options.ExporterPrefix); err != nil { + return err + } + if _, err := io.WriteString(w, m.Name); err != nil { + return err + } + if _, err := io.WriteString(w, " "); err != nil { + return err + } + if _, err := writeEscapedString(w, m.Help, false); err != nil { + return err + } + if _, err := io.WriteString(w, "\n"); err != nil { return err } } @@ -82,7 +99,22 @@ func (m *Metric) writeHeaderTo(w io.Writer, options SnapshotExportOptions) error metricType = "untyped" } if metricType != "" { - if _, err := io.WriteString(w, fmt.Sprintf("# TYPE %s%s %s\n", options.ExporterPrefix, m.Name, metricType)); err != nil { + if _, err := io.WriteString(w, "# TYPE "); err != nil { + return err + } + if _, err := io.WriteString(w, options.ExporterPrefix); err != nil { + return err + } + if _, err := io.WriteString(w, m.Name); err != nil { + return err + } + if _, err := io.WriteString(w, " "); err != nil { + return err + } + if _, err := io.WriteString(w, metricType); err != nil { + return err + } + if _, err := io.WriteString(w, "\n"); err != nil { return err } } @@ -105,6 +137,47 @@ type Number struct { Int int64 `json:"int,omitempty"` } +// Common numbers which are reused and don't need their own memory allocations. +var ( + zero = Number{} + intOne = Number{Int: 1} + floatOne = Number{Float: 1.0} + floatNaN = Number{Float: math.NaN()} + floatInf = Number{Float: math.Inf(1)} + floatNegInf = Number{Float: math.Inf(-1)} +) + +// NewInt returns a new integer Number. +func NewInt(val int64) *Number { + switch val { + case 0: + return &zero + case 1: + return &intOne + default: + return &Number{Int: val} + } +} + +// NewFloat returns a new floating-point Number. +func NewFloat(val float64) *Number { + if math.IsNaN(val) { + return &floatNaN + } + switch val { + case 0: + return &zero + case 1.0: + return &floatOne + case math.Inf(1.0): + return &floatInf + case math.Inf(-1.0): + return &floatNegInf + default: + return &Number{Float: val} + } +} + // IsInteger returns whether this number contains an integer value. // This is defined as either having the `Float` part set to zero (in which case the `Int` part takes // precedence), or having `Float` be a value equal to its own rounding and not a special float. @@ -115,7 +188,7 @@ func (n *Number) IsInteger() bool { if math.IsNaN(n.Float) || n.Float == math.Inf(-1) || n.Float == math.Inf(1) { return false } - return math.Round(n.Float) == n.Float + return n.Float < float64(math.MaxInt64) && n.Float > float64(math.MinInt64) && math.Round(n.Float) == n.Float } // String returns a string representation of this number. @@ -147,7 +220,37 @@ func (n *Number) GreaterThan(other *Number) bool { return n.Float > other.Float } +// writeInteger writes the given integer to a writer without allocating strings. +func writeInteger(w io.Writer, val int64) (int, error) { + const decimalDigits = "0123456789" + if val == 0 { + return io.WriteString(w, decimalDigits[0:1]) + } + var written int + if val < 0 { + n, err := io.WriteString(w, "-") + written += n + if err != nil { + return written, err + } + val = -val + } + decimal := int64(1) + for ; val/decimal != 0; decimal *= 10 { + } + for decimal /= 10; decimal > 0; decimal /= 10 { + digit := (val / decimal) % 10 + n, err := io.WriteString(w, decimalDigits[digit:digit+1]) + written += n + if err != nil { + return written, err + } + } + return written, nil +} + // writeTo writes the number to the given writer. +// This only causes heap allocations when the number is a non-zero, non-special float. func (n *Number) writeTo(w io.Writer) error { var s string switch { @@ -155,9 +258,10 @@ func (n *Number) writeTo(w io.Writer) error { case n.Int == 0 && n.Float == 0: s = "0" - // Integer case: + // Integer case: case n.Int != 0: - s = fmt.Sprintf("%d", n.Int) + _, err := writeInteger(w, n.Int) + return err // Special float cases: case n.Float == math.Inf(-1): @@ -227,7 +331,7 @@ func NewIntData(metric *Metric, val int64) *Data { // LabeledIntData returns a new Data struct with the given metric, labels, and value. func LabeledIntData(metric *Metric, labels map[string]string, val int64) *Data { - return &Data{Metric: metric, Labels: labels, Number: &Number{Int: val}} + return &Data{Metric: metric, Labels: labels, Number: NewInt(val)} } // NewFloatData returns a new Data struct with the given metric and value. @@ -237,7 +341,7 @@ func NewFloatData(metric *Metric, val float64) *Data { // LabeledFloatData returns a new Data struct with the given metric, labels, and value. func LabeledFloatData(metric *Metric, labels map[string]string, val float64) *Data { - return &Data{Metric: metric, Labels: labels, Number: &Number{Float: val}} + return &Data{Metric: metric, Labels: labels, Number: NewFloat(val)} } // ExportOptions contains options that control how metric data is exported in Prometheus format. @@ -262,6 +366,60 @@ type SnapshotExportOptions struct { ExtraLabels map[string]string } +// writeEscapedString writes the given string in quotation marks and with some characters escaped, +// per Prometheus spec. It does this without string allocations. +// If `quoted` is true, quote characters will surround the string, and quote characters within `s` +// will also be escaped. +func writeEscapedString(w io.Writer, s string, quoted bool) (int, error) { + const ( + quote = '"' + backslash = '\\' + newline = '\n' + quoteStr = `"` + escapedQuote = `\\"` + escapedBackslash = "\\\\" + escapedNewline = "\\\n" + ) + written := 0 + var n int + var err error + if quoted { + n, err = io.WriteString(w, quoteStr) + written += n + if err != nil { + return written, err + } + } + for _, r := range s { + switch r { + case quote: + if quoted { + n, err = io.WriteString(w, escapedQuote) + } else { + n, err = io.WriteString(w, quoteStr) + } + case backslash: + n, err = io.WriteString(w, escapedBackslash) + case newline: + n, err = io.WriteString(w, escapedNewline) + default: + n, err = io.WriteString(w, string(r)) + } + written += n + if err != nil { + return written, err + } + } + if quoted { + n, err = io.WriteString(w, quoteStr) + written += n + if err != nil { + return written, err + } + } + return written, nil +} + // writeMetricPreambleTo writes the metric name to the io.Writer. It may also // write unwritten help and type comments of the metric if they haven't been // written to the io.Writer yet. @@ -290,44 +448,198 @@ func (d *Data) writeMetricPreambleTo(w io.Writer, options SnapshotExportOptions, return nil } -// OrderedLabels returns the list of 'label_key="label_value"' in sorted order, except "le" which is -// a reserved Prometheus label name and should go last. -func OrderedLabels(labels ...map[string]string) ([]string, error) { - var le string - totalLabels := 0 - for _, labelMap := range labels { - if leVal, found := labelMap["le"]; found { - le = leVal - totalLabels += len(labelMap) - 1 - } else { - totalLabels += len(labelMap) +// keyVal is a key-value pair used in the function below. +type keyVal struct{ Key, Value string } + +// sortedIterateLabels iterates through labels and outputs them to `out` in sorted key order, +// or stops when cancelCh is written to. It runs in O(n^2) time but makes no heap allocations. +func sortedIterateLabels(labels map[string]string, out chan<- keyVal, cancelCh <-chan struct{}) { + defer close(out) + if len(labels) == 0 { + return + } + + // smallestKey is the smallest key that we've already sent to `out`. + // It starts as the empty string, which means we haven't sent anything to `out` yet. + smallestKey := "" + // Find the smallest key of the whole set and send it out. + for k := range labels { + if smallestKey == "" || k < smallestKey { + smallestKey = k } } - if le != "" { - totalLabels++ + select { + case out <- keyVal{smallestKey, labels[smallestKey]}: + case <-cancelCh: + return } - keys := make(map[string]struct{}, totalLabels) + + // Iterate until we've sent as many items as we have as input to the output channel. + // We start at 1 because the loop above already sent out the smallest key to `out`. + for numOutput := 1; numOutput < len(labels); numOutput++ { + // nextSmallestKey is the smallest key that is strictly larger than `smallestKey`. + nextSmallestKey := "" + for k := range labels { + if k > smallestKey && (nextSmallestKey == "" || k < nextSmallestKey) { + nextSmallestKey = k + } + } + + // Update smallestKey and send it out. + smallestKey = nextSmallestKey + select { + case out <- keyVal{smallestKey, labels[smallestKey]}: + case <-cancelCh: + return + } + } +} + +// LabelOrError is used in OrderedLabels. +// It represents either a key-value pair, or an error. +type LabelOrError struct { + Key, Value string + Error error +} + +// OrderedLabels streams the list of 'label_key="label_value"' in sorted order, except "le" which is +// a reserved Prometheus label name and should go last. +// If an error is encountered, it is returned as the Error field of LabelOrError, and no further +// messages will be sent on the channel. +func OrderedLabels(labels ...map[string]string) <-chan LabelOrError { + // This function is quite hot on the metric-rendering path, and its naive "just put all the + // strings in one map to ensure no dupes it, then in one slice and sort it" approach is very + // allocation-heavy. This approach is more computation-heavy (it runs in + // O(len(labels) * len(largest label map))), but the only heap allocations it does is for the + // following tiny slices and channels. In practice, the number of label maps and the size of + // each label map is tiny, so this is worth doing despite the theoretically-longer run time. + + // Initialize the channels we'll use. + mapChannels := make([]chan keyVal, 0, len(labels)) + lastKeyVal := make([]keyVal, len(labels)) + resultCh := make(chan LabelOrError) + var cancelCh chan struct{} + // outputError is a helper function for when we have encountered an error mid-way. + outputError := func(err error) { + if cancelCh != nil { + for range mapChannels { + cancelCh <- struct{}{} + } + close(cancelCh) + } + resultCh <- LabelOrError{Error: err} + close(resultCh) + } + + // Verify that no label is the empty string. It's not a valid label name, + // and we use the empty string later on in the function as a marker of having + // finished processing all labels from a given label map. for _, labelMap := range labels { for label := range labelMap { - if _, found := keys[label]; found { - return nil, fmt.Errorf("duplicate label name %q", label) + if label == "" { + go outputError(errors.New("got empty-string label")) + return resultCh } - keys[label] = struct{}{} } } - orderedKeys := make([]string, 0, totalLabels) + + // Each label map is processed in its own goroutine, + // which will stream it back to this function in sorted order. + cancelCh = make(chan struct{}, len(labels)) for _, labelMap := range labels { - for k, v := range labelMap { - if k != "le" { - orderedKeys = append(orderedKeys, fmt.Sprintf("%s=%q", k, v)) + ch := make(chan keyVal) + mapChannels = append(mapChannels, ch) + go sortedIterateLabels(labelMap, ch, cancelCh) + } + + // This goroutine is the meat of this function; it iterates through + // the results being streamed from each `sortedIterateLabels` goroutine + // that we spawned earlier, until all of them are exhausted or until we + // hit an error. + go func() { + // The "le" label is special and goes last, not in sorted order. + // gotLe is the empty string if there is no "le" label, + // otherwise it's the value of the "le" label. + var gotLe string + + // numChannelsLeft tracks the number of channels that are still live. + for numChannelsLeft := len(mapChannels); numChannelsLeft > 0; { + // Iterate over all channels and ensure we have the freshest (smallest) + // label from each of them. + for i, ch := range mapChannels { + // A nil channel is one that has been closed. + if ch == nil { + continue + } + // If we already have the latest value from this channel, + // keep it there instead of getting a new one, + if lastKeyVal[i].Key != "" { + continue + } + // Otherwise, get a new label. + kv, open := <-ch + if !open { + // Channel has been closed, no more to read from this one. + numChannelsLeft-- + mapChannels[i] = nil + continue + } + if kv.Key == "le" { + if gotLe != "" { + outputError(errors.New("got duplicate 'le' label")) + return + } + gotLe = kv.Value + continue + } + lastKeyVal[i] = kv + } + + // We have one key-value pair from each still-active channel now. + // Find the smallest one between them. + smallestKey := "" + indexForSmallest := -1 + for i, kv := range lastKeyVal { + if kv.Key == "" { + continue + } + if smallestKey == "" || kv.Key < smallestKey { + smallestKey = kv.Key + indexForSmallest = i + } else if kv.Key == smallestKey { + outputError(fmt.Errorf("got duplicate label %q", smallestKey)) + return + } + } + + if indexForSmallest == -1 { + // There are no more key-value pairs to output. We're done. + break + } + + // Output the smallest key-value pairs out of all the channels. + resultCh <- LabelOrError{ + Key: smallestKey, + Value: lastKeyVal[indexForSmallest].Value, + } + // Mark the last key-value pair from the channel that gave us the + // smallest key-value pair as no longer present, so that we get a new + // key-value pair from it in the next iteration. + lastKeyVal[indexForSmallest] = keyVal{} + } + + // Output the "le" label last. + if gotLe != "" { + resultCh <- LabelOrError{ + Key: "le", + Value: gotLe, } } - } - sort.Strings(orderedKeys) - if le != "" { - orderedKeys = append(orderedKeys, fmt.Sprintf("le=%q", le)) - } - return orderedKeys, nil + close(resultCh) + close(cancelCh) + }() + + return resultCh } // writeLabelsTo writes a set of metric labels. @@ -336,25 +648,40 @@ func (d *Data) writeLabelsTo(w io.Writer, extraLabels map[string]string, leLabel if _, err := io.WriteString(w, "{"); err != nil { return err } - var orderedLabels []string - var err error + var orderedLabels <-chan LabelOrError if leLabel != nil { - orderedLabels, err = OrderedLabels(d.Labels, extraLabels, map[string]string{"le": leLabel.String()}) + orderedLabels = OrderedLabels(d.Labels, extraLabels, map[string]string{"le": leLabel.String()}) } else { - orderedLabels, err = OrderedLabels(d.Labels, extraLabels) + orderedLabels = OrderedLabels(d.Labels, extraLabels) } - if err != nil { - return err - } - for i, keyVal := range orderedLabels { - if i != 0 { + firstLabel := true + var foundError error + for labelOrError := range orderedLabels { + if foundError != nil { + continue + } + if labelOrError.Error != nil { + foundError = labelOrError.Error + continue + } + if !firstLabel { if _, err := io.WriteString(w, ","); err != nil { return err } } - if _, err := io.WriteString(w, keyVal); err != nil { + firstLabel = false + if _, err := io.WriteString(w, labelOrError.Key); err != nil { return err } + if _, err := io.WriteString(w, "="); err != nil { + return err + } + if _, err := writeEscapedString(w, labelOrError.Value, true); err != nil { + return err + } + } + if foundError != nil { + return foundError } if _, err := io.WriteString(w, "}"); err != nil { return err @@ -382,7 +709,13 @@ func (d *Data) writeMetricLine(w io.Writer, metricSuffix string, val *Number, wh if err := val.writeTo(w); err != nil { return err } - if _, err := io.WriteString(w, fmt.Sprintf(" %d\n", when.UnixMilli())); err != nil { + if _, err := io.WriteString(w, " "); err != nil { + return err + } + if _, err := writeInteger(w, when.UnixMilli()); err != nil { + return err + } + if _, err := io.WriteString(w, "\n"); err != nil { return err } return nil @@ -463,6 +796,14 @@ func (w *countingWriter) Write(b []byte) (int, error) { return written, err } +// WriteString implements io.StringWriter.WriteString. +// This avoids going into the slow, allocation-heavy path of io.WriteString. +func (w *countingWriter) WriteString(s string) (int, error) { + written, err := w.w.WriteString(s) + w.written += written + return written, err +} + // Written returns the number of bytes written to the underlying writer (minus buffered writes). func (w *countingWriter) Written() int { return w.written - w.w.Buffered() diff --git a/pkg/prometheus/prometheus_test.go b/pkg/prometheus/prometheus_test.go index 81b8c644e..c455a4e7b 100644 --- a/pkg/prometheus/prometheus_test.go +++ b/pkg/prometheus/prometheus_test.go @@ -874,7 +874,8 @@ func TestVerifier(t *testing.T) { } at(testTime, func() { t.Logf("Test is running with simulated time: %v", testTime) - verifier, err := NewVerifier(test.Registration) + verifier, cleanup, err := NewVerifier(test.Registration) + defer cleanup() if err != nil && !test.WantVerifierCreationErr { t.Fatalf("unexpected verifier creation error: %v", err) } @@ -1459,3 +1460,145 @@ func TestGroupSameNameMetrics(t *testing.T) { t.Errorf("Seen metrics: %v\nWant metrics: %v", seenMetrics, wantSeenMetrics) } } + +func TestNumberPacker(t *testing.T) { + interestingIntegers := map[uint64]struct{}{ + uint64(0): struct{}{}, + uint64(0x5555555555555555): struct{}{}, + uint64(0xaaaaaaaaaaaaaaaa): struct{}{}, + uint64(0xffffffffffffffff): struct{}{}, + } + for numBits := 0; numBits < 2; numBits++ { + newIntegers := map[uint64]struct{}{} + for interestingInt := range interestingIntegers { + for i := 0; i < 64; i++ { + newIntegers[interestingInt|(1< unpacked version %v (int: %d)", orig, interestingInt, uint32(packed), unpacked, unpacked.Int) + } + seenDirectInteger = seenDirectInteger || (uint32(packed)&storageField) == storageFieldDirect + seenIndirectInteger = seenIndirectInteger || (uint32(packed)&storageField) == storageFieldIndirect + } + if !seenDirectInteger { + t.Error("did not encounter any integer that could be packed directly") + } + if !seenIndirectInteger { + t.Error("did not encounter any integer that was packed indirectly") + } + }) + t.Run("packing_efficiency", func(t *testing.T) { + // Verify that we actually saved space by not packing every number in numberPacker itself. + if len(p.data) >= len(interestingIntegers) { + t.Errorf("packer had %d data points stored in its data, but we expected some of it to not be stored in it (tried to pack %d integers total)", len(p.data), len(interestingIntegers)) + } + }) + t.Run("floats", func(t *testing.T) { + interestingFloats := make(map[float64]struct{}, len(interestingIntegers)+21*21+17) + for divExp := -10; divExp < 10; divExp++ { + div := math.Pow(10, float64(divExp)) + for i := -10; i < 10; i++ { + interestingFloats[float64(i)*div] = struct{}{} + } + } + interestingFloats[0.0] = struct{}{} + interestingFloats[math.NaN()] = struct{}{} + interestingFloats[math.Inf(1)] = struct{}{} + interestingFloats[math.Inf(-1)] = struct{}{} + interestingFloats[math.Pi] = struct{}{} + interestingFloats[math.Sqrt2] = struct{}{} + interestingFloats[math.E] = struct{}{} + interestingFloats[math.SqrtE] = struct{}{} + interestingFloats[math.Ln2] = struct{}{} + interestingFloats[math.MaxFloat32] = struct{}{} + interestingFloats[-math.MaxFloat32] = struct{}{} + interestingFloats[math.MaxFloat64] = struct{}{} + interestingFloats[-math.MaxFloat64] = struct{}{} + interestingFloats[math.SmallestNonzeroFloat32] = struct{}{} + interestingFloats[-math.SmallestNonzeroFloat32] = struct{}{} + interestingFloats[math.SmallestNonzeroFloat64] = struct{}{} + interestingFloats[-math.SmallestNonzeroFloat64] = struct{}{} + for interestingInt := range interestingIntegers { + interestingFloats[math.Float64frombits(interestingInt)] = struct{}{} + } + seenDirectFloat := false + seenIndirectFloat := false + for interestingFloat := range interestingFloats { + orig := NewFloat(interestingFloat) + packed, err := p.pack(orig) + if err != nil { + t.Fatalf("float %v (64bits=%x, 32bits=%x. float32-encodable=%v): cannot pack: %v", orig, math.Float64bits(interestingFloat), math.Float32bits(float32(interestingFloat)), float64(float32(interestingFloat)) == interestingFloat, err) + } + unpacked := p.unpack(packed) + switch { + case interestingFloat == 0: // Zero-valued float becomes an integer. + if !unpacked.IsInteger() { + t.Errorf("Zero-valued float %v: got non-integer number: %v", orig, unpacked) + } else if unpacked.Int != 0 { + t.Errorf("Zero-valued float %v: got non-zero integer: %d", orig, unpacked.Int) + } + case math.IsNaN(orig.Float): + if !math.IsNaN(unpacked.Float) { + t.Errorf("NaN float %v: got non-NaN unpacked version %v", orig, unpacked) + } + default: // Not NaN, not integer + if !orig.SameType(unpacked) || orig.Float != unpacked.Float { + t.Errorf("float %v (64bits=%x, 32bits=%x, float32-encodable=%v): got packed=%x => unpacked version %v (float: %f)", orig, math.Float64bits(interestingFloat), math.Float32bits(float32(interestingFloat)), float64(float32(interestingFloat)) == interestingFloat, uint32(packed), unpacked, unpacked.Float) + } + } + seenDirectFloat = seenDirectFloat || (uint32(packed)&storageField) == storageFieldDirect + seenIndirectFloat = seenIndirectFloat || (uint32(packed)&storageField) == storageFieldIndirect + } + if !seenDirectFloat { + t.Error("did not encounter any float that could be packed directly") + } + if !seenIndirectFloat { + t.Error("did not encounter any float that was packed indirectly") + } + }) +} diff --git a/pkg/prometheus/prometheus_verify.go b/pkg/prometheus/prometheus_verify.go index 7b77409af..5cf85296f 100644 --- a/pkg/prometheus/prometheus_verify.go +++ b/pkg/prometheus/prometheus_verify.go @@ -32,28 +32,231 @@ const ( maxExportStaleness = 10 * time.Second ) +// internedStringMap allows for interning strings. +type internedStringMap map[string]*string + +// Intern returns the interned version of the given string. +// If it is not already interned in the map, this function interns it. +func (m internedStringMap) Intern(s string) string { + if existing, found := m[s]; found { + return *existing + } + m[s] = &s + return s +} + +// globalInternMap is a string intern map used for globally-relevant data that repeats across +// verifiers, such as metric names and field names, but not field values or combinations of field +// values. +var ( + globalInternMu sync.Mutex + verifierCount uint64 + globalInternMap = make(internedStringMap) +) + +// globalIntern returns the interned version of the given string. +// If it is not already interned in the map, this function interns it. +func globalIntern(s string) string { + globalInternMu.Lock() + defer globalInternMu.Unlock() + return globalInternMap.Intern(s) +} + +func globalInternVerifierCreated() { + globalInternMu.Lock() + defer globalInternMu.Unlock() + verifierCount++ +} + +func globalInternVerifierReleased() { + globalInternMu.Lock() + defer globalInternMu.Unlock() + verifierCount-- + if verifierCount <= 0 { + verifierCount = 0 + // No more verifiers active, so release the global map to not keep consuming needless resources. + globalInternMap = make(internedStringMap) + } +} + +// numberPacker holds packedNumber data. It is useful to store large amounts of Number structs in a +// small memory footprint. +type numberPacker struct { + data []uint64 +} + +// packedNumber is a non-serializable but smaller-memory-footprint container for a numerical value. +// It can be unpacked out to a Number struct. +// This contains 4 bytes where we try to pack as much as possible. +// For the overhwelmingly-common case of integers that fit in 30 bits (i.e. 30 bits where the first +// 2 bits are zero, we store them directly here. Otherwise, we store the offset of a 64-bit number +// within numberPacker. +// Layout, going from highest to lowest bit: +// Bit 0 is the type: 0 for integer, 1 for float. +// Bit 1 is 0 if the number's value is stored within the next 30 bits, or 1 if the next 30 bits +// refer to an offset within numberPacker instead. +// In the case of a float, the next two bits (bits 2 and 3) may be used to encode a special value: +// - 00 means not a special value +// - 01 means NaN +// - 10 means -infinity +// - 11 means +infinity +// +// When not using a special value, the 32-bit exponent must fit in 5 bits, and is encoded using a +// bias of 2^4, meaning it ranges from -15 (encoded as 0b00000) to 16 (encoded as 0b11111), and an +// exponent of 0 is encoded as 0b01111. +// Floats that do not fit within this range must be encoded indirectly as float64s, similar to +// integers that don't fit in 30 bits. +type packedNumber uint32 + +// Useful masks and other bit-twiddling stuff for packedNumber. +const ( + typeField = uint32(1 << 31) + typeFieldInteger = uint32(0) + typeFieldFloat = uint32(typeField) + storageField = uint32(1 << 30) + storageFieldDirect = uint32(0) + storageFieldIndirect = uint32(storageField) + valueField = uint32(1<<30 - 1) + float32ExponentField = uint32(0x7f800000) + float32ExponentShift = uint32(23) + float32ExponentBias = uint32(127) + float32FractionField = uint32(0x7fffff) + packedFloatExponentField = uint32(0x0f800000) + packedFloatExponentBias = uint32(15) + packedFloatNaN = packedNumber(typeFieldFloat | storageFieldDirect | 0x10000000) + packedFloatNegInf = packedNumber(typeFieldFloat | storageFieldDirect | 0x20000000) + packedFloatInf = packedNumber(typeFieldFloat | storageFieldDirect | 0x30000000) +) + +// errOutOfPackerMemory is returned when the number cannot be packed into a numberPacker. +var errOutOfPackerMemory = errors.New("out of numberPacker memory") + +// pack packs a Number into a packedNumber. +func (p *numberPacker) pack(n *Number) (packedNumber, error) { + if n.Float == 0.0 { + v := n.Int + if v >= 0 && v <= int64(valueField) { + // We can store the integer value directly. + return packedNumber(typeFieldInteger | storageFieldDirect | uint32(v)), nil + } + // Need to allocate a new entry in packedNumber. + newIndex := uint32(len(p.data)) + if newIndex > valueField { + return 0, errOutOfPackerMemory + } + p.data = append(p.data, uint64(v)) + return packedNumber(typeFieldInteger | storageFieldIndirect | newIndex), nil + } + // n is a float. + v := n.Float + if math.IsNaN(v) { + return packedFloatNaN, nil + } + if v == math.Inf(-1) { + return packedFloatNegInf, nil + } + if v == math.Inf(1) { + return packedFloatInf, nil + } + if v >= 0.0 && float64(float32(v)) == v { + float32Bits := math.Float32bits(float32(v)) + exponent := (float32Bits&float32ExponentField)>>float32ExponentShift - float32ExponentBias + packedExponent := (exponent + packedFloatExponentBias) << float32ExponentShift + if packedExponent&packedFloatExponentField == packedExponent { + float32Fraction := float32Bits & float32FractionField + return packedNumber(typeFieldFloat | storageFieldDirect | packedExponent | float32Fraction), nil + } + } + // Need to allocate a new entry in packedNumber. + newIndex := uint32(len(p.data)) + if newIndex > valueField { + return 0, errOutOfPackerMemory + } + p.data = append(p.data, math.Float64bits(v)) + return packedNumber(typeFieldFloat | storageFieldIndirect | newIndex), nil +} + +func (p *numberPacker) mustPack(n *Number) packedNumber { + packed, err := p.pack(n) + if err != nil { + panic(err) + } + return packed +} + +func (p *numberPacker) mustPackInt(val int64) packedNumber { + n := Number{Int: val} + return p.mustPack(&n) +} + +func (p *numberPacker) mustPackFloat(val float64) packedNumber { + n := Number{Float: val} + return p.mustPack(&n) +} + +// unpack unpacks a packedNumber back into a Number. +func (p *numberPacker) unpack(n packedNumber) *Number { + switch uint32(n) & typeField { + case typeFieldInteger: + switch uint32(n) & storageField { + case storageFieldDirect: + return NewInt(int64(uint32(n) & valueField)) + case storageFieldIndirect: + return NewInt(int64(p.data[uint32(n)&valueField])) + } + case typeFieldFloat: + switch uint32(n) & storageField { + case storageFieldDirect: + switch n { + case packedFloatNaN: + return NewFloat(math.NaN()) + case packedFloatNegInf: + return NewFloat(math.Inf(-1)) + case packedFloatInf: + return NewFloat(math.Inf(1)) + default: + exponent := ((uint32(n) & packedFloatExponentField) >> float32ExponentShift) - packedFloatExponentBias + float32Bits := ((exponent + float32ExponentBias) << float32ExponentShift) | (uint32(n) & float32FractionField) + return NewFloat(float64(math.Float32frombits(float32Bits))) + } + case storageFieldIndirect: + return NewFloat(math.Float64frombits(p.data[uint32(n)&valueField])) + } + } + panic("unreachable") +} + +func (p *numberPacker) mustUnpackInt(n packedNumber) int64 { + num := p.unpack(n) + if !num.IsInteger() { + panic("not an integer") + } + return num.Int +} + // verifiableMetric verifies a single metric within a Verifier. type verifiableMetric struct { metadata *pb.MetricMetadata wantMetric Metric - numFields int + numFields uint32 + verifier *Verifier allowedFieldValues map[string]map[string]struct{} - wantBucketUpperBounds []Number + wantBucketUpperBounds []packedNumber // The following fields are used to verify that values are actually increasing monotonically. // They are only read and modified when the parent Verifier.mu is held. // They are mapped by their combination of field values. // lastCounterValue is used for counter metrics. - lastCounterValue map[string]Number + lastCounterValue map[string]packedNumber // lastBucketSamples is used for distribution ("histogram") metrics. - lastBucketSamples map[string][]uint64 + lastBucketSamples map[string][]packedNumber } // newVerifiableMetric creates a new verifiableMetric that can verify the // values of a metric with the given metadata. -func newVerifiableMetric(metadata *pb.MetricMetadata) (*verifiableMetric, error) { +func newVerifiableMetric(metadata *pb.MetricMetadata, verifier *Verifier) (*verifiableMetric, error) { if metadata.GetName() == "" || metadata.GetPrometheusName() == "" { return nil, errors.New("metric has no name") } @@ -65,7 +268,7 @@ func newVerifiableMetric(metadata *pb.MetricMetadata) (*verifiableMetric, error) return nil, fmt.Errorf("invalid character %c in prometheus metric name %q", r, metadata.GetPrometheusName()) } } - numFields := len(metadata.GetFields()) + numFields := uint32(len(metadata.GetFields())) var allowedFieldValues map[string]map[string]struct{} if numFields > 0 { seenFields := make(map[string]struct{}, numFields) @@ -84,16 +287,17 @@ func newVerifiableMetric(metadata *pb.MetricMetadata) (*verifiableMetric, error) if _, alreadyExists := fieldValues[value]; alreadyExists { return nil, fmt.Errorf("field %s has duplicate allowed value %q", fieldName, value) } - fieldValues[value] = struct{}{} + fieldValues[globalIntern(value)] = struct{}{} } - allowedFieldValues[fieldName] = fieldValues + allowedFieldValues[globalIntern(fieldName)] = fieldValues } } v := &verifiableMetric{ metadata: metadata, + verifier: verifier, wantMetric: Metric{ - Name: metadata.GetPrometheusName(), - Help: metadata.GetDescription(), + Name: globalIntern(metadata.GetPrometheusName()), + Help: globalIntern(metadata.GetDescription()), }, numFields: numFields, allowedFieldValues: allowedFieldValues, @@ -104,7 +308,7 @@ func newVerifiableMetric(metadata *pb.MetricMetadata) (*verifiableMetric, error) v.wantMetric.Type = TypeGauge if metadata.GetCumulative() { v.wantMetric.Type = TypeCounter - v.lastCounterValue = make(map[string]Number, numFieldCombinations) + v.lastCounterValue = make(map[string]packedNumber, numFieldCombinations) } case pb.MetricMetadata_TYPE_DISTRIBUTION: v.wantMetric.Type = TypeHistogram @@ -112,12 +316,12 @@ func newVerifiableMetric(metadata *pb.MetricMetadata) (*verifiableMetric, error) if numBuckets <= 1 || numBuckets > 256 { return nil, fmt.Errorf("unsupported number of buckets: %d", numBuckets) } - v.wantBucketUpperBounds = make([]Number, numBuckets) + v.wantBucketUpperBounds = make([]packedNumber, numBuckets) for i, boundary := range metadata.GetDistributionBucketLowerBounds() { - v.wantBucketUpperBounds[i] = Number{Int: boundary} + v.wantBucketUpperBounds[i] = verifier.permanentPacker.mustPackInt(boundary) } - v.wantBucketUpperBounds[numBuckets-1] = Number{Float: math.Inf(1)} - v.lastBucketSamples = make(map[string][]uint64, numFieldCombinations) + v.wantBucketUpperBounds[numBuckets-1] = verifier.permanentPacker.mustPackFloat(math.Inf(1)) + v.lastBucketSamples = make(map[string][]packedNumber, numFieldCombinations) default: return nil, fmt.Errorf("invalid type: %v", metadata.GetType()) } @@ -133,13 +337,14 @@ func (v *verifiableMetric) numFieldCombinations() int { // field values that have already been seen. `verify` should populate this. // `dataToFieldsSeen` is passed across calls to `verify` and other methods of `verifiableMetric`. // It is used to store the canonical representation of the field values seen for each *Data. +// Precondition: `Verifier.mu` is held. func (v *verifiableMetric) verify(data *Data, metricFieldsSeen map[string]struct{}, dataToFieldsSeen map[*Data]string) error { if *data.Metric != v.wantMetric { return fmt.Errorf("invalid metric definition: got %+v want %+v", data.Metric, v.wantMetric) } // Verify fields. - if len(data.Labels) != v.numFields { + if uint32(len(data.Labels)) != v.numFields { return fmt.Errorf("invalid number of fields: got %d want %d", len(data.Labels), v.numFields) } var fieldValues strings.Builder @@ -193,7 +398,7 @@ func (v *verifiableMetric) verify(data *Data, metricFieldsSeen map[string]struct return fmt.Errorf("invalid number of buckets: got %d want %d", len(data.HistogramValue.Buckets), len(v.wantBucketUpperBounds)) } for i, b := range data.HistogramValue.Buckets { - if want := v.wantBucketUpperBounds[i]; b.UpperBound != want { + if want := v.wantBucketUpperBounds[i]; b.UpperBound != *v.verifier.permanentPacker.unpack(want) { return fmt.Errorf("invalid upper bound for bucket %d (0-based): got %v want %v", i, b.UpperBound, want) } } @@ -202,6 +407,7 @@ func (v *verifiableMetric) verify(data *Data, metricFieldsSeen map[string]struct } // All passed. Update the maps that are shared across calls. + fieldValuesStr = v.verifier.internMap.Intern(fieldValuesStr) dataToFieldsSeen[data] = fieldValuesStr metricFieldsSeen[fieldValuesStr] = struct{}{} return nil @@ -209,10 +415,10 @@ func (v *verifiableMetric) verify(data *Data, metricFieldsSeen map[string]struct // verifyIncrement verifies that incremental metrics are monotonically increasing. // Preconditions: `verify` has succeeded on the given `data`, and `Verifier.mu` is held. -func (v *verifiableMetric) verifyIncrement(data *Data, fieldValues string) error { +func (v *verifiableMetric) verifyIncrement(data *Data, fieldValues string, packer *numberPacker) error { switch v.wantMetric.Type { case TypeCounter: - last := v.lastCounterValue[fieldValues] + last := packer.unpack(v.lastCounterValue[v.verifier.internMap.Intern(fieldValues)]) if !last.SameType(data.Number) { return fmt.Errorf("counter number type changed: %v vs %v", last, data.Number) } @@ -220,14 +426,14 @@ func (v *verifiableMetric) verifyIncrement(data *Data, fieldValues string) error return fmt.Errorf("counter value decreased from %v to %v", last, data.Number) } case TypeHistogram: - lastBucketSamples := v.lastBucketSamples[fieldValues] + lastBucketSamples := v.lastBucketSamples[v.verifier.internMap.Intern(fieldValues)] if lastBucketSamples == nil { - lastBucketSamples = make([]uint64, len(v.wantBucketUpperBounds)) - v.lastBucketSamples[fieldValues] = lastBucketSamples + lastBucketSamples = make([]packedNumber, len(v.wantBucketUpperBounds)) + v.lastBucketSamples[v.verifier.internMap.Intern(fieldValues)] = lastBucketSamples } for i, b := range data.HistogramValue.Buckets { - if lastBucketSamples[i] > b.Samples { - return fmt.Errorf("number of samples in bucket %d (0-based) decreased from %d to %d", i, lastBucketSamples[i], b.Samples) + if uint64(packer.mustUnpackInt(lastBucketSamples[i])) > b.Samples { + return fmt.Errorf("number of samples in bucket %d (0-based) decreased from %d to %d", i, packer.mustUnpackInt(lastBucketSamples[i]), b.Samples) } } } @@ -236,14 +442,14 @@ func (v *verifiableMetric) verifyIncrement(data *Data, fieldValues string) error // update updates incremental metrics' "last seen" data. // Preconditions: `verifyIncrement` has succeeded on the given `data`, and `Verifier.mu` is held. -func (v *verifiableMetric) update(data *Data, fieldValues string) { +func (v *verifiableMetric) update(data *Data, fieldValues string, packer *numberPacker) { switch v.wantMetric.Type { case TypeCounter: - v.lastCounterValue[fieldValues] = *data.Number + v.lastCounterValue[v.verifier.internMap.Intern(fieldValues)] = packer.mustPack(data.Number) case TypeHistogram: - lastBucketSamples := v.lastBucketSamples[fieldValues] + lastBucketSamples := v.lastBucketSamples[v.verifier.internMap.Intern(fieldValues)] for i, b := range data.HistogramValue.Buckets { - lastBucketSamples[i] = b.Samples + lastBucketSamples[i] = packer.mustPackInt(int64(b.Samples)) } } } @@ -253,29 +459,50 @@ func (v *verifiableMetric) update(data *Data, fieldValues string) { // A single Verifier should be used per sandbox. It is expected to be reused across exports such // that it can enforce the export snapshot timestamp is strictly monotonically increasing. type Verifier struct { - knownMetrics map[string]*verifiableMetric - mu sync.Mutex + knownMetrics map[string]*verifiableMetric + + // permanentPacker is used to pack numbers seen at Verifier initialization time only. + permanentPacker *numberPacker + + // mu protects the fields below. + mu sync.Mutex + + // internMap is used to intern strings relevant to this verifier only. + // Globally-relevant strings should be interned in globalInternMap. + internMap internedStringMap + + // lastPacker is a reference to the numberPacker used to pack numbers in the last successful + // verification round. + lastPacker *numberPacker + + // lastTimestamp is the snapshot timestamp of the last successfully-verified snapshot. lastTimestamp time.Time } // NewVerifier returns a new metric verifier that can verify the integrity of snapshots against // the given metric registration data. -func NewVerifier(registration *pb.MetricRegistration) (*Verifier, error) { - knownMetrics := make(map[string]*verifiableMetric) +// It returns a cleanup function that must be called when the Verifier is no longer needed. +func NewVerifier(registration *pb.MetricRegistration) (*Verifier, func(), error) { + globalInternVerifierCreated() + verifier := &Verifier{ + knownMetrics: make(map[string]*verifiableMetric), + permanentPacker: &numberPacker{}, + internMap: make(internedStringMap), + } for _, metric := range registration.GetMetrics() { metricName := metric.GetPrometheusName() - if _, alreadyExists := knownMetrics[metricName]; alreadyExists { - return nil, fmt.Errorf("metric %q registered twice", metricName) + if _, alreadyExists := verifier.knownMetrics[metricName]; alreadyExists { + globalInternVerifierReleased() + return nil, func() {}, fmt.Errorf("metric %q registered twice", metricName) } - verifiableM, err := newVerifiableMetric(metric) + verifiableM, err := newVerifiableMetric(metric, verifier) if err != nil { - return nil, fmt.Errorf("metric %q: %v", metricName, err) + globalInternVerifierReleased() + return nil, func() {}, fmt.Errorf("metric %q: %v", metricName, err) } - knownMetrics[metricName] = verifiableM + verifier.knownMetrics[globalIntern(metricName)] = verifiableM } - return &Verifier{ - knownMetrics: knownMetrics, - }, nil + return verifier, globalInternVerifierReleased, nil } // Verify verifies the integrity of a snapshot against the metric registration data of the Verifier. @@ -292,6 +519,10 @@ func (v *Verifier) Verify(snapshot *Snapshot) error { return fmt.Errorf("snapshot is too old; it is from %v, expected at least %v (%v from now)", snapshot.When, now.Add(-maxExportStaleness), maxExportStaleness) } + // Start critical section. + v.mu.Lock() + defer v.mu.Unlock() + // Metrics checks. fieldsSeen := make(map[string]map[string]struct{}, len(v.knownMetrics)) dataToFieldsSeen := make(map[*Data]string, len(snapshot.Data)) @@ -301,6 +532,7 @@ func (v *Verifier) Verify(snapshot *Snapshot) error { if !found { return fmt.Errorf("snapshot contains unknown metric %q", metricName) } + metricName = globalIntern(metricName) metricFieldsSeen, found := fieldsSeen[metricName] if !found { metricFieldsSeen = make(map[string]struct{}, verifiableM.numFieldCombinations()) @@ -311,22 +543,21 @@ func (v *Verifier) Verify(snapshot *Snapshot) error { } } - // Start the critical section. - v.mu.Lock() - defer v.mu.Unlock() if v.lastTimestamp.After(snapshot.When) { return fmt.Errorf("consecutive snapshots are not chronologically ordered: last verified snapshot was exported at %v, this one is from %v", v.lastTimestamp, snapshot.When) } for _, data := range snapshot.Data { - if err = v.knownMetrics[data.Metric.Name].verifyIncrement(data, dataToFieldsSeen[data]); err != nil { + if err = v.knownMetrics[data.Metric.Name].verifyIncrement(data, dataToFieldsSeen[data], v.lastPacker); err != nil { return fmt.Errorf("metric %q: %v", data.Metric.Name, err) } } // All checks succeeded, update last-seen data. + newPacker := &numberPacker{} v.lastTimestamp = snapshot.When for _, data := range snapshot.Data { - v.knownMetrics[data.Metric.Name].update(data, dataToFieldsSeen[data]) + v.knownMetrics[globalIntern(data.Metric.Name)].update(data, v.internMap.Intern(dataToFieldsSeen[data]), newPacker) } + v.lastPacker = newPacker return nil } diff --git a/pkg/state/types.go b/pkg/state/types.go index 8df2ac64a..b96423e14 100644 --- a/pkg/state/types.go +++ b/pkg/state/types.go @@ -324,6 +324,14 @@ var globalTypeDatabase = map[string]reflect.Type{} // reverseTypeDatabase is a reverse mapping. var reverseTypeDatabase = map[reflect.Type]string{} +// Release releases references to global type databases. +// Must only be called in contexts where they will definitely never be used, +// in order to save memory. +func Release() { + globalTypeDatabase = nil + reverseTypeDatabase = nil +} + // Register registers a type. // // This must be called on init and only done once. diff --git a/runsc/cmd/BUILD b/runsc/cmd/BUILD index 0dde688ff..bf2011b87 100644 --- a/runsc/cmd/BUILD +++ b/runsc/cmd/BUILD @@ -64,6 +64,7 @@ go_library( "//pkg/sentry/kernel", "//pkg/sentry/kernel/auth", "//pkg/sentry/platform", + "//pkg/state", "//pkg/state/pretty", "//pkg/state/statefile", "//pkg/sync", diff --git a/runsc/cmd/metric_server.go b/runsc/cmd/metric_server.go index c91996936..952d327d6 100644 --- a/runsc/cmd/metric_server.go +++ b/runsc/cmd/metric_server.go @@ -28,6 +28,7 @@ import ( "os" "os/signal" "runtime" + "runtime/debug" "runtime/pprof" "strconv" "strings" @@ -38,6 +39,7 @@ import ( "gvisor.dev/gvisor/pkg/atomicbitops" "gvisor.dev/gvisor/pkg/log" "gvisor.dev/gvisor/pkg/prometheus" + "gvisor.dev/gvisor/pkg/state" "gvisor.dev/gvisor/pkg/sync" "gvisor.dev/gvisor/runsc/cmd/util" "gvisor.dev/gvisor/runsc/config" @@ -100,6 +102,9 @@ type servedSandbox struct { // be deleted from the server. // Once set, it is immutable. verifier *prometheus.Verifier + + // cleanupVerifier holds a reference to the cleanup function of the verifier. + cleanupVerifier func() } // sandboxPrometheusLabels returns a set of Prometheus labels that identifies the sandbox running @@ -183,11 +188,12 @@ func (s *servedSandbox) load() (*sandbox.Sandbox, *prometheus.Verifier, error) { if err != nil { return nil, nil, err } - verifier, err := prometheus.NewVerifier(registeredMetrics) + verifier, cleanup, err := prometheus.NewVerifier(registeredMetrics) if err != nil { return nil, nil, err } s.verifier = verifier + s.cleanupVerifier = cleanup } s.labelsWithMetadata = make(map[string]string, len(s.extraLabels)+len(s.sandbox.MetricMetadata)) for k, v := range s.extraLabels { @@ -199,25 +205,36 @@ func (s *servedSandbox) load() (*sandbox.Sandbox, *prometheus.Verifier, error) { return s.sandbox, s.verifier, nil } +func (s *servedSandbox) cleanup() { + s.mu.Lock() + defer s.mu.Unlock() + if s.cleanupVerifier != nil { + s.cleanupVerifier() + } +} + // queryMetrics queries the sandbox for metrics data. func queryMetrics(ctx context.Context, sand *sandbox.Sandbox, verifier *prometheus.Verifier) (*prometheus.Snapshot, error) { ch := make(chan struct { snapshot *prometheus.Snapshot err error }, 1) - defer close(ch) + canceled := make(chan struct{}, 1) + defer close(canceled) go func() { snapshot, err := sand.ExportMetrics() select { + case <-canceled: case ch <- struct { snapshot *prometheus.Snapshot err error }{snapshot, err}: - default: + close(ch) } }() select { case <-ctx.Done(): + canceled <- struct{}{} return nil, ctx.Err() case ret := <-ch: if ret.err != nil { @@ -356,16 +373,19 @@ func (m *MetricServer) refreshSandboxesLocked() { } if !found { log.Warningf("Sandbox %s no longer exists but did not explicitly unregister. Removing it.", sandboxID) + sandbox.cleanup() delete(m.sandboxes, sandboxID) continue } if _, _, err := sandbox.load(); err != nil && err != container.ErrStateFileLocked { log.Warningf("Sandbox %s cannot be loaded, deleting it: %v", sandboxID, err) + sandbox.cleanup() delete(m.sandboxes, sandboxID) continue } if !sandbox.sandbox.IsRunning() { log.Infof("Sandbox %s is no longer running, deleting it.", sandboxID) + sandbox.cleanup() delete(m.sandboxes, sandboxID) continue } @@ -444,6 +464,7 @@ func (m *MetricServer) refreshSandboxesLocked() { if _, _, err := served.load(); err != nil && err != container.ErrStateFileLocked { log.Warningf("Sandbox %q cannot be loaded, ignoring it: %v", sid, err) m.lastStateFileStat[sid] = stat + served.cleanup() continue } m.sandboxes[sid] = served @@ -845,6 +866,8 @@ func logRequest(f func(w http.ResponseWriter, req *http.Request) httpResult) fun http.Error(w, result.err.Error(), result.code) log.Warningf("Request: %s %s: Failed with HTTP code %d: %v", req.Method, req.URL.Path, result.code, result.err) } + // Run GC after every request to keep memory usage as predictable and as flat as possible. + runtime.GC() } } @@ -979,6 +1002,9 @@ func (m *MetricServer) Execute(ctx context.Context, f *flag.FlagSet, args ...any log.Warningf("Profiling HTTP endpoints are exposed; this should only be used for development!") mux.HandleFunc("/runsc-metrics/profile-cpu", logRequest(m.profileCPU)) mux.HandleFunc("/runsc-metrics/profile-heap", logRequest(m.profileHeap)) + } else { + // Disable memory profiling, since we don't expose it. + runtime.MemProfileRate = 0 } mux.HandleFunc("/metrics", logRequest(m.serveMetrics)) mux.HandleFunc("/", logRequest(m.serveIndex)) @@ -994,9 +1020,18 @@ func (m *MetricServer) Execute(ctx context.Context, f *flag.FlagSet, args ...any log.Infof("Wrote PID %d to file %v.", m.pid, m.pidFile) } + // If not modified by the user from the environment, set the Go GC percentage lower than default. + if _, hasEnv := os.LookupEnv("GOGC"); !hasEnv { + debug.SetGCPercent(40) + } + + // Run GC immediately to get rid of all the initialization-related memory bloat and start from + // a clean slate. + state.Release() + runtime.GC() + // Initialization complete. log.Infof("Server serving on %s for root directory %s.", conf.MetricServer, conf.RootDir) - serveErr := m.srv.Serve(listener) log.Infof("Server has stopped accepting requests.") m.mu.Lock()