blob: f556b47a3f95eb7f6c6400c037c846eb79d49c5a [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
const defaultCategories = "app,benchmark,gfx,input,kernel:meta,kernel:sched,ledger,magma,modular,motown,view,flutter,dart,dart:compiler,dart:dart,dart:debugger,dart:embedder,dart:gc,dart:isolate,dart:profiler,dart:vm"
type captureTraceConfig struct {
Categories string
BufferSize uint // [MB]
BufferingMode string
Duration time.Duration
// Command is set differently as it is passed via positional args.
Command []string
BenchmarkResultsFile string
SpecFile string
Binary bool
Stream bool
Compress bool
Detach bool
Decouple bool
Spawn bool
func newCaptureTraceConfig(f *flag.FlagSet) *captureTraceConfig {
config := &captureTraceConfig{}
// TODO(TO-400): It would be nice to be able to specify +category or
// -category to add or subtract from the default set.
f.StringVar(&config.Categories, "categories", defaultCategories,
"Comma separated list of categories to trace. \"all\" for all categories.")
f.UintVar(&config.BufferSize, "buffer-size", 0,
"Size of trace buffer in MB.")
f.StringVar(&config.BufferingMode, "buffering-mode", "oneshot",
"Buffering mode (one of: oneshot,circular,streaming.")
f.DurationVar(&config.Duration, "duration", 0,
"Duration of trace capture (e.g. '10s'). Second resolution.")
f.StringVar(&config.SpecFile, "spec-file", "",
"Tracing specification file.")
"Relative filepath for storing benchmark results.",
f.BoolVar(&config.Binary, "binary", false,
"Capture trace in binary format on the target.")
f.BoolVar(&config.Stream, "stream", false,
"Stream trace output to a local file, instead of saving to target disk and then copying it.")
// TODO(PT-125): Update this message when compressed binary (fxt) traces are supported.
f.BoolVar(&config.Compress, "compress", false,
"Compress the trace output before writing to disk. This option is currently ignored if -stream or -binary is specified.")
f.BoolVar(&config.Detach, "detach", false,
"Don't stop the traced program when tracing finished.")
f.BoolVar(&config.Decouple, "decouple", false,
"Don't stop tracing when the traced program exits.")
f.BoolVar(&config.Spawn, "spawn", false,
"Use fdio_spawn to run a legacy app. Detach will have no effect when using this option.")
return config
func streamTraceOutput(listener net.Listener, traceOutput *os.File, streamStatus chan<- error) {
fmt.Printf("Listening for remote connection: %s\n", listener.Addr().(*net.TCPAddr).String())
conn, err := listener.Accept()
if err != nil {
streamStatus <- fmt.Errorf("Unable to accept incoming trace connection: %s", err.Error())
fmt.Printf("Incoming trace stream connected\n")
nBytes, err := io.Copy(traceOutput, conn)
if err != nil {
streamStatus <- fmt.Errorf("Error writing trace results: %s", err.Error())
} else {
fmt.Printf("Wrote trace output, %d bytes\n", nBytes)
streamStatus <- nil
func captureTrace(config *captureTraceConfig, conn *TargetConnection, traceOutput *os.File) error {
cmd := []string{"trace"}
// Pass on our verbosity level to trace.
if glog.V(2) {
cmd = append(cmd, "--verbose=2")
} else if glog.V(1) {
cmd = append(cmd, "--verbose=1")
cmd = append(cmd, "record")
if config.Categories == "" {
config.Categories = defaultCategories
if len(config.SpecFile) > 0 {
cmd = append(cmd, "--spec-file="+config.SpecFile)
if len(config.BenchmarkResultsFile) > 0 {
cmd = append(cmd, "--benchmark-results-file="+config.BenchmarkResultsFile)
if config.Categories != "all" {
cmd = append(cmd, "--categories="+config.Categories)
if config.BufferSize != 0 {
cmd = append(cmd, "--buffer-size="+
strconv.FormatUint(uint64(config.BufferSize), 10))
if config.BufferingMode == "oneshot" ||
config.BufferingMode == "circular" ||
config.BufferingMode == "streaming" {
cmd = append(cmd, "--buffering-mode="+config.BufferingMode)
} else {
return fmt.Errorf("Invalid value for --buffering-mode: %s",
if config.Duration != 0 {
cmd = append(cmd, "--duration="+
strconv.FormatUint(uint64(config.Duration.Seconds()), 10))
if config.Binary {
cmd = append(cmd, "--binary")
if config.Compress {
cmd = append(cmd, "--compress")
var listener net.Listener
var err error
streamChan := make(chan error)
if config.Stream {
listener, err = conn.client.ListenTCP(&net.TCPAddr{IP: net.IPv6loopback})
if err != nil {
return err
cmd = append(cmd, "--output-file=tcp:"+listener.Addr().(*net.TCPAddr).String())
defer func() {
if listener != nil {
glog.Info("Closing trace stream listener")
go streamTraceOutput(listener, traceOutput, streamChan)
if config.Detach {
cmd = append(cmd, "--detach")
if config.Decouple {
cmd = append(cmd, "--decouple")
if config.Spawn {
cmd = append(cmd, "--spawn")
// The program to run must appear last.
// TODO(PT-130): The handling of embedded spaces in the command
// and its arguments could be better.
if len(config.Command) > 0 {
// There is a difference between the trace failing and the
// child failing, we need to know if trace fails.
// TODO(PT-95): If we talk to trace-manager directly to get
// trace data and then talk to some other service to launch the
// child then there's no conflating of return codes.
cmd = append(cmd, "--return-child-result=false")
cmd = append(cmd, config.Command...)
// TODO(TO-401): The target `trace` command's output is misleading.
// Specifically it says "Trace file written to /tmp/trace.json" which
// references where the file is written to on the target not on the host.
// We should wrap this and offer less confusing status.
err = conn.RunCommand(strings.Join(cmd, " "))
if err != nil {
return err
// Check for an error during streaming.
if config.Stream {
fmt.Printf("Waiting to finish receiving trace stream ...\n")
err = <-streamChan
return err
func convertToHtml(generator string, outputPath string,
title string, inputPaths ...string) error {
fmt.Printf("Converting %v to %s... ", inputPaths, outputPath)
var args []string
args = append(args, "--output="+outputPath)
if title != "" {
args = append(args, "--title="+title)
args = append(args, inputPaths...)
err := runCommand(generator, args)
if err != nil {
fmt.Printf("failed: %s\n", err.Error())
fmt.Printf("Invoked as: %s %s\n", generator, args)
} else {
return err
func convertToJson(generator string, outputPath string, inputPath string) error {
fmt.Printf("Converting %s to %s... ", inputPath, outputPath)
var args []string
args = append(args, "--input-file="+inputPath)
args = append(args, "--output-file="+outputPath)
err := runCommand(generator, args)
if err != nil {
fmt.Printf("failed: %s\n", err.Error())
fmt.Printf("Invoked as: %s %s\n", generator, args)
} else {
return err