summaryrefslogtreecommitdiffstats
path: root/event/source.go
diff options
context:
space:
mode:
Diffstat (limited to 'event/source.go')
-rw-r--r--event/source.go210
1 files changed, 210 insertions, 0 deletions
diff --git a/event/source.go b/event/source.go
new file mode 100644
index 0000000..3af0bd4
--- /dev/null
+++ b/event/source.go
@@ -0,0 +1,210 @@
+// 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.
+
+//go: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 information 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]
+ entry.Name = entry.Name[dot+1:]
+ }
+ }
+ return entry
+}