From 3f44d470eb30a193aca0c9c9b604cfdf9615b7c2 Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Tue, 22 Sep 2020 13:54:23 -0400 Subject: [PATCH] Add performance monitor code --- cmd/performance/pr-bot/bot.go | 38 ++++++++ pkg/perf/monitor/constants.go | 24 +++++ pkg/perf/monitor/execute.go | 63 ++++++++++++ pkg/perf/monitor/github.go | 179 ++++++++++++++++++++++++++++++++++ 4 files changed, 304 insertions(+) create mode 100644 pkg/perf/monitor/constants.go create mode 100644 pkg/perf/monitor/execute.go create mode 100644 pkg/perf/monitor/github.go diff --git a/cmd/performance/pr-bot/bot.go b/cmd/performance/pr-bot/bot.go index 174c10d9e4..2614ea6884 100644 --- a/cmd/performance/pr-bot/bot.go +++ b/cmd/performance/pr-bot/bot.go @@ -18,12 +18,17 @@ package main import ( "context" + "fmt" "log" + "os" "time" + + "github.com/pkg/errors" ) func main() { for { + log.Print("~~~~~~~~~ Starting performance analysis ~~~~~~~~~~~~~~") if err := analyzePerformance(context.Background()); err != nil { log.Printf("error executing performance analysis: %v", err) } @@ -36,5 +41,38 @@ func main() { // 2. running mkcmp against those PRs // 3. commenting results on those PRs func analyzePerformance(ctx context.Context) error { + logsFile := "/home/performance-monitor/logs.txt" + if _, err := os.Stat(logsFile); err != nil { + return err + } + client := monitor.NewClient(context.Background(), "kubernetes", "minikube") + prs, err := client.ListOpenPRsWithLabel("") + if err != nil { + return errors.Wrap(err, "listing open prs") + } + log.Print("got prs:", prs) + // TODO: priyawadhwa@ for each PR we should comment the error if we get one? + for _, pr := range prs { + log.Printf("~~~ Analyzing PR %d ~~~", pr) + newCommitsExist, err := client.NewCommitsExist(pr, "minikube-pr-bot") + if err != nil { + return err + } + if !newCommitsExist { + log.Println("New commits don't exist, skipping rerun...") + continue + } + // TODO: priyawadhwa@ we should download mkcmp for each run? + var message string + message, err = monitor.RunMkcmp(ctx, pr) + if err != nil { + message = fmt.Sprintf("Error: %v\n%s", err, message) + } + log.Printf("got message for pr %d:\n%s\n", pr, message) + if err := client.CommentOnPR(pr, message); err != nil { + return err + } + log.Print("successfully commented on PR") + } return nil } diff --git a/pkg/perf/monitor/constants.go b/pkg/perf/monitor/constants.go new file mode 100644 index 0000000000..0221374464 --- /dev/null +++ b/pkg/perf/monitor/constants.go @@ -0,0 +1,24 @@ +/* +Copyright 2020 The Kubernetes Authors All rights reserved. + +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 monitor + +const ( + GithubAccessTokenEnvVar = "GITHUB_ACCESS_TOKEN" + OkToTestLabel = "ok-to-test" + GithubOwner = "kubernetes" + GithubRepo = "minikube" +) diff --git a/pkg/perf/monitor/execute.go b/pkg/perf/monitor/execute.go new file mode 100644 index 0000000000..543ed0114c --- /dev/null +++ b/pkg/perf/monitor/execute.go @@ -0,0 +1,63 @@ +/* +Copyright 2020 The Kubernetes Authors All rights reserved. + +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 monitor + +import ( + "bytes" + "context" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + + "github.com/pkg/errors" +) + +// RunMkcmp runs minikube built at the given pr against minikube at master +func RunMkcmp(ctx context.Context, pr int) (string, error) { + // run 'git pull' so that minikube dir is up to date + if _, err := runCmdInMinikube(ctx, []string{"git", "pull", "origin", "master"}); err != nil { + return "", errors.Wrap(err, "running git pull") + } + mkcmpPath := "out/mkcmp" + minikubePath := "out/minikube" + if _, err := runCmdInMinikube(ctx, []string{"make", mkcmpPath, minikubePath}); err != nil { + return "", errors.Wrap(err, "building minikube and mkcmp at head") + } + return runCmdInMinikube(ctx, []string{mkcmpPath, minikubePath, fmt.Sprintf("pr://%d", pr)}) +} + +// runCmdInMinikube runs the cmd and return stdout +func runCmdInMinikube(ctx context.Context, command []string) (string, error) { + cmd := exec.CommandContext(ctx, command[0], command[1:]...) + cmd.Dir = minikubeDir() + cmd.Env = os.Environ() + + buf := bytes.NewBuffer([]byte{}) + cmd.Stdout = buf + + log.Printf("Running: %v", cmd.Args) + if err := cmd.Run(); err != nil { + return "", errors.Wrapf(err, "running %v: %v", cmd.Args, buf.String()) + } + return buf.String(), nil +} + +func minikubeDir() string { + return filepath.Join(os.Getenv("HOME"), "minikube") +} diff --git a/pkg/perf/monitor/github.go b/pkg/perf/monitor/github.go new file mode 100644 index 0000000000..18afdbb580 --- /dev/null +++ b/pkg/perf/monitor/github.go @@ -0,0 +1,179 @@ +/* +Copyright 2020 The Kubernetes Authors All rights reserved. + +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 monitor + +import ( + "context" + "log" + "os" + "time" + + "github.com/google/go-github/github" + "github.com/pkg/errors" + "golang.org/x/oauth2" +) + +// Client provides the context and client with necessary auth +// for interacting with the Github API +type Client struct { + ctx context.Context + *github.Client + owner string + repo string +} + +// NewClient returns a github client with the necessary auth +func NewClient(ctx context.Context, owner, repo string) *Client { + githubToken := os.Getenv(GithubAccessTokenEnvVar) + // Setup the token for github authentication + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: githubToken}, + ) + tc := oauth2.NewClient(context.Background(), ts) + + // Return a client instance from github + client := github.NewClient(tc) + return &Client{ + ctx: ctx, + Client: client, + owner: owner, + repo: repo, + } +} + +// CommentOnPR comments message on the PR +func (g *Client) CommentOnPR(pr int, message string) error { + comment := &github.IssueComment{ + Body: &message, + } + + log.Printf("Creating comment on PR %d: %s", pr, message) + _, _, err := g.Client.Issues.CreateComment(g.ctx, g.owner, g.repo, pr, comment) + if err != nil { + return errors.Wrap(err, "creating github comment") + } + log.Printf("Successfully commented on PR %d.", pr) + return nil +} + +// ListOpenPRsWithLabel returns all open PRs with the specified label +func (g *Client) ListOpenPRsWithLabel(label string) ([]int, error) { + validPrs := []int{} + prs, _, err := g.Client.PullRequests.List(g.ctx, g.owner, g.repo, &github.PullRequestListOptions{}) + if err != nil { + return nil, errors.Wrap(err, "listing pull requests") + } + for _, pr := range prs { + if prContainsLabel(pr.Labels, "ok-to-test") { + validPrs = append(validPrs, pr.GetNumber()) + } + } + return validPrs, nil +} + +func prContainsLabel(labels []*github.Label, label string) bool { + for _, l := range labels { + if l == nil { + continue + } + if l.GetName() == label { + return true + } + } + return false +} + +// NewCommitsExist checks if new commits exist since minikube-pr-bot +// commented on the PR. If so, return true. +func (g *Client) NewCommitsExist(pr int, login string) (bool, error) { + lastCommentTime, err := g.timeOfLastComment(pr, login) + if err != nil { + return false, errors.Wrapf(err, "getting time of last comment by %s on pr %d", login, pr) + } + lastCommitTime, err := g.timeOfLastCommit(pr) + if err != nil { + return false, errors.Wrapf(err, "getting time of last commit on pr %d", pr) + } + return lastCommentTime.Before(lastCommitTime), nil +} + +func (g *Client) timeOfLastCommit(pr int) (time.Time, error) { + var commits []*github.RepositoryCommit + + page := 0 + resultsPerPage := 30 + for { + c, _, err := g.Client.PullRequests.ListCommits(g.ctx, g.owner, g.repo, pr, &github.ListOptions{ + Page: page, + PerPage: resultsPerPage, + }) + if err != nil { + return time.Time{}, nil + } + commits = append(commits, c...) + if len(c) < resultsPerPage { + break + } + page++ + } + + lastCommitTime := time.Time{} + for _, c := range commits { + if newCommitTime := c.GetCommit().GetAuthor().GetDate(); newCommitTime.After(lastCommitTime) { + lastCommitTime = newCommitTime + } + } + return lastCommitTime, nil +} + +func (g *Client) timeOfLastComment(pr int, login string) (time.Time, error) { + var comments []*github.IssueComment + + page := 0 + resultsPerPage := 30 + for { + c, _, err := g.Client.Issues.ListComments(g.ctx, g.owner, g.repo, pr, &github.IssueListCommentsOptions{ + ListOptions: github.ListOptions{ + Page: page, + PerPage: resultsPerPage, + }, + }) + if err != nil { + return time.Time{}, nil + } + comments = append(comments, c...) + if len(c) < resultsPerPage { + break + } + page++ + } + + // go through comments backwards to find the most recent + lastCommentTime := time.Time{} + + for _, c := range comments { + if u := c.GetUser(); u != nil { + if u.GetLogin() == login { + if c.GetCreatedAt().After(lastCommentTime) { + lastCommentTime = c.GetCreatedAt() + } + } + } + } + + return lastCommentTime, nil +}