github.com/clubpay/ronykit/kit@v0.14.4-0.20240515065620-d0dace45cbc7/internal/stacktrace/stacktrace.go (about) 1 // Copyright (c) 2016 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package stacktrace 22 23 import ( 24 "runtime" 25 "sync" 26 27 "github.com/clubpay/ronykit/kit/utils" 28 "github.com/clubpay/ronykit/kit/utils/buf" 29 ) 30 31 const ( 32 // stacktraceFirst captures only the first frame. 33 stacktraceFirst stacktraceDepth = iota 34 35 // stacktraceFull captures the entire call stack, allocating more 36 // storage for it if needed. 37 stacktraceFull 38 ) 39 40 var stacktracePool = sync.Pool{ 41 New: func() any { 42 return &stacktrace{ 43 storage: make([]uintptr, 64), 44 } 45 }, 46 } 47 48 type stacktrace struct { 49 pcs []uintptr // program counters; always a subslice of storage 50 frames *runtime.Frames 51 52 // The size of pcs varies depending on requirements: 53 // it will be one, if only the first frame was requested, 54 // and otherwise it will reflect the depth of the call stack. 55 // 56 // storage decouples the slice we need (pcs) from the slice we pool. 57 // We will always allocate a reasonably large storage, but we'll use 58 // only as much of it as we need. 59 storage []uintptr 60 } 61 62 // stacktraceDepth specifies how deep of a stack trace should be captured. 63 type stacktraceDepth int 64 65 // captureStacktrace captures a stack trace of the specified depth, skipping 66 // the provided number of frames. skip=0 identifies the caller of 67 // captureStacktrace. 68 // 69 // The caller must call Free on the returned stacktrace after using it. 70 func captureStacktrace(skip int, depth stacktraceDepth) *stacktrace { 71 stack := stacktracePool.Get().(*stacktrace) //nolint:forcetypeassert 72 73 switch depth { 74 case stacktraceFirst: 75 stack.pcs = stack.storage[:1] 76 case stacktraceFull: 77 stack.pcs = stack.storage 78 } 79 80 // Unlike other "skip"-based APIs, skip=0 identifies runtime.Callers 81 // itself. +2 to skip captureStacktrace and runtime.Callers. 82 numFrames := runtime.Callers( 83 skip+2, 84 stack.pcs, 85 ) 86 87 // runtime.Callers truncate the recorded stacktrace if there is no 88 // room in the provided slice. For the full stack trace, keep expanding 89 // storage until there are fewer frames than there is room. 90 if depth == stacktraceFull { 91 pcs := stack.pcs 92 for numFrames == len(pcs) { 93 pcs = make([]uintptr, len(pcs)*2) 94 numFrames = runtime.Callers(skip+2, pcs) 95 } 96 97 // Discard old storage instead of returning it to the pool. 98 // This will adjust the pool size over time if stack traces are 99 // consistently very deep. 100 stack.storage = pcs 101 stack.pcs = pcs[:numFrames] 102 } else { 103 stack.pcs = stack.pcs[:numFrames] 104 } 105 106 stack.frames = runtime.CallersFrames(stack.pcs) 107 108 return stack 109 } 110 111 // Free releases resources associated with this stacktrace 112 // and returns it back to the pool. 113 func (st *stacktrace) Free() { 114 st.frames = nil 115 st.pcs = nil 116 stacktracePool.Put(st) 117 } 118 119 // Count reports the total number of frames in this stacktrace. 120 // Count DOES NOT change as Next is called. 121 func (st *stacktrace) Count() int { 122 return len(st.pcs) 123 } 124 125 // Next returns the next frame in the stack trace, 126 // and a boolean indicating whether there are more after it. 127 func (st *stacktrace) Next() (_ runtime.Frame, more bool) { 128 return st.frames.Next() 129 } 130 131 func TakeStacktrace(skip int) string { 132 stack := captureStacktrace(skip+1, stacktraceFull) 133 defer stack.Free() 134 135 buffer := buf.GetCap(1024) 136 defer buffer.Release() 137 138 stackfmt := newStackFormatter(buffer) 139 stackfmt.FormatStack(stack) 140 141 return string(*buffer.Bytes()) 142 } 143 144 // stackFormatter formats a stack trace into a readable string representation. 145 type stackFormatter struct { 146 b *buf.Bytes 147 nonEmpty bool // whehther we've written at least one frame already 148 } 149 150 // newStackFormatter builds a new stackFormatter. 151 func newStackFormatter(b *buf.Bytes) stackFormatter { 152 return stackFormatter{b: b} 153 } 154 155 // FormatStack formats all remaining frames in the provided stacktrace -- minus 156 // the final runtime.main/runtime.goexit frame. 157 func (sf *stackFormatter) FormatStack(stack *stacktrace) { 158 // Note: On the last iteration, frames.Next() returns false, with a valid 159 // frame, but we ignore this frame. The last frame is a runtime frame which 160 // adds noise, since it's only either runtime.main or runtime.goexit. 161 for frame, more := stack.Next(); more; frame, more = stack.Next() { 162 sf.FormatFrame(frame) 163 } 164 } 165 166 // FormatFrame formats the given frame. 167 func (sf *stackFormatter) FormatFrame(frame runtime.Frame) { 168 if sf.nonEmpty { 169 sf.b.AppendByte('\n') 170 } 171 sf.nonEmpty = true 172 sf.b.AppendString(frame.Function) 173 sf.b.AppendByte('\n') 174 sf.b.AppendByte('\t') 175 sf.b.AppendString(frame.File) 176 sf.b.AppendByte(':') 177 sf.b.AppendString(utils.IntToStr(frame.Line)) 178 }