event: a new mechanism for caller detection

This avoids the need to know the stack depth to the caller by instead scanning the
stack skipping over entries that have been registered as helpers.
It also allows registration of helper packages, where every function from that package
is considered a helper function. This is used to elimitate all functions from the 
event package.

Change-Id: Id747e1a4a5ccbfa84dfd372cc33471490aa426ef
Reviewed-on: https://go-review.googlesource.com/c/exp/+/328790
Trust: Ian Cottrell <iancottrell@google.com>
Run-TryBot: Ian Cottrell <iancottrell@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/event/alloc_test.go b/event/alloc_test.go
index 07fd434..070dfdf 100644
--- a/event/alloc_test.go
+++ b/event/alloc_test.go
@@ -20,7 +20,7 @@
 	anInt := event.Label{Name: "int", Value: event.Int64Of(4)}
 	aString := event.Label{Name: "string", Value: event.StringOf("value")}
 
-	e := event.NewExporter(logfmt.NewHandler(ioutil.Discard), nil)
+	e := event.NewExporter(logfmt.NewHandler(ioutil.Discard), &event.ExporterOptions{EnableNamespaces: true})
 	ctx := event.WithExporter(context.Background(), e)
 	allocs := int(testing.AllocsPerRun(5, func() {
 		event.To(ctx).With(aString).With(anInt).Log("message1")
diff --git a/event/export.go b/event/export.go
index 7464015..eee6a92 100644
--- a/event/export.go
+++ b/event/export.go
@@ -8,8 +8,6 @@
 
 import (
 	"context"
-	"runtime"
-	"strings"
 	"sync"
 	"sync/atomic"
 	"time"
@@ -20,10 +18,10 @@
 type Exporter struct {
 	opts ExporterOptions
 
-	mu            sync.Mutex
-	handler       Handler
-	lastEvent     uint64
-	pcToNamespace map[uintptr]string
+	mu        sync.Mutex
+	handler   Handler
+	lastEvent uint64
+	sources   sources
 }
 
 type ExporterOptions struct {
@@ -64,7 +62,10 @@
 	if handler == nil {
 		panic("handler must not be nil")
 	}
-	e := &Exporter{handler: handler}
+	e := &Exporter{
+		handler: handler,
+		sources: newCallers(),
+	}
 	if opts != nil {
 		e.opts = *opts
 	}
@@ -106,17 +107,7 @@
 		ev.At = e.opts.Now()
 	}
 	if e.opts.EnableNamespaces && ev.Namespace == "" {
-		// Get the pc of the user function that delivered the event.
-		// This is sensitive to the call stack.
-		// 0: runtime.Callers
-		// 1: importPath
-		// 2: Exporter.prepare (this function)
-		// 3: Builder.{Start,End,etc.}
-		// 4: user function
-		if e.pcToNamespace == nil {
-			e.pcToNamespace = map[uintptr]string{}
-		}
-		ev.Namespace = importPath(4, e.pcToNamespace)
+		ev.Namespace = e.sources.scanStack().Space
 	}
 }
 
@@ -124,42 +115,3 @@
 func (e *Exporter) annotationsEnabled() bool { return !e.opts.DisableAnnotations }
 func (e *Exporter) tracingEnabled() bool     { return !e.opts.DisableTracing }
 func (e *Exporter) metricsEnabled() bool     { return !e.opts.DisableMetrics }
-
-func importPath(depth int, cache map[uintptr]string) string {
-	var pcs [1]uintptr
-	runtime.Callers(depth, pcs[:])
-	pc := pcs[0]
-	ns, ok := cache[pc]
-	if !ok {
-		// If we call runtime.CallersFrames(pcs[:1]) in this function, the
-		// compiler will think the pcs array escapes and will allocate.
-		f := callerFrameFunction(pc)
-		ns = namespace(f)
-		if cache != nil {
-			cache[pc] = ns
-		}
-	}
-	return ns
-}
-
-func callerFrameFunction(pc uintptr) string {
-	frame, _ := runtime.CallersFrames([]uintptr{pc}).Next()
-	return frame.Function
-}
-
-func namespace(funcPath string) string {
-	// Function is the fully-qualified function name. The name itself may
-	// have dots (for a closure, for instance), but it can't have slashes.
-	// So the package path ends at the first dot after the last slash.
-	i := strings.LastIndexByte(funcPath, '/')
-	if i < 0 {
-		i = 0
-	}
-	end := strings.IndexByte(funcPath[i:], '.')
-	if end >= 0 {
-		end += i
-	} else {
-		end = len(funcPath)
-	}
-	return funcPath[:end]
-}
diff --git a/event/metric.go b/event/metric.go
index 594cc88..a2611ac 100644
--- a/event/metric.go
+++ b/event/metric.go
@@ -38,14 +38,8 @@
 	}
 	return &MetricDescriptor{
 		name: name,
-		// Set namespace to the caller's import path.
-		// Depth:
-		//   0  runtime.Callers
-		//   1  importPath
-		//   2  this function
-		//   3  caller of this function (one of the NewXXX methods in this package)
-		//   4  caller's caller
-		namespace:   importPath(4, nil),
+		// TODO: use the global callers, will also need a lock
+		namespace:   scanStack().Space,
 		description: description,
 	}
 }
diff --git a/event/source.go b/event/source.go
new file mode 100644
index 0000000..183e336
--- /dev/null
+++ b/event/source.go
@@ -0,0 +1,208 @@
+// Copyright 2019 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.
+
+// +build !disable_events
+
+package event
+
+import (
+	"reflect"
+	"runtime"
+	"sort"
+	"strings"
+)
+
+const (
+	// this is the maximum amount of helpers we scan past to find a non helper
+	helperDepthLimit = 5
+)
+
+type sources struct {
+	entries []caller
+}
+
+type Source struct {
+	Space string
+	Owner string
+	Name  string
+}
+
+type caller struct {
+	helper bool
+	pc     uintptr
+	source Source
+}
+
+var globalCallers chan *sources
+
+// RegisterHelper records a function as being an event helper that should not
+// be used when capturing the source infomation on events.
+// v should be either a string or a function pointer.
+// If v is a string it is of the form
+//   Space.Owner.Name
+// where Owner and Name cannot contain '/' and Name also cannot contain '.'
+func RegisterHelper(v interface{}) {
+	g := <-globalCallers
+	defer func() { globalCallers <- g }()
+	switch v := v.(type) {
+	case string:
+		g.entries = append(g.entries, caller{source: splitName(v), helper: true})
+	default:
+		g.helperFunction(v)
+	}
+}
+
+func init() {
+	g := &sources{}
+	// make all entries in the event package helpers
+	globalCallers = make(chan *sources, 1)
+	globalCallers <- g
+	RegisterHelper("golang.org/x/exp/event")
+}
+
+func newCallers() sources {
+	g := <-globalCallers
+	defer func() { globalCallers <- g }()
+	c := sources{}
+	c.entries = make([]caller, len(g.entries))
+	copy(c.entries, g.entries)
+	return c
+}
+
+func (c *sources) addCaller(entry caller) {
+	i := sort.Search(len(c.entries), func(i int) bool {
+		return c.entries[i].pc >= entry.pc
+	})
+	if i >= len(c.entries) {
+		// add to end
+		c.entries = append(c.entries, entry)
+		return
+	}
+	if c.entries[i].pc == entry.pc {
+		// already present
+		return
+	}
+	//expand the array
+	c.entries = append(c.entries, caller{})
+	//make a space
+	copy(c.entries[i+1:], c.entries[i:])
+	// insert the entry
+	c.entries[i] = entry
+}
+
+func (c *sources) getCaller(pc uintptr) (caller, bool) {
+	i := sort.Search(len(c.entries), func(i int) bool {
+		return c.entries[i].pc >= pc
+	})
+	if i == len(c.entries) || c.entries[i].pc != pc {
+		return caller{}, false
+	}
+	return c.entries[i], true
+}
+
+func scanStack() Source {
+	g := <-globalCallers
+	defer func() { globalCallers <- g }()
+	return g.scanStack()
+}
+
+func (c *sources) scanStack() Source {
+	// first capture the caller stack
+	var stack [helperDepthLimit]uintptr
+	// we can skip the first three entries
+	//   runtime.Callers
+	//   event.(*sources).scanStack (this function)
+	//   another function in this package (because scanStack is private)
+	depth := runtime.Callers(3, stack[:]) // start at 2 to skip Callers and this function
+	// do a cheap first pass to see if we have an entry for this stack
+	for i := 0; i < depth; i++ {
+		pc := stack[i]
+		e, found := c.getCaller(pc)
+		if found {
+			if !e.helper {
+				// exact non helper match match found, return it
+				return e.source
+			}
+			// helper found, keep scanning
+			continue
+		}
+		// stack entry not found, we need to fill one in
+		f := runtime.FuncForPC(stack[i])
+		if f == nil {
+			// symtab lookup failed, pretend it does not exist
+			continue
+		}
+		e = caller{
+			source: splitName(f.Name()),
+			pc:     pc,
+		}
+		e.helper = c.isHelper(e)
+		c.addCaller(e)
+		if !e.helper {
+			// found a non helper entry, add it and return it
+			return e.source
+		}
+	}
+	// ran out of stack, was all helpers
+	return Source{}
+}
+
+// we do helper matching by name, if the pc matched we would have already found
+// that, but helper registration does not know the call stack pcs
+func (c *sources) isHelper(entry caller) bool {
+	// scan to see if it matches any of the helpers
+	// we match by name in case of inlining
+	for _, e := range c.entries {
+		if !e.helper {
+			// ignore all the non helper entries
+			continue
+		}
+		if isMatch(entry.source.Space, e.source.Space) &&
+			isMatch(entry.source.Owner, e.source.Owner) &&
+			isMatch(entry.source.Name, e.source.Name) {
+			return true
+		}
+	}
+	return false
+}
+
+func isMatch(value, against string) bool {
+	return len(against) == 0 || value == against
+}
+
+func (c *sources) helperFunction(v interface{}) {
+	r := reflect.ValueOf(v)
+	pc := r.Pointer()
+	f := runtime.FuncForPC(pc)
+	entry := caller{
+		source: splitName(f.Name()),
+		pc:     f.Entry(),
+		helper: true,
+	}
+	c.addCaller(entry)
+	if entry.pc != pc {
+		entry.pc = pc
+		c.addCaller(entry)
+	}
+}
+
+func splitName(full string) Source {
+	// Function is the fully-qualified function name. The name itself may
+	// have dots (for a closure, for instance), but it can't have slashes.
+	// So the package path ends at the first dot after the last slash.
+	entry := Source{Space: full}
+	slash := strings.LastIndexByte(full, '/')
+	if slash < 0 {
+		slash = 0
+	}
+	if dot := strings.IndexByte(full[slash:], '.'); dot >= 0 {
+		entry.Space = full[:slash+dot]
+		entry.Name = full[slash+dot+1:]
+		if dot = strings.LastIndexByte(entry.Name, '.'); dot >= 0 {
+			entry.Owner = entry.Name[dot+1:]
+			entry.Name = entry.Name[:dot]
+		}
+	}
+	return entry
+}