Skip to content

Commit 31aba2c

Browse files
authored
Merge pull request #558 from ccordoba12/add-profile-magics
PR: Add `profile`, `profilefile` and `profilecell` magics
2 parents 453262e + ce6a3f7 commit 31aba2c

File tree

1 file changed

+133
-6
lines changed

1 file changed

+133
-6
lines changed

spyder_kernels/customize/code_runner.py

Lines changed: 133 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,16 @@
1212
# Standard library imports
1313
import ast
1414
import bdb
15-
import builtins
1615
from contextlib import contextmanager
16+
import cProfile
17+
from functools import partial
1718
import io
1819
import logging
1920
import os
2021
import pdb
2122
import shlex
2223
import sys
24+
import tempfile
2325
import time
2426

2527
# Third-party imports
@@ -29,27 +31,37 @@
2931
leading_empty_lines,
3032
)
3133
from IPython.core.magic import (
32-
needs_local_scope,
33-
magics_class,
34-
Magics,
34+
line_cell_magic,
3535
line_magic,
36+
Magics,
37+
magics_class,
38+
needs_local_scope,
39+
no_var_expand,
3640
)
3741
from IPython.core import magic_arguments
3842

3943
# Local imports
40-
from spyder_kernels.comms.frontendcomm import frontend_request
44+
from spyder_kernels.comms.frontendcomm import CommError, frontend_request
4145
from spyder_kernels.customize.namespace_manager import NamespaceManager
4246
from spyder_kernels.customize.spyderpdb import SpyderPdb
4347
from spyder_kernels.customize.umr import UserModuleReloader
4448
from spyder_kernels.customize.utils import (
45-
capture_last_Expr, canonic, exec_encapsulate_locals
49+
capture_last_Expr, canonic, create_pathlist, exec_encapsulate_locals
4650
)
4751

4852

4953
# For logging
5054
logger = logging.getLogger(__name__)
5155

5256

57+
def profile_with_context(*args, **kwargs):
58+
"""Show a nice message when profiling is interrupted."""
59+
try:
60+
cProfile.runctx(*args, **kwargs)
61+
except KeyboardInterrupt:
62+
print("\nProfiling was interrupted")
63+
64+
5365
def runfile_arguments(func):
5466
"""Decorator to add runfile magic arguments to magic."""
5567
decorators = [
@@ -196,6 +208,28 @@ def debugfile(self, line, local_ns=None):
196208
context_locals=local_ns,
197209
)
198210

211+
@runfile_arguments
212+
@needs_local_scope
213+
@line_magic
214+
def profilefile(self, line, local_ns=None):
215+
"""Profile a file."""
216+
args, local_ns = self._parse_runfile_argstring(
217+
self.profilefile, line, local_ns
218+
)
219+
220+
with self._profile_exec() as prof_exec:
221+
self._exec_file(
222+
filename=args.filename,
223+
canonic_filename=args.canonic_filename,
224+
wdir=args.wdir,
225+
current_namespace=args.current_namespace,
226+
args=args.args,
227+
exec_fun=prof_exec,
228+
post_mortem=args.post_mortem,
229+
context_globals=args.namespace,
230+
context_locals=local_ns,
231+
)
232+
199233
@runcell_arguments
200234
@needs_local_scope
201235
@line_magic
@@ -234,6 +268,35 @@ def debugcell(self, line, local_ns=None):
234268
context_locals=local_ns,
235269
)
236270

271+
@runcell_arguments
272+
@needs_local_scope
273+
@line_magic
274+
def profilecell(self, line, local_ns=None):
275+
"""Profile a code cell."""
276+
args = self._parse_runcell_argstring(self.profilecell, line)
277+
278+
with self._profile_exec() as prof_exec:
279+
return self._exec_cell(
280+
cell_id=args.cell_id,
281+
filename=args.filename,
282+
canonic_filename=args.canonic_filename,
283+
exec_fun=prof_exec,
284+
post_mortem=args.post_mortem,
285+
context_globals=self.shell.user_ns,
286+
context_locals=local_ns,
287+
)
288+
289+
@no_var_expand
290+
@needs_local_scope
291+
@line_cell_magic
292+
def profile(self, line, cell=None, local_ns=None):
293+
"""Profile the given line."""
294+
if cell is not None:
295+
line += "\n" + cell
296+
297+
with self._profile_exec() as prof_exec:
298+
return prof_exec(line, self.shell.user_ns, local_ns)
299+
237300
@contextmanager
238301
def _debugger_exec(self, filename, continue_if_has_breakpoints):
239302
"""Get an exec function to use for debugging."""
@@ -255,6 +318,70 @@ def debug_exec(code, glob=None, loc=None):
255318
# Enter recursive debugger
256319
yield debug_exec
257320

321+
@contextmanager
322+
def _profile_exec(self):
323+
"""Get an exec function for profiling."""
324+
# Request the frontend to adjust the UI when profiling is started
325+
try:
326+
frontend_request(blocking=False).start_profiling()
327+
except CommError:
328+
logger.debug(
329+
"Could not request to start profiling to the frontend."
330+
)
331+
332+
tmp_dir = None
333+
if sys.platform.startswith('linux'):
334+
# Do not use /tmp for temporary files
335+
try:
336+
from xdg.BaseDirectory import xdg_data_home
337+
tmp_dir = os.path.join(xdg_data_home, "spyder")
338+
os.makedirs(tmp_dir, exist_ok=True)
339+
except Exception:
340+
tmp_dir = None
341+
342+
with tempfile.TemporaryDirectory(dir=tmp_dir) as tempdir:
343+
# Reset the tracing function in case we are debugging
344+
trace_fun = sys.gettrace()
345+
sys.settrace(None)
346+
347+
# Get a file to save the results
348+
profile_filename = os.path.join(tempdir, "profile.prof")
349+
350+
try:
351+
if self.shell.is_debugging():
352+
def prof_exec(code, glob=None, loc=None):
353+
"""
354+
If we are debugging (tracing), call_tracing is
355+
necessary for profiling.
356+
"""
357+
return sys.call_tracing(
358+
profile_with_context,
359+
(code, glob, loc, profile_filename),
360+
)
361+
362+
yield prof_exec
363+
else:
364+
yield partial(
365+
profile_with_context, filename=profile_filename
366+
)
367+
finally:
368+
# Reset tracing function
369+
sys.settrace(trace_fun)
370+
371+
# Send result to frontend
372+
if os.path.isfile(profile_filename):
373+
with open(profile_filename, "br") as f:
374+
profile_result = f.read()
375+
376+
try:
377+
frontend_request(blocking=False).show_profile_file(
378+
profile_result, create_pathlist()
379+
)
380+
except CommError:
381+
logger.debug(
382+
"Could not send profile result to the frontend."
383+
)
384+
258385
def _exec_file(
259386
self,
260387
filename=None,

0 commit comments

Comments
 (0)