|  | // Compiles a fiddle and then runs the fiddle. The output of both processes is | 
|  | // combined into a single JSON output. | 
|  | package main | 
|  |  | 
|  | import ( | 
|  | "bytes" | 
|  | "context" | 
|  | "encoding/base64" | 
|  | "encoding/json" | 
|  | "flag" | 
|  | "fmt" | 
|  | "io" | 
|  | "io/ioutil" | 
|  | "net/http" | 
|  | "path" | 
|  | "path/filepath" | 
|  | "runtime" | 
|  | "strings" | 
|  | "sync" | 
|  | "time" | 
|  |  | 
|  | "contrib.go.opencensus.io/exporter/stackdriver" | 
|  | "github.com/gorilla/mux" | 
|  | "go.opencensus.io/plugin/ochttp" | 
|  | "go.opencensus.io/trace" | 
|  | "go.skia.org/infra/fiddlek/go/types" | 
|  | "go.skia.org/infra/go/common" | 
|  | "go.skia.org/infra/go/exec" | 
|  | "go.skia.org/infra/go/httputils" | 
|  | "go.skia.org/infra/go/sklog" | 
|  | "go.skia.org/infra/go/util" | 
|  | "go.skia.org/infra/go/util/limitwriter" | 
|  | "golang.org/x/sync/errgroup" | 
|  | ) | 
|  |  | 
|  | const ( | 
|  | // FPS is the Frames Per Second when generating an animation. | 
|  | FPS = 60 | 
|  | ) | 
|  |  | 
|  | // flags | 
|  | var ( | 
|  | apoptosis  = flag.Duration("apoptosis", 5*time.Minute, "How long a pod should live after starting a run.") | 
|  | local      = flag.Bool("local", false, "Running locally if true. As opposed to in production.") | 
|  | fiddleRoot = flag.String("fiddle_root", "", "Directory location where all the work is done.") | 
|  | checkout   = flag.String("checkout", "", "Directory where Skia is checked out.") | 
|  | port       = flag.String("port", ":8000", "HTTP service address (e.g., ':8000')") | 
|  | ) | 
|  |  | 
|  | var ( | 
|  | mutex        sync.Mutex | 
|  | currentState types.State = types.IDLE | 
|  | version      string | 
|  | ) | 
|  |  | 
|  | func setStateStart() error { | 
|  | mutex.Lock() | 
|  | defer mutex.Unlock() | 
|  | if currentState != types.IDLE { | 
|  | return fmt.Errorf("Fiddle already being run.") | 
|  | } | 
|  | currentState = types.WRITING | 
|  | return nil | 
|  | } | 
|  |  | 
|  | func setState(s types.State) { | 
|  | mutex.Lock() | 
|  | defer mutex.Unlock() | 
|  | sklog.Info(s) | 
|  | currentState = s | 
|  | } | 
|  |  | 
|  | func getState() types.State { | 
|  | mutex.Lock() | 
|  | defer mutex.Unlock() | 
|  | return currentState | 
|  | } | 
|  |  | 
|  | func serializeOutput(ctx context.Context, w io.Writer, res *types.Result) { | 
|  | ctx, span := trace.StartSpan(ctx, "serializeOutput") | 
|  | defer span.End() | 
|  | w = limitwriter.New(w, types.MAX_JSON_SIZE) | 
|  | if err := json.NewEncoder(w).Encode(res); err != nil { | 
|  | sklog.Errorf("Failed to encode: %s", err) | 
|  | } | 
|  | } | 
|  |  | 
|  | func mainHandler(w http.ResponseWriter, r *http.Request) { | 
|  | w.Header().Set("Content-Type", "application/json") | 
|  | resp := &types.FiddlerMainResponse{ | 
|  | State:   getState(), | 
|  | Version: version, | 
|  | } | 
|  |  | 
|  | if err := json.NewEncoder(w).Encode(resp); err != nil { | 
|  | sklog.Warningf("Failed to write response: %s", err) | 
|  | } | 
|  | } | 
|  |  | 
|  | func build(ctx context.Context, cwd string, args ...string) (string, error) { | 
|  | ctx, span := trace.StartSpan(ctx, "build") | 
|  | defer span.End() | 
|  | return exec.RunCwd(ctx, cwd, args...) | 
|  | } | 
|  |  | 
|  | func runHandler(w http.ResponseWriter, r *http.Request) { | 
|  | ctx, span := trace.StartSpan(r.Context(), "fiddler") | 
|  | defer span.End() | 
|  | span.Annotate([]trace.Attribute{ | 
|  | trace.Int64Attribute("num cores", int64(runtime.NumCPU())), | 
|  | }, "fiddler") | 
|  |  | 
|  | defer util.Close(r.Body) | 
|  | if setStateStart() != nil { | 
|  | http.Error(w, "Currently running a fiddle.", http.StatusTooManyRequests) | 
|  | return | 
|  | } | 
|  | defer setState(types.IDLE) | 
|  | var request types.FiddleContext | 
|  |  | 
|  | res := &types.Result{ | 
|  | Compile: types.Compile{}, | 
|  | Execute: types.Execute{ | 
|  | Errors: "", | 
|  | Output: types.Output{}, | 
|  | }, | 
|  | } | 
|  |  | 
|  | if err := json.NewDecoder(r.Body).Decode(&request); err != nil { | 
|  | res.Execute.Errors = fmt.Sprintf("Invalid JSON Request: %s", err) | 
|  | serializeOutput(ctx, w, res) | 
|  | return | 
|  | } | 
|  |  | 
|  | // Apoptosis. | 
|  | _, cancel := context.WithCancel(context.Background()) | 
|  | defer cancel() | 
|  | go func() { | 
|  | select { | 
|  | case <-time.Tick(*apoptosis): | 
|  | sklog.Fatal("Exceeded total allowed runtime.") | 
|  | case <-ctx.Done(): | 
|  | sklog.Info("Exited cleanly.") | 
|  | } | 
|  | }() | 
|  |  | 
|  | // Compile draw.cpp into 'fiddle'. | 
|  | if err := ioutil.WriteFile(filepath.Join(*checkout, "tools", "fiddle", "draw.cpp"), []byte(request.Code), 0644); err != nil { | 
|  | res.Execute.Errors = fmt.Sprintf("Failed to write draw.cpp: %s", err) | 
|  | serializeOutput(ctx, w, res) | 
|  | return | 
|  | } | 
|  |  | 
|  | setState(types.COMPILING) | 
|  |  | 
|  | buildResults, err := build(ctx, *checkout, filepath.Join(*fiddleRoot, "depot_tools", "ninja"), "-C", "out/Static") | 
|  | buildLogs := strings.Split(buildResults, "\n") | 
|  | sklog.Info("BuildLog") | 
|  | for _, s := range buildLogs { | 
|  | sklog.Info(s) | 
|  | } | 
|  | if err != nil { | 
|  | res.Compile.Errors = err.Error() | 
|  | res.Compile.Output = buildResults | 
|  | serializeOutput(ctx, w, res) | 
|  | return | 
|  | } | 
|  |  | 
|  | setState(types.RUNNING) | 
|  |  | 
|  | if request.Options.Duration == 0 { | 
|  | oneStep(ctx, *checkout, res, 0.0, request.Options.Duration) | 
|  | serializeOutput(ctx, w, res) | 
|  | } else { | 
|  |  | 
|  | var g errgroup.Group | 
|  | // If this is an animated fiddle then: | 
|  | //   - Create tmpdir to store PNGs. | 
|  | //   - Loop over the following code to generate each frame of the animation. | 
|  | //   -   Pass the duration and frame via cmd line flags to fiddle. | 
|  | //   -   Decode and write PNGs (CPU+GPU) to their temp location. | 
|  | //   - Run ffmpeg over the resulting PNG's to generate the webm files. | 
|  | //   - Clean up tmp file. | 
|  | //   - Encode resulting webm files as base64 strings and return in JSON. | 
|  | numFrames := int(FPS * (request.Options.Duration)) | 
|  | tmpDir, err := ioutil.TempDir("", "animation") | 
|  | defer util.RemoveAll(tmpDir) | 
|  | if err != nil { | 
|  | res.Execute.Errors = fmt.Sprintf("Failed to create tmp dir for storing animation PNGs: %s", err) | 
|  | serializeOutput(ctx, w, res) | 
|  | return | 
|  | } | 
|  |  | 
|  | // frameCh is a channel of all the frames we want rendered, which feeds the | 
|  | // pool of Go routines that will do the actual rendering. | 
|  | frameCh := make(chan int) | 
|  |  | 
|  | // mutex protects glinfo. | 
|  | var mutex sync.Mutex | 
|  | // glinfo is the info about the GL version for the GPU. | 
|  | glinfo := "" | 
|  |  | 
|  | // Start a pool of workers to do the rendering. | 
|  | for i := 0; i <= 5; i++ { | 
|  | g.Go(func() error { | 
|  | // Each Go func should have its own 'res'. | 
|  | res := &types.Result{ | 
|  | Compile: types.Compile{}, | 
|  | Execute: types.Execute{ | 
|  | Errors: "", | 
|  | Output: types.Output{}, | 
|  | }, | 
|  | } | 
|  | for frameIndex := range frameCh { | 
|  | sklog.Infof("Parallel render: %d", frameIndex) | 
|  | frame := float64(frameIndex) / float64(numFrames) | 
|  | oneStep(ctx, *checkout, res, frame, request.Options.Duration) | 
|  | // Check for errors. | 
|  | if res.Execute.Errors != "" { | 
|  | return fmt.Errorf("Failed to render: %s", res.Execute.Errors) | 
|  | } | 
|  | // Extract CPU and GPU pngs to a tmp directory. | 
|  | if err := extractPNG(res.Execute.Output.Raster, res, frameIndex, "CPU", tmpDir); err != nil { | 
|  | return err | 
|  | } | 
|  | if err := extractPNG(res.Execute.Output.Gpu, res, frameIndex, "GPU", tmpDir); err != nil { | 
|  | return err | 
|  | } | 
|  | mutex.Lock() | 
|  | if glinfo == "" { | 
|  | glinfo = res.Execute.Output.GLInfo | 
|  | } | 
|  | mutex.Unlock() | 
|  | } | 
|  | return nil | 
|  | }) | 
|  | } | 
|  |  | 
|  | // Feed all the frame indices to the channel. | 
|  | for i := 0; i <= numFrames; i++ { | 
|  | frameCh <- i | 
|  | } | 
|  | close(frameCh) | 
|  |  | 
|  | // Wait for all the work to be done. | 
|  | if err := g.Wait(); err != nil { | 
|  | res.Execute.Errors = fmt.Sprintf("Failed to encode video: %s", err) | 
|  | serializeOutput(ctx, w, res) | 
|  | return | 
|  | } | 
|  | // Run ffmpeg for CPU and GPU. | 
|  | if err := createWebm(ctx, "CPU", tmpDir); err != nil { | 
|  | res.Execute.Errors = fmt.Sprintf("Failed to encode video: %s", err) | 
|  | serializeOutput(ctx, w, res) | 
|  | return | 
|  | } | 
|  | res.Execute.Output.AnimatedRaster = encodeWebm("CPU", tmpDir, res) | 
|  |  | 
|  | if err := createWebm(ctx, "GPU", tmpDir); err != nil { | 
|  | res.Execute.Errors = fmt.Sprintf("Failed to encode video: %s", err) | 
|  | serializeOutput(ctx, w, res) | 
|  | return | 
|  | } | 
|  | res.Execute.Output.AnimatedGpu = encodeWebm("GPU", tmpDir, res) | 
|  | res.Execute.Output.Raster = "" | 
|  | res.Execute.Output.Gpu = "" | 
|  | res.Execute.Output.GLInfo = glinfo | 
|  |  | 
|  | serializeOutput(ctx, w, res) | 
|  | } | 
|  | } | 
|  |  | 
|  | // encodeWebm encodes the webm as base64 and adds it to the results. | 
|  | func encodeWebm(prefix, tmpDir string, res *types.Result) string { | 
|  | b, err := ioutil.ReadFile(path.Join(tmpDir, fmt.Sprintf("%s.webm", prefix))) | 
|  | if err != nil { | 
|  | res.Execute.Errors = fmt.Sprintf("Failed to read resulting video: %s", err) | 
|  | return "" | 
|  | } | 
|  | return base64.StdEncoding.EncodeToString(b) | 
|  | } | 
|  |  | 
|  | // createWebm runs ffmpeg over the images in the given dir. | 
|  | func createWebm(ctx context.Context, prefix, tmpDir string) error { | 
|  | ctx, span := trace.StartSpan(ctx, "createWebm-"+prefix) | 
|  | defer span.End() | 
|  |  | 
|  | // ffmpeg -r $FPS -pattern_type glob -i '*.png' -c:v libvpx-vp9 -lossless 1 output.webm | 
|  | name := "ffmpeg" | 
|  | args := []string{ | 
|  | "-r", fmt.Sprintf("%d", FPS), | 
|  | "-pattern_type", "glob", "-i", prefix + "*.png", | 
|  | "-c:v", "libvpx-vp9", | 
|  | "-lossless", "1", | 
|  | fmt.Sprintf("%s.webm", prefix), | 
|  | } | 
|  | output := &bytes.Buffer{} | 
|  | runCmd := &exec.Command{ | 
|  | Name:           name, | 
|  | Args:           args, | 
|  | Dir:            tmpDir, | 
|  | CombinedOutput: output, | 
|  | } | 
|  | if err := exec.Run(ctx, runCmd); err != nil { | 
|  | return fmt.Errorf("ffmpeg failed %#v %q: %s", *runCmd, util.Truncate(output.String(), 100), err) | 
|  | } | 
|  |  | 
|  | return nil | 
|  | } | 
|  |  | 
|  | // extractPNG pulls the base64 encoded PNG out of the results and writes it to the tmpDir. | 
|  | func extractPNG(b64 string, res *types.Result, i int, prefix string, tmpDir string) error { | 
|  | body, err := base64.StdEncoding.DecodeString(b64) | 
|  | if err != nil { | 
|  | res.Execute.Errors = fmt.Sprintf("Failed to decode frame %d of %s: %s", i, prefix, err) | 
|  | return err | 
|  | } | 
|  | if err := ioutil.WriteFile(path.Join(tmpDir, fmt.Sprintf("%s_%05d.png", prefix, i)), body, 0600); err != nil { | 
|  | res.Execute.Errors = fmt.Sprintf("Failed to write frame %d of %s as a PNG: %s", i, prefix, err) | 
|  | return err | 
|  | } | 
|  | return nil | 
|  | } | 
|  |  | 
|  | func oneStep(ctx context.Context, checkout string, res *types.Result, frame float64, duration float64) { | 
|  | ctx, span := trace.StartSpan(ctx, "oneStep") | 
|  | defer span.End() | 
|  |  | 
|  | name := path.Join("/usr/local/bin/fiddle_secwrap") | 
|  |  | 
|  | args := []string{path.Join(checkout, "out", "Static", "fiddle")} | 
|  | args = append(args, "--duration", fmt.Sprintf("%f", duration), "--frame", fmt.Sprintf("%f", frame)) | 
|  | stderr := bytes.Buffer{} | 
|  | stdout := bytes.Buffer{} | 
|  | runCmd := &exec.Command{ | 
|  | Name:        name, | 
|  | Args:        args, | 
|  | Dir:         *fiddleRoot, | 
|  | InheritPath: true, | 
|  | Env:         []string{"HOME=/tmp"}, | 
|  | InheritEnv:  true, | 
|  | Stdout:      &stdout, | 
|  | Stderr:      &stderr, | 
|  | } | 
|  | if err := exec.Run(ctx, runCmd); err != nil { | 
|  | sklog.Errorf("Failed to run: %s", err) | 
|  | res.Execute.Errors = err.Error() | 
|  | } | 
|  | if res.Execute.Errors != "" && stderr.String() != "" { | 
|  | sklog.Errorf("Found stderr output: %q", stderr.String()) | 
|  | res.Execute.Errors += "\n" | 
|  | } | 
|  | res.Execute.Errors += stderr.String() | 
|  | if err := json.Unmarshal(stdout.Bytes(), &res.Execute.Output); err != nil { | 
|  | if res.Execute.Errors != "" { | 
|  | res.Execute.Errors += "\n" | 
|  | } | 
|  | res.Execute.Errors += "Failed to decode JSON output from fiddle.\n" | 
|  | res.Execute.Errors += err.Error() | 
|  | res.Execute.Errors += fmt.Sprintf("\nOutput was %q", stdout.Bytes()) | 
|  | } | 
|  | } | 
|  |  | 
|  | func main() { | 
|  | common.InitWithMust( | 
|  | "fiddler", | 
|  | ) | 
|  | if *fiddleRoot == "" { | 
|  | sklog.Fatalf("The --fiddle_root flag is required.") | 
|  | } | 
|  | if *checkout == "" { | 
|  | sklog.Fatalf("The --checkout flag is required.") | 
|  | } | 
|  |  | 
|  | if !*local { | 
|  | exporter, err := stackdriver.NewExporter(stackdriver.Options{ | 
|  | BundleDelayThreshold: time.Second / 10, | 
|  | BundleCountThreshold: 10}) | 
|  | if err != nil { | 
|  | sklog.Fatal(err) | 
|  | } | 
|  | trace.RegisterExporter(exporter) | 
|  | trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()}) | 
|  | _, span := trace.StartSpan(context.Background(), "main") | 
|  | defer span.End() | 
|  | } | 
|  |  | 
|  | b, err := ioutil.ReadFile(filepath.Join(*checkout, "VERSION")) | 
|  | if err != nil { | 
|  | sklog.Fatalf("Failed to read Skia version: %s", err) | 
|  | } | 
|  | version = strings.TrimSpace(string(b)) | 
|  |  | 
|  | r := mux.NewRouter() | 
|  | r.HandleFunc("/", mainHandler) | 
|  | r.Handle("/run", &ochttp.Handler{Handler: http.HandlerFunc(runHandler)}) // Just wrap the /run handler for tracing. | 
|  |  | 
|  | h := httputils.LoggingGzipRequestResponse(r) | 
|  | h = httputils.Healthz(r) | 
|  | sklog.Info("Ready to serve.") | 
|  |  | 
|  | srv := &http.Server{ | 
|  | Handler:      h, | 
|  | Addr:         *port, | 
|  | WriteTimeout: 120 * time.Second, | 
|  | ReadTimeout:  120 * time.Second, | 
|  | } | 
|  |  | 
|  | sklog.Fatal(srv.ListenAndServe()) | 
|  | } |