summaryrefslogtreecommitdiffhomepage
path: root/pkg/sentry/time/sampler.go
blob: 2140a99b766c6b750daa10cdf1e75995c645da43 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
// Copyright 2018 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 time

import (
	"errors"

	"gvisor.googlesource.com/gvisor/pkg/log"
)

const (
	// defaultOverheadTSC is the default estimated syscall overhead in TSC cycles.
	// It is further refined as syscalls are made.
	defaultOverheadCycles = 1 * 1000

	// maxOverheadCycles is the maximum allowed syscall overhead in TSC cycles.
	maxOverheadCycles = 100 * defaultOverheadCycles

	// maxSampleLoops is the maximum number of times to try to get a clock sample
	// under the expected overhead.
	maxSampleLoops = 5

	// maxSamples is the maximum number of samples to collect.
	maxSamples = 10
)

// errOverheadTooHigh is returned from sampler.Sample if the syscall
// overhead is too high.
var errOverheadTooHigh = errors.New("time syscall overhead exceeds maximum")

// TSCValue is a value from the TSC.
type TSCValue int64

// Rdtsc reads the TSC.
//
// Intel SDM, Vol 3, Ch 17.15:
// "The RDTSC instruction reads the time-stamp counter and is guaranteed to
// return a monotonically increasing unique value whenever executed, except for
// a 64-bit counter wraparound. Intel guarantees that the time-stamp counter
// will not wraparound within 10 years after being reset."
//
// We use int64, so we have 5 years before wrap-around.
func Rdtsc() TSCValue

// ReferenceNS are nanoseconds in the reference clock domain.
// int64 gives us ~290 years before this overflows.
type ReferenceNS int64

// Magnitude returns the absolute value of r.
func (r ReferenceNS) Magnitude() ReferenceNS {
	if r < 0 {
		return -r
	}
	return r
}

// cycleClock is a TSC-based cycle clock.
type cycleClock interface {
	// Cycles returns a count value from the TSC.
	Cycles() TSCValue
}

// tscCycleClock is a cycleClock that uses the real TSC.
type tscCycleClock struct{}

// Cycles implements cycleClock.Cycles.
func (tscCycleClock) Cycles() TSCValue {
	return Rdtsc()
}

// sample contains a sample from the reference clock, with TSC values from
// before and after the reference clock value was captured.
type sample struct {
	before TSCValue
	after  TSCValue
	ref    ReferenceNS
}

// Overhead returns the sample overhead in TSC cycles.
func (s *sample) Overhead() TSCValue {
	return s.after - s.before
}

// referenceClocks collects individual samples from a reference clock ID and
// TSC.
type referenceClocks interface {
	cycleClock

	// Sample returns a single sample from the reference clock ID.
	Sample(c ClockID) (sample, error)
}

// sampler collects samples from a reference system clock, minimizing
// the overhead in each sample.
type sampler struct {
	// clockID is the reference clock ID (e.g., CLOCK_MONOTONIC).
	clockID ClockID

	// clocks provides raw samples.
	clocks referenceClocks

	// overhead is the estimated sample overhead in TSC cycles.
	overhead TSCValue

	// samples is a ring buffer of the latest samples collected.
	samples []sample
}

// newSampler creates a sampler for clockID.
func newSampler(c ClockID) *sampler {
	return &sampler{
		clockID:  c,
		clocks:   syscallTSCReferenceClocks{},
		overhead: defaultOverheadCycles,
	}
}

// Reset discards previously collected clock samples.
func (s *sampler) Reset() {
	s.overhead = defaultOverheadCycles
	s.samples = []sample{}
}

// lowOverheadSample returns a reference clock sample with minimized syscall overhead.
func (s *sampler) lowOverheadSample() (sample, error) {
	for {
		for i := 0; i < maxSampleLoops; i++ {
			samp, err := s.clocks.Sample(s.clockID)
			if err != nil {
				return sample{}, err
			}

			if samp.before > samp.after {
				log.Warningf("TSC went backwards: %v > %v", samp.before, samp.after)
				continue
			}

			if samp.Overhead() <= s.overhead {
				return samp, nil
			}
		}

		// Couldn't get a sample with the current overhead. Increase it.
		newOverhead := 2 * s.overhead
		if newOverhead > maxOverheadCycles {
			// We'll give it one more shot with the max overhead.

			if s.overhead == maxOverheadCycles {
				return sample{}, errOverheadTooHigh
			}

			newOverhead = maxOverheadCycles
		}

		s.overhead = newOverhead
		log.Debugf("Time: Adjusting syscall overhead up to %v", s.overhead)
	}
}

// Sample collects a reference clock sample.
func (s *sampler) Sample() error {
	sample, err := s.lowOverheadSample()
	if err != nil {
		return err
	}

	s.samples = append(s.samples, sample)
	if len(s.samples) > maxSamples {
		s.samples = s.samples[1:]
	}

	// If the 4 most recent samples all have an overhead less than half the
	// expected overhead, adjust downwards.
	if len(s.samples) < 4 {
		return nil
	}

	for _, sample := range s.samples[len(s.samples)-4:] {
		if sample.Overhead() > s.overhead/2 {
			return nil
		}
	}

	s.overhead -= s.overhead / 8
	log.Debugf("Time: Adjusting syscall overhead down to %v", s.overhead)

	return nil
}

// Syscall returns the current raw reference time without storing TSC
// samples.
func (s *sampler) Syscall() (ReferenceNS, error) {
	sample, err := s.clocks.Sample(s.clockID)
	if err != nil {
		return 0, err
	}

	return sample.ref, nil
}

// Cycles returns a raw TSC value.
func (s *sampler) Cycles() TSCValue {
	return s.clocks.Cycles()
}

// Range returns the widest range of clock samples available.
func (s *sampler) Range() (sample, sample, bool) {
	if len(s.samples) < 2 {
		return sample{}, sample{}, false
	}

	return s.samples[0], s.samples[len(s.samples)-1], true
}