Skip to content

Commit

Permalink
Introduce runner
Browse files Browse the repository at this point in the history
Runner is a thin library that's able to measure precise on-CPU time for
the set of all goroutines run through it. It makes use of a (slightly)
modified Go runtime in order to do so.
  • Loading branch information
irfansharif committed Feb 21, 2022
0 parents commit f47cb8d
Show file tree
Hide file tree
Showing 16 changed files with 513 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .bazelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
build --symlink_prefix=.bazel/
run --ui_event_filters=-info,-stdout,-stderr --noshow_progress
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.bazel
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "modules/go"]
path = modules/go
url = [email protected]:irfansharif/go.git
28 changes: 28 additions & 0 deletions BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
load("@bazel_gazelle//:def.bzl", "gazelle")
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")

gazelle(
name = "gazelle",
prefix = "github.com/irfansharif/runner",
)

go_library(
name = "runner",
srcs = [
"runner.go",
"runner.s",
"runtime.go",
],
importpath = "github.com/irfansharif/runner",
visibility = ["//visibility:public"],
)

go_test(
name = "runner_test",
srcs = [
"runner_test.go",
"runtime_test.go",
],
embed = [":runner"],
deps = ["@com_github_stretchr_testify//assert"],
)
54 changes: 54 additions & 0 deletions DEPS.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
load("@bazel_gazelle//:deps.bzl", "go_repository")

def go_deps():
go_repository(
name = "com_github_davecgh_go_spew",
build_file_proto_mode = "disable_global",
importpath = "github.com/davecgh/go-spew",
sum = "h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=",
version = "v1.1.0",
)
go_repository(
name = "com_github_pmezard_go_difflib",
build_file_proto_mode = "disable_global",
importpath = "github.com/pmezard/go-difflib",
sum = "h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=",
version = "v1.0.0",
)

go_repository(
name = "com_github_stretchr_objx",
build_file_proto_mode = "disable_global",
importpath = "github.com/stretchr/objx",
sum = "h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=",
version = "v0.1.0",
)

go_repository(
name = "com_github_stretchr_testify",
build_file_proto_mode = "disable_global",
importpath = "github.com/stretchr/testify",
sum = "h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=",
version = "v1.7.0",
)
go_repository(
name = "in_gopkg_check_v1",
build_file_proto_mode = "disable_global",
importpath = "gopkg.in/check.v1",
sum = "h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=",
version = "v0.0.0-20161208181325-20d25e280405",
)
go_repository(
name = "in_gopkg_yaml_v3",
build_file_proto_mode = "disable_global",
importpath = "gopkg.in/yaml.v3",
sum = "h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=",
version = "v3.0.0-20200313102051-9f266ea9e77c",
)
go_repository(
name = "org_golang_x_sync",
build_file_proto_mode = "disable_global",
importpath = "golang.org/x/sync",
sum = "h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=",
version = "v0.0.0-20210220032951-036812b2e83c",
)
34 changes: 34 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Copyright 2022 Irfan Sharif.
#
# 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.

test:
bazel test //:all --test_arg='-test.v' --test_output=all

bench:
bazel test //:all --nocache_test_results \
--test_arg='-test.v' --test_output=all --test_arg='-test.run=-' \
--test_arg='-test.bench=.' --test_arg='-test.benchtime=10000x'

generate: FORCE
bazel run //:gazelle -- update-repos \
-from_file=go.mod -prune=true \
-build_file_proto_mode=disable_global \
-to_macro=DEPS.bzl%go_deps &> /dev/null
bazel run //:gazelle &> /dev/null

go:
git submodule update --init --recursive
cd modules/go/src && ./make.bash

FORCE: ;
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
Runner
---

[![Go Reference](https://pkg.go.dev/badge/github.com/irfansharif/runner.svg)](https://godocs.io/github.com/irfansharif/runner)

Runner is a thin library that's able to precisely measure on-CPU time for
goroutines. It relies on a slightly modified Go runtime for additional
instrumentation in order to function. It's a prototype for what finer-grained
CPU attribution could look like in Go.

### Contributing

The repo includes the necessary Go runtime as a submodule. To get up and
running (assumes you already have `go` installed and in your `PATH`):

```sh
$ git clone [email protected]:irfansharif/runner.git
$ cd runner
$ make go # set up submodules and build the modified go runtime
```

We can now use the modified Go to run tests:
```sh
$ modules/go/bin/go test -v .
```

To develop using bazel, we need to tell it to build using the modified runtime.
To do so, edit the following portion of the top-level `WORKSPACE` to point to
the cloned `go` submodule with the runtime changes:

```python
go_local_sdk(
name = "go_sdk",
path = "<path to checkout>/modules/go",
)
```

Finally, run the package tests/benchmarks or update BUILD files:

```
$ make test
$ make bench
$ make generate
```
54 changes: 54 additions & 0 deletions WORKSPACE
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
workspace(name = "hello")

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

http_archive(
name = "io_bazel_rules_go",
sha256 = "5c4bd27429b1a307d51cd23d4677126aa6315fff608f0cd85c5bfb642a13b953",
strip_prefix = "cockroachdb-rules_go-23b381c",
urls = [
"https://storage.googleapis.com/public-bazel-artifacts/bazel/cockroachdb-rules_go-v0.27.0-52-g23b381c.tar.gz",
],
)

# Load gazelle. This lets us auto-generate BUILD.bazel files throughout the
# repo.
http_archive(
name = "bazel_gazelle",
sha256 = "9fba095e4bebd8c6748154ca53c365862af47fa1651f7c0d25459e6ca5bb208f",
strip_prefix = "bazelbuild-bazel-gazelle-3ea1d64",
urls = [
# v0.24.0
"https://storage.googleapis.com/public-bazel-artifacts/bazel/bazelbuild-bazel-gazelle-v0.24.0-0-g3ea1d64.tar.gz",
],
)

# Load up go dependencies (the ones listed under go.mod).
load("//:DEPS.bzl", "go_deps")

# gazelle:repository_macro DEPS.bzl%go_deps
go_deps()

load(
"@io_bazel_rules_go//go:deps.bzl",
"go_local_sdk",
"go_rules_dependencies",
)

# TODO(irfansharif): Point to mirrored, public URL with modified runtime. For
# now this points to local checkout of go source tree.
go_local_sdk(
name = "go_sdk",
path = "/Users/irfansharif/Software/src/github.com/irfansharif/runner/modules/go",
)

go_rules_dependencies()

# Load gazelle dependencies.
load(
"@bazel_gazelle//:deps.bzl",
"gazelle_dependencies",
"go_repository",
)

gazelle_dependencies()
12 changes: 12 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module github.com/irfansharif/runner

go 1.17

require github.com/stretchr/testify v1.7.0

require (
github.com/davecgh/go-spew v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)
13 changes: 13 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
1 change: 1 addition & 0 deletions modules/go
Submodule go added at 5cc8c5
64 changes: 64 additions & 0 deletions runner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright 2022 Irfan Sharif.
//
// 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 runner is a library that's able to precisely measure on-CPU time for
// goroutines.
package runner

import (
"sync/atomic"
"time"
)

// TODO(irfansharif):
//
// - Make use of buildtags. Use smoketests to check if things are up to snuff?
// Currently we fails with an opaque link error if not using the right go SDK.
// - Write out steps to download patched runtime.
// - Provide a default singleton runner for instances where plumbing in a runner
// is inconvenient.
// - Maintain tags/task names and some bounded sketch of runner history.

// TODO(irfansharif): We only accumulate on-CPU time at the end of the invoked
// goroutine; there's no "inflight" view of ops. This post-hoc capture might be
// unsuitable for long-running goroutines unless we provide an API to
// periodically record on-CPU time, and/or perform taskgroup[1]-style tracking
// within the runtime.
//
// [1]: https://github.com/cockroachdb/cockroach/pull/60589

// Runner is able to measure precise on-CPU time for the set of all goroutines
// run through it.
type Runner struct {
nanos int64 // accessed atomically
}

// New returns a new Runner.
func New() *Runner {
return &Runner{}
}

// Run the given function in a new goroutine.
func (r *Runner) Run(f func()) {
go func() {
f()
atomic.AddInt64(&r.nanos, grunningnanos()) // record the running nanoseconds for the goroutine
}()
}

// Duration returns the total running (i.e. on CPU) duration observed by all
// goroutines run under the group.
func (r *Runner) Duration() time.Duration {
return time.Duration(atomic.LoadInt64(&r.nanos))
}
3 changes: 3 additions & 0 deletions runner.s
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// We need a .s file so the Go tool does not pass -complete to go tool compile;
// that'd prevent being able to define functions with no bodies (something this
// package uses to link into private runtime methods).
66 changes: 66 additions & 0 deletions runner_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright 2022 Irfan Sharif.
//
// 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 runner_test

import (
"fmt"
"runtime"
"sync"
"testing"
"time"

"github.com/irfansharif/runner"
"github.com/stretchr/testify/assert"
)

// TestRunDuration tests that run durations behaves as expected.
func TestRunDuration(t *testing.T) {
for _, work := range []string{"busy", "lazy"} {
work := work
t.Run(fmt.Sprintf("loop=%s", work), func(t *testing.T) {
var wg sync.WaitGroup
r := runner.New()

start := time.Now()
for i := 0; i < 10; i++ {
wg.Add(1)
r.Run(func() {
defer wg.Done()

if work == "busy" {
runner.TestingBusyFn()
} else {
runner.TestingLazyFn()
}
})
}
wg.Wait()

walltime := time.Since(start)
cputime := r.Duration()
mult := float64(cputime.Nanoseconds()) / float64(walltime.Nanoseconds())

if work == "busy" {
minexp := float64(runtime.GOMAXPROCS(-1)) - 1
assert.Greaterf(t, mult, minexp,
"expected multiplier > %f, got %f", minexp, mult)
} else {
maxexp := float64(0.1)
assert.Lessf(t, mult, maxexp,
"expected approximately zero multiplier, got %f", mult)
}
})
}
}
Loading

0 comments on commit f47cb8d

Please sign in to comment.