From 90a3f52f6b16145e37e3fd77c8fefbf517588062 Mon Sep 17 00:00:00 2001 From: Marvin Greenberg Date: Tue, 3 Oct 2017 10:47:50 -0400 Subject: [PATCH 1/3] Add ability to pass multiple profiles which are merged in call to pstat.Stats(). This is a common use case when multiple runs generate multiple '.prof' files, e.g. snakeviz prog1.*.prof When multiple profiles are passed, the "display_name" which is used for the browser tab name is changed to "Multiple profiles" --- snakeviz/cli.py | 47 ++++++++++++++++++++----------------- snakeviz/main.py | 20 +++++++++------- snakeviz/templates/viz.html | 2 +- 3 files changed, 38 insertions(+), 31 deletions(-) diff --git a/snakeviz/cli.py b/snakeviz/cli.py index b64db58..74ff6ba 100644 --- a/snakeviz/cli.py +++ b/snakeviz/cli.py @@ -43,9 +43,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('-H', '--hostname', metavar='ADDR', default='127.0.0.1', help='hostname to bind to (default: %(default)s)') @@ -74,25 +74,30 @@ 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) + 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) - if not os.path.isdir(filename): - try: - open(filename) - except IOError as e: - parser.error('the file %s could not be opened: %s' - % (filename, str(e))) - - try: - Stats(filename) - except: - parser.error(('the file %s is not a valid profile. ' % filename) + - 'Generate profiles using: \n\n' - '\tpython -m cProfile -o my_program.prof my_program.py\n') - - filename = quote_plus(filename) + # 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 @@ -123,7 +128,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 65240df..929e884 100644 --- a/snakeviz/main.py +++ b/snakeviz/main.py @@ -22,19 +22,21 @@ class VizHandler(tornado.web.RequestHandler): - def get(self, profile_name): - profile_name = unquote_plus(profile_name) + def get(self, profiles): + profiles = unquote_plus(profiles) + individual_profiles = profiles.split('&') - abspath = os.path.abspath(profile_name) - if os.path.isdir(abspath): - self._list_dir(abspath) + # 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,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 abc1591..3bd560e 100644 --- a/snakeviz/templates/viz.html +++ b/snakeviz/templates/viz.html @@ -2,7 +2,7 @@ - {{profile_name.split('/')[-1].split('.')[0]}} + {{display_name.split('/')[-1].split('.')[0]}} From 97b2877d136c83c78d6396ea0c0aa7c8f8037995 Mon Sep 17 00:00:00 2001 From: Marvin Greenberg Date: Tue, 3 Oct 2017 11:29:50 -0400 Subject: [PATCH 2/3] Fix exception syntax for python3, fix error formatting --- snakeviz/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/snakeviz/main.py b/snakeviz/main.py index 929e884..2bc5e59 100644 --- a/snakeviz/main.py +++ b/snakeviz/main.py @@ -33,8 +33,8 @@ def get(self, profiles): display_name = 'Multiple profiles' if len(individual_profiles)>1 else individual_profiles[0] try: s = Stats(*individual_profiles) # Merge one or more profiles - except Exception,e: - raise RuntimeError('Error getting stats for %s: %s' % individual_profiles, str(e)) + except Exception as e: + raise RuntimeError('Error getting stats for %s: %s' % (individual_profiles, str(e))) self.render( 'viz.html', display_name=display_name, table_rows=table_rows(s), callees=json_stats(s)) From dd589153c26f619568196f6353ab4e65db81bd26 Mon Sep 17 00:00:00 2001 From: Craig Stringham Date: Thu, 2 Dec 2021 15:43:15 -0800 Subject: [PATCH 3/3] Modifications to get multifile working again --- snakeviz/cli.py | 41 +++++++++++++++++++++-------------------- snakeviz/main.py | 4 ++-- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/snakeviz/cli.py b/snakeviz/cli.py index 96fcb60..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 @@ -79,6 +79,25 @@ def main(argv=None): if args.browser and args.server: parser.error("options --browser and --server are mutually exclusive") + filename = os.path.abspath(args.profile[0]) + if not os.path.isdir(filename): + try: + open(filename) + except IOError as e: + parser.error('the file %s could not be opened: %s' + % (filename, str(e))) + + try: + Stats(filename) + except Exception: + parser.error(('The file %s is not a valid profile. ' % filename) + + 'Generate profiles using: \n\n' + '\tpython -m cProfile -o my_program.prof my_program.py\n\n' + 'Note that snakeviz must be run under the same ' + 'version of Python as was used to create the profile.\n') + + filename = quote(filename, safe='') + paths = [os.path.abspath(p) for p in args.profile] for p in paths: if not os.path.exists(p): @@ -104,24 +123,6 @@ def main(argv=None): paths='&'.join(paths) paths = quote_plus(paths) - if not os.path.isdir(filename): - try: - open(filename) - except IOError as e: - parser.error('the file %s could not be opened: %s' - % (filename, str(e))) - - try: - Stats(filename) - except Exception: - parser.error(('The file %s is not a valid profile. ' % filename) + - 'Generate profiles using: \n\n' - '\tpython -m cProfile -o my_program.prof my_program.py\n\n' - 'Note that snakeviz must be run under the same ' - 'version of Python as was used to create the profile.\n') - - filename = quote(filename, safe='') - hostname = args.hostname port = args.port diff --git a/snakeviz/main.py b/snakeviz/main.py index e77ff97..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