Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR: Add profile, profilefile, profilecell magics #451

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 108 additions & 2 deletions spyder_kernels/customize/code_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@
import bdb
import builtins
from contextlib import contextmanager
import cProfile
from functools import partial
import io
import logging
import os
import pdb
import tempfile
import shlex
import sys
import time
Expand All @@ -30,18 +33,21 @@
)
from IPython.core.magic import (
needs_local_scope,
no_var_expand,
line_cell_magic,
magics_class,
Magics,
line_magic,
)
from IPython.core import magic_arguments

# Local imports
from spyder_kernels.comms.frontendcomm import frontend_request
from spyder_kernels.comms.frontendcomm import frontend_request, CommError
from spyder_kernels.customize.namespace_manager import NamespaceManager
from spyder_kernels.customize.spyderpdb import SpyderPdb
from spyder_kernels.customize.umr import UserModuleReloader
from spyder_kernels.customize.utils import capture_last_Expr, canonic
from spyder_kernels.customize.utils import (
capture_last_Expr, canonic, create_pathlist)


# For logging
Expand Down Expand Up @@ -192,6 +198,27 @@ def debugfile(self, line, local_ns=None):
context_locals=local_ns,
)

@runfile_arguments
@needs_local_scope
@line_magic
def profilefile(self, line, local_ns=None):
"""Profile a file."""
args, local_ns = self._parse_runfile_argstring(
self.profilefile, line, local_ns)

with self._profile_exec() as prof_exec:
self._exec_file(
filename=args.filename,
canonic_filename=args.canonic_filename,
wdir=args.wdir,
current_namespace=args.current_namespace,
args=args.args,
exec_fun=prof_exec,
post_mortem=args.post_mortem,
context_globals=args.namespace,
context_locals=local_ns,
)

@runcell_arguments
@needs_local_scope
@line_magic
Expand Down Expand Up @@ -230,6 +257,34 @@ def debugcell(self, line, local_ns=None):
context_locals=local_ns,
)

@runcell_arguments
@needs_local_scope
@line_magic
def profilecell(self, line, local_ns=None):
"""Profile a code cell."""
args = self._parse_runcell_argstring(self.profilecell, line)

with self._profile_exec() as prof_exec:
return self._exec_cell(
cell_id=args.cell_id,
filename=args.filename,
canonic_filename=args.canonic_filename,
exec_fun=prof_exec,
post_mortem=args.post_mortem,
context_globals=self.shell.user_ns,
context_locals=local_ns,
)

@no_var_expand
@needs_local_scope
@line_cell_magic
def profile(self, line, cell=None, local_ns=None):
"""Profile the given line."""
if cell is not None:
line += "\n" + cell
with self._profile_exec() as prof_exec:
return prof_exec(line, self.shell.user_ns, local_ns)

@contextmanager
def _debugger_exec(self, filename, continue_if_has_breakpoints):
"""Get an exec function to use for debugging."""
Expand All @@ -251,6 +306,57 @@ def debug_exec(code, glob, loc):
# Enter recursive debugger
yield debug_exec

@contextmanager
def _profile_exec(self):
"""Get an exec function for profiling."""
tmp_dir = None
if sys.platform.startswith('linux'):
# Do not use /tmp for temporary files
try:
from xdg.BaseDirectory import xdg_data_home
tmp_dir = xdg_data_home
os.makedirs(tmp_dir, exist_ok=True)
except Exception:
tmp_dir = None
with tempfile.TemporaryDirectory(dir=tmp_dir) as tempdir:
impact27 marked this conversation as resolved.
Show resolved Hide resolved
# Reset the tracing function in case we are debugging
trace_fun = sys.gettrace()
sys.settrace(None)

# Get a file to save the results
profile_filename = os.path.join(tempdir, "profile.prof")

try:
if self.shell.is_debugging():
def prof_exec(code, glob, loc):
"""
If we are debugging (tracing), call_tracing is
necessary for profiling.
"""
return sys.call_tracing(cProfile.runctx, (
code, glob, loc, profile_filename
))

yield prof_exec
impact27 marked this conversation as resolved.
Show resolved Hide resolved
else:
yield partial(cProfile.runctx, filename=profile_filename)
finally:
# Reset tracing function
sys.settrace(trace_fun)

# Send result to frontend
if os.path.isfile(profile_filename):
with open(profile_filename, "br") as f:
profile_result = f.read()
try:
frontend_request(blocking=False).show_profile_file(
profile_result, create_pathlist()
)
except CommError:
logger.debug(
"Could not send profile result to the frontend."
)

def _exec_file(
self,
filename=None,
Expand Down