blob: a6ba59e2b5605461aff9ee8e5401167cc143d24e [file] [log] [blame]
# Copyright 2015 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Functions used to extract and analyze stacks. Faster than Python libs."""
# pylint: disable=g-bad-name
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import collections
import inspect
import linecache
import sys
import threading
# Names for indices into TF traceback tuples.
TB_FILENAME = 0
TB_LINENO = 1
TB_FUNCNAME = 2
TB_CODEDICT = 3 # Dictionary of Python interpreter state.
stacks = threading.local()
def _source_mappers():
if not hasattr(stacks, 'source_mapper'):
stacks.source_mapper = []
return stacks.source_mapper
def _source_filters():
if not hasattr(stacks, 'source_filter'):
stacks.source_filter = []
return stacks.source_filter
class StackTraceMapper(object):
"""Allows remapping traceback information to different source code."""
def __enter__(self):
_source_mappers().append(self)
return self
def __exit__(self, unused_type, unused_value, unused_traceback):
assert _source_mappers()[-1] is self, 'Concurrent access?'
_source_mappers().pop()
def map(self, filename, lineno, name):
raise NotImplementedError('subclasses need to override this')
class StackTraceFilter(object):
"""Allows filtering traceback information by removing superfluous frames."""
def __enter__(self):
_source_filters().append(self)
return self
def __exit__(self, unused_type, unused_value, unused_traceback):
assert _source_filters()[-1] is self, 'Concurrent access?'
_source_filters().pop()
def filter(self, filename, lineno, name):
raise NotImplementedError('subclasses need to override this')
class CurrentModuleFilter(StackTraceFilter):
"""Filters stack frames from the module where this is used (best effort)."""
def __init__(self):
filter_filename = None
outer_f = None
f = inspect.currentframe()
try:
if f is not None:
# The current frame is __init__. The first outer frame should be the
# caller.
outer_f = f.f_back
if outer_f is not None:
filter_filename = inspect.getsourcefile(outer_f)
self._filename = filter_filename
finally:
# Avoid reference cycles, see:
# https://docs.python.org/3.7/library/inspect.html#the-interpreter-stack
del f
del outer_f
def should_remove(self, filename, lineno, name):
del lineno, name
return filename == self._filename
def extract_stack(limit=None):
"""A lightweight, extensible re-implementation of traceback.extract_stack.
NOTE(mrry): traceback.extract_stack eagerly retrieves the line of code for
each stack frame using linecache, which results in an abundance of stat()
calls. This implementation does not retrieve the code, and any consumer
should apply _convert_stack to the result to obtain a traceback that can
be formatted etc. using traceback methods.
Args:
limit: A limit on the number of frames to return.
Returns:
A list of 5-tuples
(filename, lineno, name, frame_globals, func_start_lineno)
corresponding to the call stack of the current thread. The returned tuples
have the innermost stack frame at the end, unlike the Python inspect
module's stack() function.
"""
try:
raise ZeroDivisionError
except ZeroDivisionError:
f = sys.exc_info()[2].tb_frame.f_back
ret = []
length = 0
while f is not None and (limit is None or length < limit):
lineno = f.f_lineno
co = f.f_code
filename = co.co_filename
name = co.co_name
frame_globals = f.f_globals
func_start_lineno = co.co_firstlineno
for mapper in _source_mappers():
# TODO(mdan): Show some indication that the frame was translated.
filename, lineno, name = mapper.map(filename, lineno, name)
keep = True
if ret: # Never filter the innermost frame.
keep = not any(
f.should_remove(filename, lineno, name) for f in _source_filters())
if keep:
ret.append((filename, lineno, name, frame_globals, func_start_lineno))
length += 1
f = f.f_back
# TODO(mdan): Also add a truncation mechanism.
ret.reverse()
return ret
FileAndLine = collections.namedtuple('FileAndLine', ['file', 'line'])
def extract_stack_file_and_line(max_length=1000):
"""A version of extract_stack that only returns filenames and line numbers.
Callers often only require filenames and line numbers, and do not need the
additional information gathered by extract_stack, as they never call
convert_stack.
As a further optimisation, we allow users to specify a limit on the number of
frames examined.
Args:
max_length: The maximum length of stack to extract.
Returns:
A list of FileAndLine objects corresponding to the call stack of the current
thread.
"""
try:
raise ZeroDivisionError
except ZeroDivisionError:
frame = sys.exc_info()[2].tb_frame.f_back
ret = []
length = 0
while frame is not None and length < max_length:
ret.append(FileAndLine(frame.f_code.co_filename, frame.f_lineno))
length += 1
frame = frame.f_back
ret.reverse()
return ret
def convert_stack(stack, include_func_start_lineno=False):
"""Converts a stack extracted using extract_stack() to a traceback stack.
Args:
stack: A list of n 5-tuples,
(filename, lineno, name, frame_globals, func_start_lineno).
include_func_start_lineno: True if function start line number should be
included as the 5th entry in return tuples.
Returns:
A tuple of n 4-tuples or 5-tuples
(filename, lineno, name, code, [optional: func_start_lineno]), where the
code tuple element is calculated from the corresponding elements of the
input tuple.
"""
def _tuple_generator(): # pylint: disable=missing-docstring
for (filename, lineno, name, frame_globals, func_start_lineno) in stack:
linecache.checkcache(filename)
line = linecache.getline(filename, lineno, frame_globals)
if line:
line = line.strip()
else:
line = None
if include_func_start_lineno:
yield (filename, lineno, name, line, func_start_lineno)
else:
yield (filename, lineno, name, line)
return tuple(_tuple_generator())