In runtime/trace, how do you annotate a stage that fans work out to several goroutines?
answer
- the context is the only carrier
- one region per goroutine, not one shared
- a region cannot cross a go statement
- the parent may time the whole fan-out
- end the task after the join
basics
~20 sPass the context returned by trace.NewTask into every goroutine so their events land under one task, and let each goroutine start and end its own region. Never share a trace.Region across goroutines, and end the task only after the workers finish.
solid answer
~50 sCreate the task once, at the top of the unit of work, and hand the derived context to each goroutine — that context is the only thing that carries the task, since a `go` statement inherits nothing. Inside each worker, `defer trace.StartRegion(ctx, "extract").End()`: one region per goroutine, all attributed to the same task, because `Region.End` must run on the goroutine that started the region. If you also want the wall-clock cost of the whole fan-out, start a region on the parent goroutine before launching and end it after joining — that is legal because both calls happen on the parent. What is never legal is starting a region on the parent and ending it in a worker. Finally, call `task.End()` after the join; end it earlier and the task's measured duration excludes work still being recorded under it.
code
go · 17 linesctx, task := trace.NewTask(ctx, "ingest")
defer task.End()
done := make(chan struct{}, len(docs))
fanout := trace.StartRegion(ctx, "extract-fanout") // starts and ends here
for _, d := range docs {
go func(d Document) {
defer trace.StartRegion(ctx, "extract").End() // this goroutine's own
extract(ctx, d)
done <- struct{}{}
}(d)
}
for range docs {
<-done
}
fanout.End()go deeper
Remember that a new goroutine starts with nothing from its creator: if you want its trace events tied to the operation, you must pass the context that NewTask returned into it.
Explain why one region cannot wrap a fan-out — End must run on the starting goroutine — and describe the shape that does work: one region per worker plus, optionally, one on the parent around the launch and join.
Show that you can instrument concurrent code so the resulting views are actually readable: per-worker regions under one task, a subtask when a unit spans goroutines, and the task ended after the join so its latency is honest.
Decide how much annotation concurrency-heavy shared code carries and who maintains it, balancing the diagnostic value against the cost every caller pays and the risk of a trace vocabulary nobody can interpret.
## The constraint that shapes everything Two rules decide the whole design: 1. A **task** is carried in a `context.Context`, so it goes wherever you pass the context — including into goroutines. 2. A **region** belongs to the goroutine that started it. `Region.End` must be called from that same goroutine, and regions within a goroutine must nest. A `go` statement inherits nothing on its own. There is no goroutine-local storage in Go, and a new goroutine begins with an empty region stack and no task. Everything the child knows about the traced operation arrives as an argument. ## The layout that works ```go ctx, task := trace.NewTask(ctx, "ingest") defer task.End() ``` Then each worker takes that `ctx` and opens its own region: ```go go func(d Document) { defer trace.StartRegion(ctx, "extract").End() extract(ctx, d) }(d) ``` Now the trace has N regions of type `extract`, one per goroutine, every one of them attributed to the single `ingest` task. In the user-region view they aggregate into a latency distribution for the stage; in the user-task view you can open one task and see all of them, on their different goroutines, under the same operation. ## Measuring the fan-out itself A per-worker region tells you how long each worker took, not how long the stage took — those differ by the scheduling and the join. If you want the stage's wall-clock cost, start a region on the parent goroutine before the loop and end it after the join: ```go fanout := trace.StartRegion(ctx, "extract-fanout") // launch workers, wait for them fanout.End() ``` This is legal because both calls run on the parent goroutine; the parent simply happens to be blocked in the middle. The illegal shape is starting the region on the parent and calling `End` inside a worker: the documented contract is that `End` runs on the starting goroutine, so nothing pairs the two events into an interval you can trust, and the parent's region is left open on its own goroutine. ## Region or subtask per worker? A region is the cheap default. Reach for a **subtask** — `wctx, wtask := trace.NewTask(ctx, "extract")` inside the worker — when either of these is true: - The worker's own work spans further goroutines, so a single-goroutine region cannot cover it. - You want that unit to have its own entry and its own latency distribution in the user-task view, while staying linked to the parent operation. A task created from a context that already carries a task is recorded as its subtask. The cost is that you now have two levels of task in the view, so do it deliberately rather than by default. ## Ending the task `Task.End` has no same-goroutine constraint, which makes it tempting to fire and forget. The trap is ending it while workers are still running: their regions and log events still carry the task, but they fall outside the interval the task measured, so the task's latency understates the operation. Whatever your join is, `task.End()` belongs after it. `defer task.End()` in the function that also performs the join gets this right for free. ## Passing the context down The most common failure of all is undramatic: some function in the middle of the chain takes no context, or creates a fresh one, and everything below it disappears from the task. If a stage's regions are missing from the trace, the first thing to check is not the annotation calls but the context plumbing — whether the worker really received the context that `NewTask` returned, or a different one. ## What a reviewer should look for - The context returned by `NewTask` is the one passed into the goroutines, not the original. - No `*trace.Region` value crosses a `go` statement or a channel. - Every `StartRegion` has a matching `End` on the same goroutine, preferably via `defer`. - `task.End()` runs after the join, not before. - Region types are a small fixed set, so the per-stage distributions stay meaningful.
- What goes wrong if you start a region on the parent and end it inside a worker?The documented contract is that Region.End runs on the goroutine that called StartRegion. Split them across goroutines and the two events land on different goroutines, so nothing pairs them into an interval you can rely on, and the parent is left with an open region. Use one region per goroutine, or a subtask when the interval genuinely spans goroutines.
- When would you create a subtask per worker instead of a region per worker?When the worker's own work spans further goroutines, or when you want that unit to have its own latency distribution in the user-task view. NewTask on the worker's context records a subtask of the enclosing task, so it is measured separately while staying linked to the parent operation. A region can do neither: it is one interval on one goroutine, aggregated only by region type.
- A stage's regions are missing from the trace entirely. Where do you look first?At the context plumbing, not the annotation calls. Somewhere in the chain a function probably takes no context or substitutes a fresh one, so the stage runs with a context that carries no task. The second candidate is that tracing was not running while that stage executed, since annotations are recorded only during collection.
saying these in an interview costs you the question
- Passes the *trace.Region into the worker goroutines
- Assumes goroutines inherit the task from their creator
- Ends the parent's region from inside a worker
- Calls task.End before the workers have joined
- Creates a fresh context inside the worker