Black Lives Matter. Support the Equal Justice Initiative.

Source file src/cmd/trace/annotations_test.go

Documentation: cmd/trace

     1  // Copyright 2018 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build !js
     6  // +build !js
     7  
     8  package main
     9  
    10  import (
    11  	"bytes"
    12  	"context"
    13  	"flag"
    14  	"fmt"
    15  	traceparser "internal/trace"
    16  	"os"
    17  	"reflect"
    18  	"runtime/debug"
    19  	"runtime/trace"
    20  	"sort"
    21  	"sync"
    22  	"testing"
    23  	"time"
    24  )
    25  
    26  var saveTraces = flag.Bool("savetraces", false, "save traces collected by tests")
    27  
    28  func TestOverlappingDuration(t *testing.T) {
    29  	cases := []struct {
    30  		start0, end0, start1, end1 int64
    31  		want                       time.Duration
    32  	}{
    33  		{
    34  			1, 10, 11, 20, 0,
    35  		},
    36  		{
    37  			1, 10, 5, 20, 5 * time.Nanosecond,
    38  		},
    39  		{
    40  			1, 10, 2, 8, 6 * time.Nanosecond,
    41  		},
    42  	}
    43  
    44  	for _, tc := range cases {
    45  		s0, e0, s1, e1 := tc.start0, tc.end0, tc.start1, tc.end1
    46  		if got := overlappingDuration(s0, e0, s1, e1); got != tc.want {
    47  			t.Errorf("overlappingDuration(%d, %d, %d, %d)=%v; want %v", s0, e0, s1, e1, got, tc.want)
    48  		}
    49  		if got := overlappingDuration(s1, e1, s0, e0); got != tc.want {
    50  			t.Errorf("overlappingDuration(%d, %d, %d, %d)=%v; want %v", s1, e1, s0, e0, got, tc.want)
    51  		}
    52  	}
    53  }
    54  
    55  // prog0 starts three goroutines.
    56  //
    57  //   goroutine 1: taskless region
    58  //   goroutine 2: starts task0, do work in task0.region0, starts task1 which ends immediately.
    59  //   goroutine 3: do work in task0.region1 and task0.region2, ends task0
    60  func prog0() {
    61  	ctx := context.Background()
    62  
    63  	var wg sync.WaitGroup
    64  
    65  	wg.Add(1)
    66  	go func() { // goroutine 1
    67  		defer wg.Done()
    68  		trace.WithRegion(ctx, "taskless.region", func() {
    69  			trace.Log(ctx, "key0", "val0")
    70  		})
    71  	}()
    72  
    73  	wg.Add(1)
    74  	go func() { // goroutine 2
    75  		defer wg.Done()
    76  		ctx, task := trace.NewTask(ctx, "task0")
    77  		trace.WithRegion(ctx, "task0.region0", func() {
    78  			wg.Add(1)
    79  			go func() { // goroutine 3
    80  				defer wg.Done()
    81  				defer task.End()
    82  				trace.WithRegion(ctx, "task0.region1", func() {
    83  					trace.WithRegion(ctx, "task0.region2", func() {
    84  						trace.Log(ctx, "key2", "val2")
    85  					})
    86  					trace.Log(ctx, "key1", "val1")
    87  				})
    88  			}()
    89  		})
    90  		ctx2, task2 := trace.NewTask(ctx, "task1")
    91  		trace.Log(ctx2, "key3", "val3")
    92  		task2.End()
    93  	}()
    94  	wg.Wait()
    95  }
    96  
    97  func TestAnalyzeAnnotations(t *testing.T) {
    98  	// TODO: classify taskless regions
    99  
   100  	// Run prog0 and capture the execution trace.
   101  	if err := traceProgram(t, prog0, "TestAnalyzeAnnotations"); err != nil {
   102  		t.Fatalf("failed to trace the program: %v", err)
   103  	}
   104  
   105  	res, err := analyzeAnnotations()
   106  	if err != nil {
   107  		t.Fatalf("failed to analyzeAnnotations: %v", err)
   108  	}
   109  
   110  	// For prog0, we expect
   111  	//   - task with name = "task0", with three regions.
   112  	//   - task with name = "task1", with no region.
   113  	wantTasks := map[string]struct {
   114  		complete   bool
   115  		goroutines int
   116  		regions    []string
   117  	}{
   118  		"task0": {
   119  			complete:   true,
   120  			goroutines: 2,
   121  			regions:    []string{"task0.region0", "", "task0.region1", "task0.region2"},
   122  		},
   123  		"task1": {
   124  			complete:   true,
   125  			goroutines: 1,
   126  		},
   127  	}
   128  
   129  	for _, task := range res.tasks {
   130  		want, ok := wantTasks[task.name]
   131  		if !ok {
   132  			t.Errorf("unexpected task: %s", task)
   133  			continue
   134  		}
   135  		if task.complete() != want.complete || len(task.goroutines) != want.goroutines || !reflect.DeepEqual(regionNames(task), want.regions) {
   136  			t.Errorf("got task %v; want %+v", task, want)
   137  		}
   138  
   139  		delete(wantTasks, task.name)
   140  	}
   141  	if len(wantTasks) > 0 {
   142  		t.Errorf("no more tasks; want %+v", wantTasks)
   143  	}
   144  
   145  	wantRegions := []string{
   146  		"", // an auto-created region for the goroutine 3
   147  		"taskless.region",
   148  		"task0.region0",
   149  		"task0.region1",
   150  		"task0.region2",
   151  	}
   152  	var gotRegions []string
   153  	for regionID := range res.regions {
   154  		gotRegions = append(gotRegions, regionID.Type)
   155  	}
   156  
   157  	sort.Strings(wantRegions)
   158  	sort.Strings(gotRegions)
   159  	if !reflect.DeepEqual(gotRegions, wantRegions) {
   160  		t.Errorf("got regions %q, want regions %q", gotRegions, wantRegions)
   161  	}
   162  }
   163  
   164  // prog1 creates a task hierarchy consisting of three tasks.
   165  func prog1() {
   166  	ctx := context.Background()
   167  	ctx1, task1 := trace.NewTask(ctx, "task1")
   168  	defer task1.End()
   169  	trace.WithRegion(ctx1, "task1.region", func() {
   170  		ctx2, task2 := trace.NewTask(ctx1, "task2")
   171  		defer task2.End()
   172  		trace.WithRegion(ctx2, "task2.region", func() {
   173  			ctx3, task3 := trace.NewTask(ctx2, "task3")
   174  			defer task3.End()
   175  			trace.WithRegion(ctx3, "task3.region", func() {
   176  			})
   177  		})
   178  	})
   179  }
   180  
   181  func TestAnalyzeAnnotationTaskTree(t *testing.T) {
   182  	// Run prog1 and capture the execution trace.
   183  	if err := traceProgram(t, prog1, "TestAnalyzeAnnotationTaskTree"); err != nil {
   184  		t.Fatalf("failed to trace the program: %v", err)
   185  	}
   186  
   187  	res, err := analyzeAnnotations()
   188  	if err != nil {
   189  		t.Fatalf("failed to analyzeAnnotations: %v", err)
   190  	}
   191  	tasks := res.tasks
   192  
   193  	// For prog0, we expect
   194  	//   - task with name = "", with taskless.region in regions.
   195  	//   - task with name = "task0", with three regions.
   196  	wantTasks := map[string]struct {
   197  		parent   string
   198  		children []string
   199  		regions  []string
   200  	}{
   201  		"task1": {
   202  			parent:   "",
   203  			children: []string{"task2"},
   204  			regions:  []string{"task1.region"},
   205  		},
   206  		"task2": {
   207  			parent:   "task1",
   208  			children: []string{"task3"},
   209  			regions:  []string{"task2.region"},
   210  		},
   211  		"task3": {
   212  			parent:   "task2",
   213  			children: nil,
   214  			regions:  []string{"task3.region"},
   215  		},
   216  	}
   217  
   218  	for _, task := range tasks {
   219  		want, ok := wantTasks[task.name]
   220  		if !ok {
   221  			t.Errorf("unexpected task: %s", task)
   222  			continue
   223  		}
   224  		delete(wantTasks, task.name)
   225  
   226  		if parentName(task) != want.parent ||
   227  			!reflect.DeepEqual(childrenNames(task), want.children) ||
   228  			!reflect.DeepEqual(regionNames(task), want.regions) {
   229  			t.Errorf("got %v; want %+v", task, want)
   230  		}
   231  	}
   232  
   233  	if len(wantTasks) > 0 {
   234  		t.Errorf("no more tasks; want %+v", wantTasks)
   235  	}
   236  }
   237  
   238  // prog2 starts two tasks; "taskWithGC" that overlaps with GC
   239  // and "taskWithoutGC" that doesn't. In order to run this reliably,
   240  // the caller needs to set up to prevent GC from running automatically.
   241  // prog2 returns the upper-bound gc time that overlaps with the first task.
   242  func prog2() (gcTime time.Duration) {
   243  	ch := make(chan bool)
   244  	ctx1, task := trace.NewTask(context.Background(), "taskWithGC")
   245  	trace.WithRegion(ctx1, "taskWithGC.region1", func() {
   246  		go func() {
   247  			defer trace.StartRegion(ctx1, "taskWithGC.region2").End()
   248  			<-ch
   249  		}()
   250  		s := time.Now()
   251  		debug.FreeOSMemory() // task1 affected by gc
   252  		gcTime = time.Since(s)
   253  		close(ch)
   254  	})
   255  	task.End()
   256  
   257  	ctx2, task2 := trace.NewTask(context.Background(), "taskWithoutGC")
   258  	trace.WithRegion(ctx2, "taskWithoutGC.region1", func() {
   259  		// do nothing.
   260  	})
   261  	task2.End()
   262  	return gcTime
   263  }
   264  
   265  func TestAnalyzeAnnotationGC(t *testing.T) {
   266  	err := traceProgram(t, func() {
   267  		oldGC := debug.SetGCPercent(10000) // gc, and effectively disable GC
   268  		defer debug.SetGCPercent(oldGC)
   269  		prog2()
   270  	}, "TestAnalyzeAnnotationGC")
   271  	if err != nil {
   272  		t.Fatalf("failed to trace the program: %v", err)
   273  	}
   274  
   275  	res, err := analyzeAnnotations()
   276  	if err != nil {
   277  		t.Fatalf("failed to analyzeAnnotations: %v", err)
   278  	}
   279  
   280  	// Check collected GC Start events are all sorted and non-overlapping.
   281  	lastTS := int64(0)
   282  	for i, ev := range res.gcEvents {
   283  		if ev.Type != traceparser.EvGCStart {
   284  			t.Errorf("unwanted event in gcEvents: %v", ev)
   285  		}
   286  		if i > 0 && lastTS > ev.Ts {
   287  			t.Errorf("overlapping GC events:\n%d: %v\n%d: %v", i-1, res.gcEvents[i-1], i, res.gcEvents[i])
   288  		}
   289  		if ev.Link != nil {
   290  			lastTS = ev.Link.Ts
   291  		}
   292  	}
   293  
   294  	// Check whether only taskWithGC reports overlapping duration.
   295  	for _, task := range res.tasks {
   296  		got := task.overlappingGCDuration(res.gcEvents)
   297  		switch task.name {
   298  		case "taskWithoutGC":
   299  			if got != 0 {
   300  				t.Errorf("%s reported %v as overlapping GC time; want 0: %v", task.name, got, task)
   301  			}
   302  		case "taskWithGC":
   303  			upperBound := task.duration()
   304  			// TODO(hyangah): a tighter upper bound is gcTime, but
   305  			// use of it will make the test flaky due to the issue
   306  			// described in golang.org/issue/16755. Tighten the upper
   307  			// bound when the issue with the timestamp computed
   308  			// based on clockticks is resolved.
   309  			if got <= 0 || got > upperBound {
   310  				t.Errorf("%s reported %v as overlapping GC time; want (0, %v):\n%v", task.name, got, upperBound, task)
   311  				buf := new(bytes.Buffer)
   312  				fmt.Fprintln(buf, "GC Events")
   313  				for _, ev := range res.gcEvents {
   314  					fmt.Fprintf(buf, " %s -> %s\n", ev, ev.Link)
   315  				}
   316  				fmt.Fprintln(buf, "Events in Task")
   317  				for i, ev := range task.events {
   318  					fmt.Fprintf(buf, " %d: %s\n", i, ev)
   319  				}
   320  
   321  				t.Logf("\n%s", buf)
   322  			}
   323  		}
   324  	}
   325  }
   326  
   327  // traceProgram runs the provided function while tracing is enabled,
   328  // parses the captured trace, and sets the global trace loader to
   329  // point to the parsed trace.
   330  //
   331  // If savetraces flag is set, the captured trace will be saved in the named file.
   332  func traceProgram(t *testing.T, f func(), name string) error {
   333  	t.Helper()
   334  	buf := new(bytes.Buffer)
   335  	if err := trace.Start(buf); err != nil {
   336  		return err
   337  	}
   338  	f()
   339  	trace.Stop()
   340  
   341  	saveTrace(buf, name)
   342  	res, err := traceparser.Parse(buf, name+".faketrace")
   343  	if err == traceparser.ErrTimeOrder {
   344  		t.Skipf("skipping due to golang.org/issue/16755: %v", err)
   345  	} else if err != nil {
   346  		return err
   347  	}
   348  
   349  	swapLoaderData(res, err)
   350  	return nil
   351  }
   352  
   353  func regionNames(task *taskDesc) (ret []string) {
   354  	for _, s := range task.regions {
   355  		ret = append(ret, s.Name)
   356  	}
   357  	return ret
   358  }
   359  
   360  func parentName(task *taskDesc) string {
   361  	if task.parent != nil {
   362  		return task.parent.name
   363  	}
   364  	return ""
   365  }
   366  
   367  func childrenNames(task *taskDesc) (ret []string) {
   368  	for _, s := range task.children {
   369  		ret = append(ret, s.name)
   370  	}
   371  	return ret
   372  }
   373  
   374  func swapLoaderData(res traceparser.ParseResult, err error) {
   375  	// swap loader's data.
   376  	parseTrace() // fool loader.once.
   377  
   378  	loader.res = res
   379  	loader.err = err
   380  
   381  	analyzeGoroutines(nil) // fool gsInit once.
   382  	gs = traceparser.GoroutineStats(res.Events)
   383  
   384  }
   385  
   386  func saveTrace(buf *bytes.Buffer, name string) {
   387  	if !*saveTraces {
   388  		return
   389  	}
   390  	if err := os.WriteFile(name+".trace", buf.Bytes(), 0600); err != nil {
   391  		panic(fmt.Errorf("failed to write trace file: %v", err))
   392  	}
   393  }
   394  

View as plain text