summaryrefslogtreecommitdiffstats
path: root/src/cmd/trace/annotations_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'src/cmd/trace/annotations_test.go')
-rw-r--r--src/cmd/trace/annotations_test.go392
1 files changed, 392 insertions, 0 deletions
diff --git a/src/cmd/trace/annotations_test.go b/src/cmd/trace/annotations_test.go
new file mode 100644
index 0000000..9c2d027
--- /dev/null
+++ b/src/cmd/trace/annotations_test.go
@@ -0,0 +1,392 @@
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build !js
+
+package main
+
+import (
+ "bytes"
+ "context"
+ "flag"
+ "fmt"
+ traceparser "internal/trace"
+ "os"
+ "reflect"
+ "runtime/debug"
+ "runtime/trace"
+ "sort"
+ "sync"
+ "testing"
+ "time"
+)
+
+var saveTraces = flag.Bool("savetraces", false, "save traces collected by tests")
+
+func TestOverlappingDuration(t *testing.T) {
+ cases := []struct {
+ start0, end0, start1, end1 int64
+ want time.Duration
+ }{
+ {
+ 1, 10, 11, 20, 0,
+ },
+ {
+ 1, 10, 5, 20, 5 * time.Nanosecond,
+ },
+ {
+ 1, 10, 2, 8, 6 * time.Nanosecond,
+ },
+ }
+
+ for _, tc := range cases {
+ s0, e0, s1, e1 := tc.start0, tc.end0, tc.start1, tc.end1
+ if got := overlappingDuration(s0, e0, s1, e1); got != tc.want {
+ t.Errorf("overlappingDuration(%d, %d, %d, %d)=%v; want %v", s0, e0, s1, e1, got, tc.want)
+ }
+ if got := overlappingDuration(s1, e1, s0, e0); got != tc.want {
+ t.Errorf("overlappingDuration(%d, %d, %d, %d)=%v; want %v", s1, e1, s0, e0, got, tc.want)
+ }
+ }
+}
+
+// prog0 starts three goroutines.
+//
+// goroutine 1: taskless region
+// goroutine 2: starts task0, do work in task0.region0, starts task1 which ends immediately.
+// goroutine 3: do work in task0.region1 and task0.region2, ends task0
+func prog0() {
+ ctx := context.Background()
+
+ var wg sync.WaitGroup
+
+ wg.Add(1)
+ go func() { // goroutine 1
+ defer wg.Done()
+ trace.WithRegion(ctx, "taskless.region", func() {
+ trace.Log(ctx, "key0", "val0")
+ })
+ }()
+
+ wg.Add(1)
+ go func() { // goroutine 2
+ defer wg.Done()
+ ctx, task := trace.NewTask(ctx, "task0")
+ trace.WithRegion(ctx, "task0.region0", func() {
+ wg.Add(1)
+ go func() { // goroutine 3
+ defer wg.Done()
+ defer task.End()
+ trace.WithRegion(ctx, "task0.region1", func() {
+ trace.WithRegion(ctx, "task0.region2", func() {
+ trace.Log(ctx, "key2", "val2")
+ })
+ trace.Log(ctx, "key1", "val1")
+ })
+ }()
+ })
+ ctx2, task2 := trace.NewTask(ctx, "task1")
+ trace.Log(ctx2, "key3", "val3")
+ task2.End()
+ }()
+ wg.Wait()
+}
+
+func TestAnalyzeAnnotations(t *testing.T) {
+ // TODO: classify taskless regions
+
+ // Run prog0 and capture the execution trace.
+ if err := traceProgram(t, prog0, "TestAnalyzeAnnotations"); err != nil {
+ t.Fatalf("failed to trace the program: %v", err)
+ }
+
+ res, err := analyzeAnnotations()
+ if err != nil {
+ t.Fatalf("failed to analyzeAnnotations: %v", err)
+ }
+
+ // For prog0, we expect
+ // - task with name = "task0", with three regions.
+ // - task with name = "task1", with no region.
+ wantTasks := map[string]struct {
+ complete bool
+ goroutines int
+ regions []string
+ }{
+ "task0": {
+ complete: true,
+ goroutines: 2,
+ regions: []string{"task0.region0", "", "task0.region1", "task0.region2"},
+ },
+ "task1": {
+ complete: true,
+ goroutines: 1,
+ },
+ }
+
+ for _, task := range res.tasks {
+ want, ok := wantTasks[task.name]
+ if !ok {
+ t.Errorf("unexpected task: %s", task)
+ continue
+ }
+ if task.complete() != want.complete || len(task.goroutines) != want.goroutines || !reflect.DeepEqual(regionNames(task), want.regions) {
+ t.Errorf("got task %v; want %+v", task, want)
+ }
+
+ delete(wantTasks, task.name)
+ }
+ if len(wantTasks) > 0 {
+ t.Errorf("no more tasks; want %+v", wantTasks)
+ }
+
+ wantRegions := []string{
+ "", // an auto-created region for the goroutine 3
+ "taskless.region",
+ "task0.region0",
+ "task0.region1",
+ "task0.region2",
+ }
+ var gotRegions []string
+ for regionID := range res.regions {
+ gotRegions = append(gotRegions, regionID.Type)
+ }
+
+ sort.Strings(wantRegions)
+ sort.Strings(gotRegions)
+ if !reflect.DeepEqual(gotRegions, wantRegions) {
+ t.Errorf("got regions %q, want regions %q", gotRegions, wantRegions)
+ }
+}
+
+// prog1 creates a task hierarchy consisting of three tasks.
+func prog1() {
+ ctx := context.Background()
+ ctx1, task1 := trace.NewTask(ctx, "task1")
+ defer task1.End()
+ trace.WithRegion(ctx1, "task1.region", func() {
+ ctx2, task2 := trace.NewTask(ctx1, "task2")
+ defer task2.End()
+ trace.WithRegion(ctx2, "task2.region", func() {
+ ctx3, task3 := trace.NewTask(ctx2, "task3")
+ defer task3.End()
+ trace.WithRegion(ctx3, "task3.region", func() {
+ })
+ })
+ })
+}
+
+func TestAnalyzeAnnotationTaskTree(t *testing.T) {
+ // Run prog1 and capture the execution trace.
+ if err := traceProgram(t, prog1, "TestAnalyzeAnnotationTaskTree"); err != nil {
+ t.Fatalf("failed to trace the program: %v", err)
+ }
+
+ res, err := analyzeAnnotations()
+ if err != nil {
+ t.Fatalf("failed to analyzeAnnotations: %v", err)
+ }
+ tasks := res.tasks
+
+ // For prog0, we expect
+ // - task with name = "", with taskless.region in regions.
+ // - task with name = "task0", with three regions.
+ wantTasks := map[string]struct {
+ parent string
+ children []string
+ regions []string
+ }{
+ "task1": {
+ parent: "",
+ children: []string{"task2"},
+ regions: []string{"task1.region"},
+ },
+ "task2": {
+ parent: "task1",
+ children: []string{"task3"},
+ regions: []string{"task2.region"},
+ },
+ "task3": {
+ parent: "task2",
+ children: nil,
+ regions: []string{"task3.region"},
+ },
+ }
+
+ for _, task := range tasks {
+ want, ok := wantTasks[task.name]
+ if !ok {
+ t.Errorf("unexpected task: %s", task)
+ continue
+ }
+ delete(wantTasks, task.name)
+
+ if parentName(task) != want.parent ||
+ !reflect.DeepEqual(childrenNames(task), want.children) ||
+ !reflect.DeepEqual(regionNames(task), want.regions) {
+ t.Errorf("got %v; want %+v", task, want)
+ }
+ }
+
+ if len(wantTasks) > 0 {
+ t.Errorf("no more tasks; want %+v", wantTasks)
+ }
+}
+
+// prog2 starts two tasks; "taskWithGC" that overlaps with GC
+// and "taskWithoutGC" that doesn't. In order to run this reliably,
+// the caller needs to set up to prevent GC from running automatically.
+// prog2 returns the upper-bound gc time that overlaps with the first task.
+func prog2() (gcTime time.Duration) {
+ ch := make(chan bool)
+ ctx1, task := trace.NewTask(context.Background(), "taskWithGC")
+ trace.WithRegion(ctx1, "taskWithGC.region1", func() {
+ go func() {
+ defer trace.StartRegion(ctx1, "taskWithGC.region2").End()
+ <-ch
+ }()
+ s := time.Now()
+ debug.FreeOSMemory() // task1 affected by gc
+ gcTime = time.Since(s)
+ close(ch)
+ })
+ task.End()
+
+ ctx2, task2 := trace.NewTask(context.Background(), "taskWithoutGC")
+ trace.WithRegion(ctx2, "taskWithoutGC.region1", func() {
+ // do nothing.
+ })
+ task2.End()
+ return gcTime
+}
+
+func TestAnalyzeAnnotationGC(t *testing.T) {
+ err := traceProgram(t, func() {
+ oldGC := debug.SetGCPercent(10000) // gc, and effectively disable GC
+ defer debug.SetGCPercent(oldGC)
+ prog2()
+ }, "TestAnalyzeAnnotationGC")
+ if err != nil {
+ t.Fatalf("failed to trace the program: %v", err)
+ }
+
+ res, err := analyzeAnnotations()
+ if err != nil {
+ t.Fatalf("failed to analyzeAnnotations: %v", err)
+ }
+
+ // Check collected GC Start events are all sorted and non-overlapping.
+ lastTS := int64(0)
+ for i, ev := range res.gcEvents {
+ if ev.Type != traceparser.EvGCStart {
+ t.Errorf("unwanted event in gcEvents: %v", ev)
+ }
+ if i > 0 && lastTS > ev.Ts {
+ t.Errorf("overlapping GC events:\n%d: %v\n%d: %v", i-1, res.gcEvents[i-1], i, res.gcEvents[i])
+ }
+ if ev.Link != nil {
+ lastTS = ev.Link.Ts
+ }
+ }
+
+ // Check whether only taskWithGC reports overlapping duration.
+ for _, task := range res.tasks {
+ got := task.overlappingGCDuration(res.gcEvents)
+ switch task.name {
+ case "taskWithoutGC":
+ if got != 0 {
+ t.Errorf("%s reported %v as overlapping GC time; want 0: %v", task.name, got, task)
+ }
+ case "taskWithGC":
+ upperBound := task.duration()
+ // TODO(hyangah): a tighter upper bound is gcTime, but
+ // use of it will make the test flaky due to the issue
+ // described in golang.org/issue/16755. Tighten the upper
+ // bound when the issue with the timestamp computed
+ // based on clockticks is resolved.
+ if got <= 0 || got > upperBound {
+ t.Errorf("%s reported %v as overlapping GC time; want (0, %v):\n%v", task.name, got, upperBound, task)
+ buf := new(bytes.Buffer)
+ fmt.Fprintln(buf, "GC Events")
+ for _, ev := range res.gcEvents {
+ fmt.Fprintf(buf, " %s -> %s\n", ev, ev.Link)
+ }
+ fmt.Fprintln(buf, "Events in Task")
+ for i, ev := range task.events {
+ fmt.Fprintf(buf, " %d: %s\n", i, ev)
+ }
+
+ t.Logf("\n%s", buf)
+ }
+ }
+ }
+}
+
+// traceProgram runs the provided function while tracing is enabled,
+// parses the captured trace, and sets the global trace loader to
+// point to the parsed trace.
+//
+// If savetraces flag is set, the captured trace will be saved in the named file.
+func traceProgram(t *testing.T, f func(), name string) error {
+ t.Helper()
+ buf := new(bytes.Buffer)
+ if err := trace.Start(buf); err != nil {
+ return err
+ }
+ f()
+ trace.Stop()
+
+ saveTrace(buf, name)
+ res, err := traceparser.Parse(buf, name+".faketrace")
+ if err == traceparser.ErrTimeOrder {
+ t.Skipf("skipping due to golang.org/issue/16755: %v", err)
+ } else if err != nil {
+ return err
+ }
+
+ swapLoaderData(res, err)
+ return nil
+}
+
+func regionNames(task *taskDesc) (ret []string) {
+ for _, s := range task.regions {
+ ret = append(ret, s.Name)
+ }
+ return ret
+}
+
+func parentName(task *taskDesc) string {
+ if task.parent != nil {
+ return task.parent.name
+ }
+ return ""
+}
+
+func childrenNames(task *taskDesc) (ret []string) {
+ for _, s := range task.children {
+ ret = append(ret, s.name)
+ }
+ return ret
+}
+
+func swapLoaderData(res traceparser.ParseResult, err error) {
+ // swap loader's data.
+ parseTrace() // fool loader.once.
+
+ loader.res = res
+ loader.err = err
+
+ analyzeGoroutines(nil) // fool gsInit once.
+ gs = traceparser.GoroutineStats(res.Events)
+
+}
+
+func saveTrace(buf *bytes.Buffer, name string) {
+ if !*saveTraces {
+ return
+ }
+ if err := os.WriteFile(name+".trace", buf.Bytes(), 0600); err != nil {
+ panic(fmt.Errorf("failed to write trace file: %v", err))
+ }
+}