app/app_profiler/app_profiler.py
author Sverre Rabbelier <srabbelier@gmail.com>
Sat, 24 Oct 2009 16:23:31 -0700
changeset 3043 187c1709756b
parent 2857 bc793800116e
permissions -rw-r--r--
GSoC Views

import cProfile
import ppstats

from google.appengine.ext import webapp
from google.appengine.api import memcache
import google.appengine.ext.webapp.util

from email.MIMEMultipart import MIMEMultipart
from email.Message import Message

import httplib
import logging
import os.path
import random
import re
import string
import zlib

mc_client = memcache.Client()

alphanumeric = string.letters + string.digits

global_profiler = None

class GAEProfiler(object):
    _save_every = 10

    def __init__(self):
        self.is_profiling = False
        self._profiler = None
        self.num_requests = 0
        self.requests_profiled = 0
        self.request_regex = None
        self.profile_key = ''.join([random.choice(alphanumeric) for x in range(4)])

    def start_profiling(self, request_regex=None, num_requests=0):
        "start profiling with this object, setting # of requests and filter"
        if self.is_profiling:
            return

        self.is_profiling = True
        if self._profiler is None:
            self._profiler = cProfile.Profile()
        self.num_requests = num_requests
        if request_regex:
            self.request_regex = re.compile(request_regex)

    def stop_profiling(self):
        self.is_profiling = False

    def resume_profiling(self):
        self.is_profiling = True
    
    def has_profiler(self):
        return self._profiler is not None

    def get_pstats(self):
        "return a ppstats object from current profile data"
        gae_base_dir = '/'.join(webapp.__file__.split('/')[:-5])
        sys_base_dir = '/'.join(logging.__file__.split('/')[:-2])

        stats = ppstats.Stats(self._profiler)
        stats.hide_directory(gae_base_dir, 'GAEHome')
        stats.hide_directory(sys_base_dir, 'SysHome')
        stats.strip_dirs()
        return stats

    def runcall(self, func, *args, **kwargs):
        "profile one call, incrementing requests_profiled and maybe saving stats"
        self.requests_profiled += 1
        if self._profiler:
            ret = self._profiler.runcall(func, *args, **kwargs)
        else:
            ret = func(*args, **kwargs)
        
#        if (self.requests_profiled % self._save_every) == 0 or \
#                self.requests_profiled == self.num_requests:
#            self.save_pstats_to_memcache()
        self.save_pstats_to_memcache()
        return ret

    def should_profile_request(self):
        "check for # of requests profiled and that SCRIPT_NAME matches regex"
        env = dict(os.environ)
        script_name = env.get('SCRIPT_NAME', '')
        logging.info(script_name)
        
        if self.num_requests and self.requests_profiled >= self.num_requests:
            return False

        if self.request_regex and not self.request_regex.search(script_name):
            return False

        return True

    def save_pstats_to_memcache(self):
        "save stats from profiler object to memcache"
        ps = self.get_pstats()
        output = ps.dump_stats_pickle()
        compressed_data = zlib.compress(output, 3)
        cache_key = cache_key_for_profile(self.profile_key)
        mc_client.set(cache_key, compressed_data)
        logging.info("Saved pstats to memcache with key %s" % cache_key)



def get_global_profiler():
    global global_profiler
    if not global_profiler:
        global_profiler = GAEProfiler()

    return global_profiler
    
def new_global_profiler():
    global global_profiler
    global_profiler = GAEProfiler()
    return global_profiler

def cache_key_for_profile(profile_key):
    "generate a memcache key"
    return "ProfileData.%s" % profile_key

def load_pstats_from_memcache(profile_key):
    "retrieve ppstats object"
    mc_data = mc_client.get(cache_key_for_profile(profile_key))
    if not mc_data:
        return None

    return ppstats.from_gz(mc_data)

def get_stats_from_global_or_request(request_obj):
    "get pstats for a key, or the global pstats"
    key = request_obj.get('key', '')
    if key:
        gp = GAEProfiler()
        gp.profile_obj = load_pstats_from_memcache(key)
        gp.profile_key = key
        return gp
    else:
        gp = get_global_profiler()
        if not gp.has_profiler():
            return None
        return gp

def mime_upload_data_as_file(field_name, filename, body):
    part = Message()
    part['Content-Disposition'] = 'form-data; name="%s"; filename="%s"' % (field_name, filename)
    part['Content-Transfer-Encoding'] = 'binary'
    part['Content-Type'] = 'application/octet-stream'
    part['Content-Length'] = str(len(body))
    part.set_payload(body)
    return part

def mime_form_value(name, value):
    part = Message()
    part['Content-Disposition'] = 'form-data; name="%s"' % name
    part.set_payload(value)
    return part

class show_profile(webapp.RequestHandler):
    def get(self):
        ps = get_stats_from_global_or_request(self.request)
        if not ps:
            self.response.out.write("<body><html><h3>No profiler.</h3><html></body>")
            return

        ps.profile_obj.set_output(self.response.out)
        sort = self.request.get('sort', 'time')
        ps.profile_obj.sort_stats(sort)
        self.response.out.write("<body><html><pre>\n")
        ps.profile_obj.print_stats(30)
        self.response.out.write("</pre></html></body>")

class download_profile_data(webapp.RequestHandler):
    def get(self):
        ps = get_stats_from_global_or_request(self.request)
        if not ps:
            self.response.out.write("<body><html><h3>No profiler.</h3><html></body>")
            return            

        output = ps.profile_obj.dump_stats_pickle()

        self.response.headers['Content-Type'] = 'application/octet-stream'

        self.response.out.write(output)

class send_profile_data(webapp.RequestHandler):
    def get(self):
        ps = get_stats_from_global_or_request(self.request)
        if not ps:
            self.response.out.write("<body><html><h3>No profiler.</h3><html></body>")
            return            

        dest = self.request.get('dest', '')
        if not dest:
            self.response.out.write("<body><html>No destination</html></body>")

        upload_form = MIMEMultipart('form-data')

        upload_filename =  'profile.%s.pstats' % ps.profile_key
        upload_field_name = 'profile_file'

        upload_form.attach(mime_upload_data_as_file('profile_file', upload_field_name, zlib.compress(ps.profile_obj.dump_stats_pickle())))
        upload_form.attach(mime_form_value('key_only', '1'))

        http_conn = httplib.HTTPConnection(dest)
        http_conn.connect()
        http_conn.request('POST', '/upload_profile', upload_form.as_string(), 
                          {'Content-Type': 'multipart/form-data; boundary=%s' % upload_form.get_boundary()})

        http_resp = http_conn.getresponse()
        remote_data = http_resp.read()
        if http_resp.status == 200:
            remote_url = "http://%s/view_profile?key=%s" % (dest, remote_data)
            self.response.out.write("<html><body>Success! <a href='%s'>%s</a></body></html>" % (remote_url, remote_url))
        else:
            self.response.out.write("Failure!\n%s: %s\n%s" % (http_resp.status, http_resp.reason, remote_data))
            
class show_profiler_status(webapp.RequestHandler):
    def get(self):
        gp = get_global_profiler()
        if not gp.has_profiler:
            self.response.out.write("<body><html><h3>No profiler.</h3><html></body>")
            return

        self.response.out.write("<html><body>")
        self.response.out.write("<b>Currently profiling:</b> %s<br>" % gp.is_profiling)
        self.response.out.write("<b>Profile Key</b>: %s<br>" % gp.profile_key)
        self.response.out.write("<b>Requests profiled so far:</b> %s<br>" % gp.requests_profiled)
        self.response.out.write("<b>Requests to profile:</b> %s<br>" % gp.num_requests)
        self.response.out.write("<b>Request regex:</b> %s<br>" % gp.request_regex)
        self.response.out.write("</body></html>")

class start_profiler(webapp.RequestHandler):
    def get(self):
        gp = new_global_profiler()
        gp.start_profiling()
        self.response.headers['Content-Type'] = "text/plain"
        self.response.out.write("Started profiling (key: %s).\n" % gp.profile_key)
        self.response.out.write("Retrieve saved results at <a href='/profiler/show?key=%(key)s'>/profiler/show?key=%(key)s).\n" % {'key':gp.profile_key})

class stop_profiler(webapp.RequestHandler):
    def get(self):
        gp = get_global_profiler()
        gp.stop_profiling()
        self.request.out.write("Content-Type: text/plain\n\n")
        self.request.out.write("done.")

class save_profile_data(webapp.RequestHandler):
    def get(self):
        gp = get_global_profiler()
        

def _add_our_endpoints(application):
    "insert our URLs into the application map"
    url_mapping = [(regex.pattern, handler) for (regex, handler) in application._url_mapping]
    return webapp.WSGIApplication(url_mapping, debug=True)

#
#  wrapper to for webapp applications
#
def run_wsgi_app(application):
    "proxy webapp.util's call to profile when needed"
    gp = get_global_profiler()
    if gp.is_profiling and gp.should_profile_request():
        return gp.runcall(google.appengine.ext.webapp.util.run_wsgi_app, *(application,))
    else:
        return google.appengine.ext.webapp.util.run_wsgi_app(application)

#
# middleware for django applications
#

class ProfileMiddleware(object):
    def __init__(self):
        self.profiler = None

    def process_request(self, request):
        self.profiler = get_global_profiler()

    def process_view(self, request, callback, callback_args, callback_kwargs):
        if self.profiler.is_profiling:
            return self.profiler.runcall(callback, request, *callback_args, **callback_kwargs)

application = webapp.WSGIApplication(
    [('/profiler/start', start_profiler),
     ('/profiler/stop', stop_profiler),
     ('/profiler/show', show_profile),
     ('/profiler/download', download_profile_data),
     ('/profiler/status', show_profiler_status),
     ('/profiler/send', send_profile_data),
     ],
    debug=True)

    
def main():
    google.appengine.ext.webapp.util.run_wsgi_app(application)

if __name__ == '__main__':
    main()