Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(333)

Unified Diff: appengine/findit/crash/stacktrace.py

Issue 2562623004: Making CallStack immutable, so it can be hashable (Closed)
Patch Set: Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: appengine/findit/crash/stacktrace.py
diff --git a/appengine/findit/crash/stacktrace.py b/appengine/findit/crash/stacktrace.py
index 45dcc4241e69dc8aa9981d4fc3d7fa4fb9a39fac..043323e844653f8ce18c6920d2c70409a8e79146 100644
--- a/appengine/findit/crash/stacktrace.py
+++ b/appengine/findit/crash/stacktrace.py
@@ -3,6 +3,7 @@
# found in the LICENSE file.
from collections import namedtuple
+import copy
import logging
import re
@@ -22,6 +23,9 @@ CALLSTACK_FORMAT_TO_PATTERN = {
FRAME_INDEX_PATTERN = re.compile(r'\s*#(\d+)\s.*')
+_DEFAULT_FORMAT_TYPE = CallStackFormatType.DEFAULT
+_DEFAULT_LANGUAGE_TYPE = CallStackLanguageType.CPP
+
class StackFrame(namedtuple('StackFrame',
['index', 'dep_path', 'function', 'file_path', 'raw_file_path',
@@ -46,6 +50,7 @@ class StackFrame(namedtuple('StackFrame',
def __new__(cls, index, dep_path, function, file_path, raw_file_path,
crashed_line_numbers, repo_url=None):
+ assert index is not None, TypeError("The index must be an int")
Martin Barbella 2016/12/08 22:30:55 Nit: single quotes for consistency with other stri
wrengr 2016/12/08 23:42:37 Done.
return super(cls, StackFrame).__new__(cls,
index, dep_path, function, file_path, raw_file_path,
crashed_line_numbers, repo_url)
@@ -76,12 +81,97 @@ class StackFrame(namedtuple('StackFrame',
return self.ToString()
-class CallStack(list):
- """Represents a call stack within a stacktrace. A list of StackFrame objects.
+# TODO(wrengr): this should be a method on ``format_type``, or a class
+# method on ``StackFrame``.
Sharu Jiang 2016/12/08 22:23:44 I think in ``StackFrame`` and change the name to P
Martin Barbella 2016/12/08 22:30:55 Is there any reason not to just do this (my usual
stgao 2016/12/08 23:42:17 Let's make this a method into the class in this CL
wrengr 2016/12/08 23:42:37 Mainly just that I ran into issues getting it to w
+def ParseStackFrame(language_type, format_type, line, deps,
+ default_stack_frame_index=None):
+ """Parse line into a StackFrame instance, if possible.
+
+ Args:
+ language_type (CallStackLanguageType): the language the line is in.
+ format_type (CallStackFormatType): the format the line is in.
+ line (str): The line to be parsed.
+ deps (dict): Map dependency path to its corresponding Dependency.
+
+ Returns:
+ A ``StackFrame`` or ``None``.
+ """
+ # TODO(wrengr): how can we avoid duplicating this logic from ``CallStack``?
+ if format_type is None: # pragma: no cover
+ format_type = _DEFAULT_FORMAT_TYPE
+
+ if language_type is None:
+ language_type = _DEFAULT_LANGUAGE_TYPE
+
+ if format_type == CallStackFormatType.JAVA:
+ language_type = CallStackLanguageType.JAVA
+
+ line = line.strip()
+ line_pattern = CALLSTACK_FORMAT_TO_PATTERN[format_type]
+
+ if format_type == CallStackFormatType.JAVA:
+ match = line_pattern.match(line)
+ if not match:
+ return None
+
+ function = match.group(1)
+ raw_file_path = parse_util.GetFullPathForJavaFrame(function)
+ crashed_line_numbers = [int(match.group(3))]
+
+ elif format_type == CallStackFormatType.SYZYASAN:
+ match = line_pattern.match(line)
+ if not match:
+ return None
+
+ function = match.group(2).strip()
+ raw_file_path = match.group(5)
+ crashed_line_numbers = [int(match.group(6))]
+
+ else:
+ line_parts = line.split()
+ if not line_parts or not line_parts[0].startswith('#'):
+ return None
+
+ match = line_pattern.match(line_parts[-1])
+ if not match: # pragma: no cover
+ return None
+
+ function = ' '.join(line_parts[3:-1])
+
+ raw_file_path = match.group(1)
+ # Fracas java stack has default format type.
+ if language_type == CallStackLanguageType.JAVA:
+ raw_file_path = parse_util.GetFullPathForJavaFrame(function)
+
+ crashed_line_numbers = parse_util.GetCrashedLineRange(
+ match.group(2) + (match.group(3) if match.group(3) else ''))
+ # Normalize the file path so that it can be compared to repository path.
+ dep_path, file_path, repo_url = parse_util.GetDepPathAndNormalizedFilePath(
+ raw_file_path, deps, language_type == CallStackLanguageType.JAVA)
+
+ # If we have the common stack frame index pattern, then use it
+ # since it is more reliable.
+ index_match = FRAME_INDEX_PATTERN.match(line)
+ if index_match:
+ stack_frame_index = int(index_match.group(1))
+ else:
+ stack_frame_index = int(default_stack_frame_index or 0)
+
+ return StackFrame(stack_frame_index, dep_path, function, file_path,
+ raw_file_path, crashed_line_numbers, repo_url)
+
+
+# N.B., because ``list`` is mutable it isn't hashable, thus cannot be
+# used as a key in a dict. Because we want to usecallstacks as keys (for
+# memoization) we has-a tuple rather than is-a list.
Sharu Jiang 2016/12/08 22:23:44 I think it's not necessary to make every parameter
stgao 2016/12/08 23:42:17 With the current implementation of MemoizedFunctio
wrengr 2016/12/08 23:42:37 Even though the old code did do a lot of mutation,
Sharu Jiang 2016/12/09 00:13:58 We can do, that's one alternative, but this will n
Sharu Jiang 2016/12/09 00:13:58 For those frames we don't even look into, we shoul
Sharu Jiang 2016/12/09 00:29:20 *Only parse top_n frames after the signature frame
Sharu Jiang 2016/12/09 00:57:15 Follow up on "redesign the stacktrace to only pars
wrengr 2016/12/09 18:58:38 All tests pass with 100% coverage. The only modifi
wrengr 2016/12/09 18:58:38 The code using MemoizedFunction is: https://coder
+class CallStack(namedtuple('CallStack',
+ ['priority', 'frames', 'format_type', 'language_type'])):
+ """A stack (sequence of ``StackFrame`` objects) in a ``Stacktrace``.
Attributes:
priority (int): The smaller the number, the higher the priority beginning
with 0.
+ frames (tuple of StackFrame): the frames in order from bottom to top.
format_type (CallStackFormatType): Represents the type of line format
within a callstack. For example:
@@ -95,77 +185,83 @@ class CallStack(list):
'#0 0x32b5982 in get third_party/WebKit/Source/wtf/RefPtr.h:61:43'
language_type (CallStackLanguageType): Either CPP or JAVA language.
"""
- def __init__(self, priority, format_type=CallStackFormatType.DEFAULT,
- language_type=CallStackLanguageType.CPP,
- frame_list=None):
- super(CallStack, self).__init__(frame_list or [])
-
- self.priority = priority
- self.format_type = format_type
- self.language_type = (
- CallStackLanguageType.JAVA if format_type == CallStackFormatType.JAVA
- else language_type)
-
- def ParseLine(self, line, deps):
- """Parse line into StackFrame instance and append it if successfully
- parsed."""
- line = line.strip()
- line_pattern = CALLSTACK_FORMAT_TO_PATTERN[self.format_type]
-
- if self.format_type == CallStackFormatType.JAVA:
- match = line_pattern.match(line)
- if not match:
- return
-
- function = match.group(1)
- raw_file_path = parse_util.GetFullPathForJavaFrame(function)
- crashed_line_numbers = [int(match.group(3))]
-
- elif self.format_type == CallStackFormatType.SYZYASAN:
- match = line_pattern.match(line)
- if not match:
- return
-
- function = match.group(2).strip()
- raw_file_path = match.group(5)
- crashed_line_numbers = [int(match.group(6))]
-
- else:
- line_parts = line.split()
- if not line_parts or not line_parts[0].startswith('#'):
- return
-
- match = line_pattern.match(line_parts[-1])
- if not match:
- return
-
- function = ' '.join(line_parts[3:-1])
-
- raw_file_path = match.group(1)
- # Fracas java stack has default format type.
- if self.language_type == CallStackLanguageType.JAVA:
- raw_file_path = parse_util.GetFullPathForJavaFrame(function)
-
- crashed_line_numbers = parse_util.GetCrashedLineRange(
- match.group(2) + (match.group(3) if match.group(3) else ''))
- # Normalize the file path so that it can be compared to repository path.
- dep_path, file_path, repo_url = parse_util.GetDepPathAndNormalizedFilePath(
- raw_file_path, deps, self.language_type == CallStackLanguageType.JAVA)
-
- # If we have the common stack frame index pattern, then use it
- # since it is more reliable.
- index_match = FRAME_INDEX_PATTERN.match(line)
- if index_match:
- stack_frame_index = int(index_match.group(1))
- else:
- stack_frame_index = len(self)
-
- self.append(StackFrame(stack_frame_index, dep_path, function, file_path,
- raw_file_path, crashed_line_numbers, repo_url))
-
+ __slots__ = ()
+ def __new__(cls, priority, format_type=None, language_type=None,
+ frame_list=None):
+ """Construct a new ``CallStack``.
+
+ N.B., we use ``None`` as the default value of the optional arguments
+ so that if callers need to explicitly provide those arguments but
+ don't have an explicit value, they can pass ``None`` to get at the
+ default without needing to be kept in sync with this constructor. For
+ example, the ``ChromeCrashParser.Parse`` constructs a stack and they
+ need to keep track of all the arguments to be passed to this function.
+
+ Args:
+ priority (int): The priority of this stack in its ``Stacktrace``.
+ format_type (CallStackFormatType): Optional. The stack's format.
+ language_type (CallStackLanguageType): Optional. The stack's language.
+ frame_list (iterable of StackFrame): Optional. The frames in the stack.
+ """
+ if format_type is None:
+ format_type = _DEFAULT_FORMAT_TYPE
+
+ if language_type is None:
+ language_type = _DEFAULT_LANGUAGE_TYPE
+
+ if format_type == CallStackFormatType.JAVA: # pragma: no cover
Martin Barbella 2016/12/08 22:30:55 Not too important, but do we really need no cover
wrengr 2016/12/08 23:42:37 We don't *need* the pragma; rather, the old versio
+ language_type = CallStackLanguageType.JAVA
+
+ if frame_list is None:
+ frame_list = []
+
+ return super(cls, CallStack).__new__(cls,
+ priority, tuple(frame_list), format_type, language_type)
+
+ def __len__(self):
+ """Returns the number of frames in this stack."""
+ return len(self.frames)
+
+ # TODO: we do actually have unittests for this, but for some reason
Martin Barbella 2016/12/08 22:30:55 Strange. Is there a bug opened for this? might be
wrengr 2016/12/08 23:42:37 Done. crbug.com/672641
+ # coverage isn't seeing them.
+ def __bool__(self): # pragma: no cover
+ """Returns whether this stack is empty."""
+ return bool(self.frames)
+
+ def __iter__(self):
+ """Iterator over the frames in this stack."""
+ return iter(self.frames)
+
+ def SliceFrames(self, low_index, high_index):
+ """Returns a new ``CallStack`` keeping only the specified frames.
+
+ Args:
+ low_index (int or None): the lowest index to keep. If ``None``
+ then defaults to 0.
+ high_index (int or None): the index after the highest one to
+ keep. If ``None`` then defaults to one after the highest index.
+
+ Returns:
+ A new ``CallStack`` instance. If both arguments are ``None`` then
+ we return the original stack object, because they are equal and
+ due to immutability there's no reason to clone the instance.
+ """
+ if low_index is None and high_index is None:
+ return self
+
+ # TODO(wrengr): can we use ``_replace`` without running into TypeErrors?
Sharu Jiang 2016/12/08 22:23:44 what TypeErrors you encountered?
wrengr 2016/12/08 23:42:37 The TypeError complains about _make not being call
+ return CallStack(self.priority,
+ format_type=self.format_type,
+ language_type=self.language_type,
+ frame_list=self.frames[low_index:high_index])
+
+
+# N.B., because ``list`` is mutable it isn't hashable, thus cannot be
+# used as a key in a dict. Because we want to usecallstacks as keys (for
+# memoization) we has-a tuple rather than is-a list.
# TODO(http://crbug.com/644476): this class needs a better name.
-class Stacktrace(list):
+class Stacktrace(object):
"""A collection of callstacks which together provide a trace of what happened.
For instance, when doing memory debugging we will have callstacks for
@@ -174,8 +270,7 @@ class Stacktrace(list):
use-after-free crashes), etc. What callstacks are included in the
trace is unspecified, since this differs for different tools."""
def __init__(self, stack_list=None, signature=None):
- super(Stacktrace, self).__init__(stack_list or [])
-
+ self.stacks = stack_list or []
self._crash_stack = None
self._signature_parts = None
if signature:
@@ -185,12 +280,23 @@ class Stacktrace(list):
# usually the top 3 important stack frames separated by '\n'.
self._signature_parts = signature.split('\n')
+ def __getitem__(self, i): # pragma: no cover
+ return self.stacks[i]
+
+ def __len__(self):
+ return len(self.stacks)
+
+ def __bool__(self): # pragma: no cover
+ return bool(self.stacks)
+
+ def __iter__(self):
+ return iter(self.stacks)
@property
def crash_stack(self):
"""Get the callstack with the highest priority (i.e., whose priority
field is numerically the smallest) in the stacktrace."""
- if not self:
+ if not self.stacks:
logging.warning('Cannot get crash stack for empty stacktrace: %s', self)
return None
@@ -204,17 +310,17 @@ class Stacktrace(list):
return False, 0
# Set the crash stack using signature callstack.
- for callstack in self:
+ for callstack in self.stacks:
is_signature_callstack, index = _IsSignatureCallstack(callstack)
if is_signature_callstack:
# Filter all the stack frames before signature.
- callstack[:] = callstack[index:]
- self._crash_stack = callstack
+ self._crash_stack = callstack.SliceFrames(index, None)
break
# If there is no signature callstack, fall back to set crash stack using
# the first least priority callstack.
if self._crash_stack is None:
- self._crash_stack = sorted(self, key=lambda stack: stack.priority)[0]
+ self._crash_stack = sorted(self.stacks,
+ key=lambda stack: stack.priority)[0]
return self._crash_stack

Powered by Google App Engine
This is Rietveld 408576698