mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 13:41:24 +00:00 
			
		
		
		
	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:
		
							parent
							
								
									e6f8e0a035
								
							
						
					
					
						commit
						c4bcc6a778
					
				
					 15 changed files with 166 additions and 44 deletions
				
			
		|  | @ -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. | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
|  | @ -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: | ||||
|  |  | |||
|  | @ -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( | ||||
|  |  | |||
|  | @ -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), | ||||
|  |  | |||
|  | @ -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()) | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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'], | ||||
|  |  | |||
|  | @ -966,6 +966,7 @@ Beomsoo Bombs Kim | |||
| Derek D. Kim | ||||
| Gihwan Kim | ||||
| Jan Kim | ||||
| Noah Kim | ||||
| Taek Joo Kim | ||||
| Yeojin Kim | ||||
| Sam Kimbrel | ||||
|  |  | |||
|  | @ -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. | ||||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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
 | ||||
|  |  | |||
|  | @ -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)) { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Noah Kim
						Noah Kim