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
* - ``"import_time"``
- :c:member:`import_time <PyConfig.import_time>`
- ``bool``
- ``int``
- Read-only
* - ``"inspect"``
- :c:member:`inspect <PyConfig.inspect>`
@ -1477,13 +1477,19 @@ PyConfig
.. 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.
Default: ``0``.
.. versionchanged:: next
Added support for ``import_time = 2``
.. c:member:: int inspect
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
name, cumulative time (including nested imports) and self time (excluding
nested imports). Note that its output may be broken in multi-threaded
application. Typical usage is ``python3 -X importtime -c 'import
asyncio'``. See also :envvar:`PYTHONPROFILEIMPORTTIME`.
application. Typical usage is ``python -X importtime -c 'import asyncio'``.
``-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
.. 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
additional runtime checks that are too expensive to be enabled by
default. See also :envvar:`PYTHONDEVMODE`.
@ -982,12 +992,17 @@ conflict.
.. envvar:: PYTHONPROFILEIMPORTTIME
If this environment variable is set to a non-empty string, Python will
show how long each import takes.
If this environment variable is set to ``1``, Python will show
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.
.. versionadded:: 3.7
.. versionchanged:: next
Added ``PYTHONPROFILEIMPORTTIME=2`` to also trace imports of loaded modules.
.. envvar:: PYTHONASYNCIODEBUG

View file

@ -789,6 +789,13 @@ Other language changes
of HMAC is not available.
(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
longer replaced with a wrapped version on class creation if they are not
explicitly overridden in the subclass.

View file

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

View file

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

View file

@ -57,7 +57,7 @@ class CAPITests(unittest.TestCase):
("home", str | None, None),
("thread_inherit_context", int, None),
("context_aware_warnings", int, None),
("import_time", bool, None),
("import_time", int, None),
("inspect", bool, None),
("install_signal_handlers", bool, 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')
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):
out = res.out.strip().decode("utf-8")
return tuple(int(i) for i in out.split())

View file

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

View file

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

View file

@ -966,6 +966,7 @@ Beomsoo Bombs Kim
Derek D. Kim
Gihwan Kim
Jan Kim
Noah Kim
Taek Joo Kim
Yeojin Kim
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
\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 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
Override sys.platlibdir.
.IP PYTHONPROFILEIMPORTTIME
If this environment variable is set to a non-empty string, Python will
show how long each import takes. This is exactly equivalent to setting
\fB\-X importtime\fP on the command line.
If this environment variable is set to \fB1\fR, Python will show
how long each import takes. If set to \fB2\fR, Python will include output for
imported modules that have already been loaded.
This is exactly equivalent to setting \fB\-X importtime\fP on the command line.
.IP PYTHONPYCACHEPREFIX
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

View file

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

View file

@ -103,6 +103,15 @@ static struct _inittab *inittab_copy = NULL;
#define FIND_AND_LOAD(interp) \
(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 */
@ -246,9 +255,13 @@ import_ensure_initialized(PyInterpreterState *interp, PyObject *mod, PyObject *n
rc = _PyModuleSpec_IsInitializing(spec);
Py_DECREF(spec);
}
if (rc <= 0) {
if (rc == 0) {
goto done;
}
else if (rc < 0) {
return rc;
}
/* Wait until module is done importing. */
PyObject *value = PyObject_CallMethodOneArg(
IMPORTLIB(interp), &_Py_ID(_lock_unlock_module), name);
@ -256,6 +269,19 @@ import_ensure_initialized(PyInterpreterState *interp, PyObject *mod, PyObject *n
return -1;
}
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;
}
@ -3686,13 +3712,7 @@ import_find_and_load(PyThreadState *tstate, PyObject *abs_name)
* _PyDict_GetItemIdWithError().
*/
if (import_time) {
#define header FIND_AND_LOAD(interp).header
if (header) {
fputs("import time: self [us] | cumulative | imported package\n",
stderr);
header = 0;
}
#undef header
_IMPORT_TIME_HEADER(interp);
import_level++;
// 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(thread_inherit_context, 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(isolated, BOOL, READ_ONLY, NO_SYS), // sys.flags.isolated
#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"
#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\
0 disables the limit; also PYTHONINTMAXSTRDIGITS\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));
config->_config_init = (int)_PyConfig_INIT_COMPAT;
config->import_time = -1;
config->isolated = -1;
config->use_environment = -1;
config->dev_mode = -1;
@ -2246,6 +2248,38 @@ config_init_run_presite(PyConfig *config)
}
#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
config_read_complex_options(PyConfig *config)
@ -2257,17 +2291,19 @@ config_read_complex_options(PyConfig *config)
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")
|| config_get_xoption(config, L"no_debug_ranges")) {
config->code_debug_ranges = 0;
}
PyStatus status;
if (config->import_time < 0) {
status = config_init_import_time(config);
if (_PyStatus_EXCEPTION(status)) {
return status;
}
}
if (config->tracemalloc < 0) {
status = config_init_tracemalloc(config);
if (_PyStatus_EXCEPTION(status)) {