diff --git a/snakeviz/cli.py b/snakeviz/cli.py index b33a9b8..0307487 100644 --- a/snakeviz/cli.py +++ b/snakeviz/cli.py @@ -14,9 +14,9 @@ from pstats import Stats try: - from urllib.parse import quote + from urllib.parse import quote, quote_plus except ImportError: - from urllib import quote + from urllib import quote, quote_plus from . import version @@ -45,9 +45,9 @@ def error(self, message): def build_parser(): parser = SVArgumentParser( - description='Start SnakeViz to view a Python profile.') + description='Start SnakeViz to view a Python profile. If multiple profiles are provided, they are merged and loaded as one.') - parser.add_argument('filename', help='Python profile to view') + parser.add_argument('profile', nargs='+', help='Python profile(s) to view, or directory containing profiles') parser.add_argument('-v', '--version', action='version', version=('%(prog)s ' + version.version)) @@ -79,10 +79,7 @@ def main(argv=None): if args.browser and args.server: parser.error("options --browser and --server are mutually exclusive") - filename = os.path.abspath(args.filename) - if not os.path.exists(filename): - parser.error('the path %s does not exist' % filename) - + filename = os.path.abspath(args.profile[0]) if not os.path.isdir(filename): try: open(filename) @@ -101,6 +98,31 @@ def main(argv=None): filename = quote(filename, safe='') + paths = [os.path.abspath(p) for p in args.profile] + for p in paths: + if not os.path.exists(p): + parser.error('path %s does not exist' % p) + + # A single directory is allowed + if len(paths) == 1 and os.path.isdir(paths[0]): + pass # OK + else: + for p in paths: + if os.path.isdir(p): + parser.error('May only specify a single directory: "%s" is a directory' % p) + try: + Stats(p) + except IOError as e: + parser.error('the file %s could not be opened: %s' + % (filename, str(e))) + except: + parser.error(('the file %s is not a valid profile. ' % p) + + 'Generate profiles using: \n\n' + '\tpython -m cProfile -o my_program.prof my_program.py\n') + + paths='&'.join(paths) + paths = quote_plus(paths) + hostname = args.hostname port = args.port @@ -137,7 +159,7 @@ def main(argv=None): print('No available port found.') return 1 - url = "http://{0}:{1}/snakeviz/{2}".format(hostname, port, filename) + url = "http://{0}:{1}/snakeviz/{2}".format(hostname, port, paths) print(('snakeviz web server started on %s:%d; enter Ctrl-C to exit' % (hostname, port))) print(url) diff --git a/snakeviz/main.py b/snakeviz/main.py index e0595c3..a352151 100644 --- a/snakeviz/main.py +++ b/snakeviz/main.py @@ -5,9 +5,9 @@ import json try: - from urllib.parse import quote + from urllib.parse import quote, unquote_plus except ImportError: - from urllib import quote + from urllib import quote, unquote_plus import tornado.ioloop import tornado.web @@ -23,17 +23,21 @@ class VizHandler(tornado.web.RequestHandler): - def get(self, profile_name): - abspath = os.path.abspath(profile_name) - if os.path.isdir(abspath): - self._list_dir(abspath) + def get(self, profiles): + profiles = unquote_plus(profiles) + individual_profiles = profiles.split('&') + + # abspath = os.path.abspath(profile_name) Already absolute from client + if len(individual_profiles) == 1 and os.path.isdir(individual_profiles[0]): + self._list_dir(individual_profiles[0]) else: + display_name = 'Multiple profiles' if len(individual_profiles)>1 else individual_profiles[0] try: - s = Stats(profile_name) - except: - raise RuntimeError('Could not read %s.' % profile_name) + s = Stats(*individual_profiles) # Merge one or more profiles + except Exception as e: + raise RuntimeError('Error getting stats for %s: %s' % (individual_profiles, str(e))) self.render( - 'viz.html', profile_name=profile_name, + 'viz.html', display_name=display_name, table_rows=table_rows(s), callees=json_stats(s)) def _list_dir(self, path): diff --git a/snakeviz/templates/viz.html b/snakeviz/templates/viz.html index 2091843..c328146 100644 --- a/snakeviz/templates/viz.html +++ b/snakeviz/templates/viz.html @@ -2,7 +2,7 @@
-