summaryrefslogtreecommitdiffstats
path: root/src/runtime/trace/annotation.go
blob: 2666d1420171d772482309993ce96cc684aaac4a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
// 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.

package trace

import (
	"context"
	"fmt"
	"sync/atomic"
	_ "unsafe"
)

type traceContextKey struct{}

// NewTask creates a task instance with the type taskType and returns
// it along with a Context that carries the task.
// If the input context contains a task, the new task is its subtask.
//
// The taskType is used to classify task instances. Analysis tools
// like the Go execution tracer may assume there are only a bounded
// number of unique task types in the system.
//
// The returned Task's [Task.End] method is used to mark the task's end.
// The trace tool measures task latency as the time between task creation
// and when the End method is called, and provides the latency
// distribution per task type.
// If the End method is called multiple times, only the first
// call is used in the latency measurement.
//
//	ctx, task := trace.NewTask(ctx, "awesomeTask")
//	trace.WithRegion(ctx, "preparation", prepWork)
//	// preparation of the task
//	go func() {  // continue processing the task in a separate goroutine.
//	    defer task.End()
//	    trace.WithRegion(ctx, "remainingWork", remainingWork)
//	}()
func NewTask(pctx context.Context, taskType string) (ctx context.Context, task *Task) {
	pid := fromContext(pctx).id
	id := newID()
	userTaskCreate(id, pid, taskType)
	s := &Task{id: id}
	return context.WithValue(pctx, traceContextKey{}, s), s

	// We allocate a new task even when
	// the tracing is disabled because the context and task
	// can be used across trace enable/disable boundaries,
	// which complicates the problem.
	//
	// For example, consider the following scenario:
	//   - trace is enabled.
	//   - trace.WithRegion is called, so a new context ctx
	//     with a new region is created.
	//   - trace is disabled.
	//   - trace is enabled again.
	//   - trace APIs with the ctx is called. Is the ID in the task
	//   a valid one to use?
	//
	// TODO(hyangah): reduce the overhead at least when
	// tracing is disabled. Maybe the id can embed a tracing
	// round number and ignore ids generated from previous
	// tracing round.
}

func fromContext(ctx context.Context) *Task {
	if s, ok := ctx.Value(traceContextKey{}).(*Task); ok {
		return s
	}
	return &bgTask
}

// Task is a data type for tracing a user-defined, logical operation.
type Task struct {
	id uint64
	// TODO(hyangah): record parent id?
}

// End marks the end of the operation represented by the [Task].
func (t *Task) End() {
	userTaskEnd(t.id)
}

var lastTaskID uint64 = 0 // task id issued last time

func newID() uint64 {
	// TODO(hyangah): use per-P cache
	return atomic.AddUint64(&lastTaskID, 1)
}

var bgTask = Task{id: uint64(0)}

// Log emits a one-off event with the given category and message.
// Category can be empty and the API assumes there are only a handful of
// unique categories in the system.
func Log(ctx context.Context, category, message string) {
	id := fromContext(ctx).id
	userLog(id, category, message)
}

// Logf is like [Log], but the value is formatted using the specified format spec.
func Logf(ctx context.Context, category, format string, args ...any) {
	if IsEnabled() {
		// Ideally this should be just Log, but that will
		// add one more frame in the stack trace.
		id := fromContext(ctx).id
		userLog(id, category, fmt.Sprintf(format, args...))
	}
}

const (
	regionStartCode = uint64(0)
	regionEndCode   = uint64(1)
)

// WithRegion starts a region associated with its calling goroutine, runs fn,
// and then ends the region. If the context carries a task, the region is
// associated with the task. Otherwise, the region is attached to the background
// task.
//
// The regionType is used to classify regions, so there should be only a
// handful of unique region types.
func WithRegion(ctx context.Context, regionType string, fn func()) {
	// NOTE:
	// WithRegion helps avoiding misuse of the API but in practice,
	// this is very restrictive:
	// - Use of WithRegion makes the stack traces captured from
	//   region start and end are identical.
	// - Refactoring the existing code to use WithRegion is sometimes
	//   hard and makes the code less readable.
	//     e.g. code block nested deep in the loop with various
	//          exit point with return values
	// - Refactoring the code to use this API with closure can
	//   cause different GC behavior such as retaining some parameters
	//   longer.
	// This causes more churns in code than I hoped, and sometimes
	// makes the code less readable.

	id := fromContext(ctx).id
	userRegion(id, regionStartCode, regionType)
	defer userRegion(id, regionEndCode, regionType)
	fn()
}

// StartRegion starts a region and returns it.
// The returned Region's [Region.End] method must be called
// from the same goroutine where the region was started.
// Within each goroutine, regions must nest. That is, regions started
// after this region must be ended before this region can be ended.
// Recommended usage is
//
//	defer trace.StartRegion(ctx, "myTracedRegion").End()
func StartRegion(ctx context.Context, regionType string) *Region {
	if !IsEnabled() {
		return noopRegion
	}
	id := fromContext(ctx).id
	userRegion(id, regionStartCode, regionType)
	return &Region{id, regionType}
}

// Region is a region of code whose execution time interval is traced.
type Region struct {
	id         uint64
	regionType string
}

var noopRegion = &Region{}

// End marks the end of the traced code region.
func (r *Region) End() {
	if r == noopRegion {
		return
	}
	userRegion(r.id, regionEndCode, r.regionType)
}

// IsEnabled reports whether tracing is enabled.
// The information is advisory only. The tracing status
// may have changed by the time this function returns.
func IsEnabled() bool {
	return tracing.enabled.Load()
}

//
// Function bodies are defined in runtime/trace.go
//

// emits UserTaskCreate event.
func userTaskCreate(id, parentID uint64, taskType string)

// emits UserTaskEnd event.
func userTaskEnd(id uint64)

// emits UserRegion event.
func userRegion(id, mode uint64, regionType string)

// emits UserLog event.
func userLog(id uint64, category, message string)