Skip to content

Commit eebec1d

Browse files
committed
Disable time collection when display is disabled.
TODO: performance testing - this increases the conditional load on every BaseException instantiation with that interp->config.field && field[0] check. If needed, we could cache the "collect or not" bool in a static global as it is fair to say this is a process wide setting rather than per interpreter, but ugh.
1 parent 75072cb commit eebec1d

5 files changed

Lines changed: 57 additions & 18 deletions

File tree

Doc/c-api/init_config.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,10 @@ Configuration Options
493493
- :c:member:`tracemalloc <PyConfig.tracemalloc>`
494494
- ``int``
495495
- Read-only
496+
* - ``"traceback_timestamps"``
497+
- :c:member:`traceback_timestamps <PyConfig.traceback_timestamps>`
498+
- ``str``
499+
- Read-only
496500
* - ``"use_environment"``
497501
- :c:member:`use_environment <PyConfig.use_environment>`
498502
- ``bool``
@@ -1879,6 +1883,25 @@ PyConfig
18791883
18801884
Default: ``-1`` in Python mode, ``0`` in isolated mode.
18811885
1886+
.. c:member:: wchar_t* traceback_timestamps
1887+
1888+
Format of timestamps shown in tracebacks.
1889+
1890+
If not ``NULL`` or an empty string, timestamps of exceptions are collected
1891+
and will be displayed in the configured format. Acceptable values are:
1892+
1893+
* ``"us"``: Display timestamps in microseconds
1894+
* ``"ns"``: Display timestamps in nanoseconds
1895+
* ``"iso"``: Display timestamps in ISO-8601 format
1896+
* ``""``: Collection and display is disabled.
1897+
1898+
Set by the :option:`-X traceback_timestamps=FORMAT <-X>` command line
1899+
option or the :envvar:`PYTHON_TRACEBACK_TIMESTAMPS` environment variable.
1900+
1901+
Default: ``NULL`` (timestamps disabled).
1902+
1903+
.. versionadded:: next
1904+
18821905
.. c:member:: int perf_profiling
18831906
18841907
Enable the Linux ``perf`` profiler support?

Lib/test/test_interpreters/utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,8 +458,10 @@ def make_module(self, name, pathentry=None, text=None):
458458

459459
@support.requires_subprocess()
460460
def run_python(self, *argv):
461+
# Make assertions of specific traceback output simpler.
462+
arguments = ["-X", "traceback_timestamps=0", *argv]
461463
proc = subprocess.run(
462-
[sys.executable, *argv],
464+
[sys.executable, *arguments],
463465
capture_output=True,
464466
text=True,
465467
)

Lib/test/test_traceback_timestamps.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ def test_traceback_timestamps_invalid_flag(self):
143143
"""Test that invalid flag values cause an error"""
144144
result = script_helper.assert_python_failure("-X", "traceback_timestamps=invalid", self.flags_script_path)
145145
stderr = result.err.decode()
146-
self.assertIn("Invalid value for -X traceback_timestamps option", stderr)
146+
self.assertIn("Invalid -X traceback_timestamps=value option", stderr)
147147

148148

149149
if __name__ == "__main__":

Objects/exceptions.c

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ get_exc_state(void)
4141
}
4242

4343

44+
static bool
45+
should_collect_traceback_timestamps(void)
46+
{
47+
/* Unset or empty means disabled. */
48+
wchar_t *traceback_timestamps = (
49+
_PyInterpreterState_GET()->config.traceback_timestamps);
50+
return traceback_timestamps && traceback_timestamps[0] != '\0';
51+
}
52+
53+
4454
/* NOTE: If the exception class hierarchy changes, don't forget to update
4555
* Lib/test/exception_hierarchy.txt
4656
*/
@@ -79,7 +89,13 @@ BaseException_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
7989

8090
static inline void BaseException_init_timestamp(PyBaseExceptionObject *self)
8191
{
82-
PyTime_TimeRaw(&self->timestamp_ns); /* fills in 0 on failure. */
92+
if (!should_collect_traceback_timestamps() ||
93+
Py_IS_TYPE(self, (PyTypeObject *)PyExc_StopIteration) ||
94+
Py_IS_TYPE(self, (PyTypeObject *)PyExc_StopAsyncIteration)) {
95+
self->timestamp_ns = 0; /* fast; frequent non-error control flow. */
96+
} else {
97+
PyTime_TimeRaw(&self->timestamp_ns); /* fills in 0 on failure. */
98+
}
8399
}
84100

85101
static int
@@ -89,12 +105,7 @@ BaseException_init(PyBaseExceptionObject *self, PyObject *args, PyObject *kwds)
89105
return -1;
90106

91107
Py_XSETREF(self->args, Py_NewRef(args));
92-
if (Py_IS_TYPE(self, (PyTypeObject *)PyExc_StopIteration) ||
93-
Py_IS_TYPE(self, (PyTypeObject *)PyExc_StopAsyncIteration)) {
94-
self->timestamp_ns = 0; /* fast; frequent non-error control flow. */
95-
} else {
96-
BaseException_init_timestamp(self);
97-
}
108+
BaseException_init_timestamp(self);
98109
return 0;
99110
}
100111

@@ -117,7 +128,7 @@ BaseException_vectorcall(PyObject *type_obj, PyObject * const*args,
117128
// The dict is created on the fly in PyObject_GenericSetAttr()
118129
self->dict = NULL;
119130
self->notes = NULL;
120-
BaseException_init_timestamp(self);
131+
BaseException_init_timestamp(self); // self.timestamp_ns = ...
121132
self->traceback = NULL;
122133
self->cause = NULL;
123134
self->context = NULL;
@@ -4429,4 +4440,3 @@ _PyException_AddNote(PyObject *exc, PyObject *note)
44294440
Py_XDECREF(r);
44304441
return res;
44314442
}
4432-

Python/initconfig.c

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1943,7 +1943,7 @@ normalize_timestamp_format(const wchar_t *value)
19431943
return L"us";
19441944
}
19451945
if (wcscmp(value, L"0") == 0) {
1946-
/* Treat "0" as empty string to disable the feature */
1946+
/* "0" means disable the feature. */
19471947
return L"";
19481948
}
19491949
return value;
@@ -1963,7 +1963,8 @@ config_init_traceback_timestamps(PyConfig *config)
19631963
/* For environment variables, silently ignore invalid values */
19641964
if (is_valid_timestamp_format(wenv)) {
19651965
const wchar_t *normalized = normalize_timestamp_format(wenv);
1966-
PyStatus status = PyConfig_SetString(config, &config->traceback_timestamps, normalized);
1966+
PyStatus status = PyConfig_SetString(
1967+
config, &config->traceback_timestamps, normalized);
19671968
PyMem_RawFree(wenv);
19681969
if (_PyStatus_EXCEPTION(status)) {
19691970
return status;
@@ -1974,21 +1975,24 @@ config_init_traceback_timestamps(PyConfig *config)
19741975
}
19751976

19761977
/* -X option overrides environment variable */
1977-
const wchar_t *xoption = config_get_xoption_value(config, L"traceback_timestamps");
1978+
const wchar_t *xoption = config_get_xoption_value(
1979+
config, L"traceback_timestamps");
19781980
if (xoption != NULL) {
1979-
/* If value is empty (just -X traceback_timestamps with no =), use "us" as default */
1981+
/* If just -X traceback_timestamps with no =, use "us" as default */
19801982
const wchar_t *value = (*xoption != '\0') ? xoption : L"us";
19811983

19821984
/* Validate command line option values, error out if invalid */
19831985
if (is_valid_timestamp_format(value)) {
19841986
const wchar_t *normalized = normalize_timestamp_format(value);
1985-
PyStatus status = PyConfig_SetString(config, &config->traceback_timestamps, normalized);
1987+
PyStatus status = PyConfig_SetString(
1988+
config, &config->traceback_timestamps, normalized);
19861989
if (_PyStatus_EXCEPTION(status)) {
19871990
return status;
19881991
}
19891992
} else {
1990-
return PyStatus_Error("Invalid value for -X traceback_timestamps option. "
1991-
"Valid values are: us, ns, iso, 0, 1 or empty.");
1993+
return PyStatus_Error(
1994+
"Invalid -X traceback_timestamps=value option. Valid "
1995+
"values are: us, ns, iso, 0, 1 or empty.");
19921996
}
19931997
}
19941998

0 commit comments

Comments
 (0)