| // 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) | |