| // Copyright (C) 2016 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package main |
| |
| import ( |
| "flag" |
| "fmt" |
| "os" |
| "path/filepath" |
| "regexp" |
| "time" |
| |
| "android.googlesource.com/platform/tools/gpu/client/adb" |
| "android.googlesource.com/platform/tools/gpu/client/android" |
| "android.googlesource.com/platform/tools/gpu/client/gapii" |
| "android.googlesource.com/platform/tools/gpu/framework/app" |
| "android.googlesource.com/platform/tools/gpu/framework/file" |
| "android.googlesource.com/platform/tools/gpu/framework/log" |
| "android.googlesource.com/platform/tools/gpu/framework/task" |
| ) |
| |
| var ( |
| device string |
| duration time.Duration |
| output string |
| localPort int |
| observeFrames uint |
| observeDraws uint |
| clearCache bool |
| inputFile string |
| recordInputs bool |
| replayInputs bool |
| useLDPreload bool |
| ) |
| |
| func init() { |
| verb := &app.Verb{ |
| Name: "trace", |
| ShortHelp: "Captures a gfx trace from an application", |
| Run: doTrace, |
| } |
| verb.Flags.StringVar(&device, "device", "", "the device to capture on") |
| verb.Flags.DurationVar(&duration, "d", 0, "duration to trace for") |
| verb.Flags.StringVar(&output, "out", "", "the file to generate") |
| verb.Flags.IntVar(&localPort, "local", 0, "capture a local program instead of using ADB") |
| verb.Flags.UintVar(&observeFrames, "observeFrames", 0, "capture the framebuffer every n frames (0 to disable)") |
| verb.Flags.UintVar(&observeDraws, "observeDraws", 0, "capture the framebuffer every n draws (0 to disable)") |
| verb.Flags.BoolVar(&clearCache, "clearCache", false, "clear package data before running it") |
| verb.Flags.StringVar(&inputFile, "input-file", "", "the file to use for recorded inputs") |
| verb.Flags.BoolVar(&recordInputs, "record-inputs", false, "record the inputs to file") |
| verb.Flags.BoolVar(&replayInputs, "replay-inputs", false, "replay the inputs from file") |
| verb.Flags.BoolVar(&useLDPreload, "ld-preload", false, "use LD_PRELOAD to register the interceptor library") |
| app.AddVerb(verb) |
| } |
| |
| func doTrace(ctx log.Context, flags flag.FlagSet) error { |
| if flags.NArg() != 1 && localPort == 0 { |
| app.Usage(ctx, "Invalid number of arguments. Expected 1, got %d", flags.NArg()) |
| return nil |
| } |
| |
| options := gapii.Options{ |
| ObserveFrameFreqency: uint32(observeFrames), |
| ObserveDrawFrequency: uint32(observeDraws), |
| } |
| |
| if localPort != 0 { |
| return captureLocal(ctx, flags, localPort, options) |
| } |
| |
| return captureADB(ctx, flags, options, useLDPreload) |
| } |
| |
| func captureLocal(ctx log.Context, flags flag.FlagSet, port int, options gapii.Options) error { |
| if output == "" { |
| output = "capture.gfxtrace" |
| } |
| return capture(ctx, options, port, output) |
| } |
| |
| func captureADB(ctx log.Context, flags flag.FlagSet, options gapii.Options, useLDPreload bool) error { |
| activity := flags.Arg(0) |
| d, err := getDevice(ctx, device) |
| if err != nil { |
| return err |
| } |
| |
| a, err := getAction(ctx, d, activity) |
| if err != nil { |
| return err |
| } |
| |
| if !useLDPreload && a.Package.Debuggable { |
| ctx.Print("Package is debuggable") |
| } else { |
| err = d.Root(ctx) |
| switch err { |
| case nil: |
| case adb.ErrDeviceNotRooted: |
| return err |
| default: |
| return fmt.Errorf("Failed to restart ADB as root: %v", err) |
| } |
| ctx.Print("Device is rooted") |
| } |
| |
| // Filenames - if no name specified, use package name. |
| if output == "" { |
| output = a.Package.Name + ".gfxtrace" |
| } |
| if inputFile == "" { |
| inputFile = a.Package.Name + ".inputs" |
| } |
| |
| if clearCache { |
| ctx.Print("Clearing package cache") |
| if err := a.Package.ClearCache(ctx); err != nil { |
| return err |
| } |
| } |
| |
| if wasScreenOn, _ := d.IsScreenOn(ctx); !wasScreenOn { |
| defer d.TurnScreenOff(ctx) // Think green! |
| } |
| |
| start := gapii.JdwpStart |
| if useLDPreload { |
| start = gapii.AdbStart |
| } |
| |
| binDir := file.ExecutablePath().Parent() |
| port, cleanup, err := start(ctx, binDir, a) |
| if err != nil { |
| return err |
| } |
| defer cleanup(ctx) |
| |
| ctx, stop := task.WithCancel(ctx) |
| if recordInputs { |
| ctx.Print("Starting input recorder") |
| cleanup, err := startRecordingInputs(ctx, d, inputFile) |
| if err != nil { |
| return err |
| } |
| defer cleanup() |
| } else if replayInputs { |
| ctx.Print("Starting input replayer") |
| if err := startReplayingInputs(ctx, d, inputFile, stop); err != nil { |
| return err |
| } |
| } |
| |
| return capture(ctx, options, port, output) |
| } |
| |
| func capture(ctx log.Context, options gapii.Options, port int, out string) error { |
| ctx.Info().S("file", out).Log("Creating file") |
| os.MkdirAll(filepath.Dir(out), 0755) |
| file, err := os.Create(out) |
| if err != nil { |
| return err |
| } |
| defer file.Close() |
| |
| if duration == 0 { |
| var cancel task.CancelFunc |
| ctx, cancel = task.WithCancel(ctx) |
| go func() { |
| println("Press enter to stop capturing...") |
| os.Stdin.Read([]byte{0}) |
| cancel() |
| }() |
| } else { |
| ctx, _ = task.WithTimeout(ctx, duration) |
| } |
| _, err = gapii.Capture(ctx, port, file, options) |
| if err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| func getDevice(ctx log.Context, pattern string) (adb.Device, error) { |
| devices, err := adb.Devices(ctx) |
| if err != nil { |
| return nil, err |
| } |
| if len(devices) == 0 { |
| return nil, fmt.Errorf("No devices found") |
| } |
| info := ctx.Info() |
| if info.Active() { |
| info.Log("Device list:") |
| for _, test := range devices { |
| info.S("serial", test.Info().Address.Path).Log("") |
| } |
| } |
| matchingDevices := []adb.Device{} |
| if pattern == "" { |
| matchingDevices = devices |
| } else { |
| re := regexp.MustCompile("(?i)" + pattern) |
| for _, test := range devices { |
| if re.MatchString(fmt.Sprint(test)) { |
| matchingDevices = append(matchingDevices, test) |
| } |
| } |
| } |
| if len(matchingDevices) == 0 { |
| return nil, fmt.Errorf("No devices matching %q found", pattern) |
| } else if len(matchingDevices) > 1 { |
| fmt.Println("Matching devices:") |
| for _, test := range matchingDevices { |
| fmt.Print(" ") |
| fmt.Println(test.Info().Address) |
| } |
| return nil, fmt.Errorf("Multiple devices matching %q found", pattern) |
| } |
| info.WithValue("device", matchingDevices[0]).Log("Tracing") |
| return matchingDevices[0], nil |
| } |
| |
| func getAction(ctx log.Context, d adb.Device, pattern string) (*android.Action, error) { |
| re := regexp.MustCompile("(?i)" + pattern) |
| packages, err := d.InstalledPackages(ctx) |
| if err != nil { |
| return nil, err |
| } |
| if len(packages) == 0 { |
| return nil, fmt.Errorf("No packages found") |
| } |
| matchingActions := []*android.Action{} |
| for _, p := range packages { |
| for _, action := range p.Actions { |
| if re.MatchString(action.String()) { |
| matchingActions = append(matchingActions, action) |
| } |
| } |
| } |
| if len(matchingActions) == 0 { |
| return nil, fmt.Errorf("No actions matching %s found", pattern) |
| } else if len(matchingActions) > 1 { |
| fmt.Println("Matching actions:") |
| for _, test := range matchingActions { |
| fmt.Print(" ") |
| fmt.Println(test) |
| } |
| return nil, fmt.Errorf("Multiple actions matching %q found", pattern) |
| } |
| ctx.Info().WithValue("action", matchingActions[0]).Log("") |
| return matchingActions[0], nil |
| } |