Skip to content

Commit a619f01

Browse files
authored
all: add tracing (#750)
Add a hidden `--trace` flag that turns on tracing to measure execution time. When set, a trace file is written (by default to trace.out) which can be inspected with `go tool trace trace.out`.
1 parent dd07aa8 commit a619f01

File tree

8 files changed

+146
-28
lines changed

8 files changed

+146
-28
lines changed

internal/boxcli/midcobra/midcobra.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,16 @@ func (ex *midcobraExecutable) AddMiddleware(mids ...Middleware) {
5555

5656
func (ex *midcobraExecutable) Execute(ctx context.Context, args []string) int {
5757
// Ensure cobra uses the same arguments
58-
ex.cmd.SetArgs(args)
58+
ex.cmd.SetContext(ctx)
59+
_ = ex.cmd.ParseFlags(args)
5960

6061
// Run the 'pre' hooks
6162
for _, m := range ex.middlewares {
6263
m.preRun(ex.cmd, args)
6364
}
6465

6566
// Execute the cobra command:
66-
err := ex.cmd.ExecuteContext(ctx)
67+
err := ex.cmd.Execute()
6768

6869
var postRunErr error
6970
var userExecErr *usererr.ExitError
@@ -76,8 +77,8 @@ func (ex *midcobraExecutable) Execute(ctx context.Context, args []string) int {
7677
// run even if the command resulted in an error. This is useful when we still want to clean up
7778
// before the program exists or we want to log something. The error, if any, gets passed
7879
// to the post hook.
79-
for _, m := range ex.middlewares {
80-
m.postRun(ex.cmd, args, postRunErr)
80+
for i := len(ex.middlewares) - 1; i >= 0; i-- {
81+
ex.middlewares[i].postRun(ex.cmd, args, postRunErr)
8182
}
8283

8384
if err != nil {

internal/boxcli/midcobra/telemetry.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"io"
99
"os"
10+
"runtime/trace"
1011
"strings"
1112
"time"
1213

@@ -55,13 +56,19 @@ func (m *telemetryMiddleware) withExecutionID(execID string) Middleware {
5556

5657
func (m *telemetryMiddleware) preRun(cmd *cobra.Command, args []string) {
5758
m.startTime = telemetry.CommandStartTime()
58-
if !m.disabled {
59-
sentry := telemetry.NewSentry(m.opts.SentryDSN)
60-
sentry.Init(m.opts.AppName, m.opts.AppVersion, m.executionID)
59+
60+
ctx := cmd.Context()
61+
defer trace.StartRegion(ctx, "telemetryPreRun").End()
62+
if m.disabled {
63+
trace.Log(ctx, "telemetry", "telemetry is disabled")
64+
return
6165
}
66+
sentry := telemetry.NewSentry(m.opts.SentryDSN)
67+
sentry.Init(m.opts.AppName, m.opts.AppVersion, m.executionID)
6268
}
6369

6470
func (m *telemetryMiddleware) postRun(cmd *cobra.Command, args []string, runErr error) {
71+
defer trace.StartRegion(cmd.Context(), "telemetryPostRun").End()
6572
if m.disabled {
6673
return
6774
}

internal/boxcli/midcobra/trace.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright 2022 Jetpack Technologies Inc and contributors. All rights reserved.
2+
// Use of this source code is governed by the license in the LICENSE file.
3+
4+
package midcobra
5+
6+
import (
7+
"context"
8+
"os"
9+
"runtime/trace"
10+
11+
"github.com/spf13/cobra"
12+
"github.com/spf13/pflag"
13+
)
14+
15+
type TraceMiddleware struct {
16+
executionID string // uuid
17+
tracef *os.File
18+
flag *pflag.Flag
19+
task *trace.Task
20+
}
21+
22+
var _ Middleware = (*DebugMiddleware)(nil)
23+
24+
func (t *TraceMiddleware) AttachToFlag(flags *pflag.FlagSet, flagName string) {
25+
flags.String(flagName, "", "write a trace to a file")
26+
t.flag = flags.Lookup(flagName)
27+
t.flag.Hidden = true
28+
t.flag.NoOptDefVal = "trace.out"
29+
}
30+
31+
func (t *TraceMiddleware) preRun(cmd *cobra.Command, args []string) {
32+
if t == nil {
33+
return
34+
}
35+
path := t.flag.Value.String()
36+
if path == "" {
37+
return
38+
}
39+
var err error
40+
t.tracef, err = os.Create(path)
41+
if err != nil {
42+
panic("error enabling tracing: " + err.Error())
43+
}
44+
if err := trace.Start(t.tracef); err != nil {
45+
panic("error enabling tracing: " + err.Error())
46+
}
47+
48+
var ctx context.Context
49+
ctx, t.task = trace.NewTask(cmd.Context(), "cliCommand")
50+
cmd.SetContext(ctx)
51+
}
52+
53+
func (t *TraceMiddleware) postRun(cmd *cobra.Command, args []string, runErr error) {
54+
if t.tracef == nil {
55+
return
56+
}
57+
t.task.End()
58+
trace.Stop()
59+
if err := t.tracef.Close(); err != nil {
60+
panic("error closing trace file: " + err.Error())
61+
}
62+
}
63+
64+
func (t *TraceMiddleware) withExecutionID(execID string) Middleware {
65+
t.executionID = execID
66+
return t
67+
}

internal/boxcli/root.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ import (
1515
"go.jetpack.io/devbox/internal/debug"
1616
)
1717

18-
var debugMiddleware *midcobra.DebugMiddleware = &midcobra.DebugMiddleware{}
18+
var (
19+
debugMiddleware *midcobra.DebugMiddleware = &midcobra.DebugMiddleware{}
20+
traceMiddleware *midcobra.TraceMiddleware = &midcobra.TraceMiddleware{}
21+
)
1922

2023
type rootCmdFlags struct {
2124
quiet bool
@@ -58,13 +61,15 @@ func RootCmd() *cobra.Command {
5861
command.PersistentFlags().BoolVarP(
5962
&flags.quiet, "quiet", "q", false, "suppresses logs")
6063
debugMiddleware.AttachToFlag(command.PersistentFlags(), "debug")
64+
traceMiddleware.AttachToFlag(command.PersistentFlags(), "trace")
6165

6266
return command
6367
}
6468

6569
func Execute(ctx context.Context, args []string) int {
6670
defer debug.Recover()
6771
exe := midcobra.New(RootCmd())
72+
exe.AddMiddleware(traceMiddleware)
6873
exe.AddMiddleware(midcobra.Telemetry())
6974
exe.AddMiddleware(debugMiddleware)
7075
return exe.Execute(ctx, args)

internal/impl/devbox.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"os"
1212
"os/exec"
1313
"path/filepath"
14+
"runtime/trace"
1415
"strconv"
1516
"strings"
1617

@@ -138,7 +139,10 @@ func (d *Devbox) Generate() error {
138139
}
139140

140141
func (d *Devbox) Shell() error {
141-
if err := d.ensurePackagesAreInstalled(ensure); err != nil {
142+
ctx, task := trace.NewTask(context.Background(), "devboxShell")
143+
defer task.End()
144+
145+
if err := d.ensurePackagesAreInstalled(ctx, ensure); err != nil {
142146
return err
143147
}
144148
fmt.Fprintln(d.writer, "Starting a devbox shell...")
@@ -153,7 +157,7 @@ func (d *Devbox) Shell() error {
153157
return err
154158
}
155159

156-
env, err := d.computeNixEnv()
160+
env, err := d.computeNixEnv(ctx)
157161
if err != nil {
158162
return err
159163
}
@@ -183,15 +187,18 @@ func (d *Devbox) Shell() error {
183187
}
184188

185189
func (d *Devbox) RunScript(cmdName string, cmdArgs []string) error {
186-
if err := d.ensurePackagesAreInstalled(ensure); err != nil {
190+
ctx, task := trace.NewTask(context.Background(), "devboxRun")
191+
defer task.End()
192+
193+
if err := d.ensurePackagesAreInstalled(ctx, ensure); err != nil {
187194
return err
188195
}
189196

190197
if err := d.writeScriptsToFiles(); err != nil {
191198
return err
192199
}
193200

194-
env, err := d.computeNixEnv()
201+
env, err := d.computeNixEnv(ctx)
195202
if err != nil {
196203
return err
197204
}
@@ -228,7 +235,10 @@ func (d *Devbox) ListScripts() []string {
228235
}
229236

230237
func (d *Devbox) PrintEnv() (string, error) {
231-
envs, err := d.computeNixEnv()
238+
ctx, task := trace.NewTask(context.Background(), "devboxPrintEnv")
239+
defer task.End()
240+
241+
envs, err := d.computeNixEnv(ctx)
232242
if err != nil {
233243
return "", err
234244
}
@@ -448,7 +458,9 @@ func (d *Devbox) generateShellFiles() error {
448458
// Note that the shellrc.tmpl template (which sources this environment) does
449459
// some additional processing. The computeNixEnv environment won't necessarily
450460
// represent the final "devbox run" or "devbox shell" environments.
451-
func (d *Devbox) computeNixEnv() (map[string]string, error) {
461+
func (d *Devbox) computeNixEnv(ctx context.Context) (map[string]string, error) {
462+
defer trace.StartRegion(ctx, "computeNixEnv").End()
463+
452464
currentEnv := os.Environ()
453465
env := make(map[string]string, len(currentEnv))
454466
for _, kv := range currentEnv {
@@ -471,7 +483,7 @@ func (d *Devbox) computeNixEnv() (map[string]string, error) {
471483
originalPath = currentEnvPath
472484
}
473485

474-
vaf, err := nix.PrintDevEnv(d.nixShellFilePath(), d.nixFlakesFilePath())
486+
vaf, err := nix.PrintDevEnv(ctx, d.nixShellFilePath(), d.nixFlakesFilePath())
475487
if err != nil {
476488
return nil, err
477489
}

internal/impl/packages.go

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package impl
22

33
import (
4+
"context"
45
"fmt"
56
"os"
67
"os/exec"
78
"path/filepath"
9+
"runtime/trace"
810
"sort"
911
"strings"
1012

@@ -24,6 +26,9 @@ import (
2426

2527
// Add adds the `pkgs` to the config (i.e. devbox.json) and nix profile for this devbox project
2628
func (d *Devbox) Add(pkgs ...string) error {
29+
ctx, task := trace.NewTask(context.Background(), "devboxAdd")
30+
defer task.End()
31+
2732
original := d.cfg.RawPackages
2833
// Check packages are valid before adding.
2934
for _, pkg := range pkgs {
@@ -45,7 +50,7 @@ func (d *Devbox) Add(pkgs ...string) error {
4550
}
4651

4752
d.pluginManager.ApplyOptions(plugin.WithAddMode())
48-
if err := d.ensurePackagesAreInstalled(install); err != nil {
53+
if err := d.ensurePackagesAreInstalled(ctx, install); err != nil {
4954
// if error installing, revert devbox.json
5055
// This is not perfect because there may be more than 1 package being
5156
// installed and we don't know which one failed. But it's better than
@@ -77,6 +82,8 @@ func (d *Devbox) Add(pkgs ...string) error {
7782

7883
// Remove removes the `pkgs` from the config (i.e. devbox.json) and nix profile for this devbox project
7984
func (d *Devbox) Remove(pkgs ...string) error {
85+
ctx, task := trace.NewTask(context.Background(), "devboxRemove")
86+
defer task.End()
8087

8188
// First, save which packages are being uninstalled. Do this before we modify d.cfg.RawPackages below.
8289
uninstalledPackages := lo.Intersect(d.cfg.RawPackages, pkgs)
@@ -99,11 +106,11 @@ func (d *Devbox) Remove(pkgs ...string) error {
99106
return err
100107
}
101108

102-
if err := d.removePackagesFromProfile(uninstalledPackages); err != nil {
109+
if err := d.removePackagesFromProfile(ctx, uninstalledPackages); err != nil {
103110
return err
104111
}
105112

106-
if err := d.ensurePackagesAreInstalled(uninstall); err != nil {
113+
if err := d.ensurePackagesAreInstalled(ctx, uninstall); err != nil {
107114
return err
108115
}
109116

@@ -122,7 +129,9 @@ const (
122129
// ensurePackagesAreInstalled ensures that the nix profile has the packages specified
123130
// in the config (devbox.json). The `mode` is used for user messaging to explain
124131
// what operations are happening, because this function may take time to execute.
125-
func (d *Devbox) ensurePackagesAreInstalled(mode installMode) error {
132+
func (d *Devbox) ensurePackagesAreInstalled(ctx context.Context, mode installMode) error {
133+
defer trace.StartRegion(ctx, "ensurePackages").End()
134+
126135
if err := d.generateShellFiles(); err != nil {
127136
return err
128137
}
@@ -131,7 +140,7 @@ func (d *Devbox) ensurePackagesAreInstalled(mode installMode) error {
131140
}
132141

133142
if featureflag.Flakes.Enabled() {
134-
if err := d.addPackagesToProfile(mode); err != nil {
143+
if err := d.addPackagesToProfile(ctx, mode); err != nil {
135144
return err
136145
}
137146

@@ -145,7 +154,7 @@ func (d *Devbox) ensurePackagesAreInstalled(mode installMode) error {
145154
}
146155

147156
// We need to re-install the packages
148-
if err := d.installNixProfile(); err != nil {
157+
if err := d.installNixProfile(ctx); err != nil {
149158
fmt.Fprintln(d.writer)
150159
return errors.Wrap(err, "apply Nix derivation")
151160
}
@@ -206,7 +215,9 @@ func (d *Devbox) printPackageUpdateMessage(
206215

207216
// installNixProfile installs or uninstalls packages to or from this
208217
// devbox's Nix profile so that it matches what's in development.nix
209-
func (d *Devbox) installNixProfile() (err error) {
218+
func (d *Devbox) installNixProfile(ctx context.Context) (err error) {
219+
defer trace.StartRegion(ctx, "installNixProfile").End()
220+
210221
profileDir, err := d.profilePath()
211222
if err != nil {
212223
return err
@@ -255,15 +266,17 @@ func (d *Devbox) profilePath() (string, error) {
255266
// addPackagesToProfile inspects the packages in devbox.json, checks which of them
256267
// are missing from the nix profile, and then installs each package individually into the
257268
// nix profile.
258-
func (d *Devbox) addPackagesToProfile(mode installMode) error {
269+
func (d *Devbox) addPackagesToProfile(ctx context.Context, mode installMode) error {
270+
defer trace.StartRegion(ctx, "addNixProfilePkgs").End()
271+
259272
if featureflag.Flakes.Disabled() {
260273
return nil
261274
}
262275
if mode == uninstall {
263276
return nil
264277
}
265278

266-
pkgs, err := d.pendingPackagesForInstallation()
279+
pkgs, err := d.pendingPackagesForInstallation(ctx)
267280
if err != nil {
268281
return err
269282
}
@@ -317,7 +330,9 @@ func (d *Devbox) addPackagesToProfile(mode installMode) error {
317330
return nil
318331
}
319332

320-
func (d *Devbox) removePackagesFromProfile(pkgs []string) error {
333+
func (d *Devbox) removePackagesFromProfile(ctx context.Context, pkgs []string) error {
334+
defer trace.StartRegion(ctx, "removeNixProfilePkgs").End()
335+
321336
if !featureflag.Flakes.Enabled() {
322337
return nil
323338
}
@@ -367,7 +382,9 @@ func (d *Devbox) removePackagesFromProfile(pkgs []string) error {
367382
return nil
368383
}
369384

370-
func (d *Devbox) pendingPackagesForInstallation() ([]string, error) {
385+
func (d *Devbox) pendingPackagesForInstallation(ctx context.Context) ([]string, error) {
386+
defer trace.StartRegion(ctx, "pendingPackages").End()
387+
371388
if featureflag.Flakes.Disabled() {
372389
return nil, errors.New("Not implemented for legacy non-flakes devbox")
373390
}

0 commit comments

Comments
 (0)