https://github.com/python/cpython/commit/c4bcc6a77864b42574ec663cde36f0cc10e97b46 commit: c4bcc6a77864b42574ec663cde36f0cc10e97b46 branch: main author: Noah Kim <n...@hudson-trading.com> committer: AA-Turner <9087854+aa-tur...@users.noreply.github.com> date: 2025-05-06T01:03:55+01:00 summary:
gh-102567: Add -X importtime=2 for logging an importtime message for already-loaded modules (#118655) Co-authored-by: Adam Turner <9087854+aa-tur...@users.noreply.github.com> files: A Misc/NEWS.d/next/Core_and_Builtins/2024-07-11-12-31-29.gh-issue-102567.weRqDn.rst M Doc/c-api/init_config.rst M Doc/using/cmdline.rst M Doc/whatsnew/3.14.rst M Lib/_pyrepl/unix_console.py M Lib/_pyrepl/windows_console.py M Lib/test/test_capi/test_config.py M Lib/test/test_cmd_line.py M Lib/test/test_embed.py M Lib/test/test_support.py M Misc/ACKS M Misc/python.man M Programs/_testembed.c M Python/import.c M Python/initconfig.c diff --git a/Doc/c-api/init_config.rst b/Doc/c-api/init_config.rst index bc5b236393be15..1f413a802c304e 100644 --- a/Doc/c-api/init_config.rst +++ b/Doc/c-api/init_config.rst @@ -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. diff --git a/Doc/using/cmdline.rst b/Doc/using/cmdline.rst index c586882b313ddf..0095046fe1f9f3 100644 --- a/Doc/using/cmdline.rst +++ b/Doc/using/cmdline.rst @@ -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 diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index c35e4365de025b..a345687813f16b 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -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. diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py index 9be6ce6d8632fd..d21cdd9b076d86 100644 --- a/Lib/_pyrepl/unix_console.py +++ b/Lib/_pyrepl/unix_console.py @@ -29,6 +29,7 @@ import struct import termios import time +import types import platform from fcntl import ioctl @@ -39,6 +40,12 @@ 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 @@ def clear(self): @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: diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index a294d627b0efb8..95749198b3b2f9 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -24,6 +24,7 @@ import sys import ctypes +import types from ctypes.wintypes import ( _COORD, WORD, @@ -58,6 +59,12 @@ def __init__(self, err: int | None, descr: str | None = None) -> None: 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 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: @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( diff --git a/Lib/test/test_capi/test_config.py b/Lib/test/test_capi/test_config.py index bf351c4defab5d..a2d70dd3af482d 100644 --- a/Lib/test/test_capi/test_config.py +++ b/Lib/test/test_capi/test_config.py @@ -57,7 +57,7 @@ def test_config_get(self): ("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), diff --git a/Lib/test/test_cmd_line.py b/Lib/test/test_cmd_line.py index 36f87e259e794d..1b40e0d05fe3bc 100644 --- a/Lib/test/test_cmd_line.py +++ b/Lib/test/test_cmd_line.py @@ -1158,6 +1158,24 @@ def test_cpu_count_default(self): 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()) diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index e06e684408ca6b..95b2d80464c349 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -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 @@ def test_init_from_config(self): '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 @@ def test_init_compat_env(self): '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 @@ def test_init_python_env(self): 'use_hash_seed': True, 'hash_seed': 42, 'tracemalloc': 2, - 'import_time': True, + 'import_time': 1, 'code_debug_ranges': False, 'malloc_stats': True, 'inspect': True, diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index 468bac82924bb1..8446da03e3645b 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -561,6 +561,7 @@ def test_args_from_interpreter_flags(self): ['-Wignore', '-X', 'dev'], ['-X', 'faulthandler'], ['-X', 'importtime'], + ['-X', 'importtime=2'], ['-X', 'showrefcount'], ['-X', 'tracemalloc'], ['-X', 'tracemalloc=3'], diff --git a/Misc/ACKS b/Misc/ACKS index 42068ec6aefbd2..610dcf9f4238de 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -966,6 +966,7 @@ Beomsoo Bombs Kim Derek D. Kim Gihwan Kim Jan Kim +Noah Kim Taek Joo Kim Yeojin Kim Sam Kimbrel diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2024-07-11-12-31-29.gh-issue-102567.weRqDn.rst b/Misc/NEWS.d/next/Core_and_Builtins/2024-07-11-12-31-29.gh-issue-102567.weRqDn.rst new file mode 100644 index 00000000000000..04ebbfa128c289 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2024-07-11-12-31-29.gh-issue-102567.weRqDn.rst @@ -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. diff --git a/Misc/python.man b/Misc/python.man index fa88a2586dcabf..15174b62d5fea4 100644 --- a/Misc/python.man +++ b/Misc/python.man @@ -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 diff --git a/Programs/_testembed.c b/Programs/_testembed.c index 6f6d0cae58010e..8a7412c7019e60 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -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; diff --git a/Python/import.c b/Python/import.c index a671a08daeb50f..afdc28eda31b9b 100644 --- a/Python/import.c +++ b/Python/import.c @@ -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 diff --git a/Python/initconfig.c b/Python/initconfig.c index 7d3043dd5d1dd9..e827091172162e 100644 --- a/Python/initconfig.c +++ b/Python/initconfig.c @@ -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)) { _______________________________________________ Python-checkins mailing list -- python-checkins@python.org To unsubscribe send an email to python-checkins-le...@python.org https://mail.python.org/mailman3/lists/python-checkins.python.org/ Member address: arch...@mail-archive.com