blob: 2da09bdf9fd3d0fe3d4cdf81f62bf3312a74502e [file]
// Workflow to run all CBB benchmarks on a particular browser / device
package internal
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"path"
"strings"
"time"
"go.temporal.io/sdk/temporal"
"go.temporal.io/sdk/workflow"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/perf/go/ingest/format"
"go.skia.org/infra/perf/go/perfresults"
"go.skia.org/infra/pinpoint/go/common"
"go.skia.org/infra/pinpoint/go/workflows"
)
// CbbRunnerParams defines the parameters for CbbRunnerWorkflow.
type CbbRunnerParams struct {
// The commit to run the benchmarks on. For CBB, this should be a commit in
// the Chromium main branch, with updated CBB marker file.
Commit *common.CombinedCommit
// Name of the device configuration, e.g. "mac-m3-pro-perf-cbb".
BotConfig string
// Name of the Browser to test. Supports "chrome", "safari", and "edge".
Browser string
// Browser channel to test. All browsers support "stable". Chrome and Edge
// also support "dev", while Safari also supports "technology-preview".
Channel string
// Name of the Google cloud storage bucket to upload results to.
Bucket string
// The set of benchmarks to run, and the number of iterations for each.
// If nil, use default.
Benchmarks []BenchmarkRunConfig
// SkipFinch is a bool flag used to disable certain Finch variation control.
// Only used for Chrome on desktop platforms.
// For Chrome Dev, it means passing --disable-field-trial-config to Chrome.
// For Chrome Stable, it means not using Finch top variations seed file.
SkipFinch bool
}
// CbbRunnerError is a custom error type for CbbRunnerWorkflow.
type CbbRunnerError struct {
// SwarmingLinks maps benchmark names to their corresponding swarming task list links.
SwarmingLinks map[string]string
Err string
WorkflowLink string
TotalBenchmarkCount int
}
func (e *CbbRunnerError) Error() string {
return e.Err
}
// Configuration for a particular benchmark.
type BenchmarkRunConfig struct {
Benchmark string
Iterations int32
}
func validateParameters(cbb *CbbRunnerParams) error {
switch cbb.Browser {
case "chrome", "edge":
if cbb.Channel != "stable" && cbb.Channel != "dev" {
return skerr.Fmt(
"Unrecognized browser channel %s, %s only supports stable and dev",
cbb.Channel, cbb.Browser)
}
case "safari":
if cbb.Channel != "stable" && cbb.Channel != "technology-preview" {
return skerr.Fmt(
"Unrecognized browser channel %s, %s only supports stable and technology-preview",
cbb.Channel, cbb.Browser)
}
default:
return skerr.Fmt(
"Unrecognized browser %s, only chrome, safari, and edge are supported",
cbb.Browser)
}
return nil
}
func setupBenchmarks(cbb *CbbRunnerParams) []BenchmarkRunConfig {
if cbb.Benchmarks != nil {
return cbb.Benchmarks
}
botConfig := cbb.BotConfig
var benchmarks []BenchmarkRunConfig
if strings.HasPrefix(botConfig, "mac-") {
benchmarks = append(benchmarks, BenchmarkRunConfig{"speedometer3", 150})
} else {
benchmarks = append(benchmarks, BenchmarkRunConfig{"speedometer3", 22})
}
benchmarks = append(benchmarks,
BenchmarkRunConfig{"jetstream2", 22},
BenchmarkRunConfig{"jetstream3", 22},
BenchmarkRunConfig{"motionmark1.3", 22},
)
if strings.HasPrefix(botConfig, "android-") {
// Loadline2 is only available on Android. It also takes considerably
// longer than other benchmarks, having 50 iterations built-in,
// so give it a lower repetition count here.
switch botConfig {
case "android-pixel-tangor-perf-cbb":
// CBB has 4 Tangor devices. Run 2 iterations on each device, for a total of 8.
benchmarks = append(benchmarks, BenchmarkRunConfig{"loadline2_tablet", 8})
case "android-pixel10-perf-cbb":
// CBB has 21 Pixel 10 devices. Use two-thirds of these, so that each device
// only needs to run a single iteration, even when some devices are dead.
benchmarks = append(benchmarks, BenchmarkRunConfig{"loadline2_phone", 14})
default:
// Don't know whether to run loadline2_tablet or loadline2_phone.
sklog.Errorf("Unknown Android config %s, please add it to setupBenchmarks function in cbb_runner.go", botConfig)
}
}
return benchmarks
}
// Info about the browser we are testing. Retrieved from CBB info file in chromium repo.
type browserInfo struct {
Browser string `json:"browser"`
Channel string `json:"channel"`
Platform string `json:"platform"`
Version string `json:"version"`
ChromiumMainBranchPosition int `json:"chromium_main_branch_position"`
}
func getBrowserInfo(ctx workflow.Context, cbb *CbbRunnerParams) (*browserInfo, error) {
var platformName string
switch {
case strings.HasPrefix(cbb.BotConfig, "mac-"):
platformName = "mac"
case strings.HasPrefix(cbb.BotConfig, "win-"):
platformName = "windows"
case strings.HasPrefix(cbb.BotConfig, "android-"):
platformName = "android"
default:
return nil, skerr.Fmt("Unable to determine platform for bot %s", cbb.BotConfig)
}
gitPath := fmt.Sprintf("testing/perf/cbb_ref_info/%s/%s/%s.json", cbb.Browser, cbb.Channel, platformName)
var content []byte
if err := workflow.ExecuteActivity(ctx, ReadGitFileActivity, cbb.Commit, gitPath).Get(ctx, &content); err != nil {
return nil, skerr.Wrapf(err, "Unable to fetch CBB info file %s", gitPath)
}
var bi browserInfo
if err := json.Unmarshal(content, &bi); err != nil {
return nil, skerr.Wrapf(err, "Unable to parse contents of CBB info file %s", gitPath)
}
sklog.Infof("CBB browser info: %v", bi)
return &bi, nil
}
func setupBrowser(cbb *CbbRunnerParams, bi *browserInfo) string {
browser := fmt.Sprintf("--official-browser=%s-%s", cbb.Browser, cbb.Channel)
if cbb.Browser == "chrome" {
browser = fmt.Sprintf("%s-%s", browser, bi.Version)
}
return browser
}
// Generate Pinpoint job ID. Uses abbreviations to keep the job ID short.
func genJobId(bi *browserInfo, cbb *CbbRunnerParams, benchmark string) string {
browser := getShortBrowserName(bi.Browser, bi.Channel)
if cbb.SkipFinch {
browser += " sf"
}
version := getShortBrowserVersion(bi.Version, bi.Browser, bi.Channel)
bot := getShortBotName(cbb.BotConfig)
// Shorten benchmark name.
switch {
case strings.HasPrefix(benchmark, "speedometer"):
benchmark = "SP"
case strings.HasPrefix(benchmark, "jetstream2"):
benchmark = "JS2"
case strings.HasPrefix(benchmark, "jetstream3"):
benchmark = "JS3"
case strings.HasPrefix(benchmark, "loadline2"):
benchmark = "LL2"
case strings.HasPrefix(benchmark, "motionmark"):
benchmark = "MM"
}
return fmt.Sprintf("CBB %s %s %s %s", browser, version, bot, benchmark)
}
// getShortBrowserName returns a shortened version of the browser name,
// together with the channel name if it is not Stable.
func getShortBrowserName(browser, channel string) string {
// * Browser name is shortened to 3 characters.
// * Channel name is omitted if it is "stable".
// * Special case: safari technology-preview is abbreviated as "stp".
if browser == "safari" && channel == "technology-preview" {
return "stp"
}
browser = browser[:3]
if channel != "stable" {
browser += " " + channel
}
return browser
}
// getShortBrowserVersion returns a shortened browser version number, when appropriate.
func getShortBrowserVersion(version, browser, channel string) string {
// Chrome and Edge versions are used as-is, but Safari versions are too long.
// * Safari Stable version looks like "1.2.3 (12345.6.7.8)". Truncate at the space.
// * STP version looks like "1.2.3 (Release 123, 12345.6.7.8)". Keep only the release number.
if browser == "safari" {
if channel == "stable" {
space := strings.Index(version, " ")
if space != -1 {
version = version[:space]
}
} else {
release := strings.Index(version, "Release ")
if release != -1 {
version = version[release+8:]
}
comma := strings.Index(version, ",")
if comma != -1 {
version = version[:comma]
}
}
}
return version
}
// getShortBotName returns a shortened version of the bot name.
func getShortBotName(bot string) string {
switch bot {
case "android-pixel-tangor-perf-cbb":
return "tang"
case "android-pixel10-perf-cbb":
return "p10"
case "mac-m3-pro-perf-cbb":
return "m3"
case "mac-m4-mini-perf-cbb":
return "m4"
case "mac-m5-pro-perf-cbb":
return "m5"
case "win-victus-perf-cbb":
return "vic"
case "win-arm64-snapdragon-elite-perf-cbb":
return "elite"
}
return bot
}
// Workflow to run all CBB benchmarks on a particular browser / bot config and upload the results.
func CbbRunnerWorkflow(ctx workflow.Context, cbb *CbbRunnerParams) (map[string]*format.Format, error) {
startTime := workflow.Now(ctx)
ctx = workflow.WithActivityOptions(ctx, regularActivityOptions)
err := validateParameters(cbb)
if err != nil {
return nil, skerr.Wrap(err)
}
bi, err := getBrowserInfo(ctx, cbb)
if err != nil {
return nil, skerr.Wrap(err)
}
benchmarks := setupBenchmarks(cbb)
browser := setupBrowser(cbb, bi)
extra_args := []string{browser}
if bi.Browser == "chrome" && bi.Platform != "android" {
switch bi.Channel {
case "stable":
if !cbb.SkipFinch {
extra_args = append(
extra_args,
"--variations-test-seed-path=../../components/variations/test_data/cipd/single_group_per_study_prefer_existing_behavior/seed.json")
}
case "dev":
if cbb.SkipFinch {
extra_args = append(extra_args, "--disable-field-trial-config")
}
}
}
// Due to b/433537961, we need to reinstall browser APK before each
// benchmark run on Pixel devices.
if strings.HasPrefix(cbb.BotConfig, "android-pixel") {
extra_args = append(extra_args, "--reinstall")
}
results := map[string]*format.Format{}
var errs []error
jobIds := map[string]string{}
for _, b := range benchmarks {
jobId := genJobId(bi, cbb, b.Benchmark)
jobIds[b.Benchmark] = jobId
p := &SingleCommitRunnerParams{
PinpointJobID: jobId,
BotConfig: cbb.BotConfig,
Benchmark: b.Benchmark,
Story: "default",
CombinedCommit: cbb.Commit,
Iterations: b.Iterations,
ExtraArgs: extra_args,
}
if !strings.HasSuffix(p.Benchmark, ".crossbench") {
p.Benchmark += ".crossbench"
}
options := cbbRunnerChildWorkflowOptions
options.WorkflowID = p.PinpointJobID
ctx = workflow.WithChildOptions(ctx, options)
var cr *CommitRun
if err := workflow.ExecuteChildWorkflow(ctx, workflows.SingleCommitRunner, p).Get(ctx, &cr); err != nil {
errs = append(errs, skerr.Wrap(err))
continue
}
// Check the success rate of swarming tasks. We require at least 80% of
// the tasks to succeed in order to accept the results.
numSuccess := 0
for _, run := range cr.Runs {
if run.Status.IsTaskSuccessful() && len(run.Values) > 0 {
numSuccess++
}
}
successRate := float64(numSuccess) / float64(b.Iterations)
requiredSuccessRate := 0.8
if successRate < requiredSuccessRate {
errs = append(errs, skerr.Fmt(
"Benchmark %s on %s only had success rate of %.0f%%, rejecting the results",
b.Benchmark, cbb.BotConfig, successRate*100))
continue
}
r := formatResult(ctx, cr, cbb.BotConfig, p.Benchmark, bi, cbb.SkipFinch, startTime, p.PinpointJobID)
results[b.Benchmark] = r
if cbb.Bucket != "" {
var swc StringWriterCloser = StringWriterCloser{
builder: new(strings.Builder),
}
if err := r.Write(swc); err != nil {
errs = append(errs, skerr.Wrap(err))
continue
}
var gsPath string
if err := workflow.ExecuteActivity(
ctx, UploadCbbResultsActivity, startTime, cbb, p.Benchmark, swc.builder.String()).Get(ctx, &gsPath); err != nil {
errs = append(errs, skerr.Wrap(err))
continue
}
sklog.Infof("Uploaded results to %s", gsPath)
} else {
sklog.Warning("No GS bucket provided to upload results")
}
}
if len(errs) > 0 {
swarmingLinks := map[string]string{}
for benchmark, jobId := range common.SortedRange(jobIds) {
swarmingLinks[benchmark] = getSwarmingLink(ctx, startTime, jobId)
}
err := CbbRunnerError{
Err: errors.Join(errs...).Error(),
WorkflowLink: getWorkflowLink(ctx),
TotalBenchmarkCount: len(benchmarks),
SwarmingLinks: swarmingLinks,
}
// The CbbRunnerError object must be wrapped in an ApplicationError in
// order to be successfully passed back to the parent workflow.
//nolint:wrapcheck // temporal error must be propagated raw
return nil, temporal.NewApplicationError(err.Err, "CbbRuntimeError", err)
}
return results, nil
}
// Provide a StringWriterCloser interface wrapper around strings.Builder,
// required for serializing the results to a string.
type StringWriterCloser struct {
builder *strings.Builder
}
func (swc StringWriterCloser) Write(p []byte) (int, error) {
n, err := swc.builder.Write(p)
if err != nil {
return n, skerr.Wrap(err)
}
return n, nil
}
func (StringWriterCloser) Close() error {
return nil
}
func getWorkflowLink(ctx workflow.Context) string {
isDev := strings.HasPrefix(workflow.GetActivityOptions(ctx).TaskQueue, "localhost.")
var temporalHost string
if isDev {
temporalHost = "temporal-ui-dev.corp.goog"
} else {
temporalHost = "skia-temporal-ui.corp.goog"
}
exec := workflow.GetInfo(ctx).WorkflowExecution
return fmt.Sprintf(
"https://%s/namespaces/perf-internal/workflows/%s/%s/history",
temporalHost, exec.ID, exec.RunID)
}
func getSwarmingLink(ctx workflow.Context, startTime time.Time, jobId string) string {
// Create a link to all swarming tasks associated with a CBB benchmark run.
// The link has these parameters:
// * c=... selects the columns to display.
// * st=%d Start time of the display.
// * et=%d End time of the display. Set to the current time, as the
// benchmark runs have just ended. Since the link is saved with the test
// results, some additional Pinpoint jobs with the same name might have
// run by the time a user clicks on the link, in which case we don't want
// the future tasks to be included. Having a time range should also speed
// up retrieving the task list.
// * n=false Sets the "Now" flag to false. Without this, the end time would be ignored.
// * f=... Filter to select the tasks with the expected Pinpoint job ID.
return fmt.Sprintf(
"https://chrome-swarming.appspot.com/tasklist?c=name&c=duration&c=bot&c=state&st=%d&et=%d&n=false&f=pinpoint_job_id-tag%%3A%s",
startTime.UnixMilli(), workflow.Now(ctx).UnixMilli(), url.PathEscape(jobId))
}
// Taking all swarming task results for one benchmark on one bot config,
// and convert the results into the format required by the perf dashboard.
func formatResult(ctx workflow.Context, cr *CommitRun, bot, benchmark string, bi *browserInfo, skipFinch bool, startTime time.Time, jobId string) *format.Format {
// Create a link to the Temporal workflow that is currently running.
// This link will be included in the pop-up for the result, to help debugging.
workflowLink := getWorkflowLink(ctx)
// Create a link to all swarming tasks associated with these results.
swarmingLink := getSwarmingLink(ctx, startTime, jobId)
data := format.Format{
Version: 1,
GitHash: fmt.Sprintf("CP:%d", cr.Build.Commit.Main.CommitPosition),
Key: map[string]string{
"master": "ChromiumPerf",
"bot": bot,
"benchmark": benchmark,
},
Links: map[string]string{
"Browser Version": bi.Version,
"Workflow": workflowLink,
"Swarming Tasks": swarmingLink,
},
}
values := map[string][]float64{}
units := map[string]string{}
for _, run := range cr.Runs {
for c, v := range common.SortedRange(run.Values) {
values[c] = append(values[c], v...)
}
for c, u := range common.SortedRange(run.Units) {
units[c] = u
}
}
// Form a human-readable browser name and channel string, such as "Chrome Stable".
browser_id := fmt.Sprintf("%s %s", bi.Browser, bi.Channel)
browser_id = strings.ReplaceAll(browser_id, "-", " ")
browser_id = cases.Title(language.English).String(browser_id)
if bi.Browser == "chrome" && bi.Platform != "android" {
if bi.Channel == "stable" && !skipFinch {
browser_id += " (Top Finch Variations)"
} else if bi.Channel == "dev" && skipFinch {
browser_id += " (Disable Field Trial)"
}
}
for c, v := range common.SortedRange(values) {
h := perfresults.Histogram{
SampleValues: v,
}
u := units[c]
r := format.Result{
Key: map[string]string{
"test": c,
"unit": u,
"subtest_1": browser_id,
},
Measurements: map[string][]format.SingleMeasurement{
// Following the convention used in CBB v2 (bench-o-matic) and v3 (swarming-in-google3),
// we use median (instead of mean) as the data value. We also return standard deviation
// as a measurement of noise. Other statistics can be added in the future when needed.
"stat": {
{
Value: "value",
Measurement: float32(h.Median()),
},
{
Value: "error",
Measurement: float32(h.Stddev()),
},
},
},
}
if strings.HasSuffix(units[c], "_smallerIsBetter") {
r.Key["improvement_direction"] = "down"
} else if strings.HasSuffix(units[c], "_biggerIsBetter") {
r.Key["improvement_direction"] = "up"
}
data.Results = append(data.Results, r)
}
return &data
}
// Activity to upload CBB results to cloud storage, for import into perf dashboard.
func UploadCbbResultsActivity(ctx context.Context, t time.Time, cbb *CbbRunnerParams, benchmark, results string) (string, error) {
if cbb.Bucket == "" {
return "", nil
}
store, err := NewStore(ctx, cbb.Bucket, false)
if err != nil {
return "", skerr.Wrap(err)
}
// The file path inside the GS bucket uses a pattern similar to the one used by perf waterfall.
datePath := fmt.Sprintf("%04d/%02d/%02d", t.Year(), t.Month(), t.Day())
timestamp := fmt.Sprintf("%02d%02d%02d", t.Hour(), t.Minute(), t.Second())
filename := fmt.Sprintf(
"cbb_results_%s_%s_%s_%s_%04d_%02d_%02d_%02d_%02d_%02d.json",
benchmark, cbb.BotConfig, cbb.Browser, cbb.Channel,
t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(),
)
storeFilePath := path.Join(
"ingest", datePath, "ChromiumPerf", cbb.BotConfig, timestamp, benchmark, filename)
err = store.WriteFile(storeFilePath, results)
if err != nil {
return "", skerr.Wrap(err)
}
gsPath := fmt.Sprintf("gs://%s/%s", cbb.Bucket, storeFilePath)
return gsPath, nil
}