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 }