golang.org/x/build@v0.0.0-20240506185731-218518f32b70/perfdata/client.go (about)

     1  // Copyright 2017 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package perfdata contains a client for the performance data storage server.
     6  package perfdata
     7  
     8  import (
     9  	"context"
    10  	"encoding/json"
    11  	"fmt"
    12  	"io"
    13  	"mime/multipart"
    14  	"net/http"
    15  	"net/url"
    16  
    17  	"golang.org/x/net/context/ctxhttp"
    18  	"golang.org/x/perf/storage/benchfmt"
    19  )
    20  
    21  // A Client issues queries to a performance data storage server.
    22  // It is safe to use from multiple goroutines simultaneously.
    23  type Client struct {
    24  	// BaseURL is the base URL of the storage server.
    25  	BaseURL string
    26  	// HTTPClient is the HTTP client for sending requests. If nil, http.DefaultClient will be used.
    27  	HTTPClient *http.Client
    28  }
    29  
    30  // httpClient returns the http.Client to use for requests.
    31  func (c *Client) httpClient() *http.Client {
    32  	if c.HTTPClient != nil {
    33  		return c.HTTPClient
    34  	}
    35  	return http.DefaultClient
    36  }
    37  
    38  // Query searches for results matching the given query string. The
    39  // result is a stream of bytes containing text benchmark data. This data
    40  // may be parsed and processed by the x/perf/benchfmt package.
    41  //
    42  // The query string is first parsed into quoted words (as in the shell)
    43  // and then each word must be formatted as one of the following:
    44  // key:value - exact match on label "key" = "value"
    45  // key>value - value greater than (useful for dates)
    46  // key<value - value less than (also useful for dates)
    47  func (c *Client) Query(ctx context.Context, q string) (io.ReadCloser, error) {
    48  	hc := c.httpClient()
    49  
    50  	resp, err := ctxhttp.Get(ctx, hc, c.BaseURL+"/search?"+url.Values{"q": []string{q}}.Encode())
    51  	if err != nil {
    52  		return nil, err
    53  	}
    54  	if resp.StatusCode != 200 {
    55  		defer resp.Body.Close()
    56  		body, err := io.ReadAll(resp.Body)
    57  		if err != nil {
    58  			return nil, err
    59  		}
    60  		return nil, fmt.Errorf("%s", body)
    61  	}
    62  	return resp.Body, nil
    63  }
    64  
    65  // UploadInfo represents an upload summary.
    66  type UploadInfo struct {
    67  	Count       int
    68  	UploadID    string
    69  	LabelValues benchfmt.Labels `json:",omitempty"`
    70  }
    71  
    72  // ListUploads searches for uploads containing results matching the given query string.
    73  // The query may be empty, in which case all uploads will be returned.
    74  // extraLabels specifies other labels to be retrieved.
    75  // If limit is 0, no limit will be provided to the server.
    76  // The uploads are returned starting with the most recent upload.
    77  func (c *Client) ListUploads(ctx context.Context, q string, extraLabels []string, limit int) *UploadList {
    78  	hc := c.httpClient()
    79  
    80  	v := url.Values{"extra_label": extraLabels}
    81  	if q != "" {
    82  		v["q"] = []string{q}
    83  	}
    84  	if limit != 0 {
    85  		v["limit"] = []string{fmt.Sprintf("%d", limit)}
    86  	}
    87  
    88  	u := c.BaseURL + "/uploads"
    89  	if len(v) > 0 {
    90  		u += "?" + v.Encode()
    91  	}
    92  	resp, err := ctxhttp.Get(ctx, hc, u)
    93  	if err != nil {
    94  		return &UploadList{err: err}
    95  	}
    96  	if resp.StatusCode != 200 {
    97  		body, err := io.ReadAll(resp.Body)
    98  		if err != nil {
    99  			return &UploadList{err: err}
   100  		}
   101  		return &UploadList{err: fmt.Errorf("%s", body)}
   102  	}
   103  	return &UploadList{body: resp.Body, dec: json.NewDecoder(resp.Body)}
   104  }
   105  
   106  // UploadList is the result of ListUploads.
   107  // Use Next to advance through the rows, making sure to call Close when done:
   108  //
   109  //	q := db.ListUploads("key:value")
   110  //	defer q.Close()
   111  //	for q.Next() {
   112  //	  id, count := q.Row()
   113  //	  labels := q.LabelValues()
   114  //	  ...
   115  //	}
   116  //	err = q.Err() // get any error encountered during iteration
   117  //	...
   118  type UploadList struct {
   119  	body io.Closer
   120  	dec  *json.Decoder
   121  	// from last call to Next
   122  	ui  UploadInfo
   123  	err error
   124  }
   125  
   126  // Next prepares the next result for reading with the Result
   127  // method. It returns false when there are no more results, either by
   128  // reaching the end of the input or an error.
   129  func (ul *UploadList) Next() bool {
   130  	if ul.err != nil {
   131  		return false
   132  	}
   133  
   134  	// Clear UploadInfo before decoding new value.
   135  	ul.ui = UploadInfo{}
   136  
   137  	ul.err = ul.dec.Decode(&ul.ui)
   138  	return ul.err == nil
   139  }
   140  
   141  // Info returns the most recent UploadInfo generated by a call to Next.
   142  func (ul *UploadList) Info() UploadInfo {
   143  	return ul.ui
   144  }
   145  
   146  // Err returns the error state of the query.
   147  func (ul *UploadList) Err() error {
   148  	if ul.err == io.EOF {
   149  		return nil
   150  	}
   151  	return ul.err
   152  }
   153  
   154  // Close frees resources associated with the query.
   155  func (ul *UploadList) Close() error {
   156  	if ul.body != nil {
   157  		err := ul.body.Close()
   158  		ul.body = nil
   159  		return err
   160  	}
   161  	return ul.Err()
   162  }
   163  
   164  // NewUpload starts a new upload to the storage server.
   165  // The upload must have Abort or Commit called on it.
   166  // If the server requires authentication for uploads, c.HTTPClient should be set to the result of oauth2.NewClient.
   167  func (c *Client) NewUpload(ctx context.Context) *Upload {
   168  	hc := c.httpClient()
   169  
   170  	pr, pw := io.Pipe()
   171  	mpw := multipart.NewWriter(pw)
   172  
   173  	req, err := http.NewRequest("POST", c.BaseURL+"/upload", pr)
   174  	if err != nil {
   175  		return &Upload{err: err}
   176  	}
   177  	req.Header.Set("Content-Type", mpw.FormDataContentType())
   178  	req.Header.Set("User-Agent", "golang.org/x/build/perfdata")
   179  	errCh := make(chan error)
   180  	u := &Upload{pw: pw, mpw: mpw, errCh: errCh}
   181  	go func() {
   182  		resp, err := ctxhttp.Do(ctx, hc, req)
   183  		if err != nil {
   184  			errCh <- err
   185  			return
   186  		}
   187  		defer resp.Body.Close()
   188  		if resp.StatusCode != 200 {
   189  			body, _ := io.ReadAll(resp.Body)
   190  			errCh <- fmt.Errorf("upload failed: %v\n%s", resp.Status, body)
   191  			return
   192  		}
   193  		status := &UploadStatus{}
   194  		if err := json.NewDecoder(resp.Body).Decode(status); err != nil {
   195  			errCh <- err
   196  		}
   197  		u.status = status
   198  		errCh <- nil
   199  	}()
   200  	return u
   201  }
   202  
   203  // UploadStatus contains information about a successful upload.
   204  type UploadStatus struct {
   205  	// UploadID is the upload ID assigned to the upload.
   206  	UploadID string `json:"uploadid"`
   207  	// FileIDs is the list of file IDs assigned to the files in the upload.
   208  	FileIDs []string `json:"fileids"`
   209  	// ViewURL is a server-supplied URL to view the results.
   210  	ViewURL string `json:"viewurl"`
   211  }
   212  
   213  // An Upload is an in-progress upload.
   214  // Use CreateFile to upload one or more files, then call Commit or Abort.
   215  //
   216  //	u := client.NewUpload()
   217  //	w, err := u.CreateFile()
   218  //	if err != nil {
   219  //	  u.Abort()
   220  //	  return err
   221  //	}
   222  //	fmt.Fprintf(w, "BenchmarkResult 1 1 ns/op\n")
   223  //	if err := u.Commit(); err != nil {
   224  //	  return err
   225  //	}
   226  type Upload struct {
   227  	pw     io.WriteCloser
   228  	mpw    *multipart.Writer
   229  	status *UploadStatus
   230  	// errCh is used to report the success/failure of the HTTP request
   231  	errCh chan error
   232  	// err is the first observed error; it is only accessed from user-called methods for thread safety
   233  	err error
   234  }
   235  
   236  // CreateFile creates a new upload with the given name.
   237  // The Writer may be used until CreateFile is called again.
   238  // name may be the empty string if the file does not have a name.
   239  func (u *Upload) CreateFile(name string) (io.Writer, error) {
   240  	if u.err != nil {
   241  		return nil, u.err
   242  	}
   243  	return u.mpw.CreateFormFile("file", name)
   244  }
   245  
   246  // Commit attempts to commit the upload.
   247  func (u *Upload) Commit() (*UploadStatus, error) {
   248  	if u.err != nil {
   249  		return nil, u.err
   250  	}
   251  	if u.err = u.mpw.WriteField("commit", "1"); u.err != nil {
   252  		u.Abort()
   253  		return nil, u.err
   254  	}
   255  	if u.err = u.mpw.Close(); u.err != nil {
   256  		u.Abort()
   257  		return nil, u.err
   258  	}
   259  	u.mpw = nil
   260  	if u.err = u.pw.Close(); u.err != nil {
   261  		u.Abort()
   262  		return nil, u.err
   263  	}
   264  	u.pw = nil
   265  	u.err = <-u.errCh
   266  	u.errCh = nil
   267  	if u.err != nil {
   268  		return nil, u.err
   269  	}
   270  	return u.status, nil
   271  }
   272  
   273  // Abort attempts to cancel the in-progress upload.
   274  func (u *Upload) Abort() error {
   275  	if u.mpw != nil {
   276  		u.mpw.WriteField("abort", "1")
   277  		// Writing the 'abort' field will cause the server to send back an error response.
   278  		u.mpw.Close()
   279  		u.mpw = nil
   280  	}
   281  	if u.pw != nil {
   282  		u.pw.Close()
   283  		u.pw = nil
   284  	}
   285  	err := <-u.errCh
   286  	u.errCh = nil
   287  	if u.err == nil {
   288  		u.err = err
   289  	}
   290  	return u.err
   291  }