Skip to content

Commit d078e03

Browse files
authored
PERF: thread local context (#1419)
1 parent cf9f220 commit d078e03

27 files changed

+154
-254
lines changed

.github/workflows/test_proj_latest.yaml

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -58,21 +58,6 @@ jobs:
5858
run: |
5959
python -m pytest
6060
61-
- name: Test Global Context
62-
shell: bash
63-
env:
64-
PYPROJ_GLOBAL_CONTEXT: ON
65-
run: |
66-
python -m pytest
67-
68-
- name: Test Network & Global Context
69-
shell: bash
70-
env:
71-
PROJ_NETWORK: ON
72-
PYPROJ_GLOBAL_CONTEXT: ON
73-
run: |
74-
python -m pytest
75-
7661
- name: Test Grids
7762
shell: bash
7863
run: |

.github/workflows/tests.yaml

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -89,23 +89,6 @@ jobs:
8989
. testenv/bin/activate
9090
python -m pytest
9191
92-
- name: Test Global Context
93-
shell: bash
94-
env:
95-
PYPROJ_GLOBAL_CONTEXT: ON
96-
run: |
97-
. testenv/bin/activate
98-
python -m pytest
99-
100-
- name: Test Network & Global Context
101-
shell: bash
102-
env:
103-
PROJ_NETWORK: ON
104-
PYPROJ_GLOBAL_CONTEXT: ON
105-
run: |
106-
. testenv/bin/activate
107-
python -m pytest
108-
10992
- name: Test Grids
11093
shell: bash
11194
run: |
@@ -197,22 +180,6 @@ jobs:
197180
run: |
198181
micromamba run -n test python -m pytest
199182
200-
- name: Test Global Context
201-
shell: bash
202-
env:
203-
PROJ_NETWORK: OFF
204-
PYPROJ_GLOBAL_CONTEXT: ON
205-
run: |
206-
micromamba run -n test python -m pytest
207-
208-
- name: Test Network & Global Context
209-
shell: bash
210-
env:
211-
PROJ_NETWORK: ON
212-
PYPROJ_GLOBAL_CONTEXT: ON
213-
run: |
214-
micromamba run -n test python -m pytest
215-
216183
- name: Test Grids
217184
shell: bash
218185
env:

.pylintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ extension-pkg-whitelist=pyproj._crs,
77
pyproj._sync,
88
pyproj._network,
99
pyproj._geod,
10-
pyproj._datadir,
10+
pyproj._context,
1111
pyproj._compat,
1212
pyproj.database,
1313
pyproj.list

docs/api/global_context.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
Global Context
44
==============
55

6+
.. deprecated:: 3.7.0 No longer necessary as there is only one context per thread now.
7+
68
If you have a single-threaded application that generates many objects,
79
enabling the use of the global context can provide performance enhancements.
810

docs/history.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Latest
77
- DEP: Minimum supported Python version 3.10 (pull #1357)
88
- DEP: Minimum PROJ version 9.2 (pull #1394)
99
- ENH: Add :meth:`CRS.is_deprecated` and :meth:`CRS.get_non_deprecated` (pull #1383)
10+
- PERF: thread local context (issue #1133)
1011

1112
3.6.1
1213
------

pyproj/__init__.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@
3232
import warnings
3333

3434
import pyproj.network
35-
from pyproj._datadir import ( # noqa: F401 pylint: disable=unused-import
36-
_pyproj_global_context_initialize,
35+
from pyproj._context import ( # noqa: F401 pylint: disable=unused-import
3736
set_use_global_context,
3837
)
3938
from pyproj._show_versions import ( # noqa: F401 pylint: disable=unused-import
@@ -85,10 +84,7 @@
8584
]
8685
__proj_version__ = proj_version_str
8786

88-
8987
try:
90-
_pyproj_global_context_initialize()
88+
pyproj.network.set_ca_bundle_path()
9189
except DataDirError as err:
9290
warnings.warn(str(err))
93-
94-
pyproj.network.set_ca_bundle_path()

pyproj/_datadir.pxd renamed to pyproj/_context.pxd

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,4 @@ include "proj.pxi"
22

33
cpdef str _get_proj_error()
44
cpdef void _clear_proj_error() noexcept
5-
cdef PJ_CONTEXT* PYPROJ_GLOBAL_CONTEXT
65
cdef PJ_CONTEXT* pyproj_context_create() except *
7-
cdef void pyproj_context_destroy(PJ_CONTEXT* context) except *
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
def _pyproj_global_context_initialize() -> None: ...
21
def get_user_data_dir(create: bool = False) -> str: ...
3-
def _global_context_set_data_dir() -> None: ...
2+
def _set_context_data_dir() -> None: ...
3+
def _set_context_ca_bundle_path(ca_bundle_path: str) -> None: ...
4+
def _set_context_network_enabled() -> None: ...
45
def set_use_global_context(active: bool | None = None) -> None: ...
56
def _clear_proj_error() -> None: ...
67
def _get_proj_error() -> str: ...

pyproj/_datadir.pyx renamed to pyproj/_context.pyx

Lines changed: 81 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import logging
22
import os
3+
import threading
34
import warnings
45

6+
from cpython.pythread cimport PyThread_tss_create, PyThread_tss_get, PyThread_tss_set
57
from libc.stdlib cimport free, malloc
68

79
from pyproj._compat cimport cstrencode
@@ -12,17 +14,22 @@ from pyproj.utils import strtobool
1214
# https://docs.python.org/3/howto/logging.html#configuring-logging-for-a-library
1315
_LOGGER = logging.getLogger("pyproj")
1416
_LOGGER.addHandler(logging.NullHandler())
15-
# default to False is the safest mode
16-
# as it supports multithreading
17-
_USE_GLOBAL_CONTEXT = strtobool(os.environ.get("PYPROJ_GLOBAL_CONTEXT", "OFF"))
1817
# static user data directory to prevent core dumping
1918
# see: https://github.com/pyproj4/pyproj/issues/678
2019
cdef const char* _USER_DATA_DIR = proj_context_get_user_writable_directory(NULL, False)
2120
# Store the message from any internal PROJ errors
2221
cdef str _INTERNAL_PROJ_ERROR = None
22+
# global variables
23+
cdef bint _NETWORK_ENABLED = strtobool(os.environ.get("PROJ_NETWORK", "OFF"))
24+
cdef char* _CA_BUNDLE_PATH = ""
25+
# The key to get the context in each thread
26+
cdef Py_tss_t CONTEXT_THREAD_KEY
27+
2328

2429
def set_use_global_context(active=None):
2530
"""
31+
.. deprecated:: 3.7.0 No longer necessary as there is only one context per thread now.
32+
2633
.. versionadded:: 3.0.0
2734
2835
Activates the usage of the global context. Using this
@@ -44,10 +51,17 @@ def set_use_global_context(active=None):
4451
the environment variable PYPROJ_GLOBAL_CONTEXT and defaults
4552
to False if it is not found.
4653
"""
47-
global _USE_GLOBAL_CONTEXT
4854
if active is None:
4955
active = strtobool(os.environ.get("PYPROJ_GLOBAL_CONTEXT", "OFF"))
50-
_USE_GLOBAL_CONTEXT = bool(active)
56+
if active:
57+
warnings.warn(
58+
(
59+
"PYPROJ_GLOBAL_CONTEXT is no longer necessary in pyproj 3.7+ "
60+
"and does not do anything."
61+
),
62+
FutureWarning,
63+
stacklevel=2,
64+
)
5165

5266

5367
def get_user_data_dir(create=False):
@@ -74,7 +88,7 @@ def get_user_data_dir(create=False):
7488
The user writable data directory.
7589
"""
7690
return proj_context_get_user_writable_directory(
77-
PYPROJ_GLOBAL_CONTEXT, bool(create)
91+
pyproj_context_create(), bool(create)
7892
)
7993

8094

@@ -124,7 +138,7 @@ cdef void set_context_data_dir(PJ_CONTEXT* context) except *:
124138
cdef bytes b_database_path = cstrencode(os.path.join(data_dir_list[0], "proj.db"))
125139
cdef const char* c_database_path = b_database_path
126140
if not proj_context_set_database_path(context, c_database_path, NULL, NULL):
127-
warnings.warn("pyproj unable to set database path.")
141+
warnings.warn("pyproj unable to set PROJ database path.")
128142
cdef int dir_list_len = len(data_dir_list)
129143
cdef const char **c_data_dir = <const char **>malloc(
130144
(dir_list_len + 1) * sizeof(const char*)
@@ -147,6 +161,8 @@ cdef void pyproj_context_initialize(PJ_CONTEXT* context) except *:
147161
proj_log_func(context, NULL, pyproj_log_function)
148162
proj_context_use_proj4_init_rules(context, 1)
149163
set_context_data_dir(context)
164+
proj_context_set_ca_bundle_path(context, _CA_BUNDLE_PATH)
165+
proj_context_set_enable_network(context, _NETWORK_ENABLED)
150166

151167

152168
cdef class ContextManager:
@@ -170,35 +186,75 @@ cdef class ContextManager:
170186
return context_manager
171187

172188

173-
# Different libraries that modify the PROJ global context will influence
174-
# each other without realizing it. Due to this, pyproj is creating it's own
175-
# global context so that it doesn't bother other libraries and is insulated
176-
# against possible external changes made to the PROJ global context.
177-
# See: https://github.com/pyproj4/pyproj/issues/722
178-
cdef PJ_CONTEXT* PYPROJ_GLOBAL_CONTEXT = proj_context_create()
179-
cdef ContextManager CONTEXT_MANAGER = ContextManager.create(PYPROJ_GLOBAL_CONTEXT)
189+
class ContextManagerLocal(threading.local):
190+
"""
191+
Threading local instance for cython ContextManager class.
192+
"""
193+
194+
def __init__(self):
195+
self.context_manager = None # Initialises in each thread
196+
super().__init__()
197+
180198

199+
_CONTEXT_MANAGER_LOCAL = ContextManagerLocal()
181200

182201
cdef PJ_CONTEXT* pyproj_context_create() except *:
183202
"""
184203
Create and initialize the context(s) for pyproj.
185204
This also manages whether the global context is used.
186205
"""
187-
if _USE_GLOBAL_CONTEXT:
188-
return PYPROJ_GLOBAL_CONTEXT
189-
return proj_context_clone(PYPROJ_GLOBAL_CONTEXT)
206+
global _CONTEXT_MANAGER_LOCAL
207+
208+
if PyThread_tss_create(&CONTEXT_THREAD_KEY) != 0:
209+
raise MemoryError("Unable to create key for PROJ context in thread.")
210+
cdef const void *thread_pyproj_context = PyThread_tss_get(&CONTEXT_THREAD_KEY)
211+
cdef PJ_CONTEXT* pyproj_context = NULL
212+
if thread_pyproj_context == NULL:
213+
pyproj_context = proj_context_create()
214+
pyproj_context_initialize(pyproj_context)
215+
PyThread_tss_set(&CONTEXT_THREAD_KEY, pyproj_context)
216+
_CONTEXT_MANAGER_LOCAL.context_manager = ContextManager.create(pyproj_context)
217+
else:
218+
pyproj_context = <PJ_CONTEXT*>thread_pyproj_context
219+
return pyproj_context
220+
221+
222+
def get_context_manager():
223+
"""
224+
This returns the manager for the context
225+
responsible for cleanup
226+
"""
227+
return _CONTEXT_MANAGER_LOCAL.context_manager
190228

191-
cdef void pyproj_context_destroy(PJ_CONTEXT* context) except *:
229+
230+
cpdef _set_context_data_dir():
192231
"""
193-
Destroy context only if not the global context
232+
Python compatible function to set the
233+
data directory on the current context
194234
"""
195-
if context != PYPROJ_GLOBAL_CONTEXT:
196-
proj_context_destroy(context)
235+
set_context_data_dir(pyproj_context_create())
236+
197237

238+
cpdef _set_context_ca_bundle_path(str ca_bundle_path):
239+
"""
240+
Python compatible function to set the
241+
CA Bundle path on the current context
242+
and cache for future generated contexts
243+
"""
244+
global _CA_BUNDLE_PATH
198245

199-
cpdef _pyproj_global_context_initialize():
200-
pyproj_context_initialize(PYPROJ_GLOBAL_CONTEXT)
246+
b_ca_bundle_path = cstrencode(ca_bundle_path)
247+
_CA_BUNDLE_PATH = b_ca_bundle_path
248+
proj_context_set_ca_bundle_path(pyproj_context_create(), _CA_BUNDLE_PATH)
201249

202250

203-
cpdef _global_context_set_data_dir():
204-
set_context_data_dir(PYPROJ_GLOBAL_CONTEXT)
251+
cpdef _set_context_network_enabled(bint enabled):
252+
"""
253+
Python compatible function to set the
254+
network enables on the current context
255+
and cache for future generated contexts
256+
"""
257+
global _NETWORK_ENABLED
258+
259+
_NETWORK_ENABLED = enabled
260+
proj_context_set_enable_network(pyproj_context_create(), _NETWORK_ENABLED)

pyproj/_crs.pxd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ cdef create_area_of_use(PJ_CONTEXT* context, PJ* projobj)
4747
cdef class Base:
4848
cdef PJ *projobj
4949
cdef PJ_CONTEXT* context
50+
cdef readonly object _context_manager
5051
cdef readonly str name
5152
cdef readonly str _remarks
5253
cdef readonly str _scope

0 commit comments

Comments
 (0)