-
Notifications
You must be signed in to change notification settings - Fork 22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for parsing Wasm stack frames of Chrome (V8), Firefox, Safari #159
Changes from 9 commits
20761c4
546e3cb
fca651a
88f13f1
bfaa7f9
9afe77d
c2eda03
6191897
e3feec4
a993d21
9e732a2
fb627fb
4316832
2dc3d3f
357e011
817b966
bebf2bd
3115aad
102c025
95abc38
33f6e8e
fc3a86d
2f885b2
360f906
db25e33
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,12 +17,38 @@ final _vmFrame = RegExp(r'^#\d+\s+(\S.*) \((.+?)((?::\d+){0,2})\)$'); | |
// at VW.call$0 (eval as fn | ||
// (https://example.com/stuff.dart.js:560:28), efn:3:28) | ||
// at https://example.com/stuff.dart.js:560:28 | ||
final _v8Frame = | ||
final _v8JsFrame = | ||
RegExp(r'^\s*at (?:(\S.*?)(?: \[as [^\]]+\])? \((.*)\)|(.*))$'); | ||
|
||
// https://example.com/stuff.dart.js:560:28 | ||
// https://example.com/stuff.dart.js:560 | ||
final _v8UrlLocation = RegExp(r'^(.*?):(\d+)(?::(\d+))?$|native$'); | ||
// | ||
// Group 1: URI, required | ||
// Group 2: line number, required | ||
// Group 3: column number, optional | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (If you are changing things anyway, consider using named capture groups here too, for consistency. Or not, can be done at any later time too.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's do unrelated changes separately. It helps with debugging, bisecting and reverting. |
||
final _v8JsUrlLocation = RegExp(r'^(.*?):(\d+)(?::(\d+))?$|native$'); | ||
|
||
// With names: | ||
// | ||
// at Error.f (wasm://wasm/0006d966:wasm-function[119]:0xbb13) | ||
// at g (wasm://wasm/0006d966:wasm-function[796]:0x143b4) | ||
// | ||
// Without names: | ||
// | ||
// at wasm://wasm/0005168a:wasm-function[119]:0xbb13 | ||
// at wasm://wasm/0005168a:wasm-function[796]:0x143b4 | ||
// | ||
// Group 1: Function name, optional | ||
// | ||
// When group 1 is available: | ||
osa1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// Group 2: URI | ||
// Group 3: Function index | ||
// | ||
// Otherwise: | ||
// Group 4: URI | ||
// Group 5: function index | ||
final _v8WasmFrame = RegExp(r'^\s*at(?: (\S+))? ' | ||
r'(?:\((wasm:\S+\[(\d+)\]\S+)\)|(wasm:\S+\[(\d+)\]\S+))$'); | ||
|
||
// eval as function (https://example.com/stuff.dart.js:560:28), efn:3:28 | ||
// eval as function (https://example.com/stuff.dart.js:560:28) | ||
|
@@ -41,7 +67,7 @@ final _firefoxEvalLocation = | |
// .VW.call$0/name<@https://example.com/stuff.dart.js:560 | ||
// .VW.call$0@https://example.com/stuff.dart.js:560:36 | ||
// https://example.com/stuff.dart.js:560 | ||
final _firefoxSafariFrame = RegExp(r'^' | ||
final _firefoxSafariJSFrame = RegExp(r'^' | ||
r'(?:' // Member description. Not present in some Safari frames. | ||
r'([^@(/]*)' // The actual name of the member. | ||
r'(?:\(.*\))?' // Arguments to the member, sometimes captured by Firefox. | ||
|
@@ -56,6 +82,38 @@ final _firefoxSafariFrame = RegExp(r'^' | |
// empty in Safari if it's unknown. | ||
r'$'); | ||
|
||
// With names: | ||
// | ||
// g@http://localhost:8080/test.wasm:wasm-function[796]:0x143b4 | ||
// f@http://localhost:8080/test.wasm:wasm-function[795]:0x143a8 | ||
// main@http://localhost:8080/test.wasm:wasm-function[792]:0x14390 | ||
// | ||
// Without names: | ||
// | ||
// @http://localhost:8080/test.wasm:wasm-function[796]:0x143b4 | ||
// @http://localhost:8080/test.wasm:wasm-function[795]:0x143a8 | ||
// @http://localhost:8080/test.wasm:wasm-function[792]:0x14390 | ||
// | ||
// Group 1: Function name, may be empty. | ||
// Group 2: URI after the '@'. | ||
// Group 3: Function index. | ||
final _firefoxWasmFrame = RegExp(r'^(.*)@(.*\[(\d+)\]:.*)$'); | ||
|
||
// With names: | ||
// | ||
// <?>.wasm-function[g]@[wasm code] | ||
osa1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// <?>.wasm-function[f]@[wasm code] | ||
// <?>.wasm-function[main]@[wasm code] | ||
// | ||
// Without names: | ||
// | ||
// <?>.wasm-function[796]@[wasm code] | ||
// <?>.wasm-function[795]@[wasm code] | ||
// <?>.wasm-function[792]@[wasm code] | ||
// | ||
// Group 1: Function name or index. | ||
final _safariWasmFrame = RegExp(r'^.*\[(.*)\]@\[wasm code\]$'); | ||
|
||
// foo/bar.dart 10:11 Foo._bar | ||
// foo/bar.dart 10:11 (anonymous function).dart.fn | ||
// https://dart.dev/foo/bar.dart Foo._bar | ||
|
@@ -163,48 +221,59 @@ class Frame { | |
|
||
/// Parses a string representation of a Chrome/V8 stack frame. | ||
factory Frame.parseV8(String frame) => _catchFormatException(frame, () { | ||
var match = _v8Frame.firstMatch(frame); | ||
if (match == null) return UnparsedFrame(frame); | ||
// Try to match a Wasm frame first: the Wasm frame regex won't match a | ||
// JS frame but the JS frame regex may match a Wasm frame. | ||
var match = _v8WasmFrame.firstMatch(frame); | ||
if (match != null) { | ||
final member = match[1]; | ||
final uri = _uriOrPathToUri(member != null ? match[2]! : match[4]!); | ||
return Frame(uri, null, null, member ?? match[5]!); | ||
} | ||
|
||
// v8 location strings can be arbitrarily-nested, since it adds a layer | ||
// of nesting for each eval performed on that line. | ||
Frame parseLocation(String location, String member) { | ||
var evalMatch = _v8EvalLocation.firstMatch(location); | ||
while (evalMatch != null) { | ||
location = evalMatch[1]!; | ||
evalMatch = _v8EvalLocation.firstMatch(location); | ||
match = _v8JsFrame.firstMatch(frame); | ||
if (match != null) { | ||
// v8 location strings can be arbitrarily-nested, since it adds a | ||
// layer of nesting for each eval performed on that line. | ||
Frame parseJsLocation(String location, String member) { | ||
var evalMatch = _v8EvalLocation.firstMatch(location); | ||
while (evalMatch != null) { | ||
location = evalMatch[1]!; | ||
evalMatch = _v8EvalLocation.firstMatch(location); | ||
} | ||
|
||
if (location == 'native') { | ||
return Frame(Uri.parse('native'), null, null, member); | ||
} | ||
|
||
var urlMatch = _v8JsUrlLocation.firstMatch(location); | ||
if (urlMatch == null) return UnparsedFrame(frame); | ||
|
||
final uri = _uriOrPathToUri(urlMatch[1]!); | ||
final line = int.parse(urlMatch[2]!); | ||
final columnMatch = urlMatch[3]; | ||
final column = columnMatch != null ? int.parse(columnMatch) : null; | ||
return Frame(uri, line, column, member); | ||
} | ||
|
||
if (location == 'native') { | ||
return Frame(Uri.parse('native'), null, null, member); | ||
// V8 stack frames can be in two forms. | ||
if (match[2] != null) { | ||
// The first form looks like " at FUNCTION (LOCATION)". V8 proper | ||
// lists anonymous functions within eval as "<anonymous>", while | ||
// IE10 lists them as "Anonymous function". | ||
return parseJsLocation( | ||
match[2]!, | ||
match[1]! | ||
.replaceAll('<anonymous>', '<fn>') | ||
.replaceAll('Anonymous function', '<fn>') | ||
.replaceAll('(anonymous function)', '<fn>')); | ||
} else { | ||
// The second form looks like " at LOCATION", and is used for | ||
// anonymous functions. | ||
return parseJsLocation(match[3]!, '<fn>'); | ||
} | ||
|
||
var urlMatch = _v8UrlLocation.firstMatch(location); | ||
if (urlMatch == null) return UnparsedFrame(frame); | ||
|
||
final uri = _uriOrPathToUri(urlMatch[1]!); | ||
final line = int.parse(urlMatch[2]!); | ||
final columnMatch = urlMatch[3]; | ||
final column = columnMatch != null ? int.parse(columnMatch) : null; | ||
return Frame(uri, line, column, member); | ||
} | ||
|
||
// V8 stack frames can be in two forms. | ||
if (match[2] != null) { | ||
// The first form looks like " at FUNCTION (LOCATION)". V8 proper | ||
// lists anonymous functions within eval as "<anonymous>", while IE10 | ||
// lists them as "Anonymous function". | ||
return parseLocation( | ||
match[2]!, | ||
match[1]! | ||
.replaceAll('<anonymous>', '<fn>') | ||
.replaceAll('Anonymous function', '<fn>') | ||
.replaceAll('(anonymous function)', '<fn>')); | ||
} else { | ||
// The second form looks like " at LOCATION", and is used for | ||
// anonymous functions. | ||
return parseLocation(match[3]!, '<fn>'); | ||
} | ||
return UnparsedFrame(frame); | ||
}); | ||
|
||
/// Parses a string representation of a JavaScriptCore stack trace. | ||
|
@@ -234,35 +303,52 @@ class Frame { | |
return Frame(uri, line, null, member); | ||
}); | ||
|
||
/// Parses a string representation of a Firefox stack frame. | ||
/// Parses a string representation of a Firefox or Safari stack frame. | ||
factory Frame.parseFirefox(String frame) => _catchFormatException(frame, () { | ||
var match = _firefoxSafariFrame.firstMatch(frame); | ||
if (match == null) return UnparsedFrame(frame); | ||
var match = _firefoxSafariJSFrame.firstMatch(frame); | ||
if (match != null) { | ||
if (match[3]!.contains(' line ')) { | ||
return Frame._parseFirefoxEval(frame); | ||
} | ||
|
||
if (match[3]!.contains(' line ')) { | ||
return Frame._parseFirefoxEval(frame); | ||
} | ||
// Normally this is a URI, but in a jsshell trace it can be a path. | ||
var uri = _uriOrPathToUri(match[3]!); | ||
|
||
// Normally this is a URI, but in a jsshell trace it can be a path. | ||
var uri = _uriOrPathToUri(match[3]!); | ||
var member = match[1]; | ||
if (member != null) { | ||
member += | ||
List.filled('/'.allMatches(match[2]!).length, '.<fn>').join(); | ||
if (member == '') member = '<fn>'; | ||
|
||
var member = match[1]; | ||
if (member != null) { | ||
member += | ||
List.filled('/'.allMatches(match[2]!).length, '.<fn>').join(); | ||
if (member == '') member = '<fn>'; | ||
// Some Firefox members have initial dots. We remove them for | ||
// consistency with other platforms. | ||
member = member.replaceFirst(_initialDot, ''); | ||
} else { | ||
member = '<fn>'; | ||
} | ||
|
||
// Some Firefox members have initial dots. We remove them for | ||
// consistency with other platforms. | ||
member = member.replaceFirst(_initialDot, ''); | ||
} else { | ||
member = '<fn>'; | ||
var line = match[4] == '' ? null : int.parse(match[4]!); | ||
var column = | ||
match[5] == null || match[5] == '' ? null : int.parse(match[5]!); | ||
return Frame(uri, line, column, member); | ||
} | ||
|
||
var line = match[4] == '' ? null : int.parse(match[4]!); | ||
var column = | ||
match[5] == null || match[5] == '' ? null : int.parse(match[5]!); | ||
return Frame(uri, line, column, member); | ||
match = _firefoxWasmFrame.firstMatch(frame); | ||
if (match != null) { | ||
final member = match[1]!; | ||
final uri = _uriOrPathToUri(match[2]!); | ||
final functionIndex = match[3]!; | ||
return Frame( | ||
uri, null, null, member.isNotEmpty ? member : functionIndex); | ||
} | ||
|
||
match = _safariWasmFrame.firstMatch(frame); | ||
if (match != null) { | ||
final member = match[1]!; | ||
return Frame(Uri(path: 'wasm code'), null, null, member); | ||
} | ||
|
||
return UnparsedFrame(frame); | ||
}); | ||
|
||
/// Parses a string representation of a Safari 6.0 stack frame. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(While here, it may be worth saying that these regexps are applied to individual lines. That's why the
^
doesn't requiremultiLine: true
and why it's not a problem that[^\]+
can match newlines. Helps the reader set expectations.)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's clear from the use sites a few lines below that these parse a single "frame", as passed to factories like
Frame.parseVM(String frame)
.However the exact format of those "frame"s is not described by this library even though the
Frame
type and its factories are public.It would be good to describe the format for the users who may want to parse a single frame.
Let's do it separately though.