Skip to content

Commit 58d07be

Browse files
committed
Add -X importtime=2 for logging an importtime message for already-loaded modules
1 parent c141340 commit 58d07be

File tree

12 files changed

+163
-31
lines changed

12 files changed

+163
-31
lines changed

Doc/c-api/init_config.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1477,9 +1477,10 @@ PyConfig
14771477
14781478
.. c:member:: int import_time
14791479
1480-
If non-zero, profile import time.
1480+
If non-zero, profile import time. If ``2``, include additional output that
1481+
indicates when an imported module has already been loaded.
14811482
1482-
Set the ``1`` by the :option:`-X importtime <-X>` option and the
1483+
Set by the :option:`-X importtime <-X>` option and the
14831484
:envvar:`PYTHONPROFILEIMPORTTIME` environment variable.
14841485
14851486
Default: ``0``.

Doc/using/cmdline.rst

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -542,8 +542,17 @@ Miscellaneous options
542542
application. Typical usage is ``python3 -X importtime -c 'import
543543
asyncio'``. See also :envvar:`PYTHONPROFILEIMPORTTIME`.
544544

545+
``-X importtime=2`` enables additional output that indicates when an
546+
imported module has already been loaded. In such cases, the string
547+
``cached`` will be printed in the self time and cumulative time columns.
548+
545549
.. versionadded:: 3.7
546550

551+
.. versionchanged:: next
552+
553+
Added ``-X importtime=2`` to also trace imports of loaded modules,
554+
and reserved values other than ``1`` and ``2`` for future use.
555+
547556
* ``-X dev``: enable :ref:`Python Development Mode <devmode>`, introducing
548557
additional runtime checks that are too expensive to be enabled by
549558
default. See also :envvar:`PYTHONDEVMODE`.
@@ -984,12 +993,17 @@ conflict.
984993

985994
.. envvar:: PYTHONPROFILEIMPORTTIME
986995

987-
If this environment variable is set to a non-empty string, Python will
988-
show how long each import takes.
996+
If this environment variable is set to ``1``, Python will show
997+
how long each import takes. If set to ``2``, Python will include output for
998+
imported modules that have already been loaded.
989999
This is equivalent to setting the :option:`-X` ``importtime`` option.
9901000

9911001
.. versionadded:: 3.7
9921002

1003+
.. versionchanged:: next
1004+
1005+
Added ``PYTHONPROFILEIMPORTTIME=2`` to also trace imports of loaded modules.
1006+
9931007

9941008
.. envvar:: PYTHONASYNCIODEBUG
9951009

Doc/whatsnew/3.14.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,13 @@ Other language changes
626626
of HMAC is not available.
627627
(Contributed by Bénédikt Tran in :gh:`99108`.)
628628

629+
* :option:`-X importtime <-X>` now accepts value ``2``, which indicates that
630+
an ``importtime`` entry should also be printed if an imported module has
631+
already been loaded. The ``self`` and ``cumulative`` times for such entries
632+
are replaced by the string ``cached``.
633+
Values above ``2`` are now reserved for future use.
634+
(Contributed by Noah Kim in :gh:`118655`.)
635+
629636
* When subclassing from a pure C type, the C slots for the new type are no
630637
longer replaced with a wrapped version on class creation if they are not
631638
explicitly overridden in the subclass.

Lib/_pyrepl/unix_console.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import struct
3030
import termios
3131
import time
32+
import types
3233
import platform
3334
from fcntl import ioctl
3435

@@ -39,6 +40,13 @@
3940
from .unix_eventqueue import EventQueue
4041
from .utils import wlen
4142

43+
# declare posix optional to allow None assignment on other platforms
44+
posix: types.ModuleType | None
45+
46+
try:
47+
import posix
48+
except ImportError:
49+
posix = None
4250

4351
TYPE_CHECKING = False
4452

@@ -566,11 +574,9 @@ def clear(self):
566574

567575
@property
568576
def input_hook(self):
569-
try:
570-
import posix
571-
except ImportError:
572-
return None
573-
if posix._is_inputhook_installed():
577+
# avoid inline imports here so the repl doesn't get flooded with import
578+
# logging from -Ximporttime=2
579+
if posix is not None and posix._is_inputhook_installed():
574580
return posix._inputhook
575581

576582
def __enable_bracketed_paste(self) -> None:

Lib/_pyrepl/windows_console.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import sys
2525

2626
import ctypes
27+
import types
2728
from ctypes.wintypes import (
2829
_COORD,
2930
WORD,
@@ -58,6 +59,12 @@ def __init__(self, err: int | None, descr: str | None = None) -> None:
5859
self.err = err
5960
self.descr = descr
6061

62+
nt: types.ModuleType | None
63+
64+
try:
65+
import nt
66+
except ImportError:
67+
nt = None
6168

6269
TYPE_CHECKING = False
6370

@@ -121,9 +128,8 @@ class _error(Exception):
121128

122129
def _supports_vt():
123130
try:
124-
import nt
125-
return nt._supports_virtual_terminal()
126-
except (ImportError, AttributeError):
131+
nt._supports_virtual_terminal()
132+
except AttributeError:
127133
return False
128134

129135
class WindowsConsole(Console):
@@ -235,11 +241,9 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None:
235241

236242
@property
237243
def input_hook(self):
238-
try:
239-
import nt
240-
except ImportError:
241-
return None
242-
if nt._is_inputhook_installed():
244+
# avoid inline imports here so the repl doesn't get flooded with import
245+
# logging from -Ximporttime=2
246+
if nt is not None and nt._is_inputhook_installed():
243247
return nt._inputhook
244248

245249
def __write_changed_line(

Lib/test/test_capi/test_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def test_config_get(self):
5757
("home", str | None, None),
5858
("thread_inherit_context", int, None),
5959
("context_aware_warnings", int, None),
60-
("import_time", bool, None),
60+
("import_time", int, None),
6161
("inspect", bool, None),
6262
("install_signal_handlers", bool, None),
6363
("int_max_str_digits", int, None),

Lib/test/test_cmd_line.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,6 +1158,28 @@ def test_cpu_count_default(self):
11581158
res = assert_python_ok('-c', code, PYTHON_CPU_COUNT='default')
11591159
self.assertEqual(self.res2int(res), (os.cpu_count(), os.process_cpu_count()))
11601160

1161+
def test_import_time(self):
1162+
code = "import os"
1163+
res = assert_python_ok('-X', 'importtime', '-c', code)
1164+
res_err = res.err.decode("utf-8")
1165+
self.assertRegex(res_err, r"import time: \s*\d+ \| \s*\d+ \| \s*os")
1166+
self.assertNotRegex(res_err, r"import time: cached\s* \| cached\s* \| os")
1167+
1168+
code = "import os"
1169+
res = assert_python_ok('-X', 'importtime=true', '-c', code)
1170+
res_err = res.err.decode("utf-8")
1171+
self.assertRegex(res_err, r"import time: \s*\d+ \| \s*\d+ \| \s*os")
1172+
self.assertNotRegex(res_err, r"import time: cached\s* \| cached\s* \| os")
1173+
1174+
code = "import os; import os"
1175+
res = assert_python_ok('-X', 'importtime=2', '-c', code)
1176+
res_err = res.err.decode("utf-8")
1177+
self.assertRegex(res_err, r"import time: \s*\d+ \| \s*\d+ \| \s*os")
1178+
self.assertRegex(res_err, r"import time: cached\s* \| cached\s* \| os")
1179+
1180+
assert_python_failure('-X', 'importtime=-1', '-c', code)
1181+
assert_python_failure('-X', 'importtime=3', '-c', code)
1182+
11611183
def res2int(self, res):
11621184
out = res.out.strip().decode("utf-8")
11631185
return tuple(int(i) for i in out.split())

Lib/test/test_embed.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
585585
'faulthandler': False,
586586
'tracemalloc': 0,
587587
'perf_profiling': 0,
588-
'import_time': False,
588+
'import_time': 0,
589589
'thread_inherit_context': DEFAULT_THREAD_INHERIT_CONTEXT,
590590
'context_aware_warnings': DEFAULT_CONTEXT_AWARE_WARNINGS,
591591
'code_debug_ranges': True,
@@ -998,7 +998,7 @@ def test_init_from_config(self):
998998
'hash_seed': 123,
999999
'tracemalloc': 2,
10001000
'perf_profiling': 0,
1001-
'import_time': True,
1001+
'import_time': 0,
10021002
'code_debug_ranges': False,
10031003
'show_ref_count': True,
10041004
'malloc_stats': True,
@@ -1064,7 +1064,7 @@ def test_init_compat_env(self):
10641064
'use_hash_seed': True,
10651065
'hash_seed': 42,
10661066
'tracemalloc': 2,
1067-
'import_time': True,
1067+
'import_time': 0,
10681068
'code_debug_ranges': False,
10691069
'malloc_stats': True,
10701070
'inspect': True,
@@ -1100,7 +1100,7 @@ def test_init_python_env(self):
11001100
'use_hash_seed': True,
11011101
'hash_seed': 42,
11021102
'tracemalloc': 2,
1103-
'import_time': True,
1103+
'import_time': 0,
11041104
'code_debug_ranges': False,
11051105
'malloc_stats': True,
11061106
'inspect': True,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:option:`-X importtime <-X>` now accepts value ``2``, which indicates that
2+
an ``importtime`` entry should also be printed if an imported module has
3+
already been loaded.

Programs/_testembed.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,7 @@ static int test_init_from_config(void)
652652
config.tracemalloc = 2;
653653

654654
putenv("PYTHONPROFILEIMPORTTIME=0");
655-
config.import_time = 1;
655+
config.import_time = 0;
656656

657657
putenv("PYTHONNODEBUGRANGES=0");
658658
config.code_debug_ranges = 0;

Python/import.c

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,16 +246,32 @@ import_ensure_initialized(PyInterpreterState *interp, PyObject *mod, PyObject *n
246246
rc = _PyModuleSpec_IsInitializing(spec);
247247
Py_DECREF(spec);
248248
}
249-
if (rc <= 0) {
249+
if (rc == 0) {
250+
goto done;
251+
}
252+
else if (rc < 0) {
250253
return rc;
251254
}
255+
252256
/* Wait until module is done importing. */
253257
PyObject *value = PyObject_CallMethodOneArg(
254258
IMPORTLIB(interp), &_Py_ID(_lock_unlock_module), name);
255259
if (value == NULL) {
256260
return -1;
257261
}
258262
Py_DECREF(value);
263+
264+
done:
265+
/* When -Ximporttime=2, print an import time entry even if an
266+
* imported module has already been loaded.
267+
*/
268+
if (_PyInterpreterState_GetConfig(interp)->import_time >= 2) {
269+
#define import_level FIND_AND_LOAD(interp).import_level
270+
fprintf(stderr, "import time: cached | cached | %*s\n",
271+
import_level*2, PyUnicode_AsUTF8(name));
272+
#undef import_level
273+
}
274+
259275
return 0;
260276
}
261277

Python/initconfig.c

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ static const PyConfigSpec PYCONFIG_SPEC[] = {
153153
SPEC(home, WSTR_OPT, READ_ONLY, NO_SYS),
154154
SPEC(thread_inherit_context, INT, READ_ONLY, NO_SYS),
155155
SPEC(context_aware_warnings, INT, READ_ONLY, NO_SYS),
156-
SPEC(import_time, BOOL, READ_ONLY, NO_SYS),
156+
SPEC(import_time, UINT, READ_ONLY, NO_SYS),
157157
SPEC(install_signal_handlers, BOOL, READ_ONLY, NO_SYS),
158158
SPEC(isolated, BOOL, READ_ONLY, NO_SYS), // sys.flags.isolated
159159
#ifdef MS_WINDOWS
@@ -312,7 +312,8 @@ The following implementation-specific options are available:\n\
312312
"-X gil=[0|1]: enable (1) or disable (0) the GIL; also PYTHON_GIL\n"
313313
#endif
314314
"\
315-
-X importtime: show how long each import takes; also PYTHONPROFILEIMPORTTIME\n\
315+
-X importtime[=2]: show how long each import takes; -X importtime=2 also prints\
316+
rows for imports of already-loaded modules; also PYTHONPROFILEIMPORTTIME\n\
316317
-X int_max_str_digits=N: limit the size of int<->str conversions;\n\
317318
0 disables the limit; also PYTHONINTMAXSTRDIGITS\n\
318319
-X no_debug_ranges: don't include extra location information in code objects;\n\
@@ -2246,6 +2247,63 @@ config_init_run_presite(PyConfig *config)
22462247
}
22472248
#endif
22482249

2250+
/* Set `config->import_time` based on `value` from `-Ximporttime(=.*)?`. */
2251+
static PyStatus
2252+
config_set_import_time(PyConfig *config, const wchar_t *value)
2253+
{
2254+
int numeric_value;
2255+
2256+
// If no value is specified or the value is not an integer, use 1.
2257+
if (*value == 0 || config_wstr_to_int(value, &numeric_value) != 0) {
2258+
config->import_time = 1;
2259+
}
2260+
2261+
/* -Ximporttime=1 incurs the default behavior. -Ximporttime=2 also
2262+
* prints import cache hits. All other numeric values are reserved.
2263+
*/
2264+
else if (0 <= numeric_value && numeric_value <= 2) {
2265+
config->import_time = numeric_value;
2266+
}
2267+
2268+
else {
2269+
return _PyStatus_ERR(
2270+
"-X importtime: numeric values other than 1 or 2 are "
2271+
"reserved for future use");
2272+
}
2273+
2274+
return _PyStatus_OK();
2275+
}
2276+
2277+
/* Configure `config->import_time` by checking -Ximporttime then the
2278+
* PYTHONPROFILEIMPORTTIME environment variable. Defaults to 0.
2279+
*/
2280+
static PyStatus
2281+
config_read_import_time(PyConfig *config)
2282+
{
2283+
/* Check the -X option first. */
2284+
const wchar_t *xoption_value = NULL;
2285+
xoption_value = config_get_xoption_value(config, L"importtime");
2286+
if (xoption_value != NULL) {
2287+
return config_set_import_time(config, xoption_value);
2288+
}
2289+
2290+
/* If there's no -Ximporttime, look for ENV flag */
2291+
wchar_t *env_value = NULL;
2292+
/* `CONFIG_GET_ENV_DUP` requires dest to be initialized to `NULL`. */
2293+
PyStatus status = CONFIG_GET_ENV_DUP(config, &env_value,
2294+
L"PYTHONPROFILEIMPORTTIME",
2295+
"PYTHONPROFILEIMPORTTIME");
2296+
if (_PyStatus_EXCEPTION(status)) {
2297+
return status;
2298+
}
2299+
if (env_value != NULL) {
2300+
status = config_set_import_time(config, env_value);
2301+
PyMem_RawFree(env_value);
2302+
return status;
2303+
}
2304+
2305+
return _PyStatus_OK();
2306+
}
22492307

22502308
static PyStatus
22512309
config_read_complex_options(PyConfig *config)
@@ -2257,17 +2315,18 @@ config_read_complex_options(PyConfig *config)
22572315
config->faulthandler = 1;
22582316
}
22592317
}
2260-
if (config_get_env(config, "PYTHONPROFILEIMPORTTIME")
2261-
|| config_get_xoption(config, L"importtime")) {
2262-
config->import_time = 1;
2263-
}
2264-
22652318
if (config_get_env(config, "PYTHONNODEBUGRANGES")
22662319
|| config_get_xoption(config, L"no_debug_ranges")) {
22672320
config->code_debug_ranges = 0;
22682321
}
22692322

22702323
PyStatus status;
2324+
2325+
status = config_read_import_time(config);
2326+
if (_PyStatus_EXCEPTION(status)) {
2327+
return status;
2328+
}
2329+
22712330
if (config->tracemalloc < 0) {
22722331
status = config_init_tracemalloc(config);
22732332
if (_PyStatus_EXCEPTION(status)) {

0 commit comments

Comments
 (0)