In Go's runtime/trace, how does a task differ from a region, and how is each one ended?
answer
- one may span goroutines, one may not
- the context is what carries the identity
- regions nest like a stack per goroutine
- End on the goroutine that started it
- WithRegion pairs start and end for you
basics
~20 sA runtime/trace task is a logical operation that may span goroutines: NewTask returns a context carrying it, and Task.End closes it. A region is one interval inside a single goroutine, ended by the goroutine that started it.
solid answer
~40 s`trace.NewTask(ctx, "ingest")` returns a derived context plus a `*trace.Task`. The task identity lives in that context, so every goroutine you hand the context to records its events under the same task, and you close it with `defer task.End()`. A region is smaller and cheaper: `trace.StartRegion(ctx, "parse")` returns a `*trace.Region` whose `End` must be called by the goroutine that started it, so regions within a goroutine nest like a stack; `trace.WithRegion(ctx, "parse", fn)` is the wrapper that gets the pairing right for you. Tasks answer "how long did this whole document take, across whatever goroutines touched it"; regions answer "which phase inside this goroutine took the time". `go tool trace` builds its user-task and user-region views out of exactly these events, aggregating by the type strings you pass in.
code
go · 14 linesfunc ingest(ctx context.Context, doc Document) {
ctx, task := trace.NewTask(ctx, "ingest")
defer task.End()
trace.Logf(ctx, "doc", "id=%s", doc.ID)
r := trace.StartRegion(ctx, "parse")
parse(doc)
r.End()
trace.WithRegion(ctx, "index", func() {
index(doc)
})
}go deeper
Know that a Go program can annotate its own execution trace: NewTask marks a logical operation and StartRegion marks a phase. Be able to spot defer task.End() and defer region.End() in a snippet and say what each one closes.
Explain that the task identity travels in the context while a region belongs to the goroutine that started it, that regions nest per goroutine, and that WithRegion is the safe wrapper around the start and end pair.
Show that you can lay annotations over a real workload: one task per unit of work, one region per phase, and low-cardinality type strings so the aggregated views stay readable rather than fragmenting into groups of one.
Own what instrumentation a shared package exports and what it costs its callers: whether annotation belongs in library code at all, who owns the type vocabulary, and how you keep several teams' traces comparable.
## Why annotate a trace at all An execution trace records what the Go runtime knows about: goroutines being created, running, parking and unparking, syscalls, garbage-collection work. What it cannot know is what any of that *meant to your program*. A multi-stage document ingestion workflow shows up as a crowd of anonymous goroutines; nothing in the raw trace says "this 40 ms belonged to document 8812, in the parse stage". The user annotation API in `runtime/trace` exists to close that gap: you add your own vocabulary to the trace, and `go tool trace` builds views out of it. There are three annotation primitives, and the first two are the ones people confuse. ## Task: a logical operation that can span goroutines ```go ctx, task := trace.NewTask(ctx, "ingest") defer task.End() ``` `NewTask` takes a parent context and a **task type** string, and returns two things: a derived `context.Context` and a `*trace.Task`. The important half is the context. The task's identity travels in it, so any function or goroutine that receives that context and calls another annotation function records its event under this task. That is what makes a task able to span goroutines: nothing is inherited automatically by a `go` statement, but a context passed into the goroutine carries the task with it. If the context you pass to `NewTask` already carries a task, the new one is recorded as its subtask, which is how you get nested units of work such as one task per document inside one task per batch. `Task.End()` marks the end of the operation. It does not have to run on the goroutine that created the task, which is exactly the freedom a region does not have. The idiom is `defer task.End()` on the line after `NewTask`, because a task with no end event has no measurable duration. ## Region: one interval on one goroutine ```go r := trace.StartRegion(ctx, "parse") parse(doc) r.End() ``` `StartRegion` takes a context and a **region type** string and returns a `*trace.Region`. Its contract is narrower and it is the source of most mistakes: `Region.End` must be called from the same goroutine that called `StartRegion`, and regions on a goroutine must nest properly, like a stack. A region is therefore never the right tool for an interval that begins on one goroutine and finishes on another — that is a task, or a pair of regions, one per goroutine. Because the pairing is easy to get wrong, two idioms dominate: - `defer trace.StartRegion(ctx, "parse").End()` at the top of a function, which measures the whole call; - `trace.WithRegion(ctx, "index", func() { ... })`, which starts a region, runs the function and ends the region for you. A region also picks up the task from the context, so it is attributed to the enclosing task. If the context carries no task, the region is still recorded for that goroutine; it simply belongs to no user task, and you see it only in the region view. ## Log: an instant, not an interval `trace.Log(ctx, category, message)` and `trace.Logf(ctx, category, format, args...)` attach a point-in-time message to the task in the context. Tasks and regions give you intervals; logs give you the facts that identify *which* interval you are looking at — the document id, the batch size, the retry count. ## Choosing the strings The views aggregate on the type strings. `go tool trace` groups tasks by task type and regions by region type, and shows a latency distribution per type. So the type vocabulary must be small and stable: `"ingest"`, `"parse"`, `"index"`. Encoding a document id into the type string — `"ingest-8812"` — technically works and destroys the aggregation, because every operation becomes its own type with a population of one. Per-instance detail belongs in a log message, not in a type. ## Cost and lifetime Annotations are recorded only while a trace is actually being collected. When tracing is off, these calls fall back to essentially nothing: `StartRegion` hands back a no-op region, so calling `End` on it is safe, and the whole pattern is cheap enough to leave permanently in code that is not in an extremely hot loop. What is *not* free is whatever you compute to build a log message, which is why `trace.IsEnabled` exists as a guard. ## Putting it together A sane layout for a staged pipeline is: one task per unit of work, created where the work enters the system and ended when it leaves; one region per stage, started and ended on the goroutine that runs that stage; and log events carrying the identifiers you will want when you are staring at a trace at 3am. That gives you, in the user-task view, a latency distribution per task type, and inside any single task a breakdown of which stage consumed the time — the thing the raw goroutine timeline can never tell you.
- What happens in the user-task view if you forget to call Task.End?No end event is recorded, so the task has no measurable latency and cannot join the latency distribution for its type. You can still see everything recorded under it — the regions it started and its log messages — and that is often exactly how a stuck operation announces itself. The habit is `defer task.End()` on the line after NewTask.
- Can a goroutine start a region if its context carries no task?Yes. StartRegion works with any context; with no task in it the region is simply recorded for that goroutine and shows up in the user-region view, associated with no user task. It is still useful for a per-type latency breakdown, but you lose the ability to say which document or request that interval belonged to, so in practice you create the task first.
- Why should the task type and region type strings be a small fixed set?Because the trace views aggregate on them: tasks are grouped by task type and regions by region type, each with a latency distribution. A type string built from a per-request identifier gives every operation its own group of one, which makes the distributions useless. Keep the vocabulary stable and put the identifiers in trace.Log messages instead.
A task is an order number that travels with the paperwork from department to department. A region is one department's stopwatch, and you cannot start it in one room and stop it in another.
saying these in an interview costs you the question
- Says a region can be ended from a different goroutine
- Thinks child goroutines inherit the task without the context
- Uses a per-document string as the task type
- Treats tasks and regions as the same thing at different scales
- Believes calling NewTask starts trace collection