Skip to content

Commit 2cd2ed4

Browse files
committed
Shuffled code inside ~._child_process_profiling
line_profiler/_child_process_profiling/ - The functions `pth_hook.py::_setup_in_child_process()` and `::_wrap_os_fork()` have been relocated to eponymous instance methods of `cache.py::LineProfilingCache` - The implementations of `pth_hook.py::{write,load}_pth_hook()` and `multiprocessing_patches.py::PickleHook.__setstate__()` are updated accordingly
1 parent 3ca5caf commit 2cd2ed4

3 files changed

Lines changed: 124 additions & 135 deletions

File tree

line_profiler/_child_process_profiling/cache.py

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44
"""
55
from __future__ import annotations
66

7+
import atexit
78
import dataclasses
89
import os
910
try:
1011
import _pickle as pickle
1112
except ImportError:
1213
import pickle # type: ignore[assignment,no-redef]
1314
from collections.abc import Collection, Callable
14-
from functools import partial, cached_property
15+
from functools import partial, cached_property, wraps
1516
from operator import setitem
1617
from pathlib import Path
1718
from pickle import HIGHEST_PROTOCOL
@@ -20,6 +21,14 @@
2021
from typing_extensions import Self, ParamSpec
2122

2223
from .. import _diagnostics as diagnostics
24+
from ..autoprofile.autoprofile import (
25+
# Note: we need this to equip the profiler with the
26+
# `.add_imported_function_or_module()` pseudo-method
27+
# (see `kernprof.py::_write_preimports()`), which is required for
28+
# the preimports to work
29+
_extend_line_profiler_for_profiling_imports as upgrade_profiler,
30+
)
31+
from ..curated_profiling import CuratedProfilerContext
2332
from ..line_profiler import LineProfiler, LineStats
2433
# Note: this should have been defined here in this file, but we moved it
2534
# over to `~._child_process_hook` because that module contains the .pth
@@ -227,6 +236,116 @@ def _debug_output(self, msg: str) -> None:
227236
except OSError: # Cache dir may have been rm-ed during cleanup
228237
pass
229238

239+
def _setup_in_child_process(
240+
self,
241+
wrap_os_fork: bool = False,
242+
context: str = '',
243+
prof: LineProfiler | None = None,
244+
) -> bool:
245+
"""
246+
Set up shop in a forked/spawned child process so that
247+
(line-)profiling can extend therein.
248+
249+
Args:
250+
wrap_os_fork (bool):
251+
Whether to wrap :py:func:`os.fork` which handles
252+
profiling; already-forked child processes should set
253+
this to false
254+
context (str):
255+
Optional context from which the function is called, to
256+
be used in log messages
257+
prof (LineProfiler | None):
258+
Optional profiler instance to associate with the cache;
259+
if not provided, an instance is created
260+
261+
Returns:
262+
has_set_up (bool):
263+
False the instance has already been set up prior to
264+
calling this function, true otherwise
265+
"""
266+
if not context:
267+
context = '...'
268+
self._debug_output(f'Setting up ({context})...')
269+
if self.profiler is not None: # Already set up
270+
self._debug_output(f'Setup aborted ({context})')
271+
return False
272+
273+
# Create a profiler instance and manage it with
274+
# `CuratedProfilerContext`
275+
if prof is None:
276+
prof = LineProfiler()
277+
self.profiler = prof
278+
upgrade_profiler(prof)
279+
ctx = CuratedProfilerContext(prof, insert_builtin=self.insert_builtin)
280+
ctx.install()
281+
self.add_cleanup(ctx.uninstall)
282+
self._debug_output(f'Set up `.profiler` at {id(prof):#x}')
283+
284+
# Do the preimports at `cache.preimports_module` where
285+
# appropriate
286+
if self.preimports_module:
287+
self._debug_output('Loading preimports...')
288+
with open(self.preimports_module, mode='rb') as fobj:
289+
code = compile(fobj.read(), self.preimports_module, 'exec')
290+
exec(code, {}) # Use a fresh, empty namespace
291+
292+
# Occupy a tempfile slot in `.cache_dir` and set the profiler
293+
# up to write thereto when the process terminates (with high
294+
# priority)
295+
prof_outfile = self.make_tempfile(
296+
prefix='child-prof-output-{}-{}-{:#x}-'
297+
.format(self.main_pid, os.getpid(), id(prof)),
298+
suffix='.lprof',
299+
)
300+
self._add_cleanup(prof.dump_stats, -1, prof_outfile)
301+
302+
# Set up `os.fork()` wrapping if needed (i.e. in a spawned
303+
# process)
304+
if wrap_os_fork:
305+
self._wrap_os_fork()
306+
307+
# Set `.cleanup()` as an atexit hook to handle everything when
308+
# the child process is about to terminate
309+
atexit.register(self.cleanup)
310+
311+
self._debug_output(f'Setup successful ({context})')
312+
return True
313+
314+
def _wrap_os_fork(self) -> None:
315+
"""
316+
Create a wrapper around :py:func:`os.fork` which handles
317+
profiling.
318+
319+
Side effects:
320+
- :py:func:`os.fork` (if available) replaced with the
321+
wrapper
322+
- :py:meth:`~.cleanup` callback registered undoing that
323+
"""
324+
try:
325+
fork = os.fork
326+
except AttributeError: # Can't fork on this platform
327+
return
328+
329+
@wraps(fork)
330+
def wrapper() -> int:
331+
result = fork()
332+
if result:
333+
return result
334+
# If we're here, we are in the fork
335+
forked = self.copy() # Ditch inherited cleanups
336+
if forked._replace_loaded_instance():
337+
forked._debug_output(
338+
'Superseded cached `.load()`-ed instance in forked process'
339+
)
340+
# Note: we can reuse the profiler instance in the fork, but
341+
# it needs to go through setup so that the separate
342+
# profiling results are dumped into another output file
343+
forked._setup_in_child_process(False, 'fork', self.profiler)
344+
return result
345+
346+
os.fork = wrapper
347+
self.add_cleanup(setattr, os, 'fork', fork)
348+
230349
def make_tempfile(self, **kwargs) -> Path:
231350
"""
232351
Create a fresh tempfile under :py:attr:`~.cache_dir`. The other

line_profiler/_child_process_profiling/multiprocessing_patches.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828

2929
from .. import _diagnostics as diagnostics
3030
from .cache import LineProfilingCache
31-
from .pth_hook import _setup_in_child_process
3231
from .runpy_patches import create_runpy_wrapper
3332

3433

@@ -80,7 +79,7 @@ def __setstate__(*_) -> None:
8079
# We're in a child process created by `multiprocessing`, so set
8180
# up shop here.
8281
lp_cache = LineProfilingCache.load()
83-
_setup_in_child_process(lp_cache, False, 'multiprocessing')
82+
lp_cache._setup_in_child_process(False, 'multiprocessing')
8483
# In a child process, we don't care about polluting the
8584
# `multiprocessing` namespace, so don't bother with cleanup
8685
if not getattr(multiprocessing, _PATCHED_MARKER, False):

line_profiler/_child_process_profiling/pth_hook.py

Lines changed: 3 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,11 @@
2020
from typing import TYPE_CHECKING
2121

2222
if TYPE_CHECKING:
23-
from collections.abc import Callable # noqa: F401
2423
from pathlib import Path # noqa: F401
25-
from typing import Any # noqa: F401
26-
from ..line_profiler import LineProfiler # noqa: F401
2724
from .cache import LineProfilingCache # noqa: F401
2825

2926

30-
__all__ = (
31-
'write_pth_hook', 'load_pth_hook', '_setup_in_child_process'
32-
)
27+
__all__ = ('write_pth_hook', 'load_pth_hook')
3328

3429
INHERITED_PID_ENV_VARNAME = (
3530
'LINE_PROFILER_PROFILE_CHILD_PROCESSES_CACHE_PID'
@@ -83,7 +78,7 @@ def write_pth_hook(cache): # type: (LineProfilingCache) -> Path
8378
finally: # Not closing the handle causes issues on Windows
8479
os.close(handle)
8580

86-
_wrap_os_fork(cache)
81+
cache._wrap_os_fork()
8782

8883
return fpath
8984

@@ -118,135 +113,11 @@ def load_pth_hook(ppid): # type: (int) -> None
118113
return
119114
try:
120115
cache = LineProfilingCache.load()
121-
_setup_in_child_process(cache, True, 'pth')
122-
# _setup_in_child_process(LineProfilingCache.load())
116+
cache._setup_in_child_process(True, 'pth')
123117
except Exception as e:
124118
if DEBUG:
125119
msg = f'{type(e)}: {e}'
126120
warnings.warn(msg)
127121
log.warning(msg)
128122
finally:
129123
load_pth_hook.called = True # type: ignore[attr-defined]
130-
131-
132-
def _wrap_os_fork(cache): # type: (LineProfilingCache) -> None
133-
"""
134-
Create a wrapper around :py:func:`os.fork` which handles profiling.
135-
136-
Args:
137-
cache (:py:class:`~.LineProfilingCache`):
138-
Cache object
139-
140-
Side effects:
141-
- :py:func:`os.fork` (if available) replaced with the wrapper
142-
- Cleanup callback registered at ``cache`` undoing that
143-
"""
144-
import os
145-
from functools import wraps
146-
147-
try:
148-
fork = os.fork
149-
except AttributeError: # Can't fork on this platform
150-
return
151-
152-
@wraps(fork)
153-
def wrapper(): # type: () -> int
154-
result = fork()
155-
if result:
156-
return result
157-
# If we're here, we are in the fork
158-
forked = cache.copy()
159-
if forked._replace_loaded_instance():
160-
forked._debug_output(
161-
'Superseded cached `.load()`-ed instance in forked process'
162-
)
163-
# Note: we can reuse the profiler instance in the fork, but it
164-
# needs to go through setup so that the separate profiling
165-
# results are dumped into another output file
166-
_setup_in_child_process(forked, False, 'fork', cache.profiler)
167-
return result
168-
169-
os.fork = wrapper
170-
cache.add_cleanup(setattr, os, 'fork', fork)
171-
172-
173-
def _setup_in_child_process(cache, wrap_os_fork=False, context='', prof=None):
174-
# type: (LineProfilingCache, bool, str, LineProfiler | None) -> bool
175-
"""
176-
Set up shop in a forked/spawned child process so that
177-
(line-)profiling can extend therein.
178-
179-
Args:
180-
cache (LineProfilingCache):
181-
Cache object
182-
wrap_os_fork (bool):
183-
Whether to wrap :py:func:`os.fork` which handles profiling;
184-
already-forked child processes should set this to false
185-
context (str):
186-
Optional context from which the function is called, to be
187-
used in log messages
188-
prof (LineProfiler | None):
189-
Optional profiler instance to associate with the cache;
190-
if not provided, an instance is created
191-
192-
Returns:
193-
has_set_up (bool):
194-
False is ``cache`` has already been set up prior to calling
195-
this function, true otherwise
196-
"""
197-
if not context:
198-
context = '...'
199-
cache._debug_output(f'Setting up ({context})...')
200-
if cache.profiler is not None: # Already set up
201-
cache._debug_output(f'Setup aborted ({context})')
202-
return False
203-
204-
import os
205-
from atexit import register
206-
from ..autoprofile.autoprofile import (
207-
# Note: we need this to equip the profiler with the
208-
# `.add_imported_function_or_module()` pseudo-method
209-
# (see `kernprof.py::_write_preimports()`), which is required
210-
# for the preimports to work
211-
_extend_line_profiler_for_profiling_imports as upgrade_profiler,
212-
)
213-
from ..curated_profiling import CuratedProfilerContext
214-
from ..line_profiler import LineProfiler # noqa: F811
215-
216-
# Create a profiler instance and manage it with
217-
# `CuratedProfilerContext`
218-
if prof is None:
219-
prof = LineProfiler()
220-
cache.profiler = prof
221-
upgrade_profiler(prof)
222-
ctx = CuratedProfilerContext(prof, insert_builtin=cache.insert_builtin)
223-
ctx.install()
224-
cache.add_cleanup(ctx.uninstall)
225-
cache._debug_output(f'Set up `.profiler` at {id(prof):#x}')
226-
227-
# Do the preimports at `cache.preimports_module` where appropriate
228-
if cache.preimports_module:
229-
cache._debug_output('Loading preimports...')
230-
with open(cache.preimports_module, mode='rb') as fobj:
231-
code = compile(fobj.read(), cache.preimports_module, 'exec')
232-
exec(code, {}) # Use a fresh, empty namespace
233-
234-
# Occupy a tempfile slot in `cache.cache_dir` and set the profiler
235-
# up to write thereto when the process terminates
236-
prof_outfile = cache.make_tempfile(
237-
prefix='child-prof-output-{}-{}-{:#x}-'
238-
.format(cache.main_pid, os.getpid(), id(prof)),
239-
suffix='.lprof',
240-
)
241-
cache._add_cleanup(prof.dump_stats, -1, prof_outfile)
242-
243-
# Set up `os.fork()` wrapping if needed (i.e. in a spawned process)
244-
if wrap_os_fork:
245-
_wrap_os_fork(cache)
246-
247-
# Set `cache.cleanup()` as an atexit hook to handle everything when
248-
# the child process is about to terminate
249-
register(cache.cleanup)
250-
251-
cache._debug_output(f'Setup successful ({context})')
252-
return True

0 commit comments

Comments
 (0)