gh-102567: Add -X importtime=2 for logging an importtime message for already-loaded modules (#118655)

Co-authored-by: Adam Turner <9087854+aa-turner@users.noreply.github.com>
This commit is contained in:
Noah Kim 2025-05-05 20:03:55 -04:00 committed by GitHub
parent e6f8e0a035
commit c4bcc6a778
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 166 additions and 44 deletions

View file

@ -363,7 +363,7 @@ Configuration Options
- Read-only - Read-only
* - ``"import_time"`` * - ``"import_time"``
- :c:member:`import_time <PyConfig.import_time>` - :c:member:`import_time <PyConfig.import_time>`
- ``bool`` - ``int``
- Read-only - Read-only
* - ``"inspect"`` * - ``"inspect"``
- :c:member:`inspect <PyConfig.inspect>` - :c:member:`inspect <PyConfig.inspect>`
@ -1477,13 +1477,19 @@ PyConfig
.. c:member:: int import_time .. c:member:: int import_time
If non-zero, profile import time. If ``1``, profile import time.
If ``2``, include additional output that indicates
when an imported module has already been loaded.
Set the ``1`` by the :option:`-X importtime <-X>` option and the Set by the :option:`-X importtime <-X>` option and the
:envvar:`PYTHONPROFILEIMPORTTIME` environment variable. :envvar:`PYTHONPROFILEIMPORTTIME` environment variable.
Default: ``0``. Default: ``0``.
.. versionchanged:: next
Added support for ``import_time = 2``
.. c:member:: int inspect .. c:member:: int inspect
Enter interactive mode after executing a script or a command. Enter interactive mode after executing a script or a command.

View file

@ -539,11 +539,21 @@ Miscellaneous options
* ``-X importtime`` to show how long each import takes. It shows module * ``-X importtime`` to show how long each import takes. It shows module
name, cumulative time (including nested imports) and self time (excluding name, cumulative time (including nested imports) and self time (excluding
nested imports). Note that its output may be broken in multi-threaded nested imports). Note that its output may be broken in multi-threaded
application. Typical usage is ``python3 -X importtime -c 'import application. Typical usage is ``python -X importtime -c 'import asyncio'``.
asyncio'``. See also :envvar:`PYTHONPROFILEIMPORTTIME`.
``-X importtime=2`` enables additional output that indicates when an
imported module has already been loaded. In such cases, the string
``cached`` will be printed in both time columns.
See also :envvar:`PYTHONPROFILEIMPORTTIME`.
.. versionadded:: 3.7 .. versionadded:: 3.7
.. versionchanged:: next
Added ``-X importtime=2`` to also trace imports of loaded modules,
and reserved values other than ``1`` and ``2`` for future use.
* ``-X dev``: enable :ref:`Python Development Mode <devmode>`, introducing * ``-X dev``: enable :ref:`Python Development Mode <devmode>`, introducing
additional runtime checks that are too expensive to be enabled by additional runtime checks that are too expensive to be enabled by
default. See also :envvar:`PYTHONDEVMODE`. default. See also :envvar:`PYTHONDEVMODE`.
@ -982,12 +992,17 @@ conflict.
.. envvar:: PYTHONPROFILEIMPORTTIME .. envvar:: PYTHONPROFILEIMPORTTIME
If this environment variable is set to a non-empty string, Python will If this environment variable is set to ``1``, Python will show
show how long each import takes. how long each import takes. If set to ``2``, Python will include output for
imported modules that have already been loaded.
This is equivalent to setting the :option:`-X` ``importtime`` option. This is equivalent to setting the :option:`-X` ``importtime`` option.
.. versionadded:: 3.7 .. versionadded:: 3.7
.. versionchanged:: next
Added ``PYTHONPROFILEIMPORTTIME=2`` to also trace imports of loaded modules.
.. envvar:: PYTHONASYNCIODEBUG .. envvar:: PYTHONASYNCIODEBUG

View file

@ -789,6 +789,13 @@ Other language changes
of HMAC is not available. of HMAC is not available.
(Contributed by Bénédikt Tran in :gh:`99108`.) (Contributed by Bénédikt Tran in :gh:`99108`.)
* The import time flag can now track modules that are already loaded ('cached'),
via the new :option:`-X importtime=2 <-X>`.
When such a module is imported, the ``self`` and ``cumulative`` times
are replaced by the string ``cached``.
Values above ``2`` for ``-X importtime`` are now reserved for future use.
(Contributed by Noah Kim and Adam Turner in :gh:`118655`.)
* When subclassing from a pure C type, the C slots for the new type are no * When subclassing from a pure C type, the C slots for the new type are no
longer replaced with a wrapped version on class creation if they are not longer replaced with a wrapped version on class creation if they are not
explicitly overridden in the subclass. explicitly overridden in the subclass.

View file

@ -29,6 +29,7 @@ import signal
import struct import struct
import termios import termios
import time import time
import types
import platform import platform
from fcntl import ioctl from fcntl import ioctl
@ -39,6 +40,12 @@ from .trace import trace
from .unix_eventqueue import EventQueue from .unix_eventqueue import EventQueue
from .utils import wlen from .utils import wlen
# declare posix optional to allow None assignment on other platforms
posix: types.ModuleType | None
try:
import posix
except ImportError:
posix = None
TYPE_CHECKING = False TYPE_CHECKING = False
@ -556,11 +563,9 @@ class UnixConsole(Console):
@property @property
def input_hook(self): def input_hook(self):
try: # avoid inline imports here so the repl doesn't get flooded
import posix # with import logging from -X importtime=2
except ImportError: if posix is not None and posix._is_inputhook_installed():
return None
if posix._is_inputhook_installed():
return posix._inputhook return posix._inputhook
def __enable_bracketed_paste(self) -> None: def __enable_bracketed_paste(self) -> None:

View file

@ -24,6 +24,7 @@ import os
import sys import sys
import ctypes import ctypes
import types
from ctypes.wintypes import ( from ctypes.wintypes import (
_COORD, _COORD,
WORD, WORD,
@ -58,6 +59,12 @@ except:
self.err = err self.err = err
self.descr = descr self.descr = descr
# declare nt optional to allow None assignment on other platforms
nt: types.ModuleType | None
try:
import nt
except ImportError:
nt = None
TYPE_CHECKING = False TYPE_CHECKING = False
@ -121,9 +128,8 @@ class _error(Exception):
def _supports_vt(): def _supports_vt():
try: try:
import nt
return nt._supports_virtual_terminal() return nt._supports_virtual_terminal()
except (ImportError, AttributeError): except AttributeError:
return False return False
class WindowsConsole(Console): class WindowsConsole(Console):
@ -235,11 +241,9 @@ class WindowsConsole(Console):
@property @property
def input_hook(self): def input_hook(self):
try: # avoid inline imports here so the repl doesn't get flooded
import nt # with import logging from -X importtime=2
except ImportError: if nt is not None and nt._is_inputhook_installed():
return None
if nt._is_inputhook_installed():
return nt._inputhook return nt._inputhook
def __write_changed_line( def __write_changed_line(

View file

@ -57,7 +57,7 @@ class CAPITests(unittest.TestCase):
("home", str | None, None), ("home", str | None, None),
("thread_inherit_context", int, None), ("thread_inherit_context", int, None),
("context_aware_warnings", int, None), ("context_aware_warnings", int, None),
("import_time", bool, None), ("import_time", int, None),
("inspect", bool, None), ("inspect", bool, None),
("install_signal_handlers", bool, None), ("install_signal_handlers", bool, None),
("int_max_str_digits", int, None), ("int_max_str_digits", int, None),

View file

@ -1158,6 +1158,24 @@ class CmdLineTest(unittest.TestCase):
res = assert_python_ok('-c', code, PYTHON_CPU_COUNT='default') res = assert_python_ok('-c', code, PYTHON_CPU_COUNT='default')
self.assertEqual(self.res2int(res), (os.cpu_count(), os.process_cpu_count())) self.assertEqual(self.res2int(res), (os.cpu_count(), os.process_cpu_count()))
def test_import_time(self):
# os is not imported at startup
code = 'import os; import os'
for case in 'importtime', 'importtime=1', 'importtime=true':
res = assert_python_ok('-X', case, '-c', code)
res_err = res.err.decode('utf-8')
self.assertRegex(res_err, r'import time: \s*\d+ \| \s*\d+ \| \s*os')
self.assertNotRegex(res_err, r'import time: cached\s* \| cached\s* \| os')
res = assert_python_ok('-X', 'importtime=2', '-c', code)
res_err = res.err.decode('utf-8')
self.assertRegex(res_err, r'import time: \s*\d+ \| \s*\d+ \| \s*os')
self.assertRegex(res_err, r'import time: cached\s* \| cached\s* \| os')
assert_python_failure('-X', 'importtime=-1', '-c', code)
assert_python_failure('-X', 'importtime=3', '-c', code)
def res2int(self, res): def res2int(self, res):
out = res.out.strip().decode("utf-8") out = res.out.strip().decode("utf-8")
return tuple(int(i) for i in out.split()) return tuple(int(i) for i in out.split())

View file

@ -585,7 +585,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
'faulthandler': False, 'faulthandler': False,
'tracemalloc': 0, 'tracemalloc': 0,
'perf_profiling': 0, 'perf_profiling': 0,
'import_time': False, 'import_time': 0,
'thread_inherit_context': DEFAULT_THREAD_INHERIT_CONTEXT, 'thread_inherit_context': DEFAULT_THREAD_INHERIT_CONTEXT,
'context_aware_warnings': DEFAULT_CONTEXT_AWARE_WARNINGS, 'context_aware_warnings': DEFAULT_CONTEXT_AWARE_WARNINGS,
'code_debug_ranges': True, 'code_debug_ranges': True,
@ -998,7 +998,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
'hash_seed': 123, 'hash_seed': 123,
'tracemalloc': 2, 'tracemalloc': 2,
'perf_profiling': 0, 'perf_profiling': 0,
'import_time': True, 'import_time': 2,
'code_debug_ranges': False, 'code_debug_ranges': False,
'show_ref_count': True, 'show_ref_count': True,
'malloc_stats': True, 'malloc_stats': True,
@ -1064,7 +1064,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
'use_hash_seed': True, 'use_hash_seed': True,
'hash_seed': 42, 'hash_seed': 42,
'tracemalloc': 2, 'tracemalloc': 2,
'import_time': True, 'import_time': 1,
'code_debug_ranges': False, 'code_debug_ranges': False,
'malloc_stats': True, 'malloc_stats': True,
'inspect': True, 'inspect': True,
@ -1100,7 +1100,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
'use_hash_seed': True, 'use_hash_seed': True,
'hash_seed': 42, 'hash_seed': 42,
'tracemalloc': 2, 'tracemalloc': 2,
'import_time': True, 'import_time': 1,
'code_debug_ranges': False, 'code_debug_ranges': False,
'malloc_stats': True, 'malloc_stats': True,
'inspect': True, 'inspect': True,

View file

@ -561,6 +561,7 @@ class TestSupport(unittest.TestCase):
['-Wignore', '-X', 'dev'], ['-Wignore', '-X', 'dev'],
['-X', 'faulthandler'], ['-X', 'faulthandler'],
['-X', 'importtime'], ['-X', 'importtime'],
['-X', 'importtime=2'],
['-X', 'showrefcount'], ['-X', 'showrefcount'],
['-X', 'tracemalloc'], ['-X', 'tracemalloc'],
['-X', 'tracemalloc=3'], ['-X', 'tracemalloc=3'],

View file

@ -966,6 +966,7 @@ Beomsoo Bombs Kim
Derek D. Kim Derek D. Kim
Gihwan Kim Gihwan Kim
Jan Kim Jan Kim
Noah Kim
Taek Joo Kim Taek Joo Kim
Yeojin Kim Yeojin Kim
Sam Kimbrel Sam Kimbrel

View file

@ -0,0 +1,4 @@
:option:`-X importtime <-X>` now accepts value ``2``, which indicates that
an ``importtime`` entry should also be printed if an imported module has
already been loaded.
Patch by Noah Kim and Adam Turner.

View file

@ -344,6 +344,10 @@ Set implementation-specific option. The following options are available:
application. Typical usage is application. Typical usage is
\fBpython3 \-X importtime \-c 'import asyncio'\fR \fBpython3 \-X importtime \-c 'import asyncio'\fR
\fB\-X importtime=2\fR enables additional output that indicates when an
imported module has already been loaded. In such cases, the string
\fBcached\fR will be printed in both time columns.
\fB\-X faulthandler\fR: enable faulthandler \fB\-X faulthandler\fR: enable faulthandler
\fB\-X frozen_modules=\fR[\fBon\fR|\fBoff\fR]: whether or not frozen modules \fB\-X frozen_modules=\fR[\fBon\fR|\fBoff\fR]: whether or not frozen modules
@ -648,9 +652,10 @@ See also the \fB\-X perf\fR option.
.IP PYTHONPLATLIBDIR .IP PYTHONPLATLIBDIR
Override sys.platlibdir. Override sys.platlibdir.
.IP PYTHONPROFILEIMPORTTIME .IP PYTHONPROFILEIMPORTTIME
If this environment variable is set to a non-empty string, Python will If this environment variable is set to \fB1\fR, Python will show
show how long each import takes. This is exactly equivalent to setting how long each import takes. If set to \fB2\fR, Python will include output for
\fB\-X importtime\fP on the command line. imported modules that have already been loaded.
This is exactly equivalent to setting \fB\-X importtime\fP on the command line.
.IP PYTHONPYCACHEPREFIX .IP PYTHONPYCACHEPREFIX
If this is set, Python will write \fB.pyc\fR files in a mirror directory tree If this is set, Python will write \fB.pyc\fR files in a mirror directory tree
at this path, instead of in \fB__pycache__\fR directories within the source at this path, instead of in \fB__pycache__\fR directories within the source

View file

@ -651,8 +651,8 @@ static int test_init_from_config(void)
putenv("PYTHONTRACEMALLOC=0"); putenv("PYTHONTRACEMALLOC=0");
config.tracemalloc = 2; config.tracemalloc = 2;
putenv("PYTHONPROFILEIMPORTTIME=0"); putenv("PYTHONPROFILEIMPORTTIME=1");
config.import_time = 1; config.import_time = 2;
putenv("PYTHONNODEBUGRANGES=0"); putenv("PYTHONNODEBUGRANGES=0");
config.code_debug_ranges = 0; config.code_debug_ranges = 0;

View file

@ -103,6 +103,15 @@ static struct _inittab *inittab_copy = NULL;
#define FIND_AND_LOAD(interp) \ #define FIND_AND_LOAD(interp) \
(interp)->imports.find_and_load (interp)->imports.find_and_load
#define _IMPORT_TIME_HEADER(interp) \
do { \
if (FIND_AND_LOAD((interp)).header) { \
fputs("import time: self [us] | cumulative | imported package\n", \
stderr); \
FIND_AND_LOAD((interp)).header = 0; \
} \
} while (0)
/*******************/ /*******************/
/* the import lock */ /* the import lock */
@ -246,9 +255,13 @@ import_ensure_initialized(PyInterpreterState *interp, PyObject *mod, PyObject *n
rc = _PyModuleSpec_IsInitializing(spec); rc = _PyModuleSpec_IsInitializing(spec);
Py_DECREF(spec); Py_DECREF(spec);
} }
if (rc <= 0) { if (rc == 0) {
goto done;
}
else if (rc < 0) {
return rc; return rc;
} }
/* Wait until module is done importing. */ /* Wait until module is done importing. */
PyObject *value = PyObject_CallMethodOneArg( PyObject *value = PyObject_CallMethodOneArg(
IMPORTLIB(interp), &_Py_ID(_lock_unlock_module), name); IMPORTLIB(interp), &_Py_ID(_lock_unlock_module), name);
@ -256,6 +269,19 @@ import_ensure_initialized(PyInterpreterState *interp, PyObject *mod, PyObject *n
return -1; return -1;
} }
Py_DECREF(value); Py_DECREF(value);
done:
/* When -X importtime=2, print an import time entry even if an
imported module has already been loaded.
*/
if (_PyInterpreterState_GetConfig(interp)->import_time == 2) {
_IMPORT_TIME_HEADER(interp);
#define import_level FIND_AND_LOAD(interp).import_level
fprintf(stderr, "import time: cached | cached | %*s\n",
import_level*2, PyUnicode_AsUTF8(name));
#undef import_level
}
return 0; return 0;
} }
@ -3686,13 +3712,7 @@ import_find_and_load(PyThreadState *tstate, PyObject *abs_name)
* _PyDict_GetItemIdWithError(). * _PyDict_GetItemIdWithError().
*/ */
if (import_time) { if (import_time) {
#define header FIND_AND_LOAD(interp).header _IMPORT_TIME_HEADER(interp);
if (header) {
fputs("import time: self [us] | cumulative | imported package\n",
stderr);
header = 0;
}
#undef header
import_level++; import_level++;
// ignore error: don't block import if reading the clock fails // ignore error: don't block import if reading the clock fails

View file

@ -153,7 +153,7 @@ static const PyConfigSpec PYCONFIG_SPEC[] = {
SPEC(home, WSTR_OPT, READ_ONLY, NO_SYS), SPEC(home, WSTR_OPT, READ_ONLY, NO_SYS),
SPEC(thread_inherit_context, INT, READ_ONLY, NO_SYS), SPEC(thread_inherit_context, INT, READ_ONLY, NO_SYS),
SPEC(context_aware_warnings, INT, READ_ONLY, NO_SYS), SPEC(context_aware_warnings, INT, READ_ONLY, NO_SYS),
SPEC(import_time, BOOL, READ_ONLY, NO_SYS), SPEC(import_time, UINT, READ_ONLY, NO_SYS),
SPEC(install_signal_handlers, BOOL, READ_ONLY, NO_SYS), SPEC(install_signal_handlers, BOOL, READ_ONLY, NO_SYS),
SPEC(isolated, BOOL, READ_ONLY, NO_SYS), // sys.flags.isolated SPEC(isolated, BOOL, READ_ONLY, NO_SYS), // sys.flags.isolated
#ifdef MS_WINDOWS #ifdef MS_WINDOWS
@ -312,7 +312,8 @@ The following implementation-specific options are available:\n\
"-X gil=[0|1]: enable (1) or disable (0) the GIL; also PYTHON_GIL\n" "-X gil=[0|1]: enable (1) or disable (0) the GIL; also PYTHON_GIL\n"
#endif #endif
"\ "\
-X importtime: show how long each import takes; also PYTHONPROFILEIMPORTTIME\n\ -X importtime[=2]: show how long each import takes; use -X importtime=2 to\
log imports of already-loaded modules; also PYTHONPROFILEIMPORTTIME\n\
-X int_max_str_digits=N: limit the size of int<->str conversions;\n\ -X int_max_str_digits=N: limit the size of int<->str conversions;\n\
0 disables the limit; also PYTHONINTMAXSTRDIGITS\n\ 0 disables the limit; also PYTHONINTMAXSTRDIGITS\n\
-X no_debug_ranges: don't include extra location information in code objects;\n\ -X no_debug_ranges: don't include extra location information in code objects;\n\
@ -1004,6 +1005,7 @@ _PyConfig_InitCompatConfig(PyConfig *config)
memset(config, 0, sizeof(*config)); memset(config, 0, sizeof(*config));
config->_config_init = (int)_PyConfig_INIT_COMPAT; config->_config_init = (int)_PyConfig_INIT_COMPAT;
config->import_time = -1;
config->isolated = -1; config->isolated = -1;
config->use_environment = -1; config->use_environment = -1;
config->dev_mode = -1; config->dev_mode = -1;
@ -2246,6 +2248,38 @@ config_init_run_presite(PyConfig *config)
} }
#endif #endif
static PyStatus
config_init_import_time(PyConfig *config)
{
int importtime = 0;
const char *env = config_get_env(config, "PYTHONPROFILEIMPORTTIME");
if (env) {
if (_Py_str_to_int(env, &importtime) != 0) {
importtime = 1;
}
if (importtime < 0 || importtime > 2) {
return _PyStatus_ERR(
"PYTHONPROFILEIMPORTTIME: numeric values other than 1 and 2 "
"are reserved for future use.");
}
}
const wchar_t *x_value = config_get_xoption_value(config, L"importtime");
if (x_value) {
if (*x_value == 0 || config_wstr_to_int(x_value, &importtime) != 0) {
importtime = 1;
}
if (importtime < 0 || importtime > 2) {
return _PyStatus_ERR(
"-X importtime: values other than 1 and 2 "
"are reserved for future use.");
}
}
config->import_time = importtime;
return _PyStatus_OK();
}
static PyStatus static PyStatus
config_read_complex_options(PyConfig *config) config_read_complex_options(PyConfig *config)
@ -2257,17 +2291,19 @@ config_read_complex_options(PyConfig *config)
config->faulthandler = 1; config->faulthandler = 1;
} }
} }
if (config_get_env(config, "PYTHONPROFILEIMPORTTIME")
|| config_get_xoption(config, L"importtime")) {
config->import_time = 1;
}
if (config_get_env(config, "PYTHONNODEBUGRANGES") if (config_get_env(config, "PYTHONNODEBUGRANGES")
|| config_get_xoption(config, L"no_debug_ranges")) { || config_get_xoption(config, L"no_debug_ranges")) {
config->code_debug_ranges = 0; config->code_debug_ranges = 0;
} }
PyStatus status; PyStatus status;
if (config->import_time < 0) {
status = config_init_import_time(config);
if (_PyStatus_EXCEPTION(status)) {
return status;
}
}
if (config->tracemalloc < 0) { if (config->tracemalloc < 0) {
status = config_init_tracemalloc(config); status = config_init_tracemalloc(config);
if (_PyStatus_EXCEPTION(status)) { if (_PyStatus_EXCEPTION(status)) {