Skip to content

Commit 58415ad

Browse files
EtiennePerotgvisor-bot
authored andcommitted
starttime: Get process start time from /proc/self/status if possible.
This more accurately determines when the process was started, since it is not affected by the overhead of running linked library `init` code and initializing `runsc`'s CLI code. Additionally, when `runsc` execs itself, preserve this time across the exec. Also move the code that determines the start time earlier in the file. PiperOrigin-RevId: 752025678
1 parent 90ce1ab commit 58415ad

File tree

7 files changed

+108
-14
lines changed

7 files changed

+108
-14
lines changed

pkg/timing/timing.go

+21-4
Original file line numberDiff line numberDiff line change
@@ -103,16 +103,24 @@ func (s *Timeline) Reached(name string) {
103103
if s == nil {
104104
return
105105
}
106-
now := time.Now()
106+
s.ReachedAt(name, time.Now())
107+
}
108+
109+
// ReachedAt records a new midpoint on the root Timeline of the Timer with
110+
// the given timestamp.
111+
func (s *Timeline) ReachedAt(name string, when time.Time) {
112+
if s == nil {
113+
return
114+
}
107115
s.midpoints = append(s.midpoints, MidPoint{
108-
when: now,
116+
when: when,
109117
name: name,
110118
})
111119
if log.IsLogging(log.Debug) {
112120
if s.timer != nil {
113-
log.Debugf("Timer for %s: Timeline %s reached midpoint %s at %s (unix nanos: %d)", s.timer.root.name, s.fullName, name, now.Format(fullTimestampFormat), now.UnixNano())
121+
log.Debugf("Timer for %s: Timeline %s reached midpoint %s at %s (unix nanos: %d)", s.timer.root.name, s.fullName, name, when.Format(fullTimestampFormat), when.UnixNano())
114122
} else {
115-
log.Debugf("Orphaned timeline %s reached midpoint %s at %s (unix nanos: %d)", s.name, name, now.Format(fullTimestampFormat), now.UnixNano())
123+
log.Debugf("Orphaned timeline %s reached midpoint %s at %s (unix nanos: %d)", s.name, name, when.Format(fullTimestampFormat), when.UnixNano())
116124
}
117125
}
118126
}
@@ -432,6 +440,15 @@ func (t *Timer) Reached(name string) {
432440
t.root.Reached(name)
433441
}
434442

443+
// ReachedAt records a new midpoint on the root Timeline of the Timer with
444+
// the given timestamp.
445+
func (t *Timer) ReachedAt(name string, when time.Time) {
446+
if t == nil {
447+
return
448+
}
449+
t.root.ReachedAt(name, when)
450+
}
451+
435452
// Fork creates a new Timeline that is a child of the root Timeline of this
436453
// Timer.
437454
// The returned Timeline is initially owned by the caller, but may be passed

runsc/cli/main.go

+10-3
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ var (
6262

6363
// Main is the main entrypoint.
6464
func Main() {
65+
// Set the start time as soon as possible.
66+
startTime := starttime.Get()
67+
6568
// Register all commands.
6669
forEachCmd(subcommands.Register)
6770

@@ -133,9 +136,6 @@ func Main() {
133136
// case that does not occur.
134137
_ = time.Local.String()
135138

136-
// Set the start time as soon as possible.
137-
startTime := starttime.Get()
138-
139139
var emitters log.MultiEmitter
140140
if *debugLogFD > -1 {
141141
f := os.NewFile(uintptr(*debugLogFD), "debug log file")
@@ -197,6 +197,13 @@ func Main() {
197197
log.Debugf("Page size: 0x%x (%d bytes)", os.Getpagesize(), os.Getpagesize())
198198
log.Infof("Args: %v", os.Args)
199199
conf.Log()
200+
if log.IsLogging(log.Debug) {
201+
if goStartTime := starttime.GoStartTime(); goStartTime.Equal(startTime) {
202+
log.Debugf("Go started execution at %s. Could not measure process spawn time (no /proc/self/status?)", goStartTime.Format("15:04:05.000000"))
203+
} else {
204+
log.Debugf("runsc process spawned at %v, Go started execution at %s. Startup overhead: %v", startTime.Format("15:04:05.000000"), goStartTime.Format("15:04:05.000000"), goStartTime.Sub(startTime))
205+
}
206+
}
200207
log.Infof(delimString)
201208

202209
if *coverageFD >= 0 {

runsc/cmd/BUILD

+1
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ go_library(
116116
"//runsc/mitigate",
117117
"//runsc/profile",
118118
"//runsc/specutils",
119+
"//runsc/starttime",
119120
"@com_github_google_subcommands//:go_default_library",
120121
"@com_github_moby_sys_capability//:go_default_library",
121122
"@com_github_opencontainers_runtime_spec//specs-go:go_default_library",

runsc/cmd/cmd.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"gvisor.dev/gvisor/pkg/fd"
2828
"gvisor.dev/gvisor/pkg/log"
2929
"gvisor.dev/gvisor/runsc/specutils"
30+
"gvisor.dev/gvisor/runsc/starttime"
3031
)
3132

3233
// intFlags can be used with int flags that appear multiple times. It supports
@@ -90,7 +91,7 @@ func setCapsAndCallSelf(args []string, caps *specs.LinuxCapabilities) error {
9091
binPath := specutils.ExePath
9192

9293
log.Infof("Execve %q again, bye!", binPath)
93-
err := unix.Exec(binPath, args, os.Environ())
94+
err := unix.Exec(binPath, args, starttime.AppendEnviron(os.Environ()))
9495
return fmt.Errorf("error executing %s: %v", binPath, err)
9596
}
9697

@@ -116,6 +117,6 @@ func callSelfAsNobody(args []string) error {
116117
binPath := specutils.ExePath
117118

118119
log.Infof("Execve %q again, bye!", binPath)
119-
err := unix.Exec(binPath, args, os.Environ())
120+
err := unix.Exec(binPath, args, starttime.AppendEnviron(os.Environ()))
120121
return fmt.Errorf("error executing %s: %v", binPath, err)
121122
}

runsc/starttime/BUILD

+1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ go_library(
1111
"starttime.go",
1212
],
1313
visibility = ["//runsc:__subpackages__"],
14+
deps = ["//pkg/timing"],
1415
)

runsc/starttime/starttime.go

+70-5
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,84 @@
1717
package starttime
1818

1919
import (
20+
"fmt"
21+
"os"
22+
"strconv"
23+
"strings"
2024
"sync"
2125
"time"
26+
27+
"gvisor.dev/gvisor/pkg/timing"
2228
)
2329

2430
var (
25-
setOnce sync.Once
26-
startTime time.Time
31+
setOnce sync.Once
32+
processStart time.Time
33+
goStartTime time.Time
34+
envStartTime time.Time
2735
)
2836

29-
// Get returns the time the `runsc` command started.
37+
// envStartTimeKey is the environment variable that holds the time the `runsc`
38+
// command started. This is used to track the actual start time across
39+
// re-execs of `runsc`.
40+
const envStartTimeKey = "RUNSC_START_TIME_NANOS"
41+
42+
// Get returns the time the `runsc` command started on a best-effort basis.
43+
// If the RUNSC_START_TIME_NANOS environment variable is set, it is used.
44+
// Otherwise, it tries to get the time from /proc/self/status.
45+
// If neither is available, it returns the time the function was first called.
3046
func Get() time.Time {
3147
setOnce.Do(func() {
32-
startTime = time.Now()
48+
goStartTime = time.Now()
49+
if startTimeStr, found := os.LookupEnv(envStartTimeKey); found {
50+
if startTime, err := strconv.ParseInt(startTimeStr, 10, 64); err == nil {
51+
envStartTime = time.Unix(0, startTime)
52+
return // No need to check /proc/self/status.
53+
}
54+
}
55+
if st, err := os.Stat("/proc/self/status"); err == nil {
56+
processStart = st.ModTime()
57+
}
3358
})
34-
return startTime
59+
if !envStartTime.IsZero() {
60+
return envStartTime
61+
}
62+
if !processStart.IsZero() {
63+
return processStart
64+
}
65+
return goStartTime
66+
}
67+
68+
// GoStartTime returns the time the `runsc` command's Go code started.
69+
func GoStartTime() time.Time {
70+
Get()
71+
return goStartTime
72+
}
73+
74+
// Timer returns a Timer object that is rooted at `runsc` execution start.
75+
// If `runsc` was re-exec'd, this timer will have a midpoint called "re-exec"
76+
// that corresponds to the time of the re-exec.
77+
func Timer(name string) *timing.Timer {
78+
timer := timing.New(name, Get())
79+
if !envStartTime.IsZero() {
80+
if !processStart.IsZero() {
81+
timer.ReachedAt("re-exec", processStart)
82+
} else {
83+
timer.ReachedAt("re-exec", goStartTime)
84+
}
85+
}
86+
return timer
87+
}
88+
89+
// AppendEnviron appends the RUNSC_START_TIME_NANOS environment variable to
90+
// the given environment, if it is not already present. Otherwise, it is
91+
// preserved.
92+
func AppendEnviron(env []string) []string {
93+
const envVarPrefix = envStartTimeKey + "="
94+
for _, e := range env {
95+
if strings.HasPrefix(e, envVarPrefix) {
96+
return env
97+
}
98+
}
99+
return append(env, fmt.Sprintf("%s=%d", envStartTimeKey, Get().UnixNano()))
35100
}

test/root/runsc_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"os"
2222
"os/exec"
2323
"path/filepath"
24+
"regexp"
2425
"strconv"
2526
"strings"
2627
"testing"
@@ -171,6 +172,7 @@ func TestSandboxProcessEnv(t *testing.T) {
171172
if err != nil {
172173
t.Fatal(err)
173174
}
175+
got = regexp.MustCompile("(^|\x00)RUNSC_START_TIME_NANOS=\\d+\x00").ReplaceAll(got, []byte("$1"))
174176
if len(got) != 0 && string(got) != "GLIBC_TUNABLES=glibc.pthread.rseq=0\x00" {
175177
t.Errorf("sandbox process's environment is not empty: got %s (%v)", string(got), got)
176178
}

0 commit comments

Comments
 (0)