stackcounter.go (5293B)
1 // Copyright 2023 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package counter 6 7 import ( 8 "fmt" 9 "runtime" 10 "strings" 11 "sync" 12 ) 13 14 // On the disk, and upstream, stack counters look like sets of 15 // regular counters with names that include newlines. 16 17 // a StackCounter is the in-memory knowledge about a stack counter. 18 // StackCounters are more expensive to use than regular Counters, 19 // requiring, at a minimum, a call to runtime.Callers. 20 type StackCounter struct { 21 name string 22 depth int 23 file *file 24 25 mu sync.Mutex 26 // as this is a detail of the implementation, it could be replaced 27 // by a more efficient mechanism 28 stacks []stack 29 } 30 31 type stack struct { 32 pcs []uintptr 33 counter *Counter 34 } 35 36 func NewStack(name string, depth int) *StackCounter { 37 return &StackCounter{name: name, depth: depth, file: &defaultFile} 38 } 39 40 // Inc increments a stack counter. It computes the caller's stack and 41 // looks up the corresponding counter. It then increments that counter, 42 // creating it if necessary. 43 func (c *StackCounter) Inc() { 44 pcs := make([]uintptr, c.depth) 45 n := runtime.Callers(2, pcs) // caller of Inc 46 pcs = pcs[:n] 47 48 c.mu.Lock() 49 defer c.mu.Unlock() 50 51 // Existing counter? 52 var ctr *Counter 53 for _, s := range c.stacks { 54 if eq(s.pcs, pcs) { 55 if s.counter != nil { 56 ctr = s.counter 57 break 58 } 59 } 60 } 61 62 if ctr == nil { 63 // Create new counter. 64 ctr = &Counter{ 65 name: EncodeStack(pcs, c.name), 66 file: c.file, 67 } 68 c.stacks = append(c.stacks, stack{pcs: pcs, counter: ctr}) 69 } 70 71 ctr.Inc() 72 } 73 74 // EncodeStack returns the name of the counter to 75 // use for the given stack of program counters. 76 // The name encodes the stack. 77 func EncodeStack(pcs []uintptr, prefix string) string { 78 var locs []string 79 lastImport := "" 80 frs := runtime.CallersFrames(pcs) 81 for { 82 fr, more := frs.Next() 83 // TODO(adonovan): this CutLast(".") operation isn't 84 // appropriate for generic function symbols. 85 path, fname := cutLastDot(fr.Function) 86 if path == lastImport { 87 path = `"` // (a ditto mark) 88 } else { 89 lastImport = path 90 } 91 var loc string 92 if fr.Func != nil { 93 // Use function-relative line numbering. 94 // f:+2 means two lines into function f. 95 // f:-1 should never happen, but be conservative. 96 // 97 // An inlined call is replaced by a NOP instruction 98 // with the correct pclntab information. 99 _, entryLine := fr.Func.FileLine(fr.Entry) 100 loc = fmt.Sprintf("%s.%s:%+d,+0x%x", path, fname, fr.Line-entryLine, fr.PC-fr.Entry) 101 } else { 102 // The function is non-Go code or is fully inlined: 103 // use absolute line number within enclosing file. 104 // 105 // For inlined calls, the PC and Entry values 106 // both refer to the enclosing combined function. 107 // For example, both these PCs are relative to "caller": 108 // 109 // callee:=1,+0x12 ('=' means inlined) 110 // caller:+2,+0x34 111 loc = fmt.Sprintf("%s.%s:=%d,+0x%x", path, fname, fr.Line, fr.PC-fr.Entry) 112 } 113 locs = append(locs, loc) 114 if !more { 115 break 116 } 117 } 118 119 name := prefix + "\n" + strings.Join(locs, "\n") 120 if len(name) > maxNameLen { 121 const bad = "\ntruncated\n" 122 name = name[:maxNameLen-len(bad)] + bad 123 } 124 return name 125 } 126 127 // DecodeStack expands the (compressed) stack encoded in the counter name. 128 func DecodeStack(ename string) string { 129 if !strings.Contains(ename, "\n") { 130 return ename // not a stack counter 131 } 132 lines := strings.Split(ename, "\n") 133 var lastPath string // empty or ends with . 134 for i, line := range lines { 135 path, rest := cutLastDot(line) 136 if len(path) == 0 { 137 continue // unchanged 138 } 139 if len(path) == 1 && path[0] == '"' { 140 lines[i] = lastPath + rest 141 } else { 142 lastPath = path + "." 143 // line unchanged 144 } 145 } 146 return strings.Join(lines, "\n") // trailing \n? 147 } 148 149 // input is <import path>.<function name> 150 // output is (import path, function name) 151 func cutLastDot(x string) (before, after string) { 152 i := strings.LastIndex(x, ".") 153 if i < 0 { 154 return "", x 155 } 156 return x[:i], x[i+1:] 157 } 158 159 // Names reports all the counter names associated with a StackCounter. 160 func (c *StackCounter) Names() []string { 161 c.mu.Lock() 162 defer c.mu.Unlock() 163 names := make([]string, len(c.stacks)) 164 for i, s := range c.stacks { 165 names[i] = s.counter.Name() 166 } 167 return names 168 } 169 170 // Counters returns the known Counters for a StackCounter. 171 // There may be more in the count file. 172 func (c *StackCounter) Counters() []*Counter { 173 c.mu.Lock() 174 defer c.mu.Unlock() 175 counters := make([]*Counter, len(c.stacks)) 176 for i, s := range c.stacks { 177 counters[i] = s.counter 178 } 179 return counters 180 } 181 182 func eq(a, b []uintptr) bool { 183 if len(a) != len(b) { 184 return false 185 } 186 for i := range a { 187 if a[i] != b[i] { 188 return false 189 } 190 } 191 return true 192 } 193 194 // ReadStack reads the given stack counter. 195 // This is the implementation of 196 // golang.org/x/telemetry/counter/countertest.ReadStackCounter. 197 func ReadStack(c *StackCounter) (map[string]uint64, error) { 198 ret := map[string]uint64{} 199 for _, ctr := range c.Counters() { 200 v, err := Read(ctr) 201 if err != nil { 202 return nil, err 203 } 204 ret[DecodeStack(ctr.Name())] = v 205 } 206 return ret, nil 207 } 208 209 // IsStackCounter reports whether the counter name is for a stack counter. 210 func IsStackCounter(name string) bool { 211 return strings.Contains(name, "\n") 212 }