blob: c3f06c91bf73c959869529a1a712ed84f116f4fb [file] [log] [blame]
package pinpoint
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"go.skia.org/infra/go/auth"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/metrics2"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/pinpoint/go/bot_configs"
"golang.org/x/oauth2/google"
)
const (
pinpointURL = "https://chromeperf.appspot.com/pinpoint/new/bisect"
// use the legacy endpoint to run A/B Try jobs (pairwise jobs)
pinpointLegacyURL = "https://pinpoint-dot-chromeperf.appspot.com/api/new"
contentType = "application/json"
tryJobComparisonMode = "try"
)
type CreateLegacyTryRequest struct {
Name string `json:"name"`
BaseGitHash string `json:"base_git_hash"`
// although "experiment" makes more sense in this context, the legacy Pinpoint API
// explicitly defines the experiment commit as "end_git_hash" and defines
// the experiment patch as "experiment_patch"
EndGitHash string `json:"end_git_hash"`
BasePatch string `json:"base_patch"`
ExperimentPatch string `json:"experiment_patch"`
Configuration string `json:"configuration"`
Benchmark string `json:"benchmark"`
Story string `json:"story"`
ExtraTestArgs string `json:"extra_test_args"`
Repository string `json:"repository"`
BugId string `json:"bug_id"`
User string `json:"user"`
}
type CreateBisectRequest struct {
ComparisonMode string `json:"comparison_mode"`
StartGitHash string `json:"start_git_hash"`
EndGitHash string `json:"end_git_hash"`
Configuration string `json:"configuration"`
Benchmark string `json:"benchmark"`
Story string `json:"story"`
Chart string `json:"chart"`
Statistic string `json:"statistic"`
ComparisonMagnitude string `json:"comparison_magnitude"`
Pin string `json:"pin"`
Project string `json:"project"`
BugId string `json:"bug_id"`
User string `json:"user"`
AlertIDs string `json:"alert_ids"`
}
type CreatePinpointResponse struct {
JobID string `json:"jobId"`
JobURL string `json:"jobUrl"`
}
type Client struct {
httpClient *http.Client
createBisectCalled metrics2.Counter
createBisectFailed metrics2.Counter
createTryJobCalled metrics2.Counter
createTryJobFailed metrics2.Counter
}
// New returns a new PinpointClient instance.
func New(ctx context.Context) (*Client, error) {
tokenSource, err := google.DefaultTokenSource(ctx, auth.ScopeUserinfoEmail)
if err != nil {
return nil, skerr.Wrapf(err, "Failed to create pinpoint client.")
}
client := httputils.DefaultClientConfig().WithTokenSource(tokenSource).Client()
return &Client{
httpClient: client,
createBisectCalled: metrics2.GetCounter("pinpoint_create_bisect_called"),
createBisectFailed: metrics2.GetCounter("pinpoint_create_bisect_failed"),
createTryJobCalled: metrics2.GetCounter("pinpoint_create_try_job_called"),
createTryJobFailed: metrics2.GetCounter("pinpoint_create_try_job_failed"),
}, nil
}
// CreateTryJob calls the legacy pinpoint API to create a try job.
func (pc *Client) CreateTryJob(ctx context.Context, req CreateLegacyTryRequest) (*CreatePinpointResponse, error) {
pc.createTryJobCalled.Inc(1)
requestURL, err := buildTryJobRequestURL(req)
if err != nil {
pc.createTryJobFailed.Inc(1)
return nil, skerr.Wrapf(err, "Failed to generate Pinpoint request URL.")
}
sklog.Debugf("Preparing to call this Pinpoint service URL: %s", requestURL)
httpResponse, err := httputils.PostWithContext(ctx, pc.httpClient, requestURL, contentType, nil)
if err != nil {
pc.createTryJobFailed.Inc(1)
return nil, skerr.Wrapf(err, "Failed to get pinpoint response.")
}
sklog.Debugf("Got response from Pinpoint service: %+v", *httpResponse)
respBody, err := io.ReadAll(httpResponse.Body)
if err != nil {
pc.createTryJobFailed.Inc(1)
return nil, skerr.Wrapf(err, "Failed to read body from pinpoint response.")
}
if httpResponse.StatusCode != http.StatusOK {
errMsg := fmt.Sprintf("The try job request failed with status code %d and body: %s", httpResponse.StatusCode, string(respBody))
err = errors.New(errMsg)
pc.createTryJobFailed.Inc(1)
return nil, skerr.Wrapf(err, "Pinpoint request failed.")
}
resp := CreatePinpointResponse{}
err = json.Unmarshal([]byte(respBody), &resp)
if err != nil {
pc.createTryJobFailed.Inc(1)
return nil, skerr.Wrapf(err, "Failed to parse pinpoint response body.")
}
return &resp, nil
}
func buildTryJobRequestURL(req CreateLegacyTryRequest) (string, error) {
if req.Benchmark == "" {
return "", skerr.Fmt("Benchmark must be specified but is empty.")
}
if req.Configuration == "" {
return "", skerr.Fmt("Configuration must be specified but is empty.")
}
target, err := bot_configs.GetIsolateTarget(req.Configuration, req.Benchmark)
if err != nil {
return "", skerr.Wrapf(err, "Could not find target of bot %s and benchmark %s", req.Configuration, req.Benchmark)
}
params := url.Values{}
// Pinpoint try jobs always use comparison mode try
params.Set("comparison_mode", tryJobComparisonMode)
if req.Name != "" {
params.Set("name", req.Name)
}
if req.BaseGitHash != "" {
params.Set("base_git_hash", req.BaseGitHash)
}
if req.EndGitHash != "" {
params.Set("end_git_hash", req.EndGitHash)
}
if req.BasePatch != "" {
params.Set("base_patch", req.BasePatch)
}
if req.ExperimentPatch != "" {
params.Set("experiment_patch", req.ExperimentPatch)
}
if req.Configuration != "" {
params.Set("configuration", req.Configuration)
}
if req.Benchmark != "" {
params.Set("benchmark", req.Benchmark)
}
if req.Story != "" {
params.Set("story", req.Story)
}
if req.ExtraTestArgs != "" {
params.Set("extra_test_args", req.ExtraTestArgs)
}
if req.Repository != "" {
params.Set("repository", req.Repository)
}
if req.BugId != "" {
params.Set("bug_id", req.BugId)
}
if req.User != "" {
params.Set("user", req.User)
}
params.Set("target", target)
params.Set("tags", "{\"origin\":\"skia_perf\"}")
return fmt.Sprintf("%s?%s", pinpointLegacyURL, params.Encode()), nil
}
// CreateBisect calls pinpoint API to create bisect job.
func (pc *Client) CreateBisect(ctx context.Context, createBisectRequest CreateBisectRequest) (*CreatePinpointResponse, error) {
pc.createBisectCalled.Inc(1)
requestURL := buildBisectRequestURL(createBisectRequest)
sklog.Debugf("Preparing to call this Pinpoint service URL: %s", requestURL)
httpResponse, err := httputils.PostWithContext(ctx, pc.httpClient, requestURL, contentType, nil)
if err != nil {
pc.createBisectFailed.Inc(1)
return nil, skerr.Wrapf(err, "Failed to get pinpoint response.")
}
sklog.Debugf("Got response from Pinpoint service: %+v", *httpResponse)
respBody, err := io.ReadAll(httpResponse.Body)
if err != nil {
pc.createBisectFailed.Inc(1)
return nil, skerr.Wrapf(err, "Failed to read body from pinpoint response.")
}
if httpResponse.StatusCode != http.StatusOK {
errMsg := fmt.Sprintf("The bisect request failed with status code %d and body: %s", httpResponse.StatusCode, string(respBody))
err = errors.New(errMsg)
pc.createBisectFailed.Inc(1)
return nil, skerr.Wrapf(err, "Pinpoint request failed.")
}
resp := CreatePinpointResponse{}
err = json.Unmarshal([]byte(respBody), &resp)
if err != nil {
pc.createBisectFailed.Inc(1)
return nil, skerr.Wrapf(err, "Failed to parse pinpoint response body.")
}
return &resp, nil
}
func buildBisectRequestURL(createBisectRequest CreateBisectRequest) string {
params := url.Values{}
if createBisectRequest.ComparisonMode != "" {
params.Set("comparison_mode", createBisectRequest.ComparisonMode)
}
if createBisectRequest.StartGitHash != "" {
params.Set("start_git_hash", createBisectRequest.StartGitHash)
}
if createBisectRequest.EndGitHash != "" {
params.Set("end_git_hash", createBisectRequest.EndGitHash)
}
if createBisectRequest.Configuration != "" {
params.Set("configuration", createBisectRequest.Configuration)
}
if createBisectRequest.Benchmark != "" {
params.Set("benchmark", createBisectRequest.Benchmark)
}
if createBisectRequest.Story != "" {
params.Set("story", createBisectRequest.Story)
}
if createBisectRequest.Chart != "" {
params.Set("chart", createBisectRequest.Chart)
}
if createBisectRequest.Statistic != "" {
params.Set("statistic", createBisectRequest.Statistic)
}
if createBisectRequest.ComparisonMagnitude != "" {
params.Set("comparison_magnitude", createBisectRequest.ComparisonMagnitude)
}
if createBisectRequest.Pin != "" {
params.Set("pin", createBisectRequest.Pin)
}
if createBisectRequest.Project != "" {
params.Set("project", createBisectRequest.Project)
}
if createBisectRequest.BugId != "" {
params.Set("bug_id", createBisectRequest.BugId)
} else {
// Pinpoint requires the presence of bug_id param.
params.Set("bug_id", "null")
}
if createBisectRequest.User != "" {
params.Set("user", createBisectRequest.User)
}
if createBisectRequest.AlertIDs != "" {
params.Set("alert_ids", createBisectRequest.AlertIDs)
}
params.Set("tags", "{\"origin\":\"skia_perf\"}")
// test_path is needed by the API, which is a legacy key from
// Chromeperf. The format is
// {master}/{bot}/{benchmark}/{chart}/{story}
// and will cut off at the lowest available piece.
test_path_parts := []string{"ChromiumPerf"}
required_pieces := []string{
createBisectRequest.Configuration,
createBisectRequest.Benchmark,
createBisectRequest.Chart,
createBisectRequest.Story,
}
for _, val := range required_pieces {
if val == "" {
break
}
test_path_parts = append(test_path_parts, val)
}
params.Set("test_path", strings.Join(test_path_parts, "/"))
return fmt.Sprintf("%s?%s", pinpointURL, params.Encode())
}