blob: 16ad93c42a0e580422e4c13d94230efa4056f88a [file] [log] [blame]
package api
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/url"
"time"
"github.com/go-chi/chi/v5"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/paramtools"
"go.skia.org/infra/go/query"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/perf/go/config"
"go.skia.org/infra/perf/go/dataframe"
"go.skia.org/infra/perf/go/psrefresh"
"go.skia.org/infra/perf/go/ui/frame"
)
// queryApi provides a struct handle api requests related to the query dialog.
type queryApi struct {
paramsetRefresher psrefresh.ParamSetRefresher
}
// NewQueryApi returns a new instance of queryApi struct.
func NewQueryApi(paramsetRefresher psrefresh.ParamSetRefresher) queryApi {
return queryApi{
paramsetRefresher: paramsetRefresher,
}
}
// RegisterHandlers registers the api handlers for their respective routes.
func (api queryApi) RegisterHandlers(router *chi.Mux) {
router.HandleFunc("/_/initpage/", api.initpageHandler)
router.Post("/_/count/", api.countHandler)
router.Post("/_/nextParamList/", api.nextParamListHandler)
}
// NextParamListHandlerRequest is the JSON format for NextParamListHandler request.
type NextParamListHandlerRequest struct {
Query string `json:"q"`
}
// NextParamListHandlerResponse is the JSON format for NextParamListHandler response.
type NextParamListHandlerResponse struct {
Count int `json:"count"`
Paramset paramtools.ReadOnlyParamSet `json:"paramset"`
}
// CountHandlerRequest is the JSON format for the countHandler request.
type CountHandlerRequest struct {
Q string `json:"q"`
Begin int `json:"begin"`
End int `json:"end"`
}
// CountHandlerResponse is the JSON format if the countHandler response.
type CountHandlerResponse struct {
Count int `json:"count"`
Paramset paramtools.ReadOnlyParamSet `json:"paramset"`
}
// PreflightQuery generates the query and calls PreflightQuery on dfBuilder
func (api *queryApi) PreflightQuery(ctx context.Context, w http.ResponseWriter, qs string) (int, paramtools.ReadOnlyParamSet, error) {
u, err := url.ParseQuery(qs)
if err != nil {
httputils.ReportError(w, err, "Invalid URL query.", http.StatusInternalServerError)
return 0, nil, err
}
q, err := query.New(u)
if err != nil {
httputils.ReportError(w, err, "Invalid query.", http.StatusInternalServerError)
return 0, nil, err
}
fullPS := api.getParamSet()
if qs == "" {
return 0, fullPS, nil
} else {
count, ps, err := api.paramsetRefresher.GetParamSetForQuery(ctx, q, u)
if err != nil {
httputils.ReportError(w, err, "Failed to Preflight the query, too many key-value pairs selected. Limit is 200.", http.StatusBadRequest)
return 0, nil, err
}
return int(count), filterParamSetIfNeeded(ps.Freeze()), nil
}
}
// nextParamListHandler takes the POST'd query and runs that against the current
// dataframe and returns how many traces match the query.
// Notice that nextParamListHandler is a chromeperf specific version of countHander.
// The differences here are:
// - in the UI, the parameter fields take the user intpus in strict order.
// e.g., end users are expect to first input benchmark, and then bot,
// and then measurement, etc. The order is defined in include_params in the config file.
// - the reponse does not includes all paramsets. It only returns the values for the
// 'next' parameter in order.
func (api *queryApi) nextParamListHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
defer cancel()
w.Header().Set("Content-Type", "application/json")
var npr NextParamListHandlerRequest
if err := json.NewDecoder(r.Body).Decode(&npr); err != nil {
httputils.ReportError(w, err, "Failed to decode JSON.", http.StatusInternalServerError)
return
}
nextParam, err := findNextParamInQueryString(npr.Query)
if err != nil {
httputils.ReportError(w, err, "Error in findNextParamInQueryString.", http.StatusInternalServerError)
return
}
count, ps, err := api.PreflightQuery(ctx, w, npr.Query)
if err != nil {
httputils.ReportError(w, err, "Error in nextParamListHandler.", http.StatusInternalServerError)
return
}
resp := NextParamListHandlerResponse{
Count: count,
}
if nextParam == "" {
// There is no next parameter. No filtering is needed.
resp.Paramset = map[string][]string{}
} else {
// There's a next parameter, but there's no matching paramset.
if _, ok := ps[nextParam]; !ok {
resp.Paramset = map[string][]string{}
} else {
resp.Paramset = map[string][]string{nextParam: ps[nextParam]}
}
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
httputils.ReportError(w, err, "Failed to encode nextparam response.", http.StatusInternalServerError)
}
}
// countHandler takes the POST'd query and runs that against the current
// dataframe and returns how many traces match the query.
func (api *queryApi) countHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
defer cancel()
w.Header().Set("Content-Type", "application/json")
var cr CountHandlerRequest
if err := json.NewDecoder(r.Body).Decode(&cr); err != nil {
httputils.ReportError(w, err, "Failed to decode JSON.", http.StatusInternalServerError)
return
}
count, ps, err := api.PreflightQuery(ctx, w, cr.Q)
if err != nil {
sklog.Errorf("Error in nextParamListHandler: %s", err)
}
resp := CountHandlerResponse{
Count: count,
Paramset: ps,
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
sklog.Errorf("Failed to encode paramset: %s", err)
}
}
// initpageHandler returns the paramset to initialize the page.
func (f *queryApi) initpageHandler(w http.ResponseWriter, _ *http.Request) {
resp := &frame.FrameResponse{
DataFrame: &dataframe.DataFrame{
ParamSet: f.getParamSet(),
},
Skps: []int{},
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(resp); err != nil {
sklog.Errorf("Failed to encode paramset: %s", err)
}
}
// given a query string from UI, find the next paramter which is:
// - not in the query string, and
// - in the include_params next in order.
//
// e.g., if end user has selected values for 'benchmark' and 'bot', and the
// include_params is ["benchmark","bot","test","subtest_1","subtest_2","subtest_3"],
// the next expected parameter is 'test'.
func findNextParamInQueryString(qs string) (string, error) {
// If no include_params is defined in config, we have no way to tell
// which is 'next'.
if config.Config.QueryConfig.IncludedParams == nil {
err := errors.New("no included parameter list in config")
sklog.Error("No included parameter list in config.", err)
return "", err
}
qKeyValues, err := url.ParseQuery(qs)
if err != nil {
sklog.Error("Invalid URL query. %s", err)
return "", err
}
for _, key := range config.Config.QueryConfig.IncludedParams {
// when scanning the parameter list in order, if the included parameter
// key is not in the query, it is the next we are looking for.
if _, ok := qKeyValues[key]; !ok {
return key, nil
}
}
// all included parameter keys are in the query.
return "", nil
}
// getParamSet returns a fresh paramtools.ParamSet that represents all the
// traces stored in the two most recent tiles in the trace store. It is filtered
// if such filtering is turned on in the config.
func (api *queryApi) getParamSet() paramtools.ReadOnlyParamSet {
paramSet := api.paramsetRefresher.GetAll()
return filterParamSetIfNeeded(paramSet)
}
// filterParamSetIfNeeded filters the paramset if any filters have been specified in
// the query config.
func filterParamSetIfNeeded(paramSet paramtools.ReadOnlyParamSet) paramtools.ReadOnlyParamSet {
if config.Config.QueryConfig.IncludedParams != nil {
filteredParamSet := paramtools.NewParamSet()
for _, key := range config.Config.QueryConfig.IncludedParams {
if val, ok := paramSet[key]; ok {
existing, exists := filteredParamSet[key]
if exists {
existing = append(existing, val...)
} else {
existing = val
}
filteredParamSet[key] = existing
}
}
paramSet = paramtools.ReadOnlyParamSet(filteredParamSet)
}
return paramSet
}