Skip to content

Commit a8f8d5b

Browse files
authored
bpo-29585: optimize site.py startup time (GH-136)
Avoid importing `sysconfig` from `site` by copying minimum code. Python startup is 5% faster on Linux and 30% faster on macOS
1 parent 79d37ae commit a8f8d5b

File tree

8 files changed

+96
-43
lines changed

8 files changed

+96
-43
lines changed

Lib/site.py

Lines changed: 52 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ def removeduppaths():
124124
# if they only differ in case); turn relative paths into absolute
125125
# paths.
126126
dir, dircase = makepath(dir)
127-
if not dircase in known_paths:
127+
if dircase not in known_paths:
128128
L.append(dir)
129129
known_paths.add(dircase)
130130
sys.path[:] = L
@@ -234,6 +234,46 @@ def check_enableusersite():
234234

235235
return True
236236

237+
238+
# NOTE: sysconfig and it's dependencies are relatively large but site module
239+
# needs very limited part of them.
240+
# To speedup startup time, we have copy of them.
241+
#
242+
# See https://bugs.python.org/issue29585
243+
244+
# Copy of sysconfig._getuserbase()
245+
def _getuserbase():
246+
env_base = os.environ.get("PYTHONUSERBASE", None)
247+
if env_base:
248+
return env_base
249+
250+
def joinuser(*args):
251+
return os.path.expanduser(os.path.join(*args))
252+
253+
if os.name == "nt":
254+
base = os.environ.get("APPDATA") or "~"
255+
return joinuser(base, "Python")
256+
257+
if sys.platform == "darwin" and sys._framework:
258+
return joinuser("~", "Library", sys._framework,
259+
"%d.%d" % sys.version_info[:2])
260+
261+
return joinuser("~", ".local")
262+
263+
264+
# Same to sysconfig.get_path('purelib', os.name+'_user')
265+
def _get_path(userbase):
266+
version = sys.version_info
267+
268+
if os.name == 'nt':
269+
return f'{userbase}/Python{version[0]}{version[1]}/site-packages'
270+
271+
if sys.platform == 'darwin' and sys._framework:
272+
return f'{userbase}/lib/python/site-packages'
273+
274+
return f'{userbase}/lib/python{version[0]}.{version[1]}/site-packages'
275+
276+
237277
def getuserbase():
238278
"""Returns the `user base` directory path.
239279
@@ -242,33 +282,23 @@ def getuserbase():
242282
it.
243283
"""
244284
global USER_BASE
245-
if USER_BASE is not None:
246-
return USER_BASE
247-
from sysconfig import get_config_var
248-
USER_BASE = get_config_var('userbase')
285+
if USER_BASE is None:
286+
USER_BASE = _getuserbase()
249287
return USER_BASE
250288

289+
251290
def getusersitepackages():
252291
"""Returns the user-specific site-packages directory path.
253292
254293
If the global variable ``USER_SITE`` is not initialized yet, this
255294
function will also set it.
256295
"""
257296
global USER_SITE
258-
user_base = getuserbase() # this will also set USER_BASE
259-
260-
if USER_SITE is not None:
261-
return USER_SITE
262-
263-
from sysconfig import get_path
297+
userbase = getuserbase() # this will also set USER_BASE
264298

265-
if sys.platform == 'darwin':
266-
from sysconfig import get_config_var
267-
if get_config_var('PYTHONFRAMEWORK'):
268-
USER_SITE = get_path('purelib', 'osx_framework_user')
269-
return USER_SITE
299+
if USER_SITE is None:
300+
USER_SITE = _get_path(userbase)
270301

271-
USER_SITE = get_path('purelib', '%s_user' % os.name)
272302
return USER_SITE
273303

274304
def addusersitepackages(known_paths):
@@ -310,15 +340,11 @@ def getsitepackages(prefixes=None):
310340
else:
311341
sitepackages.append(prefix)
312342
sitepackages.append(os.path.join(prefix, "lib", "site-packages"))
313-
if sys.platform == "darwin":
314-
# for framework builds *only* we add the standard Apple
315-
# locations.
316-
from sysconfig import get_config_var
317-
framework = get_config_var("PYTHONFRAMEWORK")
318-
if framework:
319-
sitepackages.append(
320-
os.path.join("/Library", framework,
321-
'%d.%d' % sys.version_info[:2], "site-packages"))
343+
# for framework builds *only* we add the standard Apple locations.
344+
if sys.platform == "darwin" and sys._framework:
345+
sitepackages.append(
346+
os.path.join("/Library", framework,
347+
'%d.%d' % sys.version_info[:2], "site-packages"))
322348
return sitepackages
323349

324350
def addsitepackages(known_paths, prefixes=None):

Lib/sysconfig.py

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
'scripts': '{base}/Scripts',
5252
'data': '{base}',
5353
},
54+
# NOTE: When modifying "purelib" scheme, update site._get_path() too.
5455
'nt_user': {
5556
'stdlib': '{userbase}/Python{py_version_nodot}',
5657
'platstdlib': '{userbase}/Python{py_version_nodot}',
@@ -177,32 +178,25 @@ def _get_default_scheme():
177178
return os.name
178179

179180

181+
# NOTE: site.py has copy of this function.
182+
# Sync it when modify this function.
180183
def _getuserbase():
181184
env_base = os.environ.get("PYTHONUSERBASE", None)
185+
if env_base:
186+
return env_base
182187

183188
def joinuser(*args):
184189
return os.path.expanduser(os.path.join(*args))
185190

186191
if os.name == "nt":
187192
base = os.environ.get("APPDATA") or "~"
188-
if env_base:
189-
return env_base
190-
else:
191-
return joinuser(base, "Python")
193+
return joinuser(base, "Python")
192194

193-
if sys.platform == "darwin":
194-
framework = get_config_var("PYTHONFRAMEWORK")
195-
if framework:
196-
if env_base:
197-
return env_base
198-
else:
199-
return joinuser("~", "Library", framework, "%d.%d" %
200-
sys.version_info[:2])
195+
if sys.platform == "darwin" and sys._framework:
196+
return joinuser("~", "Library", sys._framework,
197+
"%d.%d" % sys.version_info[:2])
201198

202-
if env_base:
203-
return env_base
204-
else:
205-
return joinuser("~", ".local")
199+
return joinuser("~", ".local")
206200

207201

208202
def _parse_makefile(filename, vars=None):

Lib/test/test_site.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,13 @@ def test_addsitedir(self):
180180
finally:
181181
pth_file.cleanup()
182182

183+
def test_getuserbase(self):
184+
self.assertEqual(site._getuserbase(), sysconfig._getuserbase())
185+
186+
def test_get_path(self):
187+
self.assertEqual(site._get_path(site._getuserbase()),
188+
sysconfig.get_path('purelib', os.name + '_user'))
189+
183190
@unittest.skipUnless(site.ENABLE_USER_SITE, "requires access to PEP 370 "
184191
"user-site (site.ENABLE_USER_SITE)")
185192
def test_s_option(self):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Avoid importing ``sysconfig`` from ``site`` to improve startup speed. Python
2+
startup is about 5% faster on Linux and 30% faster on macOS.

Python/sysmodule.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1965,6 +1965,7 @@ _PySys_BeginInit(void)
19651965
SET_SYS_FROM_STRING("_git",
19661966
Py_BuildValue("(szz)", "CPython", _Py_gitidentifier(),
19671967
_Py_gitversion()));
1968+
SET_SYS_FROM_STRING("_framework", PyUnicode_FromString(PYTHONFRAMEWORK));
19681969
SET_SYS_FROM_STRING("api_version",
19691970
PyLong_FromLong(PYTHON_API_VERSION));
19701971
SET_SYS_FROM_STRING("copyright",

configure

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,7 @@ infodir
783783
docdir
784784
oldincludedir
785785
includedir
786+
runstatedir
786787
localstatedir
787788
sharedstatedir
788789
sysconfdir
@@ -896,6 +897,7 @@ datadir='${datarootdir}'
896897
sysconfdir='${prefix}/etc'
897898
sharedstatedir='${prefix}/com'
898899
localstatedir='${prefix}/var'
900+
runstatedir='${localstatedir}/run'
899901
includedir='${prefix}/include'
900902
oldincludedir='/usr/include'
901903
docdir='${datarootdir}/doc/${PACKAGE_TARNAME}'
@@ -1148,6 +1150,15 @@ do
11481150
| -silent | --silent | --silen | --sile | --sil)
11491151
silent=yes ;;
11501152

1153+
-runstatedir | --runstatedir | --runstatedi | --runstated \
1154+
| --runstate | --runstat | --runsta | --runst | --runs \
1155+
| --run | --ru | --r)
1156+
ac_prev=runstatedir ;;
1157+
-runstatedir=* | --runstatedir=* | --runstatedi=* | --runstated=* \
1158+
| --runstate=* | --runstat=* | --runsta=* | --runst=* | --runs=* \
1159+
| --run=* | --ru=* | --r=*)
1160+
runstatedir=$ac_optarg ;;
1161+
11511162
-sbindir | --sbindir | --sbindi | --sbind | --sbin | --sbi | --sb)
11521163
ac_prev=sbindir ;;
11531164
-sbindir=* | --sbindir=* | --sbindi=* | --sbind=* | --sbin=* \
@@ -1285,7 +1296,7 @@ fi
12851296
for ac_var in exec_prefix prefix bindir sbindir libexecdir datarootdir \
12861297
datadir sysconfdir sharedstatedir localstatedir includedir \
12871298
oldincludedir docdir infodir htmldir dvidir pdfdir psdir \
1288-
libdir localedir mandir
1299+
libdir localedir mandir runstatedir
12891300
do
12901301
eval ac_val=\$$ac_var
12911302
# Remove trailing slashes.
@@ -1438,6 +1449,7 @@ Fine tuning of the installation directories:
14381449
--sysconfdir=DIR read-only single-machine data [PREFIX/etc]
14391450
--sharedstatedir=DIR modifiable architecture-independent data [PREFIX/com]
14401451
--localstatedir=DIR modifiable single-machine data [PREFIX/var]
1452+
--runstatedir=DIR modifiable per-process data [LOCALSTATEDIR/run]
14411453
--libdir=DIR object code libraries [EPREFIX/lib]
14421454
--includedir=DIR C header files [PREFIX/include]
14431455
--oldincludedir=DIR C header files for non-gcc [/usr/include]
@@ -3231,6 +3243,12 @@ fi
32313243

32323244

32333245

3246+
3247+
cat >>confdefs.h <<_ACEOF
3248+
#define PYTHONFRAMEWORK "${PYTHONFRAMEWORK}"
3249+
_ACEOF
3250+
3251+
32343252
##AC_ARG_WITH(dyld,
32353253
## AS_HELP_STRING([--with-dyld],
32363254
## [Use (OpenStep|Rhapsody) dynamic linker]))

configure.ac

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,8 @@ AC_SUBST(FRAMEWORKPYTHONW)
355355
AC_SUBST(FRAMEWORKUNIXTOOLSPREFIX)
356356
AC_SUBST(FRAMEWORKINSTALLAPPSPREFIX)
357357

358+
AC_DEFINE_UNQUOTED(PYTHONFRAMEWORK, "${PYTHONFRAMEWORK}", [framework name])
359+
358360
##AC_ARG_WITH(dyld,
359361
## AS_HELP_STRING([--with-dyld],
360362
## [Use (OpenStep|Rhapsody) dynamic linker]))

pyconfig.h.in

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,6 +1247,9 @@
12471247
/* Define as the preferred size in bits of long digits */
12481248
#undef PYLONG_BITS_IN_DIGIT
12491249

1250+
/* framework name */
1251+
#undef PYTHONFRAMEWORK
1252+
12501253
/* Define if you want to coerce the C locale to a UTF-8 based locale */
12511254
#undef PY_COERCE_C_LOCALE
12521255

0 commit comments

Comments
 (0)