blob: eef57d374352479cc9f1b40f4bea19b2b1b1a0fd [file]
// Copyright 2019 The Bazel Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package starlark_test
import (
"bytes"
"fmt"
"os"
"os/exec"
"regexp"
"strings"
"testing"
"time"
"go.starlark.net/starlark"
)
// TestProfile is a simple integration test that the profiler
// emits minimally plausible pprof-compatible output.
func TestProfile(t *testing.T) {
const src = `
def fibonacci(n):
x, y = 1, 1
for i in range(n):
x, y = y, x+y
return y
fibonacci(100000)
`
got := profile(t, func() error {
thread := new(starlark.Thread)
_, err := starlark.ExecFile(thread, "foo.star", src, nil)
return err
})
// Typical output (may vary by go release):
//
// Type: wall
// Time: Apr 4, 2019 at 11:10am (EDT)
// Duration: 251.62ms, Total samples = 250ms (99.36%)
// Showing nodes accounting for 250ms, 100% of 250ms total
// flat flat% sum% cum cum%
// 320ms 100% 100% 320ms 100% fibonacci
// 0 0% 100% 320ms 100% foo.star
//
// We'll assert a few key substrings are present.
for _, want := range []string{
"flat%",
"fibonacci",
"foo.star",
} {
if !strings.Contains(got, want) {
t.Errorf("output did not contain %q", want)
}
}
if t.Failed() {
t.Logf("stdout=%v", got)
}
}
// sleep is a built-in that takes 100ms.
var sleep = starlark.NewBuiltin("sleep", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
time.Sleep(100 * time.Millisecond)
return starlark.None, nil
})
// TestProfileBuiltinCallback checks that time spent in a Starlark
// function called by a built-in is not also attributed to the built-in,
// and that the built-in's own time before and after the call is.
func TestProfileBuiltinCallback(t *testing.T) {
call := starlark.NewBuiltin("call", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
time.Sleep(50 * time.Millisecond)
v, err := starlark.Call(thread, args[0], nil, nil)
time.Sleep(50 * time.Millisecond)
return v, err
})
predeclared := starlark.StringDict{"sleep": sleep, "call": call}
const src = `
def f():
sleep()
call(f)
`
var elapsed time.Duration
got := profile(t, func() error {
thread := new(starlark.Thread)
start := time.Now()
_, err := starlark.ExecFile(thread, "foo.star", src, predeclared)
elapsed = time.Since(start)
return err
})
// A thread's spans do not overlap, so its samples
// cannot add up to more than its elapsed time.
checkSamples(t, got, elapsed)
// The built-in's span resumes after the callback returns.
if flat := pprofDuration(t, got, `(?m)^\s*(\S+)\s.*\scall$`); flat < 100*time.Millisecond {
t.Errorf("call flat time = %v, want at least 100ms; output=<<%s>>", flat, got)
}
}
// TestProfileLoadCallback checks that a Load function that calls back
// into Starlark on the importing thread does not resume the importer's
// span, which LOAD has ended. Otherwise the importer would be charged
// for the Load function's work, even work done by other threads.
func TestProfileLoadCallback(t *testing.T) {
noop := starlark.NewBuiltin("noop", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
return starlark.None, nil
})
load := func(thread *starlark.Thread, module string) (starlark.StringDict, error) {
// Call back into Starlark on the importing thread
// before and after executing a file on another thread.
if _, err := starlark.Call(thread, noop, nil, nil); err != nil {
return nil, err
}
predeclared := starlark.StringDict{"sleep": sleep}
if _, err := starlark.ExecFile(new(starlark.Thread), "other.star", "sleep()", predeclared); err != nil {
return nil, err
}
if _, err := starlark.Call(thread, noop, nil, nil); err != nil {
return nil, err
}
return starlark.StringDict{"x": starlark.None}, nil
}
var elapsed time.Duration
got := profile(t, func() error {
thread := &starlark.Thread{Load: load}
start := time.Now()
_, err := starlark.ExecFile(thread, "main.star", `load("m", "x")`, nil)
elapsed = time.Since(start)
return err
})
// The threads run one at a time, so their samples
// cannot add up to more than the elapsed time.
checkSamples(t, got, elapsed)
}
// checkSamples checks that the sleep built-in accounts for at
// least 100ms of the profile output got, and that the total
// samples do not exceed the elapsed time.
func checkSamples(t *testing.T, got string, elapsed time.Duration) {
t.Helper()
if flat := pprofDuration(t, got, `(?m)^\s*(\S+)\s.*\ssleep$`); flat < 100*time.Millisecond {
t.Errorf("sleep flat time = %v, want at least 100ms; output=<<%s>>", flat, got)
}
if total := pprofDuration(t, got, `Total samples = (\S+)`); total > elapsed {
t.Errorf("total samples = %v, exceeds elapsed time %v; output=<<%s>>", total, elapsed, got)
}
}
// pprofDuration returns the duration matched by the
// sole group of pattern in the profile output got.
func pprofDuration(t *testing.T, got, pattern string) time.Duration {
t.Helper()
m := regexp.MustCompile(pattern).FindStringSubmatch(got)
if m == nil {
t.Fatalf("output does not match %q: %s", pattern, got)
}
d, err := time.ParseDuration(m[1])
if err != nil {
t.Fatalf("parsing %q: %v", m[1], err)
}
return d
}
// profile calls run with the profiler enabled, and returns
// the output of 'go tool pprof -top' for the resulting profile.
func profile(t *testing.T, run func() error) string {
prof, err := os.CreateTemp(t.TempDir(), "profile_test")
if err != nil {
t.Fatal(err)
}
defer prof.Close()
if err := starlark.StartProfile(prof); err != nil {
t.Fatal(err)
}
stopped := false
defer func() {
if !stopped {
starlark.StopProfile() // run called t.Fatal
}
}()
err = run()
stopped = true
if err := starlark.StopProfile(); err != nil {
t.Fatal(err)
}
if err != nil {
t.Fatal(err)
}
// Use a fixed unit; pprof's automatic units include
// "hrs", which time.ParseDuration does not accept.
cmd := exec.Command("go", "tool", "pprof", "-top", "-unit=ns", prof.Name())
cmd.Stderr = new(bytes.Buffer)
cmd.Stdout = new(bytes.Buffer)
if err := cmd.Run(); err != nil {
t.Fatalf("pprof failed: %v; output=<<%s>>", err, cmd.Stderr)
}
return fmt.Sprint(cmd.Stdout)
}