cmd/govulncheck: add -cpuprofile flag

Most of the time is spent in callGraph:
5s in CHA, 8s in VTA(1), 5s in VTA(2) when analyzing k8s.

Change-Id: If428095c68a8a5d47e9b709e02161d5f22952807
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/457955
Reviewed-by: Tim King <taking@google.com>
Run-TryBot: Alan Donovan <adonovan@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/cmd/govulncheck/main.go b/cmd/govulncheck/main.go
index 96c4a2b..2eefce3 100644
--- a/cmd/govulncheck/main.go
+++ b/cmd/govulncheck/main.go
@@ -10,6 +10,8 @@
 	"fmt"
 	"os"
 	"path/filepath"
+	"runtime/pprof"
+
 	"strings"
 
 	"golang.org/x/tools/go/buildutil"
@@ -24,7 +26,9 @@
 	jsonFlag    = flag.Bool("json", false, "output JSON")
 	verboseFlag = flag.Bool("v", false, "print a full call stack for each vulnerability")
 	testFlag    = flag.Bool("test", false, "analyze test files. Only valid for source code.")
-	tagsFlag    buildutil.TagsFlag
+	cpuprofile  = flag.String("cpuprofile", "", "write CPU profile to file")
+
+	tagsFlag buildutil.TagsFlag
 
 	// testmode flags. See main_testmode.go.
 	dirFlag string
@@ -58,6 +62,16 @@
 		os.Exit(1)
 	}
 
+	// Profiling support.
+	if *cpuprofile != "" {
+		f, err := os.Create(*cpuprofile)
+		if err != nil {
+			fmt.Fprintln(os.Stderr, err)
+			os.Exit(1)
+		}
+		pprof.StartCPUProfile(f)
+	}
+
 	patterns := flag.Args()
 
 	sourceAnalysis := true
@@ -66,14 +80,18 @@
 	}
 	validateFlags(sourceAnalysis)
 
-	if err := doGovulncheck(patterns, sourceAnalysis); err != nil {
-		die(fmt.Sprintf("govulncheck: %v", err))
+	err := doGovulncheck(patterns, sourceAnalysis)
+	pprof.StopCPUProfile()
+	if err != nil {
+		if code, ok := err.(exitCode); ok {
+			os.Exit(int(code))
+		}
+		die("govulncheck: %v", err)
 	}
 }
 
-// doGovulncheck performs main govulncheck functionality and exits the
-// program upon success with an appropriate exit status. Otherwise,
-// returns an error.
+// doGovulncheck performs the main govulncheck functionality and
+// returns an error, possibly an exitCode.
 func doGovulncheck(patterns []string, sourceAnalysis bool) error {
 	ctx := context.Background()
 	dir := filepath.FromSlash(dirFlag)
@@ -127,11 +145,11 @@
 
 	if *jsonFlag {
 		// Following golang.org/x/tools/go/analysis/singlechecker,
-		// return 0 exit code in -json mode.
+		// -json mode is always a success.
 		if err := printJSON(res); err != nil {
 			return err
 		}
-		os.Exit(0)
+		return nil // success
 	}
 
 	printText(res, *verboseFlag, sourceAnalysis)
@@ -144,16 +162,21 @@
 	if sourceAnalysis {
 		for _, v := range res.Vulns {
 			if v.IsCalled() {
-				os.Exit(3)
+				return exitCode(3)
 			}
 		}
 	} else if len(res.Vulns) > 0 {
-		os.Exit(3)
+		return exitCode(3)
 	}
-	os.Exit(0)
 	return nil
 }
 
+// exitCode is an error returned by doGovulncheck to indicate
+// that the the program should silently exit with the specified code.
+type exitCode int
+
+func (code exitCode) Error() string { return fmt.Sprintf("exit code %d", code) }
+
 func validateFlags(source bool) {
 	if !source {
 		if *testFlag {
diff --git a/cmd/govulncheck/main_command_118_test.go b/cmd/govulncheck/main_command_118_test.go
index 9a0516c..385380b 100644
--- a/cmd/govulncheck/main_command_118_test.go
+++ b/cmd/govulncheck/main_command_118_test.go
@@ -36,7 +36,10 @@
 	if err != nil {
 		t.Fatal(err)
 	}
-	ts.DisableLogging = false
+
+	// Comment this out to log all command output (very verbose).
+	ts.DisableLogging = true
+
 	// Define a command that runs govulncheck with our local DB. We can't use
 	// cmdtest.Program for this because it doesn't let us set the environment,
 	// and that is the only way to tell govulncheck about an alternative vuln
diff --git a/cmd/govulncheck/testdata/usage.ct b/cmd/govulncheck/testdata/usage.ct
index 72afbcc..92c8610 100644
--- a/cmd/govulncheck/testdata/usage.ct
+++ b/cmd/govulncheck/testdata/usage.ct
@@ -3,6 +3,8 @@
 	govulncheck [flags] package...
 	govulncheck [flags] binary
 
+  -cpuprofile string
+    	write CPU profile to file
   -dir string
     	directory to use for loading source files
   -json
@@ -22,6 +24,8 @@
 	govulncheck [flags] package...
 	govulncheck [flags] binary
 
+  -cpuprofile string
+    	write CPU profile to file
   -dir string
     	directory to use for loading source files
   -json