1212# Standard library imports
1313import ast
1414import bdb
15- import builtins
1615from contextlib import contextmanager
16+ import cProfile
17+ from functools import partial
1718import io
1819import logging
1920import os
2021import pdb
2122import shlex
2223import sys
24+ import tempfile
2325import time
2426
2527# Third-party imports
2931 leading_empty_lines ,
3032)
3133from 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)
3741from 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
4145from spyder_kernels .customize .namespace_manager import NamespaceManager
4246from spyder_kernels .customize .spyderpdb import SpyderPdb
4347from spyder_kernels .customize .umr import UserModuleReloader
4448from 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
5054logger = 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 ("\n Profiling was interrupted" )
63+
64+
5365def 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