Skip to content

Commit c5a9ccd

Browse files
committed
Proof of concept: Add timestamps to tracebacks.
This came up at work as a suggestion to make debugging what happened in big async servers with lots of exception groups and exceptions easier. Timestamps when emitting exception groups containing tracebacks often with their own nested causes would allow some semblance of order to be understood. This is a demo. If we want such a feature, we should settle on semantics in a Discuss thread and write it up as a PEP. This should be simpler than exception notes (PEP-678) was. One thought was just to store the timestamp as a note; but that'd involve string and list creation on every exception. Performance testing needs to be done. This is the kind of thing that is visually distracting, so not all applications want to _see_ the timestamps. A knob to turn that on for those who do seems more useful rather than making that the default. But the performance impact of merely collecting the timestamps is worth knowing.
1 parent 7d27561 commit c5a9ccd

3 files changed

Lines changed: 28 additions & 12 deletions

File tree

Include/cpython/pyerrors.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
# error "this header file must not be included directly"
33
#endif
44

5+
#include "cpython/pytime.h" /* for PyTime_t */
6+
57
/* Error objects */
68

79
/* PyException_HEAD defines the initial segment of every exception class. */
810
#define PyException_HEAD PyObject_HEAD PyObject *dict;\
9-
PyObject *args; PyObject *notes; PyObject *traceback;\
11+
PyObject *args; PyObject *notes;\
12+
PyTime_t timestamp_ns; PyObject *traceback;\
1013
PyObject *context; PyObject *cause;\
1114
char suppress_context;
1215

Lib/traceback.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -179,19 +179,21 @@ def format_exception_only(exc, /, value=_sentinel, *, show_group=False, **kwargs
179179

180180
# -- not official API but folk probably use these two functions.
181181

182-
def _format_final_exc_line(etype, value, *, insert_final_newline=True, colorize=False):
182+
def _format_final_exc_line(etype, value, *, insert_final_newline=True, colorize=False, timestamp=0):
183183
valuestr = _safe_string(value, 'exception')
184184
end_char = "\n" if insert_final_newline else ""
185+
ts = f" <@t={timestamp:.6f}>" if timestamp else ""
185186
if colorize:
187+
timestamp = f"{ANSIColors.GREY}{ts}{ANSIColors.RESET}" if timestamp else ""
186188
if value is None or not valuestr:
187-
line = f"{ANSIColors.BOLD_MAGENTA}{etype}{ANSIColors.RESET}{end_char}"
189+
line = f"{ANSIColors.BOLD_MAGENTA}{etype}{ANSIColors.RESET}{ts}{end_char}"
188190
else:
189-
line = f"{ANSIColors.BOLD_MAGENTA}{etype}{ANSIColors.RESET}: {ANSIColors.MAGENTA}{valuestr}{ANSIColors.RESET}{end_char}"
191+
line = f"{ANSIColors.BOLD_MAGENTA}{etype}{ANSIColors.RESET}: {ANSIColors.MAGENTA}{valuestr}{ANSIColors.RESET}{ts}{end_char}"
190192
else:
191193
if value is None or not valuestr:
192-
line = f"{etype}{end_char}"
194+
line = f"{etype}{ts}{end_char}"
193195
else:
194-
line = f"{etype}: {valuestr}{end_char}"
196+
line = f"{etype}: {valuestr}{ts}{end_char}"
195197
return line
196198

197199

@@ -1004,6 +1006,8 @@ class TracebackException:
10041006
10051007
- :attr:`__cause__` A TracebackException of the original *__cause__*.
10061008
- :attr:`__context__` A TracebackException of the original *__context__*.
1009+
- :attr:`__notes__` A reference to the original *__notes__* list.
1010+
- :attr:`timestamp` When the original exception was created (seconds).
10071011
- :attr:`exceptions` For exception groups - a list of TracebackException
10081012
instances for the nested *exceptions*. ``None`` for other exceptions.
10091013
- :attr:`__suppress_context__` The *__suppress_context__* value from the
@@ -1057,6 +1061,8 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
10571061
self.__notes__ = [
10581062
f'Ignored error getting __notes__: {_safe_string(e, '__notes__', repr)}']
10591063

1064+
self.timestamp = exc_value.__timestamp_ns__ / 1_000_000_000
1065+
10601066
self._is_syntax_error = False
10611067
self._have_exc_type = exc_type is not None
10621068
if exc_type is not None:
@@ -1228,22 +1234,22 @@ def format_exception_only(self, *, show_group=False, _depth=0, **kwargs):
12281234

12291235
indent = 3 * _depth * ' '
12301236
if not self._have_exc_type:
1231-
yield indent + _format_final_exc_line(None, self._str, colorize=colorize)
1237+
yield indent + _format_final_exc_line(None, self._str, colorize=colorize, timestamp=self.timestamp)
12321238
return
12331239

12341240
stype = self.exc_type_str
12351241
if not self._is_syntax_error:
12361242
if _depth > 0:
12371243
# Nested exceptions needs correct handling of multiline messages.
12381244
formatted = _format_final_exc_line(
1239-
stype, self._str, insert_final_newline=False, colorize=colorize
1245+
stype, self._str, insert_final_newline=False, colorize=colorize, timestamp=self.timestamp
12401246
).split('\n')
12411247
yield from [
12421248
indent + l + '\n'
12431249
for l in formatted
12441250
]
12451251
else:
1246-
yield _format_final_exc_line(stype, self._str, colorize=colorize)
1252+
yield _format_final_exc_line(stype, self._str, colorize=colorize, timestamp=self.timestamp)
12471253
else:
12481254
yield from [indent + l for l in self._format_syntax_error(stype, colorize=colorize)]
12491255

Objects/exceptions.c

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ BaseException_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
5959
/* the dict is created on the fly in PyObject_GenericSetAttr */
6060
self->dict = NULL;
6161
self->notes = NULL;
62+
PyTime_TimeRaw(&self->timestamp_ns); /* fills in 0 on failure. */
6263
self->traceback = self->cause = self->context = NULL;
6364
self->suppress_context = 0;
6465

@@ -83,6 +84,7 @@ BaseException_init(PyBaseExceptionObject *self, PyObject *args, PyObject *kwds)
8384
return -1;
8485

8586
Py_XSETREF(self->args, Py_NewRef(args));
87+
PyTime_TimeRaw(&self->timestamp_ns); /* fills in 0 on failure. */
8688
return 0;
8789
}
8890

@@ -105,6 +107,7 @@ BaseException_vectorcall(PyObject *type_obj, PyObject * const*args,
105107
// The dict is created on the fly in PyObject_GenericSetAttr()
106108
self->dict = NULL;
107109
self->notes = NULL;
110+
PyTime_TimeRaw(&self->timestamp_ns); /* fills in 0 on failure. */
108111
self->traceback = NULL;
109112
self->cause = NULL;
110113
self->context = NULL;
@@ -184,11 +187,13 @@ BaseException_repr(PyBaseExceptionObject *self)
184187
Py_BEGIN_CRITICAL_SECTION(self);
185188
const char *name = _PyType_Name(Py_TYPE(self));
186189
if (PyTuple_GET_SIZE(self->args) == 1) {
187-
res = PyUnicode_FromFormat("%s(%R)", name,
188-
PyTuple_GET_ITEM(self->args, 0));
190+
res = PyUnicode_FromFormat("%s(%R) [@t=%lldns]", name,
191+
PyTuple_GET_ITEM(self->args, 0),
192+
self->timestamp_ns);
189193
}
190194
else {
191-
res = PyUnicode_FromFormat("%s%R", name, self->args);
195+
res = PyUnicode_FromFormat("%s%R [@t=%lldns]", name, self->args,
196+
self->timestamp_ns);
192197
}
193198
Py_END_CRITICAL_SECTION();
194199
return res;
@@ -597,6 +602,8 @@ PyExceptionClass_Name(PyObject *ob)
597602
static struct PyMemberDef BaseException_members[] = {
598603
{"__suppress_context__", Py_T_BOOL,
599604
offsetof(PyBaseExceptionObject, suppress_context)},
605+
{"__timestamp_ns__", Py_T_LONGLONG,
606+
offsetof(PyBaseExceptionObject, timestamp_ns)},
600607
{NULL}
601608
};
602609

0 commit comments

Comments
 (0)