funcbench/env.go (187 lines of code) (raw):
// Copyright 2020 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"bytes"
"context"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/google/go-github/v29/github"
"github.com/pkg/errors"
"golang.org/x/oauth2"
"golang.org/x/perf/benchstat"
)
type Environment interface {
BenchFunc() string
CompareTarget() string
SetHashStrings(compareTargetHash, repoHeadHashString string)
PostErr(err string) error
PostResults(tables []*benchstat.Table, extraInfo ...string) error
Repo() *git.Repository
}
type environment struct {
logger Logger
benchFunc string
compareTarget string
compareTargetHashString string
repoHeadHashString string
}
func (e environment) BenchFunc() string { return e.benchFunc }
func (e environment) CompareTarget() string { return e.compareTarget }
func (e *environment) SetHashStrings(compareTargetHash, repoHeadHashString string) {
e.compareTargetHashString = compareTargetHash
e.repoHeadHashString = repoHeadHashString
}
type Local struct {
environment
repo *git.Repository
}
func newLocalEnv(e environment) (Environment, error) {
r, err := git.PlainOpenWithOptions(".", &git.PlainOpenOptions{DetectDotGit: true})
if err != nil {
return nil, err
}
e.logger.Println("[Local Mode]", "\nBenchmarking current version versus:", e.compareTarget, "\nBenchmark func regex:", e.benchFunc)
return &Local{environment: e, repo: r}, nil
}
func (l *Local) PostErr(string) error { return nil } // Noop. We will see error anyway.
func (l *Local) PostResults(tables []*benchstat.Table, extraInfo ...string) error {
legend := fmt.Sprintf("Old: %s\nNew: %s",
l.compareTargetHashString,
l.repoHeadHashString,
)
fmt.Printf("Results:\n%s\n", legend)
var buf bytes.Buffer
benchstat.FormatText(&buf, tables)
os.Stdout.Write(buf.Bytes())
return nil
}
func (l *Local) Repo() *git.Repository { return l.repo }
// TODO: Add unit test(!).
type GitHub struct {
environment
repo *git.Repository
client *gitHubClient
ctx context.Context
}
func newGitHubEnv(ctx context.Context, e environment, gc *gitHubClient, workspace string) (Environment, error) {
var r *git.Repository
var err error
retryTime := 10 * time.Second
// Retry 10 times.
for i := 1; i <= 10; i++ {
if err := os.RemoveAll(filepath.Join(workspace, gc.repo)); err != nil {
return nil, err
}
e.logger.Println("Cloning ", gc.owner, ":", gc.repo, " is in progress. Checking in ", retryTime)
time.Sleep(retryTime)
r, err = git.PlainCloneContext(ctx, filepath.Join(workspace, gc.repo), false, &git.CloneOptions{
URL: fmt.Sprintf("https://github.com/%s/%s.git", gc.owner, gc.repo),
Progress: os.Stdout,
})
if err == nil {
break
}
}
if err != nil {
return nil, errors.Wrap(err, "clone git repository")
}
if err := os.Chdir(filepath.Join(workspace, gc.repo)); err != nil {
return nil, errors.Wrapf(err, "changing to %s/%s dir", workspace, gc.repo)
}
g := &GitHub{
environment: e,
repo: r,
client: gc,
ctx: ctx,
}
if err := os.Setenv("CGO_ENABLED", "0"); err != nil {
return nil, err
}
wt, err := g.repo.Worktree()
if err != nil {
return nil, err
}
if err := r.FetchContext(ctx, &git.FetchOptions{
RefSpecs: []config.RefSpec{
config.RefSpec(fmt.Sprintf("+refs/pull/%d/head:refs/heads/pullrequest", gc.prNumber)),
},
Progress: os.Stdout,
}); err != nil && err != git.NoErrAlreadyUpToDate {
return nil, errors.Wrap(err, "fetch to pull request branch")
}
if err = wt.Checkout(&git.CheckoutOptions{
Branch: plumbing.NewBranchReferenceName("pullrequest"),
}); err != nil {
return nil, errors.Wrap(err, "switch to pull request branch")
}
e.logger.Println("[GitHub Mode]", gc.owner, ":", gc.repo, "\nBenchmarking PR -", gc.prNumber, "versus:", e.compareTarget, "\nBenchmark func regex:", e.benchFunc)
return g, nil
}
func (g *GitHub) PostErr(txt string) error {
c := fmt.Sprintf(
"Old: `%v`\nNew: `PR-%v`\n%v",
g.compareTarget,
g.client.prNumber,
txt,
)
if err := g.client.postComment(c); err != nil {
return err
}
return nil
}
func (g *GitHub) PostResults(tables []*benchstat.Table, extraInfo ...string) error {
b := bytes.Buffer{}
if err := formatMarkdown(&b, tables); err != nil {
return err
}
legend := fmt.Sprintf("Old: `%v`/`%v`\nNew: `PR-%v`/`%v`",
g.compareTarget,
g.compareTargetHashString,
g.client.prNumber,
g.repoHeadHashString,
)
result := fmt.Sprintf(
"<details><summary>Click to check benchmark result</summary>\n\n%s\n%s\n%s</details>",
legend,
strings.Join(extraInfo, "\n"),
b.String(),
)
return g.client.postComment(result)
}
func (g *GitHub) Repo() *git.Repository { return g.repo }
type gitHubClient struct {
owner string
repo string
prNumber int
client *github.Client
nocomment bool
ctx context.Context
}
func newGitHubClient(ctx context.Context, owner, repo string, prNumber int, nocomment bool) (*gitHubClient, error) {
ghToken, ok := os.LookupEnv("GITHUB_TOKEN")
if !ok && !nocomment {
return nil, fmt.Errorf("GITHUB_TOKEN missing")
}
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: ghToken})
tc := oauth2.NewClient(ctx, ts)
c := gitHubClient{
client: github.NewClient(tc),
owner: owner,
repo: repo,
prNumber: prNumber,
nocomment: nocomment,
ctx: ctx,
}
return &c, nil
}
func (c *gitHubClient) postComment(comment string) error {
if c.nocomment {
return nil
}
issueComment := &github.IssueComment{Body: github.String(comment)}
_, _, err := c.client.Issues.CreateComment(c.ctx, c.owner, c.repo, c.prNumber, issueComment)
return err
}