golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
124.16k stars 17.69k forks source link

proposal: runtime/trace: add Helper() to hide helpers from callers stack for region API #60887

Open dolmen opened 1 year ago

dolmen commented 1 year ago

Add an Helper() function to allow clients of the runtime/trace region API to create helpers but hide them from the stack traces. The API would be similar to testing.Helper().

Use case

The region API (trace.WithRegion, trace.StartRegion) is quite intrusive: to enhance existing code with regions, one must wrap the existing block in a func(). It is even more verbose if the existing block returned values to use after the block (ex: error).

To limit the verbosity, I used generics to write wrappers around the trace.StartRegion API (see below).

Unfortunately, with Go 1.20 the go tool trace output shows the helper function as the encapsulated code instead of the real user's code. This makes the tracing annotations much less useful.

It would be helpful to have a way to declare a caller of the trace.StartRegion as an helper. The testing package has Helper() which would be a good inspiration (but a different API might be needed as runtime/trace performance is critical).

Example helpers

Example wrappers in user code that would be declared as helpers:

// traceWithRegionErr wraps [trace.WithRegion] to ease handling of failure cases.
func traceWithRegionErr(ctx context.Context, regionType string, fn func() error) error {
    defer trace.StartRegion(ctx, regionType).End()
    err := fn()
    if err != nil {
        return fmt.Errorf("%s: %w", regionType, err)
    }
    return nil
}

// traceWithRegion1Err1 wraps [trace.WithRegion] to ease defining a region for a function
// with one input parameter and returning a value and an error.
func traceWithRegion1Err1[I1 any, O1 any](ctx context.Context, regionType string, fn func(I1) (O1, error)) func(I1) (O1, error) {
    return func(i1 I1) (O1, error) {
        defer trace.StartRegion(ctx, regionType).End()
        o1, err := fn(i1)
        if err != nil {
            return o1, fmt.Errorf("%s: %w", regionType, err)
        }
        return o1, nil
    }
}

// traceWithRegionCtx1Err1 wraps [trace.WithRegion] to ease defining a region for a function
// with a context, another input parameter and returning a value and an error.
func traceWithRegionCtx1Err1[I1 any, O1 any](ctx context.Context, regionType string, fn func(context.Context, I1) (O1, error)) func(I1) (O1, error) {
    return func(i1 I1) (O1, error) {
        defer trace.StartRegion(ctx, regionType).End()
        o1, err := fn(ctx, i1)
        if err != nil {
            return o1, fmt.Errorf("%s: %w", regionType, err)
        }
        return o1, nil
    }
}

// traceWithRegionCtx31 wraps [trace.WithRegion] to ease defining a region for a function
// with a context, 3 other input parameters and returning one value.
func traceWithRegionCtx31[I1 any, I2 any, I3 any, O1 any](ctx context.Context, regionType string, fn func(context.Context, I1, I2, I3) O1) func(I1, I2, I3) O1 {
    return func(i1 I1, i2 I2, i3 I3) O1 {
        defer trace.StartRegion(ctx, regionType).End()
        return fn(ctx, i1, i2, i3)
    }
}
dolmen commented 1 year ago

Cc: @mknyszek

earthboundkid commented 1 year ago

What about Helper(int) to specify how many levels to skip?

mknyszek commented 1 year ago

To keep traceback costs (the dominating cost) low, Helper would probably be implemented as a trace event that marks a function by name, and the filtering could be applied lazily in the trace parser.

But I also wonder if we could just do something to make the runtime/trace region API more ergonomic to begin with. I'm not certain generics are powerful enough to cover every case. One possible approach could be to annotate a function (e.g. //go:traceRegion <regionType>) and have the compiler generate a wrapper (which is a fairly straightforward transformation) assuming the first argument is a context.Context. Though, I'm not sure we'd want to add more special annotations of this form.

Just thinking out loud; this needs more thought.