Introduce conditionally-compiled metrics.

This change introduces metrics which are disabled at compile time by default,
but can be enabled using a specified go tag. To demonstrate this, it replaces
the metrics used to profile the KVM platform to these new ones.

These profiling metrics can be enabled with the "condmetric_profiling" tag
when building runsc.

The motivation behind this is twofold:
- We have observed that using metrics in hot-paths can be quite expensive
  (i.e. syscall counter, timer metrics on KVM platform machine.Get()).
- There are classes of metrics that we generally care about using only on
  specific occasions, and don't need to have on by default.

PiperOrigin-RevId: 464636315
This commit is contained in:
Konstantin Bogomolov
2022-08-01 15:21:48 -07:00
committed by gVisor bot
parent 991841786a
commit 8d3ad7f293
11 changed files with 446 additions and 56 deletions
+22 -1
View File
@@ -5,6 +5,9 @@ package(licenses = ["notice"])
go_library(
name = "metric",
srcs = [
"condmetric_profiling.go",
"condmetric_profiling_fake.go",
"fake_metric.go",
"metric.go",
"metric_unsafe.go",
],
@@ -31,7 +34,10 @@ proto_library(
go_test(
name = "metric_test",
srcs = ["metric_test.go"],
srcs = [
"metric_test.go",
"utils_test.go",
],
library = ":metric",
deps = [
":metric_go_proto",
@@ -40,3 +46,18 @@ go_test(
"@org_golang_google_protobuf//proto:go_default_library",
],
)
go_test(
name = "condmetric_test",
srcs = [
"condmetric_profiling_disabled_test.go",
"condmetric_profiling_enabled_test.go",
"utils_test.go",
],
library = ":metric",
deps = [
":metric_go_proto",
"//pkg/eventchannel",
"@org_golang_google_protobuf//proto:go_default_library",
],
)
+64
View File
@@ -0,0 +1,64 @@
// Copyright 2022 The gVisor 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.
//go:build condmetric_profiling
// +build condmetric_profiling
package metric
// This file defines conditional metrics that are meant to be used when profiling
// runsc during benchmark tests.
// ProfilingUint64Metric is a metric type that is registered and used only when
// the "condmetric_profiling" go tag is specified for go build.
//
// Otherwise it is exactly like a Uint64Metric.
type ProfilingUint64Metric = Uint64Metric
// ProfilingDistributionMetric is a metric type that is registered and used only
// when the "condmetric_profiling" go tag is specified for go build.
//
// Otherwise it is exactly like a DistributionMetric.
type ProfilingDistributionMetric = DistributionMetric
// ProfilingTimerMetric is a metric type that is registered and used only when
// the "condmetric_profiling" go tag is specified for go build.
//
// Otherwise it is exactly like a TimerMetric.
type ProfilingTimerMetric = TimerMetric
// NewProfilingUint64Metric is equivalent to NewUint64Metric except it creates a
// ProfilingUint64Metric
var NewProfilingUint64Metric = NewUint64Metric
// MustCreateNewProfilingUint64Metric is equivalent to MustCreateNewUint64Metric
// except it creates a ProfilingUint64Metric.
var MustCreateNewProfilingUint64Metric = MustCreateNewUint64Metric
// NewProfilingDistributionMetric is equivalent to NewDistributionMetric except
// it creates a ProfilingDistributionMetric.
var NewProfilingDistributionMetric = NewDistributionMetric
// MustCreateNewProfilingDistributionMetric is equivalent to
// MustCreateNewDistributionMetric except it creates a
// ProfilingDistributionMetric.
var MustCreateNewProfilingDistributionMetric = MustCreateNewDistributionMetric
// NewProfilingTimerMetric is equivalent to NewTimerMetric except it creates a
// ProfilingTimerMetric.
var NewProfilingTimerMetric = NewTimerMetric
// MustCreateNewProfilingTimerMetric is equivalent to MustCreateNewTimerMetric
// except it creates a ProfilingTimerMetric.
var MustCreateNewProfilingTimerMetric = MustCreateNewTimerMetric
@@ -0,0 +1,61 @@
// Copyright 2022 The gVisor 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.
//go:build !condmetric_profiling
// +build !condmetric_profiling
package metric
import (
"testing"
pb "gvisor.dev/gvisor/pkg/metric/metric_go_proto"
)
func TestProfilingMetricsDisabled(t *testing.T) {
defer resetTest()
_, err := NewProfilingUint64Metric("counterM", false, pb.MetricMetadata_UNITS_NONE, "One uint64 metric")
if err != nil {
t.Fatalf("NewProfilingUint64Metric got err %v want nil", err)
}
bucketer := NewExponentialBucketer(3, 2, 0, 1)
_, err = NewProfilingDistributionMetric("distribM", false, bucketer, pb.MetricMetadata_UNITS_NANOSECONDS, "One distribution metric")
if err != nil {
t.Fatalf("NewProfilingDistributionMetric got err %v want nil", err)
}
_, err = NewProfilingTimerMetric("timerM", bucketer, "One timer metric")
if err != nil {
t.Fatalf("NewProfilingTimerMetric got err %v want nil", err)
}
if err := Initialize(); err != nil {
t.Fatalf("Initialize(): %s", err)
}
if len(emitter) != 1 {
t.Fatalf("Initialize emitted %d events want 1", len(emitter))
}
mr, ok := emitter[0].(*pb.MetricRegistration)
if !ok {
t.Fatalf("emitter %v got %T want pb.MetricRegistration", emitter[0], emitter[0])
}
if len(mr.Metrics) != 0 {
t.Errorf("MetricRegistration got %d metrics want %d", len(mr.Metrics), 0)
}
}
@@ -0,0 +1,61 @@
// Copyright 2022 The gVisor 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.
//go:build condmetric_profiling
// +build condmetric_profiling
package metric
import (
"testing"
pb "gvisor.dev/gvisor/pkg/metric/metric_go_proto"
)
func TestProfilingMetricsEnabled(t *testing.T) {
defer resetTest()
_, err := NewProfilingUint64Metric("counterM", false, pb.MetricMetadata_UNITS_NONE, "One uint64 metric")
if err != nil {
t.Fatalf("NewProfilingUint64Metric got err %v want nil", err)
}
bucketer := NewExponentialBucketer(3, 2, 0, 1)
_, err = NewProfilingDistributionMetric("distribM", false, bucketer, pb.MetricMetadata_UNITS_NANOSECONDS, "One distribution metric")
if err != nil {
t.Fatalf("NewProfilingDistributionMetric got err %v want nil", err)
}
_, err = NewProfilingTimerMetric("timerM", bucketer, "One timer metric")
if err != nil {
t.Fatalf("NewProfilingTimerMetric got err %v want nil", err)
}
if err := Initialize(); err != nil {
t.Fatalf("Initialize(): %s", err)
}
if len(emitter) != 1 {
t.Fatalf("Initialize emitted %d events want 1", len(emitter))
}
mr, ok := emitter[0].(*pb.MetricRegistration)
if !ok {
t.Fatalf("emitter %v got %T want pb.MetricRegistration", emitter[0], emitter[0])
}
if len(mr.Metrics) != 3 {
t.Errorf("MetricRegistration got %d metrics want %d", len(mr.Metrics), 3)
}
}
+61
View File
@@ -0,0 +1,61 @@
// Copyright 2022 The gVisor 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.
//go:build !condmetric_profiling
// +build !condmetric_profiling
package metric
// ProfilingUint64Metric is a metric type that is registered and used only when
// the "condmetric_profiling" go tag is specified when building runsc.
//
// Otherwise it is exactly like a Uint64Metric.
type ProfilingUint64Metric = FakeUint64Metric
// ProfilingDistributionMetric is a metric type that is registered and used only
// when the "condmetric_profiling" go tag is specified when building runsc.
//
// Otherwise it is exactly like a DistributionMetric.
type ProfilingDistributionMetric = FakeDistributionMetric
// ProfilingTimerMetric is a metric type that is registered and used only when
// the "condmetric_profiling" go tag is specified when building runsc.
//
// Otherwise it is exactly like a TimerMetric.
type ProfilingTimerMetric = FakeTimerMetric
// NewProfilingUint64Metric is equivalent to NewUint64Metric except it creates a
// ProfilingUint64Metric
var NewProfilingUint64Metric = NewFakeUint64Metric
// MustCreateNewProfilingUint64Metric is equivalent to MustCreateNewUint64Metric
// except it creates a ProfilingUint64Metric.
var MustCreateNewProfilingUint64Metric = MustCreateNewFakeUint64Metric
// NewProfilingDistributionMetric is equivalent to NewDistributionMetric except
// it creates a ProfilingDistributionMetric.
var NewProfilingDistributionMetric = NewFakeDistributionMetric
// MustCreateNewProfilingDistributionMetric is equivalent to
// MustCreateNewDistributionMetric except it creates a
// ProfilingDistributionMetric.
var MustCreateNewProfilingDistributionMetric = MustCreateNewFakeDistributionMetric
// NewProfilingTimerMetric is equivalent to NewTimerMetric except it creates a
// ProfilingTimerMetric.
var NewProfilingTimerMetric = NewFakeTimerMetric
// MustCreateNewProfilingTimerMetric is equivalent to MustCreateNewTimerMetric
// except it creates a ProfilingTimerMetric.
var MustCreateNewProfilingTimerMetric = MustCreateNewFakeTimerMetric
+104
View File
@@ -0,0 +1,104 @@
// Copyright 2022 The gVisor 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 metric
import pb "gvisor.dev/gvisor/pkg/metric/metric_go_proto"
// FakeUint64Metric is a type that implements all the methods of a Uint64Metric
// as a no-op.
type FakeUint64Metric struct{}
// FakeDistributionMetric is a type that implements all the methods of a
// DistributionMetric as a no-op.
type FakeDistributionMetric struct{}
// FakeTimerMetric is a type that implements all the methods of a TimerMetric
// as a no-op.
type FakeTimerMetric struct{}
// FakeTimedOperation is a type that implements all the methods of a
// TimedOperation as a no-op.
type FakeTimedOperation struct{}
// Value from a FakeUint64Metric always returns a meaningless value.
//
//go:nosplit
func (m *FakeUint64Metric) Value(fieldValues ...string) uint64 {
return 0
}
// Increment on a FakeUint64Metric does nothing.
//
//go:nosplit
func (m *FakeUint64Metric) Increment(fieldValues ...string) {}
// IncrementBy on a FakeUint64Metric does nothing.
//
//go:nosplit
func (m *FakeUint64Metric) IncrementBy(v uint64, fieldValues ...string) {}
// AddSample on a FakeUint64Metric does nothing.
//
//go:nosplit
func (d *FakeDistributionMetric) AddSample(sample int64, fields ...string) {}
// Start on a FakeUint64Metric returns a FakeTimedOperation struct, which does
// nothing and does not keep the time.
//
//go:nosplit
func (t *FakeTimerMetric) Start(fields ...string) FakeTimedOperation {
return FakeTimedOperation{}
}
// Finish on a FakeTimedOperation does nothing.
//
//go:nosplit
func (o FakeTimedOperation) Finish(extraFields ...string) {}
// NewFakeUint64Metric is equivalent to NewUint64Metric except it creates a
// FakeUint64Metric
func NewFakeUint64Metric(name string, sync bool, units pb.MetricMetadata_Units, description string, fields ...Field) (*FakeUint64Metric, error) {
return &FakeUint64Metric{}, nil
}
// MustCreateNewFakeUint64Metric is equivalent to MustCreateNewUint64Metric
// except it creates a FakeUint64Metric.
func MustCreateNewFakeUint64Metric(name string, sync bool, description string, fields ...Field) *FakeUint64Metric {
return &FakeUint64Metric{}
}
// NewFakeDistributionMetric is equivalent to NewDistributionMetric except
// it creates a FakeDistributionMetric.
func NewFakeDistributionMetric(name string, sync bool, bucketer Bucketer, unit pb.MetricMetadata_Units, description string, fields ...Field) (*FakeDistributionMetric, error) {
return &FakeDistributionMetric{}, nil
}
// MustCreateNewFakeDistributionMetric is equivalent to
// MustCreateNewDistributionMetric except it creates a FakeDistributionMetric.
func MustCreateNewFakeDistributionMetric(name string, sync bool, bucketer Bucketer, unit pb.MetricMetadata_Units, description string, fields ...Field) *FakeDistributionMetric {
return &FakeDistributionMetric{}
}
// NewFakeTimerMetric is equivalent to NewTimerMetric except it creates a
// FakeTimerMetric.
func NewFakeTimerMetric(name string, nanoBucketer Bucketer, description string, fields ...Field) (*FakeTimerMetric, error) {
return &FakeTimerMetric{}, nil
}
// MustCreateNewFakeTimerMetric is equivalent to MustCreateNewTimerMetric
// except it creates a FakeTimerMetric.
func MustCreateNewFakeTimerMetric(name string, nanoBucketer Bucketer, description string, fields ...Field) *FakeTimerMetric {
return &FakeTimerMetric{}
}
+2 -2
View File
@@ -827,8 +827,8 @@ type metricSet struct {
}
// makeMetricSet returns a new metricSet.
func makeMetricSet() metricSet {
return metricSet{
func makeMetricSet() *metricSet {
return &metricSet{
uint64Metrics: make(map[string]customUint64Metric),
distributionMetrics: make(map[string]*DistributionMetric),
finished: make([]stageTiming, 0, len(allStages)),
+6 -45
View File
@@ -22,49 +22,10 @@ import (
"time"
"google.golang.org/protobuf/proto"
"gvisor.dev/gvisor/pkg/eventchannel"
pb "gvisor.dev/gvisor/pkg/metric/metric_go_proto"
"gvisor.dev/gvisor/pkg/sync"
)
// sliceEmitter implements eventchannel.Emitter by appending all messages to a
// slice.
type sliceEmitter []proto.Message
// Emit implements eventchannel.Emitter.Emit.
func (s *sliceEmitter) Emit(msg proto.Message) (bool, error) {
*s = append(*s, msg)
return false, nil
}
// Emit implements eventchannel.Emitter.Close.
func (s *sliceEmitter) Close() error {
return nil
}
// Reset clears all events in s.
func (s *sliceEmitter) Reset() {
*s = nil
}
// emitter is the eventchannel.Emitter used for all tests. Package eventchannel
// doesn't allow removing Emitters, so we must use one global emitter for all
// test cases.
var emitter sliceEmitter
func init() {
reset()
eventchannel.AddEmitter(&emitter)
}
// reset clears all global state in the metric package.
func reset() {
initialized = false
allMetrics = makeMetricSet()
emitter.Reset()
}
const (
fooDescription = "Foo!"
barDescription = "Bar Baz"
@@ -73,7 +34,7 @@ const (
)
func TestInitialize(t *testing.T) {
defer reset()
defer resetTest()
_, err := NewUint64Metric("/foo", false, pb.MetricMetadata_UNITS_NONE, fooDescription)
if err != nil {
@@ -181,7 +142,7 @@ func TestInitialize(t *testing.T) {
}
func TestDisable(t *testing.T) {
defer reset()
defer resetTest()
_, err := NewUint64Metric("/foo", false, pb.MetricMetadata_UNITS_NONE, fooDescription)
if err != nil {
@@ -217,7 +178,7 @@ func TestDisable(t *testing.T) {
}
func TestEmitMetricUpdate(t *testing.T) {
defer reset()
defer resetTest()
foo, err := NewUint64Metric("/foo", false, pb.MetricMetadata_UNITS_NONE, fooDescription)
if err != nil {
@@ -411,7 +372,7 @@ func TestEmitMetricUpdate(t *testing.T) {
}
func TestEmitMetricUpdateWithFields(t *testing.T) {
defer reset()
defer resetTest()
field := Field{
name: "weirdness_type",
@@ -498,7 +459,7 @@ func TestEmitMetricUpdateWithFields(t *testing.T) {
}
func TestMetricUpdateStageTiming(t *testing.T) {
defer reset()
defer resetTest()
expectedTimings := map[InitStage]struct{ min, max time.Duration }{}
measureStage := func(stage InitStage, body func()) {
@@ -642,7 +603,7 @@ func TestMetricUpdateStageTiming(t *testing.T) {
}
func TestTimerMetric(t *testing.T) {
defer reset()
defer resetTest()
// This bucketer just has 2 finite buckets: [0, 500ms) and [500ms, 1s).
bucketer := NewExponentialBucketer(2, uint64((500 * time.Millisecond).Nanoseconds()), 0, 1)
field1 := NewField("field1", []string{"foo", "bar"})
+57
View File
@@ -0,0 +1,57 @@
// Copyright 2022 The gVisor 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 metric
import (
"google.golang.org/protobuf/proto"
"gvisor.dev/gvisor/pkg/eventchannel"
)
// sliceEmitter implements eventchannel.Emitter by appending all messages to a
// slice.
type sliceEmitter []proto.Message
// Emit implements eventchannel.Emitter.Emit.
func (s *sliceEmitter) Emit(msg proto.Message) (bool, error) {
*s = append(*s, msg)
return false, nil
}
// Emit implements eventchannel.Emitter.Close.
func (s *sliceEmitter) Close() error {
return nil
}
// Reset clears all events in s.
func (s *sliceEmitter) Reset() {
*s = nil
}
// emitter is the eventchannel.Emitter used for all tests. Package eventchannel
// doesn't allow removing Emitters, so we must use one global emitter for all
// test cases.
var emitter sliceEmitter
func init() {
resetTest()
eventchannel.AddEmitter(&emitter)
}
func resetTest() {
initialized = false
allMetrics = makeMetricSet()
emitter.Reset()
}
+2 -2
View File
@@ -600,12 +600,12 @@ type Task struct {
var (
// syscallCounter is a metric that tracks how many syscalls the sentry has
// executed.
syscallCounter = metric.MustCreateNewUint64Metric(
syscallCounter = metric.MustCreateNewProfilingUint64Metric(
"/task/syscalls", false, "The number of syscalls the sentry has executed for the user.")
// faultCounter is a metric that tracks how many faults the sentry has had to
// handle.
faultCounter = metric.MustCreateNewUint64Metric(
faultCounter = metric.MustCreateNewProfilingUint64Metric(
"/task/faults", false, "The number of faults the sentry has handled.")
)
+6 -6
View File
@@ -106,32 +106,32 @@ const (
var (
// hostExitCounter is a metric that tracks how many times the sentry
// performed a host to guest world switch.
hostExitCounter = metric.MustCreateNewUint64Metric(
hostExitCounter = metric.MustCreateNewProfilingUint64Metric(
"/kvm/host_exits", false, "The number of times the sentry performed a host to guest world switch.")
// userExitCounter is a metric that tracks how many times the sentry has
// had an exit from userspace. Analogous to vCPU.userExits.
userExitCounter = metric.MustCreateNewUint64Metric(
userExitCounter = metric.MustCreateNewProfilingUint64Metric(
"/kvm/user_exits", false, "The number of times the sentry has had an exit from userspace.")
// interruptCounter is a metric that tracks how many times execution returned
// to the KVM host to handle a pending signal.
interruptCounter = metric.MustCreateNewUint64Metric(
interruptCounter = metric.MustCreateNewProfilingUint64Metric(
"/kvm/interrupts", false, "The number of times the signal handler was invoked.")
// mmapCallCounter is a metric that tracks how many times the function
// seccompMmapSyscall has been called.
mmapCallCounter = metric.MustCreateNewUint64Metric(
mmapCallCounter = metric.MustCreateNewProfilingUint64Metric(
"/kvm/mmap_calls", false, "The number of times seccompMmapSyscall has been called.")
// getVCPUCounter is a metric that tracks how many times different paths of
// machine.Get() are triggered.
getVCPUCounter = metric.MustCreateNewUint64Metric(
getVCPUCounter = metric.MustCreateNewProfilingUint64Metric(
"/kvm/get_vcpu", false, "The number of times that machine.Get() was called, split by path the function took.",
metric.NewField("acquisition_type", []string{"fast_reused", "reused", "unused", "stolen"}))
// asInvalidateDuration are durations of calling addressSpace.invalidate().
asInvalidateDuration = metric.MustCreateNewTimerMetric("/kvm/address_space_invalidate",
asInvalidateDuration = metric.MustCreateNewProfilingTimerMetric("/kvm/address_space_invalidate",
metric.NewExponentialBucketer(15, uint64(time.Nanosecond*100), 1, 2),
"Duration of calling addressSpace.invalidate().")
)