blob: a152c3245c45413841b07e429c89c189ce7c3ef3 [file] [log] [blame]
// Copyright 2020 The Chromium OS 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 (
"bufio"
"flag"
"fmt"
"math"
"os"
"path/filepath"
"sort"
"trace_profiling/cmd/harvest/config"
"trace_profiling/cmd/harvest/utils"
"trace_profiling/cmd/profile/profile"
)
const (
defaultTraceCacheDir = "/tmp/traces"
defaultProfileBinPath = "/usr/local/graphics/profile"
defaultCrostiniBundle = "crostini-bundle-template.json"
defaultCroutonBundle = "crouton-bundle-template.json"
)
// PerfConfig is used to read the performance configuration parameters from
// a JSON file.
type PerfConfig struct {
Traces []string `json:"traces"`
TraceCacheDir string `json:"traceCacheDir"`
KeepTracesInCache bool `json:"keepTracesInCache"`
ProfileBinPath string `json:"profileBinPath"`
CrostiniBundle string `json:"crostiniBundleTemplate"`
CroutonBundle string `json:"croutonBundleTemplate"`
}
// Cmd-line arguments.
var argVerbose bool
var argOutputFile string
var argConfigFilepath string
var argEnableCompareFps bool
var argToolToRun string
// Parsers for reading config data from JSON config file.
var harvestConfig *config.HarvestConfigParser
var deviceInfoConfig *config.DeviceInfoConfigParser
var gpuVisConfig *config.GpuVisConfigParser
var targetDevice1 *config.TargetDevice
var targetDevice2 *config.TargetDevice
// Tools.
var profileTool *utils.TraceProfile
var deviceInfoTool *utils.DeviceInfoTool
var gpuVisTool *utils.GpuVisTool
// Setup the two target devices. It's possible for a target device to be nil, in
// which case it is simply ignored.
func setupTargetDevices() {
targetDevice1 = harvestConfig.GetTargetDeviceConfig1()
targetDevice2 = harvestConfig.GetTargetDeviceConfig2()
}
// Setup the tools used to carry out the selected action.
func setupTools() {
profileTool = utils.NewTraceProfile(argVerbose)
deviceInfoTool = utils.NewDeviceInfoTool(argVerbose)
gpuVisTool = utils.NewGpuVisTool(argVerbose)
}
// Generate the FPS comparative output to the target output file. Note that
// FPS data is always appended to the file.
func generateFpsOutput(data []utils.FPSRecord) {
if len(data) == 0 {
return
}
outFile, err := os.OpenFile(argOutputFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err == nil {
defer outFile.Close()
fileInfo, _ := outFile.Stat()
dataWriter := bufio.NewWriter(outFile)
sort.SliceStable(data, func(i, j int) bool {
return data[i].TestFpsPercent < data[j].TestFpsPercent
})
// Add header, but only to new files.
if fileInfo.Size() == 0 {
dataWriter.WriteString(fmt.Sprintf("%8s %8s %% %s\n",
data[0].TestDeviceEnv, data[0].RefDeviceEnv, "Trace name"))
}
for _, fps := range data {
if fps.FpsError != nil {
dataWriter.WriteString(fmt.Sprintf("%s error getting FPS: %s", fps.TraceName, fps.FpsError))
} else {
dataWriter.WriteString(fmt.Sprintf("%8.2f, %8.2f, ", fps.TestFps, fps.ReferenceFps))
if fps.TestFpsPercent != math.Inf(1) {
dataWriter.WriteString(fmt.Sprintf(" %6.2f%% ", fps.TestFpsPercent))
} else {
dataWriter.WriteString(" INF!\n")
}
dataWriter.WriteString(fps.TraceName)
dataWriter.WriteString("\n")
}
}
dataWriter.Flush()
}
}
// Run the traces on the target devices to gather profile data. If requested,
// also compare the FPS data for all the traces.
func doHarvestProfiles() {
printIfVerbose("\nHarvesting profile data with traces:\n")
printIfVerbose("===================================\n")
errorFeed := make(chan error)
profileTool.Setup(targetDevice1, targetDevice2, errorFeed,
harvestConfig.GetProfilerBinPath(), harvestConfig.ShouldKeepTraceAfterUse())
go func() {
profileTool.RunTraces(harvestConfig.GetTraces(), harvestConfig.GetTraceCacheDir())
close(errorFeed)
}()
for err := range errorFeed {
fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error())
}
if argEnableCompareFps {
generateFpsOutput(profileTool.GetFPSData())
}
}
// Run the device-info tool on the target devices.
func doHarvestDeviceInfo() {
printIfVerbose("\nHarvesting device info:\n======================\n")
machineConfig := deviceInfoConfig.GetMachineConfig()
if machineConfig != nil {
if targetDevice1 != nil {
execEnv := targetDevice1.DeviceConfig.ExecEnv
if execEnv == "" {
fmt.Fprintf(os.Stderr, "Error: Fetching machine info aborted for TargetDevice1.\n"+
"Device config does not define ExecEnv.\n")
} else {
doHarvestDeviceInfoOnTarget(machineConfig, targetDevice1.ProfilerConfig,
targetDevice1.DeviceConfig)
}
}
if targetDevice2 != nil {
execEnv := targetDevice2.DeviceConfig.ExecEnv
if execEnv == "" {
fmt.Fprintf(os.Stderr, "Error: Fetching machine info aborted for TargetDevice2.\n"+
"Device config does not define ExecEnv.\n")
} else {
doHarvestDeviceInfoOnTarget(machineConfig, targetDevice2.ProfilerConfig,
targetDevice2.DeviceConfig)
}
}
}
}
// Run the device-info tool on a single specific target.
func doHarvestDeviceInfoOnTarget(
machineConfig *config.MachineInfoConfig,
profilerConfig *profile.ProfilerConfigRecord,
targetDevice *profile.DeviceConfigRecord) {
targetLabel := targetDevice.ExecEnv
if !machineConfig.Enabled {
printIfVerbose("Skipping %s device: disabled in config.\n", targetLabel)
} else {
printIfVerbose("Getting device info from %s device.\n", targetLabel)
deviceInfoTool.Setup(deviceInfoConfig.GetDeviceInfoBinPath(),
targetDevice.Name, deviceInfoConfig.GetOwner(), targetDevice.ExecEnv,
profilerConfig.SSHConfig, profilerConfig.TunnelConfig)
if err := deviceInfoTool.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error getting machine-info for %s: %s\n", targetLabel, err.Error())
return
}
// TODO (gwink): upload protobuf to DB if requested.
err := deviceInfoTool.WriteProtoBufToFile(deviceInfoConfig.GetProtoBufsOutputDir(),
machineConfig.OutputFileTemplate)
if err != nil {
fmt.Fprintf(os.Stderr, "Error writing machine-info to protobuf for %s: %s\n",
targetLabel, err.Error())
}
}
}
// Run the GpuVisTool on one target device.
func doHarvestGpuVisData() {
printIfVerbose("\nHarvesting GpuVis performance data:\n==================================\n")
if gpuVisConfig == nil {
fmt.Fprintf(os.Stderr, "Error: no GpuVis parameters in configuration.\n")
return
}
// GpuVisTool can only run on one device. Pick non-nil device as target. If both
// are non-nil, use targetDevice1 and print a warning.
targetDevice := targetDevice2
if targetDevice1 != nil {
targetDevice = targetDevice1
if targetDevice2 != nil {
fmt.Printf("Warning: GpuVis can only run on one device. Running on %s\n",
targetDevice.DeviceConfig.ExecEnv)
}
}
err := gpuVisTool.Setup(targetDevice, gpuVisConfig.GetGPUPerfConfig(),
harvestConfig.GetProfilerBinPath(), harvestConfig.GetTraceCacheDir(),
harvestConfig.GetTraces(), harvestConfig.ShouldKeepTraceAfterUse())
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting GpuVis trace data from %s: %s\n",
targetDevice.DeviceConfig.ExecEnv, err.Error())
return
}
// Run GpuVisTool asynchronously. The for loop below will loop until channel
// errorFeed is closed.
errorFeed := make(chan error)
go func() {
gpuVisTool.Run(errorFeed)
close(errorFeed)
}()
for err := range errorFeed {
fmt.Fprintf(os.Stderr, "Error getting GpuVis trace data from %s: %s\n",
targetDevice.DeviceConfig.ExecEnv, err.Error())
}
}
// Read and parse the Harvest config json file and leave the result in global
// var harvestConfig.
func readHarvestConfigFromFile(jsonFilepath string) error {
harvestConfigParser := config.CreateHarvestConfigParser()
deviceInfoConfigParser := config.NewDeviceInfoConfigParser()
gpuVisConfigParser := config.NewGpuVisConfigParser()
harvestConfigParser.AddHandler("DeviceInfoTool", deviceInfoConfigParser)
harvestConfigParser.AddHandler("GpuVis", gpuVisConfigParser)
if err := harvestConfigParser.OpenJSONFile(jsonFilepath); err != nil {
return err
}
if err := harvestConfigParser.Process(); err != nil {
return err
}
harvestConfig = harvestConfigParser
deviceInfoConfig = deviceInfoConfigParser
gpuVisConfig = gpuVisConfigParser
return nil
}
// If verbose mode is enabled, print the formatted string.
func printIfVerbose(format string, a ...interface{}) {
if argVerbose {
fmt.Printf(format, a...)
}
}
func printUsage() {
appName := filepath.Base(os.Args[0])
fmt.Fprintf(os.Stderr, "\nUsage: %s -config config-file.json [other options]\n", appName)
fmt.Fprintf(os.Stderr, "Available options:\n")
flag.PrintDefaults()
os.Exit(2)
}
func main() {
flag.Usage = printUsage
flag.StringVar(&argConfigFilepath, "config", "", "Filename for JSON config data")
flag.StringVar(&argOutputFile, "out", "compare_out.prof", "Output file")
flag.BoolVar(&argVerbose, "verbose", false, "Enable verbose mode")
flag.BoolVar(&argEnableCompareFps, "compare-fps", false, "Extract FPS from profile data and compare")
flag.StringVar(&argToolToRun, "tool", "profile", "Tool to run, one of profile, device-info")
flag.Parse()
setupTools()
err := readHarvestConfigFromFile(argConfigFilepath)
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
return
}
setupTargetDevices()
switch argToolToRun {
case "profile":
doHarvestProfiles()
case "device-info":
doHarvestDeviceInfo()
case "gpuvis":
doHarvestGpuVisData()
}
}