Skip to content

Commit c395ba4

Browse files
Merge pull request gopherjs#1446 from Workiva/optimizeCallFrames1
[go1.21] Optimizing call frames
2 parents 378e333 + 8947c3b commit c395ba4

3 files changed

Lines changed: 357 additions & 106 deletions

File tree

compiler/natives/src/runtime/runtime.go

Lines changed: 37 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -130,33 +130,25 @@ var (
130130
// PC=0 as "no caller available" (see e.g. log/slog.Record.PC), and packages
131131
// using PCs, initialize a uintptr to zero and expect runtime.CallersFrames
132132
// and runtime.FuncForPC to treat it as unknown.
133-
knownPositions = map[string]uintptr{}
133+
knownPositions = map[basicFrame]uintptr{}
134134
positionCounters = []*Func{nil}
135135
)
136136

137-
func registerPosition(funcName string, file string, line int, col int) uintptr {
138-
key := file + ":" + itoa(line) + ":" + itoa(col)
139-
if pc, found := knownPositions[key]; found {
137+
func registerPosition(frame basicFrame) uintptr {
138+
if pc, found := knownPositions[frame]; found {
140139
return pc
141140
}
142141
f := &Func{
143-
name: funcName,
144-
file: file,
145-
line: line,
142+
name: frame.FuncName,
143+
file: frame.File,
144+
line: frame.Line,
146145
}
147146
pc := uintptr(len(positionCounters))
148147
positionCounters = append(positionCounters, f)
149-
knownPositions[key] = pc
148+
knownPositions[frame] = pc
150149
return pc
151150
}
152151

153-
// itoa converts an integer to a string.
154-
//
155-
// Can't use strconv.Itoa() in the `runtime` package due to a cyclic dependency.
156-
func itoa(i int) string {
157-
return js.Global.Get("String").New(i).String()
158-
}
159-
160152
// basicFrame contains stack trace information extracted from JS stack trace.
161153
type basicFrame struct {
162154
FuncName string
@@ -166,9 +158,13 @@ type basicFrame struct {
166158
}
167159

168160
func callstack(skip, limit int) []basicFrame {
169-
skip = skip + 1 /*skip error message*/ + 1 /*skip callstack's own frame*/
170-
lines := js.Global.Get("Error").New().Get("stack").Call("split", "\n").Call("slice", skip, skip+limit)
171-
return parseCallstack(lines)
161+
if limit <= 0 {
162+
return []basicFrame{}
163+
}
164+
165+
skip = skip + 2 // skip callstack's own frame and $callstack's frame
166+
lines := js.Global.Call("$callstack", skip, limit)
167+
return parseCallstack(lines, limit)
172168
}
173169

174170
var (
@@ -187,80 +183,33 @@ var (
187183
}
188184
)
189185

190-
func parseCallstack(lines *js.Object) []basicFrame {
186+
func parseCallstack(lines *js.Object, limit int) []basicFrame {
187+
// Parse all the frames skipping frames as needed.
191188
frames := []basicFrame{}
192189
l := lines.Length()
193190
for i := 0; i < l; i++ {
194-
frame := ParseCallFrame(lines.Index(i))
195-
if hiddenFrames[frame.FuncName] {
191+
frame := js.Global.Call("$parseCallFrame", lines.Index(i))
192+
funcName := frame.Index(0).String()
193+
if hiddenFrames[funcName] {
196194
continue
197195
}
198-
if alias, ok := knownFrames[frame.FuncName]; ok {
199-
frame.FuncName = alias
196+
if alias, ok := knownFrames[funcName]; ok {
197+
funcName = alias
200198
}
201-
frames = append(frames, frame)
202-
if frame.FuncName == "runtime.goexit" {
199+
frames = append(frames, basicFrame{
200+
FuncName: funcName,
201+
File: frame.Index(1).String(),
202+
Line: frame.Index(2).Int(),
203+
Col: frame.Index(3).Int(),
204+
})
205+
if funcName == "runtime.goexit" {
203206
break // We've reached the bottom of the goroutine stack.
204207
}
205-
}
206-
return frames
207-
}
208-
209-
// ParseCallFrame is exported for the sake of testing. See this discussion for context https://github.com/gopherjs/gopherjs/pull/1097/files/561e6381406f04ccb8e04ef4effedc5c7887b70f#r776063799
210-
//
211-
// TLDR; never use this function!
212-
func ParseCallFrame(info *js.Object) basicFrame {
213-
// FireFox
214-
if info.Call("indexOf", "@").Int() >= 0 {
215-
split := js.Global.Get("RegExp").New("[@:]")
216-
parts := info.Call("split", split)
217-
return basicFrame{
218-
File: parts.Call("slice", 1, parts.Length()-2).Call("join", ":").String(),
219-
Line: parts.Index(parts.Length() - 2).Int(),
220-
Col: parts.Index(parts.Length() - 1).Int(),
221-
FuncName: parts.Index(0).String(),
222-
}
223-
}
224-
225-
// Chrome / Node.js
226-
openIdx := info.Call("lastIndexOf", "(").Int()
227-
if openIdx == -1 {
228-
parts := info.Call("split", ":")
229-
230-
return basicFrame{
231-
File: parts.Call("slice", 0, parts.Length()-2).Call("join", ":").
232-
Call("replace", js.Global.Get("RegExp").New(`^\s*at `), "").String(),
233-
Line: parts.Index(parts.Length() - 2).Int(),
234-
Col: parts.Index(parts.Length() - 1).Int(),
235-
FuncName: "<none>",
208+
if len(frames) >= limit {
209+
break // We've reached the requested limit.
236210
}
237211
}
238-
239-
var file, funcName string
240-
var line, col int
241-
242-
pos := info.Call("substring", openIdx+1, info.Call("indexOf", ")").Int())
243-
parts := pos.Call("split", ":")
244-
245-
if pos.String() == "<anonymous>" {
246-
file = "<anonymous>"
247-
} else {
248-
file = parts.Call("slice", 0, parts.Length()-2).Call("join", ":").String()
249-
line = parts.Index(parts.Length() - 2).Int()
250-
col = parts.Index(parts.Length() - 1).Int()
251-
}
252-
fn := info.Call("substring", info.Call("indexOf", "at ").Int()+3, info.Call("indexOf", " (").Int())
253-
if idx := fn.Call("indexOf", "[as ").Int(); idx > 0 {
254-
fn = fn.Call("substring", idx+4, fn.Call("indexOf", "]"))
255-
}
256-
funcName = fn.String()
257-
258-
return basicFrame{
259-
File: file,
260-
Line: line,
261-
Col: col,
262-
FuncName: funcName,
263-
}
212+
return frames
264213
}
265214

266215
func Caller(skip int) (pc uintptr, file string, line int, ok bool) {
@@ -269,8 +218,9 @@ func Caller(skip int) (pc uintptr, file string, line int, ok bool) {
269218
if len(frames) != 1 {
270219
return 0, "", 0, false
271220
}
272-
pc = registerPosition(frames[0].FuncName, frames[0].File, frames[0].Line, frames[0].Col)
273-
return pc, frames[0].File, frames[0].Line, true
221+
frame := frames[0]
222+
pc = registerPosition(frame)
223+
return pc, frame.File, frame.Line, true
274224
}
275225

276226
// Callers fills the slice pc with the return program counters of function
@@ -293,7 +243,7 @@ func Caller(skip int) (pc uintptr, file string, line int, ok bool) {
293243
func Callers(skip int, pc []uintptr) int {
294244
frames := callstack(skip, len(pc))
295245
for i, frame := range frames {
296-
pc[i] = registerPosition(frame.FuncName, frame.File, frame.Line, frame.Col)
246+
pc[i] = registerPosition(frame)
297247
}
298248
return len(frames)
299249
}
@@ -308,7 +258,7 @@ func Callers(skip int, pc []uintptr) int {
308258
// For those, FuncForPC returns nil and we emit a Frame with the original PC
309259
// and empty symbol fields, like Go will.
310260
// - GopherJS's internal frames such as $callDeferred and $goroutine were
311-
// already filtered (or aliased) by parseCallstack at capture time, so anything
261+
// already filtered (or aliased) by callstack at capture time, so anything
312262
// reaching CallersFrames is either a real Go frame or a PC we can't resolve.
313263
func CallersFrames(callers []uintptr) *Frames {
314264
result := Frames{}
@@ -472,7 +422,7 @@ func FuncForPC(pc uintptr) *Func {
472422
// created without a real caller (record.go's PC field)
473423
// - test/fixedbugs/issue29735.go deliberately walks past the
474424
// end of a function looking for the next. This will cause it to
475-
// not secceed at what it is trying to do but will allow the test to pass.
425+
// not succeed at what it is trying to do but will allow the test to pass.
476426
// - test/fixedbugs/issue58300.go and test/fixedbugs/issue58300b.go give
477427
// FuncForPC a function pointer from `reflect.ValueOf(fn).Pointer()`,
478428
// which is not produced by Caller/Callers and so isn't in our table.

compiler/prelude/prelude.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,93 @@ if (($global.process !== undefined) && $global.require) {
8989
}
9090
var $println = console.log
9191

92+
// $callstack captures a stack trace and returns a list of lines, typically
93+
// there will be "limit" numbers of lines returned.
94+
// skip=0 means "the direct caller of $callstack"; limit caps frames captured.
95+
var $callstack = (skip, limit) => {
96+
const oldLimit = Error.stackTraceLimit;
97+
var stack;
98+
try {
99+
Error.stackTraceLimit = skip+limit
100+
stack = new Error().stack
101+
} finally {
102+
Error.stackTraceLimit = oldLimit;
103+
}
104+
105+
// If `new Error().stack` doesn't exist like on older IE versions
106+
// or something went wrong getting the stack, just return empty.
107+
if (!stack) return [];
108+
// Drop any tailing "\n" (and any trailing space before first "at " if there is one).
109+
stack = stack.trim();
110+
// V8 prepends an "Error" or "Error: msg" header line that isn't a frame.
111+
// Firefox and Safari do not. Detect by checking for "@" or starts with "at ".
112+
// This check could still be wrong if the Error message itself has an "@" in it,
113+
// (e.g. `new Error("a@b")`), however since we are calling `new Error().stack`
114+
// it should be fine.
115+
const firstNl = stack.indexOf("\n");
116+
const firstLine = firstNl >= 0 ? stack.substring(0, firstNl) : stack;
117+
if (!firstLine.includes("@") && !firstLine.startsWith("at ")) {
118+
skip++; // skip "Error" header line.
119+
}
120+
121+
// Remove the skipped amount of the stack and the error header if there was one.
122+
return stack.split("\n").slice(skip);
123+
}
124+
125+
// $parseCallFrame parses a call frame string and
126+
// returns the tuple [funcName, file, line, col].
127+
var $parseCallFrame = (frame) => {
128+
// The first time this is called, compile the regexp,
129+
// then reuse it for all following calls.
130+
const posRe = /^(.+?)(?::(\d+)(?::(\d+))?)?$/;
131+
const parsePos = (fnName, framePos) => {
132+
const m = posRe.exec(framePos);
133+
if (m) {
134+
const file = m[1] || "";
135+
const line = m[2] || 0; // Will be read with a js.Object:Int
136+
const col = m[3] || 0; // Will be read with a js.Object:Int
137+
return [fnName, file, line, col];
138+
}
139+
return [fnName, "", 0, 0];
140+
}
141+
$parseCallFrame = (frame) => {
142+
// FireFox
143+
const atIdx = frame.indexOf("@")
144+
if (atIdx >= 0) {
145+
const fnName = frame.substring(0, atIdx) || "<none>";
146+
return parsePos(fnName, frame.substring(atIdx + 1));
147+
}
148+
149+
// Chrome / Node.js
150+
const atLeadIdx = frame.indexOf("at ");
151+
if (atLeadIdx >= 0) frame = frame.substring(atLeadIdx + 3);
152+
const openIdx = frame.lastIndexOf("(");
153+
if (openIdx === -1) {
154+
// No-parens form: "at file:line:col"
155+
return parsePos("<none>", frame);
156+
}
157+
158+
// With-parens form: "at func (file:line:col)"
159+
var fnName = frame.substring(0, frame.indexOf("(")).trim();
160+
const asIdx = fnName.indexOf("[as ");
161+
if (asIdx > 0) {
162+
var closeIdx = fnName.indexOf("]");
163+
if (closeIdx === -1) closeIdx = fnName.length;
164+
fnName = fnName.substring(asIdx+4, closeIdx).trim();
165+
}
166+
167+
var closeIdx = frame.indexOf(")", openIdx);
168+
if (closeIdx === -1) closeIdx = frame.length;
169+
170+
var pos = frame.substring(openIdx + 1, closeIdx);
171+
if (pos === "<anonymous>") {
172+
return [fnName, "<anonymous>", 0, 0]
173+
}
174+
return parsePos(fnName, pos);
175+
};
176+
return $parseCallFrame(frame);
177+
};
178+
92179
var $callForAllPackages = (methodName) => {
93180
var names = $keys($packages);
94181
for (var i = 0; i < names.length; i++) {

0 commit comments

Comments
 (0)