Skip to content

Commit 0ed040c

Browse files
committed
Add -X importtime=2 for logging an importtime message for already-loaded modules
1 parent 6163841 commit 0ed040c

File tree

11 files changed

+151
-23
lines changed

11 files changed

+151
-23
lines changed

Doc/c-api/init_config.rst

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

Doc/using/cmdline.rst

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

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

548+
.. versionchanged:: next
549+
550+
Added ``-X importtime=2`` to also trace imports of loaded modules,
551+
and reserved values other than ``1`` and ``2`` for future use.
552+
544553
* ``-X dev``: enable :ref:`Python Development Mode <devmode>`, introducing
545554
additional runtime checks that are too expensive to be enabled by
546555
default. See also :envvar:`PYTHONDEVMODE`.
@@ -981,12 +990,17 @@ conflict.
981990

982991
.. envvar:: PYTHONPROFILEIMPORTTIME
983992

984-
If this environment variable is set to a non-empty string, Python will
985-
show how long each import takes.
993+
If this environment variable is set to ``1``, Python will show
994+
how long each import takes. If set to ``2``, Python will include output for
995+
imported modules that have already been loaded.
986996
This is equivalent to setting the :option:`-X` ``importtime`` option.
987997

988998
.. versionadded:: 3.7
989999

1000+
.. versionchanged:: next
1001+
1002+
Added ``PYTHONPROFILEIMPORTTIME=2`` to also trace imports of loaded modules.
1003+
9901004

9911005
.. envvar:: PYTHONASYNCIODEBUG
9921006

Doc/whatsnew/3.14.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,13 @@ Other language changes
463463
This implementation is used as a fallback when the OpenSSL implementation
464464
of HMAC is not available.
465465
(Contributed by Bénédikt Tran in :gh:`99108`.)
466+
467+
* :option:`-X importtime <-X>` now accepts value ``2``, which indicates that
468+
an ``importtime`` entry should also be printed if an imported module has
469+
already been loaded. The ``self`` and ``cumulative`` times for such entries
470+
are replaced by the string ``cached``.
471+
Values above ``2`` are now reserved for future use.
472+
(Contributed by Noah Kim in :gh:`118655`.)
466473

467474

468475
.. _whatsnew314-pep765:

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/test/test_capi/test_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def test_config_get(self):
5656
("home", str | None, None),
5757
("thread_inherit_context", int, None),
5858
("context_aware_warnings", int, None),
59-
("import_time", bool, None),
59+
("import_time", int, None),
6060
("inspect", bool, None),
6161
("install_signal_handlers", bool, None),
6262
("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
@@ -1067,6 +1067,28 @@ def test_cpu_count_default(self):
10671067
res = assert_python_ok('-c', code, PYTHON_CPU_COUNT='default')
10681068
self.assertEqual(self.res2int(res), (os.cpu_count(), os.process_cpu_count()))
10691069

1070+
def test_import_time(self):
1071+
code = "import os"
1072+
res = assert_python_ok('-X', 'importtime', '-c', code)
1073+
res_err = res.err.decode("utf-8")
1074+
self.assertRegex(res_err, r"import time: \s*\d+ \| \s*\d+ \| \s*os")
1075+
self.assertNotRegex(res_err, r"import time: cached\s* \| cached\s* \| os")
1076+
1077+
code = "import os"
1078+
res = assert_python_ok('-X', 'importtime=true', '-c', code)
1079+
res_err = res.err.decode("utf-8")
1080+
self.assertRegex(res_err, r"import time: \s*\d+ \| \s*\d+ \| \s*os")
1081+
self.assertNotRegex(res_err, r"import time: cached\s* \| cached\s* \| os")
1082+
1083+
code = "import os; import os"
1084+
res = assert_python_ok('-X', 'importtime=2', '-c', code)
1085+
res_err = res.err.decode("utf-8")
1086+
self.assertRegex(res_err, r"import time: \s*\d+ \| \s*\d+ \| \s*os")
1087+
self.assertRegex(res_err, r"import time: cached\s* \| cached\s* \| os")
1088+
1089+
assert_python_failure('-X', 'importtime=-1', '-c', code)
1090+
assert_python_failure('-X', 'importtime=3', '-c', code)
1091+
10701092
def res2int(self, res):
10711093
out = res.out.strip().decode("utf-8")
10721094
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
@@ -232,16 +232,32 @@ import_ensure_initialized(PyInterpreterState *interp, PyObject *mod, PyObject *n
232232
rc = _PyModuleSpec_IsInitializing(spec);
233233
Py_DECREF(spec);
234234
}
235-
if (rc <= 0) {
235+
if (rc == 0) {
236+
goto done;
237+
}
238+
else if (rc < 0) {
236239
return rc;
237240
}
241+
238242
/* Wait until module is done importing. */
239243
PyObject *value = PyObject_CallMethodOneArg(
240244
IMPORTLIB(interp), &_Py_ID(_lock_unlock_module), name);
241245
if (value == NULL) {
242246
return -1;
243247
}
244248
Py_DECREF(value);
249+
250+
done:
251+
/* When -Ximporttime=2, print an import time entry even if an
252+
* imported module has already been loaded.
253+
*/
254+
if (_PyInterpreterState_GetConfig(interp)->import_time >= 2) {
255+
#define import_level FIND_AND_LOAD(interp).import_level
256+
fprintf(stderr, "import time: cached | cached | %*s\n",
257+
import_level*2, PyUnicode_AsUTF8(name));
258+
#undef import_level
259+
}
260+
245261
return 0;
246262
}
247263

0 commit comments

Comments
 (0)