diff --git a/README.md b/README.md index 6faa0e2..71d3595 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,8 @@ Usage of go-callvis: Enable verbose log. -file string output filename - omit to use server mode + -cacheDir string + Enable caching to avoid unnecessary re-rendering. -focus string Focus specific package using name or import path. (default "main") -format string diff --git a/analysis.go b/analysis.go index 982e422..b2c5f4d 100644 --- a/analysis.go +++ b/analysis.go @@ -78,16 +78,18 @@ func mainPackages(pkgs []*ssa.Package) ([]*ssa.Package, error) { } type renderOpts struct { - focus string - group []string - ignore []string - include []string - limit []string - nointer bool - nostd bool + cacheDir string + focus string + group []string + ignore []string + include []string + limit []string + nointer bool + refresh bool + nostd bool } -func (a *analysis) render(opts renderOpts) ([]byte, error) { +func (a *analysis) render(opts *renderOpts) ([]byte, error) { var ( err error ssaPkg *ssa.Package diff --git a/handler.go b/handler.go index e97bfbf..51432e3 100644 --- a/handler.go +++ b/handler.go @@ -3,20 +3,24 @@ package main import ( "errors" "fmt" + "io" "log" "net/http" + "os" + "path/filepath" "strings" ) func analysisSetup() (r renderOpts) { r = renderOpts{ - focus: *focusFlag, - group: []string{*groupFlag}, - ignore: []string{*ignoreFlag}, - include: []string{*includeFlag}, - limit: []string{*limitFlag}, - nointer: *nointerFlag, - nostd: *nostdFlag} + cacheDir: *cacheDir, + focus: *focusFlag, + group: []string{*groupFlag}, + ignore: []string{*ignoreFlag}, + include: []string{*includeFlag}, + limit: []string{*limitFlag}, + nointer: *nointerFlag, + nostd: *nostdFlag} return r } @@ -76,6 +80,52 @@ func handler(w http.ResponseWriter, r *http.Request) { logf(" => handling request: %v", r.URL) logf("----------------------") + opts := buildOptionsFromRequest(r) + + var img string + if img = findCachedImg(opts); img != "" { + log.Println("serving file:", img) + http.ServeFile(w, r, img) + return + } + + // Convert list-style args to []string + if e := processListArgs(opts); e != nil { + http.Error(w, "invalid parameters", http.StatusBadRequest) + return + } + + output, err := Analysis.render(opts) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if r.Form.Get("format") == "dot" { + log.Println("writing dot output..") + fmt.Fprint(w, string(output)) + return + } + + log.Printf("converting dot to %s..\n", *outputFormat) + + img, err = dotToImage("", *outputFormat, output) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + err = cacheImg(opts, img) + if err != nil { + http.Error(w, "cache img error: "+err.Error(), http.StatusBadRequest) + return + } + + log.Println("serving file:", img) + http.ServeFile(w, r, img) +} + +func buildOptionsFromRequest(r *http.Request) *renderOpts { // get cmdline default for analysis opts := analysisSetup() @@ -91,6 +141,9 @@ func handler(w http.ResponseWriter, r *http.Request) { if inter := r.FormValue("nointer"); inter != "" { opts.nointer = true } + if refresh := r.FormValue("refresh"); refresh != "" { + opts.refresh = true + } if g := r.FormValue("group"); g != "" { opts.group[0] = g } @@ -104,32 +157,89 @@ func handler(w http.ResponseWriter, r *http.Request) { opts.include[0] = inc } - // Convert list-style args to []string - if e := processListArgs(&opts); e != nil { - http.Error(w, "invalid parameters", http.StatusBadRequest) - return + return &opts +} + +func findCachedImg(opts *renderOpts) string { + if opts.cacheDir == "" || opts.refresh { + return "" } - output, err := Analysis.render(opts) + focus := opts.focus + if focus == "" { + focus = "all" + } + focusFilePath := focus + "." + *outputFormat + absFilePath := filepath.Join(opts.cacheDir, focusFilePath) + + if exists, err := pathExists(absFilePath); err != nil || !exists { + log.Println("not cached img:", absFilePath) + return "" + } + + log.Println("hit cached img") + return absFilePath +} + +func cacheImg(opts *renderOpts, img string) error { + if opts.cacheDir == "" || img == "" { + return nil + } + + focus := opts.focus + if focus == "" { + focus = "all" + } + absCacheDirPrefix := filepath.Join(opts.cacheDir, focus) + absCacheDirPath := strings.TrimRightFunc(absCacheDirPrefix, func(r rune) bool { + return r != '\\' && r != '/' + }) + err := os.MkdirAll(absCacheDirPath, os.ModePerm) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + return err } - if r.Form.Get("format") == "dot" { - log.Println("writing dot output..") - fmt.Fprint(w, string(output)) - return + absFilePath := absCacheDirPrefix + "." + *outputFormat + _, err = copyFile(img, absFilePath) + if err != nil { + return err } - log.Printf("converting dot to %s..\n", *outputFormat) + return nil +} + +func pathExists(path string) (bool, error) { + _, err := os.Stat(path) + if err == nil { + return true, nil + } + if os.IsNotExist(err) { + return false, nil + } + return false, err +} - img, err := dotToImage("", *outputFormat, output) +func copyFile(src, dst string) (int64, error) { + sourceFileStat, err := os.Stat(src) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + return 0, err } - log.Println("serving file:", img) - http.ServeFile(w, r, img) + if !sourceFileStat.Mode().IsRegular() { + return 0, fmt.Errorf("%s is not a regular file", src) + } + + source, err := os.Open(src) + if err != nil { + return 0, err + } + defer source.Close() + + destination, err := os.Create(dst) + if err != nil { + return 0, err + } + defer destination.Close() + nBytes, err := io.Copy(destination, source) + return nBytes, err } diff --git a/main.go b/main.go index 80aafa9..3c42f76 100644 --- a/main.go +++ b/main.go @@ -32,6 +32,7 @@ var ( skipBrowser = flag.Bool("skipbrowser", false, "Skip opening browser.") outputFile = flag.String("file", "", "output filename - omit to use server mode") outputFormat = flag.String("format", "svg", "output file format [svg | png | jpg | ...]") + cacheDir = flag.String("cacheDir", "", "Enable caching to avoid unnecessary re-rendering, you can force rendering by adding 'refresh=true' to the URL query or emptying the cache directory") debugFlag = flag.Bool("debug", false, "Enable verbose log.") versionFlag = flag.Bool("version", false, "Show version and exit.") @@ -63,7 +64,7 @@ func outputDot(fname string, outputFormat string) { log.Fatalf("%v\n", e) } - output, err := Analysis.render(opts) + output, err := Analysis.render(&opts) if err != nil { log.Fatalf("%v\n", err) }