blob: ec6d4ca9df0cb9af945f8f249e4486eb21f89df9 [file] [log] [blame]
package chromeperf
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
slices0 "slices"
"go.opencensus.io/trace"
"go.skia.org/infra/go/auth"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
"golang.org/x/exp/slices"
"golang.org/x/oauth2/google"
)
const (
// Change to "http://localhost:8080" for local testing of skia-bridge.
chromePerfBaseUrl = "https://skia-bridge-dot-chromeperf.appspot.com"
chromePerfLegacyBaseUrl = "https://chromeperf.appspot.com"
)
// ChromePerfClient defines an interface for accessing chromeperf apis.
type ChromePerfClient interface {
// SendGetRequest sends a GET request to chromeperf api with the specified parameters.
// The url is of the format <host>/{apiName}/{functionName}?{queryParams}.
// The response from the api is unmarshalled into the provided response object.
SendGetRequest(ctx context.Context, apiName string, functionName string, queryParams url.Values, response interface{}) error
// SendPostRequest sends a POST request to chromeperf api with the specified parameters.
// The url is of the format <host>/{apiName}/{functionName}.
// The {requestObj} is marshalled into JSON and added to the body of the http object.
// The response from the api is unmarshalled into the provided response object.
// {acceptedStatusCodes} is a list of HTTP response codes that are considered successful. The function will return an error if any other status code is returned.
SendPostRequest(ctx context.Context, apiName string, functionName string, requestObj interface{}, responseObj interface{}, acceptedStatusCodes []int) error
}
// chromePerfClientImpl implements ChromePerfClient.
type chromePerfClientImpl struct {
httpClient *http.Client
urlOverride string
// If true, requests are sent to chromeperf without skia-bridge
directCallLegacy bool
}
// NewChromePerfClient creates a new instance of ChromePerfClient.
func NewChromePerfClient(ctx context.Context, urlOverride string, directCall bool) (ChromePerfClient, error) {
tokenSource, err := google.DefaultTokenSource(ctx, auth.ScopeUserinfoEmail)
if err != nil {
return nil, skerr.Wrapf(err, "Failed to create chrome perf client.")
}
return &chromePerfClientImpl{
httpClient: httputils.DefaultClientConfig().WithTokenSource(tokenSource).Client(),
urlOverride: urlOverride,
directCallLegacy: directCall,
}, nil
}
func isAllowedStatusCode(statusCode int) bool {
allowedSuccessCodes := []int{
http.StatusOK, // 200
http.StatusNoContent, // 204
http.StatusPartialContent, // 206
http.StatusNonAuthoritativeInfo, // 203
http.StatusNotModified, // 304
}
return slices0.Contains(allowedSuccessCodes, statusCode)
}
// SendGetRequest sends a GET request to chromeperf api with the specified parameters.
func (client *chromePerfClientImpl) SendGetRequest(ctx context.Context, apiName string, functionName string, queryParams url.Values, response interface{}) error {
targetUrl := generateTargetUrl(client.urlOverride, client.directCallLegacy, apiName, functionName)
targetUrl = fmt.Sprintf("%s?%s", targetUrl, queryParams.Encode())
httpResponse, err := httputils.GetWithContext(ctx, client.httpClient, targetUrl)
if err != nil {
return skerr.Wrapf(err, "Failed to get chrome perf response.")
}
// Ensure the response body is closed no matter how the function exits.
defer httpResponse.Body.Close()
if !isAllowedStatusCode(httpResponse.StatusCode) {
return skerr.Fmt("chrome perf request failed: %v", httpResponse.StatusCode)
}
respBody, err := io.ReadAll(httpResponse.Body)
if err != nil {
return skerr.Wrapf(err, "Failed to read body from chrome perf response.")
}
err = json.Unmarshal([]byte(respBody), &response)
if err != nil {
return skerr.Wrapf(err, "Failed to parse chrome perf response body.")
}
return nil
}
// SendPostRequest sends a POST request to chromeperf api with the specified parameters.
func (client *chromePerfClientImpl) SendPostRequest(ctx context.Context, apiName string, functionName string, requestObj interface{}, responseObj interface{}, acceptedStatusCodes []int) error {
ctx, span := trace.StartSpan(ctx, "chromeperf.chromePerfClientImpl.SendPostRequest")
defer span.End()
targetUrl := generateTargetUrl(client.urlOverride, client.directCallLegacy, apiName, functionName)
var buff bytes.Buffer
err := json.NewEncoder(&buff).Encode(requestObj)
if err != nil {
return skerr.Wrapf(err, "Failed to create chrome perf request.")
}
requestBodyJSONStr := buff.String()
sklog.Debugf("Sending Post request to chromePerf: %s", requestBodyJSONStr)
httpResponse, err := httputils.PostWithContext(
ctx,
client.httpClient,
targetUrl,
"application/json",
strings.NewReader(string(requestBodyJSONStr)))
if err != nil {
return skerr.Wrapf(err, "Failed to get chrome perf response.")
}
// Ensure the response body is closed no matter how the function exits.
defer httpResponse.Body.Close()
if !slices.Contains(acceptedStatusCodes, httpResponse.StatusCode) {
return skerr.Fmt("Receive status %d from chromeperf", httpResponse.StatusCode)
}
if err := json.NewDecoder(httpResponse.Body).Decode(&responseObj); err != nil {
return skerr.Wrapf(err, "Failed to parse chrome perf response body.")
}
return nil
}
func generateTargetUrl(urlOverride string, directCallLegacy bool, apiName string, functionName string) string {
if len(urlOverride) > 0 {
return urlOverride
}
if directCallLegacy {
return fmt.Sprintf("%s/%s", chromePerfLegacyBaseUrl, apiName)
}
return fmt.Sprintf("%s/%s/%s", chromePerfBaseUrl, apiName, functionName)
}