Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

First pass at Python 2.7 WebPutty

  • Loading branch information...
commit ab4ea57438a8103e174cc0fe04704b67f760cf95 1 parent 442141e
@tghw tghw authored
Showing with 2,086 additions and 3,612 deletions.
  1. +89 −88 app.yaml
  2. +32 −37 bootstrap.py
  3. +177 −182 fabfile.py
  4. +47 −55 incoming_email_handler.py
  5. +77 −37 libs/gae_mini_profiler/README.md
  6. +192 −0 libs/gae_mini_profiler/cleanup.py
  7. +10 −7 libs/gae_mini_profiler/config.py
  8. +50 −0 libs/gae_mini_profiler/cookies.py
  9. +263 −69 libs/gae_mini_profiler/profiler.py
  10. +83 −3 libs/gae_mini_profiler/static/css/profiler.css
  11. +166 −45 libs/gae_mini_profiler/static/js/profiler.js
  12. +80 −9 libs/gae_mini_profiler/static/js/template.tmpl
  13. +0 −6 libs/gae_mini_profiler/templates/headjs_includes.html
  14. +1 −1  libs/gae_mini_profiler/templates/includes.html
  15. +1 −1  libs/gae_mini_profiler/templates/shared.html
  16. +16 −9 libs/gae_mini_profiler/templatetags.py
  17. +181 −0 libs/gae_mini_profiler/unformatter/__init__.py
  18. +14 −0 libs/gae_mini_profiler/unformatter/examples.txt
  19. +238 −0 libs/gae_mini_profiler/unformatter/formatting.py
  20. +223 −230 libs/livecount/counter.py
  21. +112 −123 libs/livecount/counter_admin.py
  22. +0 −2,676 libs/pkg_resources.py
  23. +34 −33 settings.py
  24. +0 −1  ziplibs/flaskext/__init__.py
View
177 app.yaml
@@ -1,88 +1,89 @@
-application: your-gae-application
-version: 1
-runtime: python
-api_version: 1
-default_expiration: "365d"
-
-inbound_services:
-- mail
-- channel_presence
-- warmup
-
-handlers:
-
-- url: /favicon\.ico
- static_files: static/img/favicon.ico
- upload: static/img/favicon\.ico
- mime_type: image/x-icon
-
-- url: /respond-proxy\.html
- static_files: static/respond/respond-proxy.html
- upload: static/respond/respond-proxy\.html
-
-- url: /static/img/(.*\.(gif|png|jpg))
- static_files: static/img/\1
- upload: static/img/(.*\.(gif|png|jpg))
-
-- url: /static/img/docs/(.*\.(gif|png|jpg))
- static_files: static/img/docs/\1
- upload: static/img/docs/(.*\.(gif|png|jpg))
-
-- url: /static/css/(.*\.css)
- mime_type: text/css
- static_files: static/css/\1
- upload: static/css/(.*\.css)
-
-- url: /static/js/(.*\.js)
- mime_type: text/javascript
- static_files: static/js/\1
- upload: static/js/(.*\.js)
-
-- url: /static/codemirror/(.*\.css)
- mime_type: text/css
- static_files: static/codemirror/\1
- upload: static/codemirror/(.*\.css)
-
-- url: /static/codemirror/(.*\.js)
- mime_type: text/javascript
- static_files: static/codemirror/\1
- upload: static/codemirror/(.*\.js)
-
-- url: /_ah/mail/.+
- script: incoming_email_handler.py
- login: admin
-
-- url: /_migrate
- script: bootstrap.py
- login: admin
-
-- url: /gae_mini_profiler/static/js/(.*\.tmpl)
- mime_type: text/html
- static_files: libs/gae_mini_profiler/static/js/\1
- upload: libs/gae_mini_profiler/static/js/(.*\.tmpl)
-
-- url: /gae_mini_profiler/static
- static_dir: libs/gae_mini_profiler/static
-
-- url: /gae_mini_profiler/.*
- script: libs/gae_mini_profiler/main.py
-
-- url: /livecount/counter_admin
- script: libs/livecount/counter_admin.py
- login: admin
-
-- url: /livecount/.*
- script: libs/livecount/counter.py
- login: admin
-
-- url: /tasks/.*
- script: bootstrap.py
- login: admin
-
-- url: .*\.(jpg|gif|png)
- static_files: static/img/404.png
- upload: static/img/404.png
-
-- url: .*
- script: bootstrap.py
-
+application: tghwputty
+version: 1
+runtime: python27
+threadsafe: yes
+api_version: 1
+default_expiration: "365d"
+
+inbound_services:
+- mail
+- channel_presence
+- warmup
+
+handlers:
+
+- url: /favicon\.ico
+ static_files: static/img/favicon.ico
+ upload: static/img/favicon\.ico
+ mime_type: image/x-icon
+
+- url: /respond-proxy\.html
+ static_files: static/respond/respond-proxy.html
+ upload: static/respond/respond-proxy\.html
+
+- url: /static/img/(.*\.(gif|png|jpg))
+ static_files: static/img/\1
+ upload: static/img/(.*\.(gif|png|jpg))
+
+- url: /static/img/docs/(.*\.(gif|png|jpg))
+ static_files: static/img/docs/\1
+ upload: static/img/docs/(.*\.(gif|png|jpg))
+
+- url: /static/css/(.*\.css)
+ mime_type: text/css
+ static_files: static/css/\1
+ upload: static/css/(.*\.css)
+
+- url: /static/js/(.*\.js)
+ mime_type: text/javascript
+ static_files: static/js/\1
+ upload: static/js/(.*\.js)
+
+- url: /static/codemirror/(.*\.css)
+ mime_type: text/css
+ static_files: static/codemirror/\1
+ upload: static/codemirror/(.*\.css)
+
+- url: /static/codemirror/(.*\.js)
+ mime_type: text/javascript
+ static_files: static/codemirror/\1
+ upload: static/codemirror/(.*\.js)
+
+- url: /_ah/mail/.+
+ script: incoming_email_handler.application
+ login: admin
+
+- url: /_migrate
+ script: bootstrap.app
+ login: admin
+
+- url: /gae_mini_profiler/static/js/(.*\.tmpl)
+ mime_type: text/html
+ static_files: libs/gae_mini_profiler/static/js/\1
+ upload: libs/gae_mini_profiler/static/js/(.*\.tmpl)
+
+- url: /gae_mini_profiler/static
+ static_dir: libs/gae_mini_profiler/static
+
+- url: /gae_mini_profiler/.*
+ script: libs.gae_mini_profiler.main.app
+
+- url: /livecount/counter_admin
+ script: libs.livecount.counter_admin.application
+ login: admin
+
+- url: /livecount/.*
+ script: libs.livecount.counter.application
+ login: admin
+
+- url: /tasks/.*
+ script: bootstrap.app
+ login: admin
+
+- url: .*\.(jpg|gif|png)
+ static_files: static/img/404.png
+ upload: static/img/404.png
+
+- url: .*
+ script: bootstrap.app
+
View
69 bootstrap.py
@@ -1,37 +1,32 @@
-#!/usr/bin/env python
-
-"""Google App Engine uses this file to run your Flask application."""
-
-import os
-from wsgiref.handlers import CGIHandler
-import settings
-from utils import adjust_sys_path
-
-adjust_sys_path()
-if settings.debug:
- adjust_sys_path('ziplibs')
- # Enable ctypes for Jinja debugging
- from google.appengine.tools.dev_appserver import HardenedModulesHook
- HardenedModulesHook._WHITE_LIST_C_MODULES += ['_ctypes', 'gestalt']
-else:
- adjust_sys_path(os.path.join('ziplibs.zip', 'ziplibs'))
-
-from app import create_app
-from werkzeug_debugger_appengine import get_debugged_app
-from flaskext.csrf import csrf
-from gae_mini_profiler import profiler, config as profiler_config
-
-def main():
- app = create_app()
- csrf(app)
- # If we're on the local server, let's enable Flask debugging.
- # For more information: http://goo.gl/RNofH
- if settings.debug:
- app.debug = True
- app = get_debugged_app(app)
- settings.debug_profiler_enabled = profiler_config.should_profile(app)
- app = profiler.ProfilerWSGIMiddleware(app)
- CGIHandler().run(app)
-
-if __name__ == '__main__':
- main()
+#!/usr/bin/env python
+
+"""Google App Engine uses this file to run your Flask application."""
+
+import os
+import settings
+from utils import adjust_sys_path
+
+adjust_sys_path()
+if settings.debug:
+ adjust_sys_path('ziplibs')
+ # Enable ctypes for Jinja debugging
+ from google.appengine.tools.dev_appserver import HardenedModulesHook
+ HardenedModulesHook._WHITE_LIST_C_MODULES += ['_ctypes', 'gestalt']
+else:
+ adjust_sys_path(os.path.join('ziplibs.zip', 'ziplibs'))
+
+from app import create_app
+from werkzeug_debugger_appengine import get_debugged_app
+from flaskext.csrf import csrf
+from gae_mini_profiler import profiler, config as profiler_config
+
+profiler_config.enabled_profiler_emails = settings.admin_emails
+
+app = create_app()
+csrf(app)
+# If we're on the local server, let's enable Flask debugging.
+# For more information: http://goo.gl/RNofH
+if settings.debug:
+ app.debug = True
+ app = get_debugged_app(app)
+app = profiler.ProfilerWSGIMiddleware(app)
View
359 fabfile.py
@@ -1,182 +1,177 @@
-from __future__ import with_statement
-
-import functools
-import os
-import sys
-import shutil
-import getpass
-import re
-import webbrowser
-import zipfile
-from fabric.api import env, local, abort, prompt
-
-import compress
-
-#Some environment information to customize
-if os.name == 'posix':
- APPENGINE_PATH = '/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine'
- PYTHON = '/usr/bin/python2.5'
-else:
- APPENGINE_PATH = r'C:\Program Files (x86)\Google\google_appengine'
- PYTHON = r'C:\Python25\python.exe'
-APPENGINE_APP_CFG = os.path.join(APPENGINE_PATH, 'appcfg.py')
-print APPENGINE_APP_CFG
-
-env.gae_email = None
-env.gae_src = os.path.dirname(__file__)
-
-#default values
-env.dryrun = False
-
-EXTRA_PATHS = [
- APPENGINE_PATH,
- os.path.join(APPENGINE_PATH, 'lib', 'antlr3'),
- os.path.join(APPENGINE_PATH, 'lib', 'django'),
- os.path.join(APPENGINE_PATH, 'lib', 'fancy_urllib'),
- os.path.join(APPENGINE_PATH, 'lib', 'ipaddr'),
- os.path.join(APPENGINE_PATH, 'lib', 'webob'),
- os.path.join(APPENGINE_PATH, 'lib', 'yaml', 'lib'),
-]
-
-sys.path = EXTRA_PATHS + sys.path
-
-from google.appengine.api import appinfo
-
-def _include_appcfg(func):
- '''Decorator that ensures the current Fabric env has a GAE app.yaml config
- attached to it.'''
- @functools.wraps(func)
- def decorated_func(*args, **kwargs):
- if not hasattr(env, 'app'):
- appcfg = appinfo.LoadSingleAppInfo(open(os.path.join(env.gae_src, 'app.yaml')))
- env.app = appcfg
- return func(*args, **kwargs)
- return decorated_func
-
-def dryrun():
- env.dryrun = True
-
-@_include_appcfg
-def deploy(tag=None):
- env.deploy_path = env.gae_src
- prompt('Email:', 'gae_email')
-
- compress_js(env.deploy_path)
- compress_css(env.deploy_path)
- ziplibs(env.deploy_path)
- _clean_babel()
-
- if not env.dryrun:
- print 'Deploying %s' % env.app.version
- local('%s "%s" -A %s -V %s --email=%s update %s' % (PYTHON, APPENGINE_APP_CFG, env.app.application, env.app.version, env.gae_email, env.deploy_path), capture=False)
- webbrowser.open('https://%s.appspot.com/' % env.app.application)
- else:
- print 'This is where we\'d actually deploy to App Engine, but this is a dryrun so we skip that part.'
-
- clean_packages(env.deploy_path)
-
-def compress_js(path=None):
- if not path: path = env.gae_src
- print 'Compressing JavaScript'
- compress.compress_all_javascript(path)
-
-def compress_css(path=None):
- if not path: path = env.gae_src
- print 'Compressing stylesheets'
- compress.compress_all_stylesheets(path)
-
-def clean_packages(base_path=None):
- compress.revert_js_css_hashes(base_path)
-
-def update_translations():
- local('pybabel extract -F babel.cfg -o app/messages.pot --project=WebPutty --version=%s --copyright-holder="Fog Creek Software, Inc." --msgid-bugs-address=customer-service@fogcreek.com app' % tag)
- local('pybabel update -i app/messages.pot -d app/translations')
- _update_piglatin()
- _update_english()
- local('pybabel compile -d app/translations')
-
-def add_locale():
- prompt('Locale:', 'locale')
- if env.locale:
- local('pybabel init -i app/messages.pot -d app/translations -l %s' % env.locale)
- else:
- print 'You must enter a locale.'
-
-def _update_english():
- from babel.messages.pofile import read_po, write_po
- from babel.messages.catalog import Catalog
- with open('app/messages.pot', 'r') as f:
- template = read_po(f)
- catalog = Catalog()
- for message in template:
- catalog.add(message.id, message.id, locations=message.locations)
- with open('app/translations/en/LC_MESSAGES/messages.po', 'w') as f:
- write_po(f, catalog)
- with open('app/translations/en_US/LC_MESSAGES/messages.po', 'w') as f:
- write_po(f, catalog)
-
-def _update_piglatin():
- from babel.messages.pofile import read_po, write_po
- from babel.messages.catalog import Catalog
- with open('app/messages.pot', 'r') as f:
- template = read_po(f)
- catalog = Catalog()
- for message in template:
- trans = ' '.join([_piglatin_translate(w) for w in message.id.split(' ')])
- catalog.add(message.id, trans, locations=message.locations)
- with open('app/translations/aa/LC_MESSAGES/messages.po', 'w') as f:
- write_po(f, catalog)
-
-def _piglatin_translate(word):
- """ convert one word into pig latin """
- word = unicode(word)
- m = len(word)
- vowels = "a", "e", "i", "o", "u", "y"
- if m < 3 or word == "the" or word.startswith('%'): # short words are not converted
- return word
- else:
- for i in vowels:
- if word.find(i) < m and word.find(i) != -1:
- m = word.find(i)
- if m==0:
- return word + u"way"
- else:
- return word[m:] + word[:m] + u"ay"
-
-def _clean_babel():
- from settings import available_locales
- locale_codes = [t[0] for t in available_locales]
- if not env.deploy_path:
- return
- print 'Cleaning up unused localedata.'
- localedata_dir = os.path.join(env.deploy_path, 'libs', 'babel', 'localedata')
- for name in os.listdir(localedata_dir):
- remove = True
- for code in locale_codes:
- if name.startswith(code):
- remove = False
- break
- if remove:
- os.unlink(os.path.join(localedata_dir, name))
-
-def ziplibs(root_dir=None):
- if not root_dir:
- root_dir = os.path.abspath(os.path.dirname(__file__))
- to_zip = os.path.join(root_dir, 'ziplibs')
- print 'Cleaning %s of pyc files.' % to_zip
- def rem_ext(ext, dirname, names):
- for name in names:
- if name.endswith(ext):
- os.unlink(os.path.join(dirname, name))
- os.path.walk(to_zip, rem_ext, '.pyc')
- print 'Zipping ziplibs.'
- zip_file = zipfile.ZipFile(to_zip + '.zip', 'w', compression=zipfile.ZIP_DEFLATED)
- def add_file(args, dir_name, names):
- zip_file, common_base = args
- for name in names:
- zip_file.write(os.path.join(dir_name, name), os.path.join(dir_name[len(common_base):], name))
- os.path.walk(to_zip, add_file, (zip_file, os.path.dirname(to_zip)))
- zip_file.close()
-
-def lint():
- local('pylint --rcfile=.pylintrc app')
+from __future__ import with_statement
+
+import functools
+import os
+import sys
+import webbrowser
+import zipfile
+from fabric.api import env, local, prompt
+
+import compress
+
+#Some environment information to customize
+if os.name == 'posix':
+ APPENGINE_PATH = '/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine'
+ PYTHON = '/usr/bin/python2.7'
+else:
+ APPENGINE_PATH = r'C:\Program Files (x86)\Google\google_appengine'
+ PYTHON = r'C:\Python27\python.exe'
+APPENGINE_APP_CFG = os.path.join(APPENGINE_PATH, 'appcfg.py')
+print APPENGINE_APP_CFG
+
+env.gae_src = os.path.dirname(__file__)
+
+#default values
+env.dryrun = False
+
+EXTRA_PATHS = [
+ APPENGINE_PATH,
+ os.path.join(APPENGINE_PATH, 'lib', 'antlr3'),
+ os.path.join(APPENGINE_PATH, 'lib', 'django'),
+ os.path.join(APPENGINE_PATH, 'lib', 'fancy_urllib'),
+ os.path.join(APPENGINE_PATH, 'lib', 'ipaddr'),
+ os.path.join(APPENGINE_PATH, 'lib', 'webob'),
+ os.path.join(APPENGINE_PATH, 'lib', 'yaml', 'lib'),
+]
+
+sys.path = EXTRA_PATHS + sys.path
+
+from google.appengine.api import appinfo
+
+def _include_appcfg(func):
+ '''Decorator that ensures the current Fabric env has a GAE app.yaml config
+ attached to it.'''
+ @functools.wraps(func)
+ def decorated_func(*args, **kwargs):
+ if not hasattr(env, 'app'):
+ appcfg = appinfo.LoadSingleAppInfo(open(os.path.join(env.gae_src, 'app.yaml')))
+ env.app = appcfg
+ return func(*args, **kwargs)
+ return decorated_func
+
+def dryrun():
+ env.dryrun = True
+
+@_include_appcfg
+def deploy():
+ env.deploy_path = env.gae_src
+
+ compress_js(env.deploy_path)
+ compress_css(env.deploy_path)
+ ziplibs(env.deploy_path)
+ _clean_babel()
+
+ if not env.dryrun:
+ print 'Deploying %s' % env.app.version
+ local('%s "%s" -A %s -V %s --oauth2 update %s' % (PYTHON, APPENGINE_APP_CFG, env.app.application, env.app.version, env.deploy_path), capture=False)
+ webbrowser.open('https://%s.appspot.com/' % env.app.application)
+ else:
+ print 'This is where we\'d actually deploy to App Engine, but this is a dryrun so we skip that part.'
+
+ clean_packages(env.deploy_path)
+
+def compress_js(path=None):
+ if not path: path = env.gae_src
+ print 'Compressing JavaScript'
+ compress.compress_all_javascript(path)
+
+def compress_css(path=None):
+ if not path: path = env.gae_src
+ print 'Compressing stylesheets'
+ compress.compress_all_stylesheets(path)
+
+def clean_packages(base_path=None):
+ compress.revert_js_css_hashes(base_path)
+
+def update_translations():
+ local('pybabel extract -F babel.cfg -o app/messages.pot --project=WebPutty --copyright-holder="Fog Creek Software, Inc." --msgid-bugs-address=customer-service@fogcreek.com app')
+ local('pybabel update -i app/messages.pot -d app/translations')
+ _update_piglatin()
+ _update_english()
+ local('pybabel compile -d app/translations')
+
+def add_locale():
+ prompt('Locale:', 'locale')
+ if env.locale:
+ local('pybabel init -i app/messages.pot -d app/translations -l %s' % env.locale)
+ else:
+ print 'You must enter a locale.'
+
+def _update_english():
+ from babel.messages.pofile import read_po, write_po
+ from babel.messages.catalog import Catalog
+ with open('app/messages.pot', 'r') as f:
+ template = read_po(f)
+ catalog = Catalog()
+ for message in template:
+ catalog.add(message.id, message.id, locations=message.locations)
+ with open('app/translations/en/LC_MESSAGES/messages.po', 'w') as f:
+ write_po(f, catalog)
+ with open('app/translations/en_US/LC_MESSAGES/messages.po', 'w') as f:
+ write_po(f, catalog)
+
+def _update_piglatin():
+ from babel.messages.pofile import read_po, write_po
+ from babel.messages.catalog import Catalog
+ with open('app/messages.pot', 'r') as f:
+ template = read_po(f)
+ catalog = Catalog()
+ for message in template:
+ trans = ' '.join([_piglatin_translate(w) for w in message.id.split(' ')])
+ catalog.add(message.id, trans, locations=message.locations)
+ with open('app/translations/aa/LC_MESSAGES/messages.po', 'w') as f:
+ write_po(f, catalog)
+
+def _piglatin_translate(word):
+ """ convert one word into pig latin """
+ word = unicode(word)
+ m = len(word)
+ vowels = "a", "e", "i", "o", "u", "y"
+ if m < 3 or word == "the" or word.startswith('%'): # short words are not converted
+ return word
+ else:
+ for i in vowels:
+ if word.find(i) < m and word.find(i) != -1:
+ m = word.find(i)
+ if m==0:
+ return word + u"way"
+ else:
+ return word[m:] + word[:m] + u"ay"
+
+def _clean_babel():
+ from settings import available_locales
+ locale_codes = [t[0] for t in available_locales]
+ if not env.deploy_path:
+ return
+ print 'Cleaning up unused localedata.'
+ localedata_dir = os.path.join(env.deploy_path, 'libs', 'babel', 'localedata')
+ for name in os.listdir(localedata_dir):
+ remove = True
+ for code in locale_codes:
+ if name.startswith(code):
+ remove = False
+ break
+ if remove:
+ os.unlink(os.path.join(localedata_dir, name))
+
+def ziplibs(root_dir=None):
+ if not root_dir:
+ root_dir = os.path.abspath(os.path.dirname(__file__))
+ to_zip = os.path.join(root_dir, 'ziplibs')
+ print 'Cleaning %s of pyc files.' % to_zip
+ def rem_ext(ext, dirname, names):
+ for name in names:
+ if name.endswith(ext):
+ os.unlink(os.path.join(dirname, name))
+ os.path.walk(to_zip, rem_ext, '.pyc')
+ print 'Zipping ziplibs.'
+ zip_file = zipfile.ZipFile(to_zip + '.zip', 'w', compression=zipfile.ZIP_DEFLATED)
+ def add_file(args, dir_name, names):
+ zip_file, common_base = args
+ for name in names:
+ zip_file.write(os.path.join(dir_name, name), os.path.join(dir_name[len(common_base):], name))
+ os.path.walk(to_zip, add_file, (zip_file, os.path.dirname(to_zip)))
+ zip_file.close()
+
+def lint():
+ local('pylint --rcfile=.pylintrc app')
View
102 incoming_email_handler.py
@@ -1,55 +1,47 @@
-import logging
-import settings
-from textwrap import dedent
-from google.appengine.ext import webapp
-from google.appengine.ext.webapp.util import run_wsgi_app
-from google.appengine.api import mail
-from google.appengine.ext.webapp.mail_handlers import InboundMailHandler
-
-class ReceiveEmail(InboundMailHandler):
- def receive(self,message):
- headers = dedent("""\
- From: %s
- To: %s
- Subject: %s
- """ % (
- message.sender,
- message.to,
- message.subject,
- ))
-
- log_msg = headers + 'Body:\n'
- for content_type, body in message.bodies():
- log_msg += '%s\n%s\n-------------------------------\n' % (content_type, body.decode())
-
- if settings.log_all_incoming:
- logging.info(log_msg)
-
- email_subject = '%s (from %s)' % (message.subject, message.sender)
-
- email_body = headers
- for content_type, body in message.bodies('text/plain'):
- email_body += body.decode()
-
- email_html = '<p>' + headers.replace('\n', '<br />') + '</p>'
- for content_type, body in message.bodies('text/html'):
- email_html += body.decode()
-
- mail.send_mail(
- sender = settings.incoming_sender_email,
- reply_to = message.sender,
- to = settings.forward_mail_to,
- subject = email_subject,
- body = email_body,
- html = email_html,
- )
- return 'OK'
-
-application = webapp.WSGIApplication([
- ReceiveEmail.mapping()
-], debug=True)
-
-def main():
- run_wsgi_app(application)
-if __name__ == "__main__":
- main()
+import logging
+import settings
+from textwrap import dedent
+from google.appengine.ext import webapp
+from google.appengine.api import mail
+from google.appengine.ext.webapp.mail_handlers import InboundMailHandler
+
+class ReceiveEmail(InboundMailHandler):
+ def receive(self,message):
+ headers = dedent("""\
+ From: %s
+ To: %s
+ Subject: %s
+ """ % (
+ message.sender,
+ message.to,
+ message.subject,
+ ))
+
+ log_msg = headers + 'Body:\n'
+ for content_type, body in message.bodies():
+ log_msg += '%s\n%s\n-------------------------------\n' % (content_type, body.decode())
+
+ if settings.log_all_incoming:
+ logging.info(log_msg)
+
+ email_subject = '%s (from %s)' % (message.subject, message.sender)
+
+ email_body = headers
+ for content_type, body in message.bodies('text/plain'):
+ email_body += body.decode()
+
+ email_html = '<p>' + headers.replace('\n', '<br />') + '</p>'
+ for content_type, body in message.bodies('text/html'):
+ email_html += body.decode()
+
+ mail.send_mail(
+ sender = settings.incoming_sender_email,
+ reply_to = message.sender,
+ to = settings.forward_mail_to,
+ subject = email_subject,
+ body = email_body,
+ html = email_html,
+ )
+ return 'OK'
+
+application = webapp.WSGIApplication([ReceiveEmail.mapping()], debug=True)
View
114 libs/gae_mini_profiler/README.md
@@ -6,11 +6,13 @@ This project is heavily inspired by the impressive [mvc-mini-profiler](http://co
gae_mini_profiler is [MIT licensed](http://en.wikipedia.org/wiki/MIT_License).
-* <a href="#demo">Demo</a>
-* <a href="#screens">Screenshots</a>
-* <a href="#start">Getting Started</a>
-* <a href="#features">Features</a>
-* <a href="#bonus">Bonus</a>
+* <a href="#demo">Demo</a>
+* <a href="#screens">Screenshots</a>
+* <a href="#start">Getting Started</a>
+* <a href="#features">Features</a>
+* <a href="#dependencies">Dependencies</a>
+* <a href="#bonus">Bonus</a>
+* <a href="#faq">FAQ</a>
## <a name="demo">Demo</a>
@@ -22,52 +24,90 @@ You can play around with one of GAE's sample applications with gae_mini_profiler
<img src="http://gae-mini-profiler.appspot.com/images/gae-mini-profiler/expanded.png"/><br/><em>...to show more details...</em><br/><br/>
<img src="http://gae-mini-profiler.appspot.com/images/gae-mini-profiler/rpc.png"/><br/><em>...about remote procedure call performance...</em><br/><br/>
<img src="http://gae-mini-profiler.appspot.com/images/gae-mini-profiler/profile.png"/><br/><em>...or standard profiler output.</em><br/><br/>
-<img src="http://gae-mini-profiler.appspot.com/images/gae-mini-profiler/ajax-corner.png?test"/><br/><em>Ajax requests are also profiled and details made available as they are received.</em>
+<img src="http://gae-mini-profiler.appspot.com/images/gae-mini-profiler/ajax-corner.png?test"/><br/><em>Ajax requests are also profiled and details made available as they are received.</em><br/><br/>
+<img src="http://i.imgur.com/SG0dp.png"/><br/><em>Any Python logging module output is also available for easy access.</em>
## <a name="start">Getting Started</a>
1. Download this repository's source and copy the `gae_mini_profiler/` folder into your App Engine project's root directory.
+
2. Add the following two handler definitions to `app.yaml`:
-<pre>
-handlers:
-&ndash; url: /gae_mini_profiler/static
-&nbsp;&nbsp;static_dir: gae_mini_profiler/static<br/>
-&ndash; url: /gae_mini_profiler/.*
-&nbsp;&nbsp;script: gae_mini_profiler/main.py
-</pre>
-3. Modify the WSGI application you want to profile by wrapping it with the gae_mini_profiler WSGI application:
-<pre>
-&#35; Example of existing application
-application = webapp.WSGIApplication(...existing application...)<br/>
-&#35; Add the following
-from gae_mini_profiler import profiler
-application = profiler.ProfilerWSGIMiddleware(application)
-</pre>
-4. Insert the `profiler_includes` template tag below jQuery somewhere (preferably at the end of your template):
-<pre>
- ...your html...
- {% profiler_includes %}
- &lt;/body&gt;
-&lt;/html&gt;
-</pre>
-5. You're all set! Just choose the users for whom you'd like to enable profiling in `gae_mini_profiler/config.py`:
-<pre>
-&#35; If using the default should_profile implementation, the profiler
-&#35; will only be enabled for requests made by the following GAE users.
-enabled_profiler_emails = [
- "kamens@gmail.com",
-]
-</pre>
+
+ handlers:
+ - url: /gae_mini_profiler/static
+ static_dir: gae_mini_profiler/static
+ - url: /gae_mini_profiler/.*
+ script: gae_mini_profiler/main.py
+
+3. Modify the WSGI application you want to profile by wrapping it with the gae_mini_profiler WSGI application. This is usually done in `appengine_config.py`:
+
+ import gae_mini_profiler.profiler
+ gae_mini_profiler_ENABLED_PROFILER_EMAILS = ['m.dornseif@hudora.de']
+
+ def webapp_add_wsgi_middleware(app):
+ """Called with each WSGI handler initialisation"""
+ app = gae_mini_profiler.profiler.ProfilerWSGIMiddleware(app)
+ return app
+
+4. If you use Django Templates insert the `profiler_includes` template tag below jQuery somewhere (preferably at the end of your template):
+
+ ...your html...
+ {% profiler_includes %}
+ </body>
+ </html>
+
+ Alternatively on any other template system you can hardcode the call.
+
+ For example in jinja2 first add a function to template globals that can retrieve the request_id, something like:
+
+ from gae_mini_profiler import profiler
+ def get_request_id():
+ return profiler.request_id
+ jinja_env.globals['get_request_id'] = get_request_id
+
+ Than add this to your template:
+
+ <link rel="stylesheet" type="text/css" href="/gae_mini_profiler/static/css/profiler.css" />
+ <script type="text/javascript" src="/gae_mini_profiler/static/js/profiler.js"></script>
+ <script type="text/javascript">GaeMiniProfiler.init("{{get_request_id()}}", false)</script>
+
+ If you use the static inclusion you probably should use your template engine to include the code only
+for admins or other profiling-prone users.
+
+5. You're all set! Just choose the users for whom you'd like to enable profiling by putting the respective E-Mail addresses in `gae_mini_profiler/config.py`:
+
+ enabled_profiler_emails = ['user1@example.com',
+ 'user2@example.com']
+
+For more sophisticated choice of what to profile check `gae_mini_profiler/config.py`.
+
## <a name="features">Features</a>
* Production profiling without impacting normal users
* Easily profile all requests, including ajax calls
* Summaries of RPC call types and their performance so you can quickly figure out whether datastore, memcache, or urlfetch is your bottleneck
+* Redirect chains are tracked -- quickly examine the profile of not just the currently rendered request, but any preceding request that issued a 302 redirect leading to the current page.
* Share individual profile results with others by sending link
* Duplicate RPC calls are flagged for easy spotting in case you're repeating memcache or datastore queries.
* Quickly sort and examine profiler stats and call stacks
+## <a name="dependencies">Dependencies</a>
+
+* jQuery must be included somewhere on your page.
+* (Optional) If you want the fancy slider selector for the Logs output, jQuery UI must also be included with its Slider plugin.
+
## <a name="bonus">Bonus</a>
-gae_mini_profiler is currently in production use at Khan Academy (http://khanacademy.org). If you make find good use of it elsewhere, be sure to let me know.
+gae_mini_profiler is currently in production use at [Khan Academy](http://khanacademy.org) as well as [WebPutty](http://www.webputty.net). If you make good use of it elsewhere, be sure to let me know.
+
+## <a name="faq">FAQ</a>
+
+1. I had my appstats_RECORD_FRACTION variable set to 0.1, which means only 10% of my queries were getting profiles generated. This meant that most of the time gae_mini_profiler was failing with a javascript error, because the appstats variable was null.
+
+ If you are using appengine_config.py to customize Appstats behavior you should add this to the top of your "appstats_should_record" method.
+<pre>def appstats_should_record(env):
+ from gae_mini_profiler.config import should_profile
+ if should_profile(env):
+ return True
+</pre>
View
192 libs/gae_mini_profiler/cleanup.py
@@ -0,0 +1,192 @@
+import StringIO
+
+def cleanup(request, response):
+ '''
+ Convert request and response dicts to a human readable format where
+ possible.
+ '''
+ request_short = None
+ response_short = None
+ miss = 0
+
+ if "MemcacheGetRequest" in request:
+ request = request["MemcacheGetRequest"]
+ response = response["MemcacheGetResponse"]
+ request_short = memcache_get(request)
+ response_short, miss = memcache_get_response(response)
+ elif "MemcacheSetRequest" in request:
+ request_short = memcache_set(request["MemcacheSetRequest"])
+ elif "Query" in request:
+ request_short = datastore_query(request["Query"])
+ elif "GetRequest" in request:
+ request_short = datastore_get(request["GetRequest"])
+ elif "PutRequest" in request:
+ request_short = datastore_put(request["PutRequest"])
+ # todo:
+ # TaskQueueBulkAddRequest
+ # BeginTransaction
+ # Transaction
+
+ return request_short, response_short, miss
+
+def memcache_get_response(response):
+ response_miss = 0
+ items = response['item_']
+ for i, item in enumerate(items):
+ if type(item) == dict:
+ item = item['MemcacheGetResponse_Item']['value_']
+ item = truncate(repr(item))
+ items[i] = item
+ response_short = "\n".join(items)
+ if not items:
+ response_miss = 1
+ return response_short, response_miss
+
+def memcache_get(request):
+ keys = request['key_']
+ request_short = "\n".join([truncate(k) for k in keys])
+ namespace = ''
+ if 'name_space_' in request:
+ namespace = request['name_space_']
+ if len(keys) > 1:
+ request_short += '\n'
+ else:
+ request_short += ' '
+ request_short += '(ns:%s)' % truncate(namespace)
+ return request_short
+
+def memcache_set(request):
+ keys = [truncate(i["MemcacheSetRequest_Item"]["key_"]) for i in request["item_"]]
+ return "\n".join(keys)
+
+def datastore_query(query):
+ kind = query.get('kind_', 'UnknownKind')
+ count = query.get('count_', '')
+
+ filters_clean = datastore_query_filter(query)
+ orders_clean = datastore_query_order(query)
+
+ s = StringIO.StringIO()
+ s.write("SELECT FROM %s\n" % kind)
+ if filters_clean:
+ s.write("WHERE\n")
+ for name, op, value in filters_clean:
+ s.write("%s %s %s\n" % (name, op, value))
+ if orders_clean:
+ s.write("ORDER BY\n")
+ for prop, direction in orders_clean:
+ s.write("%s %s\n" % (prop, direction))
+ if count:
+ s.write("LIMIT %s\n" % count)
+
+ result = s.getvalue()
+ s.close()
+ return result
+
+def datastore_query_filter(query):
+ _Operator_NAMES = {
+ 0: "?",
+ 1: "<",
+ 2: "<=",
+ 3: ">",
+ 4: ">=",
+ 5: "=",
+ 6: "IN",
+ 7: "EXISTS",
+ }
+ filters = query.get('filter_', [])
+ filters_clean = []
+ for f in filters:
+ if 'Query_Filter' not in f:
+ continue
+ f = f["Query_Filter"]
+ op = _Operator_NAMES[int(f.get('op_', 0))]
+ props = f["property_"]
+ for p in props:
+ p = p["Property"]
+ name = p["name_"] if "name_" in p else "UnknownName"
+
+ if 'value_' in p:
+
+ propval = p['value_']['PropertyValue']
+
+ if 'stringvalue_' in propval:
+ value = propval["stringvalue_"]
+ elif 'referencevalue_' in propval:
+ ref = propval['referencevalue_']['PropertyValue_ReferenceValue']
+ els = ref['pathelement_']
+ paths = []
+ for el in els:
+ path = el['PropertyValue_ReferenceValuePathElement']
+ paths.append("%s(%s)" % (path['type_'], id_or_name(path)))
+ value = "->".join(paths)
+ elif 'booleanvalue_' in propval:
+ value = propval["booleanvalue_"]
+ elif 'uservalue_' in propval:
+ value = 'User(' + propval['uservalue_']['PropertyValue_UserValue']['email_'] + ')'
+ elif '...' in propval:
+ value = '...'
+ elif 'int64value_' in propval:
+ value = propval["int64value_"]
+ else:
+ raise Exception(propval)
+ else:
+ value = ''
+ filters_clean.append((name, op, value))
+ return filters_clean
+
+def datastore_query_order(query):
+ orders = query.get('order_', [])
+ _Direction_NAMES = {
+ 0: "?DIR",
+ 1: "ASC",
+ 2: "DESC",
+ }
+ orders_clean = []
+ for order in orders:
+ order = order['Query_Order']
+ direction = _Direction_NAMES[int(order.get('direction_', 0))]
+ prop = order.get('property_', 'UnknownProperty')
+ orders_clean.append((prop, direction))
+ return orders_clean
+
+def id_or_name(path):
+ if 'name_' in path:
+ return path['name_']
+ else:
+ return path['id_']
+
+def datastore_get(request):
+ keys = request["key_"]
+ if len(keys) > 1:
+ keylist = cleanup_key(keys.pop(0))
+ for key in keys:
+ keylist += ", " + cleanup_key(key)
+ return keylist
+ elif keys:
+ return cleanup_key(keys[0])
+
+def cleanup_key(key):
+ if 'Reference' not in key:
+ #sometimes key is passed in as '...'
+ return key
+ els = key['Reference']['path_']['Path']['element_']
+ paths = []
+ for el in els:
+ path = el['Path_Element']
+ paths.append("%s(%s)" % (path['type_'] if 'type_' in path
+ else 'UnknownType', id_or_name(path)))
+ return "->".join(paths)
+
+def datastore_put(request):
+ entities = request["entity_"]
+ keys = []
+ for entity in entities:
+ keys.append(cleanup_key(entity["EntityProto"]["key_"]))
+ return "\n".join(keys)
+
+def truncate(value, limit=100):
+ if len(value) > limit:
+ return value[:limit - 3] + "..."
+ else:
+ return value
View
17 libs/gae_mini_profiler/config.py
@@ -5,14 +5,17 @@
enabled_profiler_emails = [
"test@example.com",
"test1@example.com",
-# "hickswright@gmail.com",
- "tghw@fogcreek.com",
- "dane@fogcreek.com",
-# "dbertram@gmail.com",
]
# Customize should_profile to return true whenever a request should be profiled.
# This function will be run once per request, so make sure its contents are fast.
-def should_profile(environ):
- user = users.get_current_user()
- return user and user.email() in enabled_profiler_emails
+class ProfilerConfigProduction:
+ @staticmethod
+ def should_profile(environ):
+ user = users.get_current_user()
+ return user and user.email() in enabled_profiler_emails
+
+class ProfilerConfigDevelopment:
+ @staticmethod
+ def should_profile(environ):
+ return users.is_current_user_admin()
View
50 libs/gae_mini_profiler/cookies.py
@@ -0,0 +1,50 @@
+import Cookie
+import logging
+import os
+
+def get_cookie_value(key):
+ cookies = None
+ try:
+ cookies = Cookie.BaseCookie(os.environ.get('HTTP_COOKIE',''))
+ except Cookie.CookieError, error:
+ logging.debug("Ignoring Cookie Error, skipping get cookie: '%s'" % error)
+
+ if not cookies:
+ return None
+
+ cookie = cookies.get(key)
+
+ if not cookie:
+ return None
+
+ return cookie.value
+
+# Cookie handling from http://appengine-cookbook.appspot.com/recipe/a-simple-cookie-class/
+def set_cookie_value(key, value='', max_age=None,
+ path='/', domain=None, secure=None, httponly=False,
+ version=None, comment=None):
+ cookies = Cookie.BaseCookie()
+ cookies[key] = value
+ for var_name, var_value in [
+ ('max-age', max_age),
+ ('path', path),
+ ('domain', domain),
+ ('secure', secure),
+ #('HttpOnly', httponly), Python 2.6 is required for httponly cookies
+ ('version', version),
+ ('comment', comment),
+ ]:
+ if var_value is not None and var_value is not False:
+ cookies[key][var_name] = str(var_value)
+ if max_age is not None:
+ cookies[key]['expires'] = max_age
+
+ cookies_header = cookies[key].output(header='').lstrip()
+
+ if httponly:
+ # We have to manually add this part of the header until GAE uses Python 2.6.
+ cookies_header += "; HttpOnly"
+
+ return cookies_header
+
+
View
332 libs/gae_mini_profiler/profiler.py
@@ -1,17 +1,33 @@
import datetime
+import time
import logging
import os
-import pickle
-import simplejson
+import cPickle as pickle
+import re
+
+# use json in Python 2.7, fallback to simplejson for Python 2.5
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
import StringIO
-import sys
from types import GeneratorType
import zlib
from google.appengine.ext.webapp import template, RequestHandler
from google.appengine.api import memcache
-from gae_mini_profiler import config
+import unformatter
+from pprint import pformat
+import cleanup
+import cookies
+
+import gae_mini_profiler.config
+if os.environ["SERVER_SOFTWARE"].startswith("Devel"):
+ config = gae_mini_profiler.config.ProfilerConfigDevelopment
+else:
+ config = gae_mini_profiler.config.ProfilerConfigProduction
# request_id is a per-request identifier accessed by a couple other pieces of gae_mini_profiler
request_id = None
@@ -20,9 +36,15 @@ class SharedStatsHandler(RequestHandler):
def get(self):
path = os.path.join(os.path.dirname(__file__), "templates/shared.html")
+
+ request_id = self.request.get("request_id")
+ if not RequestStats.get(request_id):
+ self.response.out.write("Profiler stats no longer exist for this request.")
+ return
+
self.response.out.write(
template.render(path, {
- "request_id": self.request.get("request_id")
+ "request_id": request_id
})
)
@@ -32,19 +54,39 @@ def get(self):
self.response.headers["Content-Type"] = "application/json"
- request_stats = RequestStats.get(self.request.get("request_id"))
- if not request_stats:
- return
+ list_request_ids = []
+
+ request_ids = self.request.get("request_ids")
+ if request_ids:
+ list_request_ids = request_ids.split(",")
+
+ list_request_stats = []
+
+ for request_id in list_request_ids:
+
+ request_stats = RequestStats.get(request_id)
+
+ if request_stats and not request_stats.disabled:
+
+ dict_request_stats = {}
+ for property in RequestStats.serialized_properties:
+ dict_request_stats[property] = request_stats.__getattribute__(property)
- dict_request_stats = {}
- for property in RequestStats.serialized_properties:
- dict_request_stats[property] = request_stats.__getattribute__(property)
+ list_request_stats.append(dict_request_stats)
- self.response.out.write(simplejson.dumps(dict_request_stats))
+ # Don't show temporary redirect profiles more than once automatically, as they are
+ # tied to URL params and may be copied around easily.
+ if request_stats.temporary_redirect:
+ request_stats.disabled = True
+ request_stats.store()
+
+ self.response.out.write(json.dumps(list_request_stats))
class RequestStats(object):
- serialized_properties = ["request_id", "url", "url_short", "s_dt", "profiler_results", "appstats_results"]
+ serialized_properties = ["request_id", "url", "url_short", "s_dt",
+ "profiler_results", "appstats_results", "simple_timing",
+ "temporary_redirect", "logs"]
def __init__(self, request_id, environ, middleware):
self.request_id = request_id
@@ -57,10 +99,15 @@ def __init__(self, request_id, environ, middleware):
if len(self.url_short) > 26:
self.url_short = self.url_short[:26] + "..."
+ self.simple_timing = middleware.simple_timing
self.s_dt = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.profiler_results = RequestStats.calc_profiler_results(middleware)
self.appstats_results = RequestStats.calc_appstats_results(middleware)
+ self.logs = middleware.logs
+
+ self.temporary_redirect = middleware.temporary_redirect
+ self.disabled = False
def store(self):
# Store compressed results so we stay under the memcache 1MB limit
@@ -107,6 +154,12 @@ def short_rpc_file_fmt(s):
@staticmethod
def calc_profiler_results(middleware):
+
+ if middleware.simple_timing:
+ return {
+ "total_time": RequestStats.seconds_fmt(middleware.end - middleware.start),
+ }
+
import pstats
# Make sure nothing is printed to stdout
@@ -128,15 +181,15 @@ def calc_profiler_results(middleware):
callers_names = map(lambda func_name: pstats.func_std_string(func_name), callers.keys())
callers_desc = map(
- lambda name: {"func_desc": name, "func_desc_short": RequestStats.short_method_fmt(name)},
+ lambda name: {"func_desc": name, "func_desc_short": RequestStats.short_method_fmt(name)},
callers_names)
results["calls"].append({
- "primitive_call_count": primitive_call_count,
- "total_call_count": total_call_count,
- "total_time": RequestStats.seconds_fmt(total_time),
+ "primitive_call_count": primitive_call_count,
+ "total_call_count": total_call_count,
+ "total_time": RequestStats.seconds_fmt(total_time),
"per_call": RequestStats.seconds_fmt(total_time / total_call_count) if total_call_count else "",
- "cumulative_time": RequestStats.seconds_fmt(cumulative_time),
+ "cumulative_time": RequestStats.seconds_fmt(cumulative_time),
"per_call_cumulative": RequestStats.seconds_fmt(cumulative_time / primitive_call_count) if primitive_call_count else "",
"func_desc": func_desc,
"func_desc_short": RequestStats.short_method_fmt(func_desc),
@@ -146,7 +199,7 @@ def calc_profiler_results(middleware):
output.close()
return results
-
+
@staticmethod
def calc_appstats_results(middleware):
if middleware.recorder:
@@ -156,50 +209,75 @@ def calc_appstats_results(middleware):
calls = []
service_totals_dict = {}
likely_dupes = False
+ end_offset_last = 0
+
+ requests_set = set()
- dict_requests = {}
+ appstats_key = long(middleware.recorder.start_timestamp * 1000)
for trace in middleware.recorder.traces:
total_call_count += 1
+
total_time += trace.duration_milliseconds()
+ # Don't accumulate total RPC time for traces that overlap asynchronously
+ if trace.start_offset_milliseconds() < end_offset_last:
+ total_time -= (end_offset_last - trace.start_offset_milliseconds())
+ end_offset_last = trace.start_offset_milliseconds() + trace.duration_milliseconds()
+
service_prefix = trace.service_call_name()
if "." in service_prefix:
service_prefix = service_prefix[:service_prefix.find(".")]
- if not service_totals_dict.has_key(service_prefix):
- service_totals_dict[service_prefix] = {"total_call_count": 0, "total_time": 0}
+ if service_prefix not in service_totals_dict:
+ service_totals_dict[service_prefix] = {
+ "total_call_count": 0,
+ "total_time": 0,
+ "total_misses": 0,
+ }
service_totals_dict[service_prefix]["total_call_count"] += 1
service_totals_dict[service_prefix]["total_time"] += trace.duration_milliseconds()
stack_frames_desc = []
- for frame in trace.call_stack_:
- stack_frames_desc.append("%s:%s %s" %
- (RequestStats.short_rpc_file_fmt(frame.class_or_file_name()),
- frame.line_number(),
+ for frame in trace.call_stack_list():
+ stack_frames_desc.append("%s:%s %s" %
+ (RequestStats.short_rpc_file_fmt(frame.class_or_file_name()),
+ frame.line_number(),
frame.function_name()))
request = trace.request_data_summary()
- request_short = request
- if len(request_short) > 100:
- request_short = request_short[:100] + "..."
+ response = trace.response_data_summary()
- likely_dupe = dict_requests.has_key(request)
+ likely_dupe = request in requests_set
likely_dupes = likely_dupes or likely_dupe
+ requests_set.add(request)
+
+ request_short = request_pretty = None
+ response_short = response_pretty = None
+ miss = 0
+ try:
+ request_object = unformatter.unformat(request)
+ response_object = unformatter.unformat(response)
- dict_requests[request] = True
+ request_short, response_short, miss = cleanup.cleanup(request_object, response_object)
- response = trace.response_data_summary()[:100]
+ request_pretty = pformat(request_object)
+ response_pretty = pformat(response_object)
+ except Exception, e:
+ logging.warning("Prettifying RPC calls failed.\n%s", e)
+
+ service_totals_dict[service_prefix]["total_misses"] += miss
calls.append({
"service": trace.service_call_name(),
"start_offset": RequestStats.milliseconds_fmt(trace.start_offset_milliseconds()),
"total_time": RequestStats.milliseconds_fmt(trace.duration_milliseconds()),
- "request": request,
- "request_short": request_short,
- "response": response,
+ "request": request_pretty or request,
+ "response": response_pretty or response,
+ "request_short": request_short or cleanup.truncate(request),
+ "response_short": response_short or cleanup.truncate(response),
"stack_frames_desc": stack_frames_desc,
"likely_dupe": likely_dupe,
})
@@ -209,19 +287,22 @@ def calc_appstats_results(middleware):
service_totals.append({
"service_prefix": service_prefix,
"total_call_count": service_totals_dict[service_prefix]["total_call_count"],
+ "total_misses": service_totals_dict[service_prefix]["total_misses"],
"total_time": RequestStats.milliseconds_fmt(service_totals_dict[service_prefix]["total_time"]),
})
service_totals = sorted(service_totals, reverse=True, key=lambda service_total: float(service_total["total_time"]))
return {
+ "appstats_available": True,
"total_call_count": total_call_count,
"total_time": RequestStats.milliseconds_fmt(total_time),
"calls": calls,
"service_totals": service_totals,
"likely_dupes": likely_dupes,
+ "appstats_key": appstats_key,
}
- return None
+ return { "appstats_available": False, }
class ProfilerWSGIMiddleware(object):
@@ -231,6 +312,12 @@ def __init__(self, app):
self.app_clean = app
self.prof = None
self.recorder = None
+ self.temporary_redirect = False
+ self.handler = None
+ self.logs = None
+ self.simple_timing = False
+ self.start = None
+ self.end = None
def __call__(self, environ, start_response):
@@ -241,59 +328,94 @@ def __call__(self, environ, start_response):
self.app = self.app_clean
self.prof = None
self.recorder = None
+ self.temporary_redirect = False
+ self.simple_timing = cookies.get_cookie_value("g-m-p-disabled") == "1"
- if config.should_profile(environ):
+ # Never profile calls to the profiler itself to avoid endless recursion.
+ if config.should_profile(environ) and not environ.get("PATH_INFO", "").startswith("/gae_mini_profiler/"):
# Set a random ID for this request so we can look up stats later
import base64
- import os
- request_id = base64.urlsafe_b64encode(os.urandom(15))
+ request_id = base64.urlsafe_b64encode(os.urandom(5))
# Send request id in headers so jQuery ajax calls can pick
# up profiles.
def profiled_start_response(status, headers, exc_info = None):
+
+ if status.startswith("302 "):
+ # Temporary redirect. Add request identifier to redirect location
+ # so next rendered page can show this request's profile.
+ headers = ProfilerWSGIMiddleware.headers_with_modified_redirect(environ, headers)
+ self.temporary_redirect = True
+
+ # Append headers used when displaying profiler results from ajax requests
headers.append(("X-MiniProfiler-Id", request_id))
+ headers.append(("X-MiniProfiler-QS", environ.get("QUERY_STRING")))
+
return start_response(status, headers, exc_info)
- # Configure AppStats output, keeping a high level of request
- # content so we can detect dupe RPCs more accurately
- from google.appengine.ext.appstats import recording
- recording.config.MAX_REPR = 750
+ if self.simple_timing:
- # Turn on AppStats monitoring for this request
- old_app = self.app
- def wrapped_appstats_app(environ, start_response):
- # Use this wrapper to grab the app stats recorder for RequestStats.save()
+ # Detailed recording is disabled. Just track simple start/stop time.
+ self.start = time.clock()
- if hasattr(recording.recorder, "get_for_current_request"):
- self.recorder = recording.recorder.get_for_current_request()
- else:
- self.recorder = recording.recorder
+ result = self.app(environ, profiled_start_response)
+ for value in result:
+ yield value
- return old_app(environ, start_response)
- self.app = recording.appstats_wsgi_middleware(wrapped_appstats_app)
+ self.end = time.clock()
- # Turn on cProfile profiling for this request
- import cProfile
- self.prof = cProfile.Profile()
+ else:
- # Get profiled wsgi result
- result = self.prof.runcall(lambda *args, **kwargs: self.app(environ, profiled_start_response), None, None)
+ # Add logging handler
+ self.add_handler()
- self.recorder = recording.recorder
+ # Monkey patch appstats.formatting to fix string quoting bug
+ # See http://code.google.com/p/googleappengine/issues/detail?id=5976
+ import unformatter.formatting
+ import google.appengine.ext.appstats.formatting
+ google.appengine.ext.appstats.formatting._format_value = unformatter.formatting._format_value
- # If we're dealing w/ a generator, profile all of the .next calls as well
- if type(result) == GeneratorType:
+ # Configure AppStats output, keeping a high level of request
+ # content so we can detect dupe RPCs more accurately
+ from google.appengine.ext.appstats import recording
+ recording.config.MAX_REPR = 750
- while True:
- try:
- yield self.prof.runcall(result.next)
- except StopIteration:
- break
+ # Turn on AppStats monitoring for this request
+ old_app = self.app
+ def wrapped_appstats_app(environ, start_response):
+ # Use this wrapper to grab the app stats recorder for RequestStats.save()
- else:
- for value in result:
- yield value
+ if recording.recorder_proxy.has_recorder_for_current_request():
+ self.recorder = recording.recorder_proxy.get_for_current_request()
+
+ return old_app(environ, start_response)
+ self.app = recording.appstats_wsgi_middleware(wrapped_appstats_app)
+
+ # Turn on cProfile profiling for this request
+ import cProfile
+ self.prof = cProfile.Profile()
+
+ # Get profiled wsgi result
+ result = self.prof.runcall(lambda *args, **kwargs: self.app(environ, profiled_start_response), None, None)
+
+ # If we're dealing w/ a generator, profile all of the .next calls as well
+ if type(result) == GeneratorType:
+
+ while True:
+ try:
+ yield self.prof.runcall(result.next)
+ except StopIteration:
+ break
+
+ else:
+ for value in result:
+ yield value
+
+ self.logs = self.get_logs(self.handler)
+ logging.getLogger().removeHandler(self.handler)
+ self.handler.stream.close()
+ self.handler = None
# Store stats for later access
RequestStats(request_id, environ, self).store()
@@ -307,3 +429,75 @@ def wrapped_appstats_app(environ, start_response):
result = self.app(environ, start_response)
for value in result:
yield value
+
+ def add_handler(self):
+ if self.handler is None:
+ self.handler = ProfilerWSGIMiddleware.create_handler()
+ logging.getLogger().addHandler(self.handler)
+
+ @staticmethod
+ def create_handler():
+ handler = logging.StreamHandler(StringIO.StringIO())
+ handler.setLevel(logging.DEBUG)
+ formatter = logging.Formatter("\t".join([
+ '%(levelno)s',
+ '%(asctime)s%(msecs)d',
+ '%(funcName)s',
+ '%(filename)s',
+ '%(lineno)d',
+ '%(message)s',
+ ]), '%M:%S.')
+ handler.setFormatter(formatter)
+ return handler
+
+ @staticmethod
+ def get_logs(handler):
+ raw_lines = [l for l in handler.stream.getvalue().split("\n") if l]
+
+ lines = []
+ for line in raw_lines:
+ if "\t" in line:
+ fields = line.split("\t")
+ lines.append(fields)
+ else: # line is part of a multiline log message (prob a traceback)
+ prevline = lines[-1][-1]
+ if prevline: # ignore leading blank lines in the message
+ prevline += "\n"
+ prevline += line
+ lines[-1][-1] = prevline
+
+ return lines
+
+ @staticmethod
+ def headers_with_modified_redirect(environ, headers):
+ headers_modified = []
+
+ for header in headers:
+ if header[0] == "Location":
+ reg = re.compile("mp-r-id=([^&]+)")
+
+ # Keep any chain of redirects around
+ request_id_chain = request_id
+ match = reg.search(environ.get("QUERY_STRING"))
+ if match:
+ request_id_chain = ",".join([match.groups()[0], request_id])
+
+ # Remove any pre-existing miniprofiler redirect id
+ location = header[1]
+ location = reg.sub("", location)
+ location_hash = False
+ if "#" in location:
+ location, location_hash = location.split("#", 1)
+
+ # Add current request id as miniprofiler redirect id
+ location += ("&" if "?" in location else "?")
+ location = location.replace("&&", "&")
+ location += "mp-r-id=%s" % request_id_chain
+ if location_hash:
+ location += "#%s" % location_hash
+
+ headers_modified.append((header[0], location))
+ else:
+ headers_modified.append(header)
+
+ return headers_modified
View
86 libs/gae_mini_profiler/static/css/profiler.css
@@ -1,3 +1,7 @@
+.g-m-p-corner .entry {
+ height: auto;
+ margin: auto;
+}
.g-m-p-corner {
padding: 4px;
@@ -64,7 +68,7 @@
-moz-border-radius-bottomright: 12px;
border-bottom-right-radius: 12px;
- z-index: 1000;
+ z-index: 99999;
}
.g-m-p .title {
@@ -85,10 +89,19 @@
color: #CCC;
}
+.g-m-p .appstats-link {
+ float:right;
+}
+
.g-m-p .url {
font-size: 125%;
}
+.g-m-p .redirect {
+ float: left;
+ color: #444;
+}
+
.g-m-p .total {
float: right;
}
@@ -99,7 +112,8 @@
}
.g-m-p .summary {
- float:right;
+ float: right;
+ display: inline;
}
.g-m-p .details {
@@ -120,7 +134,7 @@
}
.g-m-p .details table {
- margin-top: 18px;
+ margin-top: 6px;
border-spacing: 0;
font-size: 12px;
}
@@ -159,10 +173,12 @@
.g-m-p .details .left {
text-align: left;
+ float: none;
}
.g-m-p .details .right {
text-align: right;
+ float: none;
}
.g-m-p-shared .shared-teaser-container {
@@ -180,6 +196,70 @@
font-weight: bold;
}
+.g-m-p span.loglevel {
+ display: inline-block;
+ height: 1.2em;
+ width: 1.2em;
+ line-height: 1.2;
+ text-align: center;
+ text-transform: capitalize;
+ font-weight: bold;
+ border-radius: 2px;
+ -moz-border-radius: 2px;
+ -webkit-border-radius: 2px;
+}
+
+.g-m-p span.loglevel span {
+ display: none;
+}
+
+.g-m-p .boxes li {
+ display: inline;
+ margin: 0 10px 0 0;
+}
+
+.g-m-p div.simple-timing {
+ margin-top: 18px;
+ text-align: center;
+}
+
+.g-m-p .summary span.loglevel {
+ margin: 0 .1em 0 .6em;
+}
+.g-m-p span.loglevel.ll50:before { content: 'C'; }
+.g-m-p span.loglevel.ll40:before { content: 'E'; }
+.g-m-p span.loglevel.ll30:before { content: 'W'; }
+.g-m-p span.loglevel.ll20:before { content: 'I'; }
+.g-m-p span.loglevel.ll10:before { content: 'D'; }
+
+.g-m-p .ll50 { background-color: #F22; }
+.g-m-p .ll40 { background-color: #F90; }
+.g-m-p .ll30 { background-color: #Fd0; }
+.g-m-p .ll20 { background-color: #3C0; }
+.g-m-p .ll10 { background-color: #09F; }
+
+#slider {
+ padding: 5px 0 3px 0;
+}
+
+#slider .container {
+ width: 150px;
+ display: inline-block;
+}
+
+#slider .minlevel-text {
+ display: inline-block;
+ width: 4em;
+}
+
+#slider .loglevel {
+ margin-left: .5em;
+}
+
+#slider .ui-slider-horizontal {
+ /*height: .2em;*/
+}
+
/* Borrowed from those smart guys @ Fog Creek. Props to Justin & Bobby */
.g-m-p .fancy-scrollbar::-webkit-scrollbar {
height: 8px;
View
211 libs/gae_mini_profiler/static/js/profiler.js
@@ -1,25 +1,50 @@
-
var GaeMiniProfiler = {
init: function(requestId, fShowImmediately) {
// Fetch profile results for any ajax calls
// (see http://code.google.com/p/mvc-mini-profiler/source/browse/MvcMiniProfiler/UI/Includes.js)
- $(document).ajaxComplete(function (e, xhr, settings) {
+ jQuery(document).ajaxComplete(function (e, xhr, settings) {
if (xhr) {
var requestId = xhr.getResponseHeader('X-MiniProfiler-Id');
if (requestId) {
- GaeMiniProfiler.fetch(requestId);
+ var queryString = xhr.getResponseHeader('X-MiniProfiler-QS');
+ GaeMiniProfiler.fetch(requestId, queryString);
}
}
});
- GaeMiniProfiler.fetch(requestId, fShowImmediately);
+ GaeMiniProfiler.fetch(requestId, window.location.search, fShowImmediately);
+ },
+
+ toggleEnabled: function(link) {
+ var disabled = !!jQuery.cookiePlugin("g-m-p-disabled");
+
+ jQuery.cookiePlugin("g-m-p-disabled", (disabled ? null : "1"), {path: '/'});
+
+ jQuery(link).replaceWith("<em>" + (disabled ? "Enabled" : "Disabled") + "</em>");
+ },
+
+ appendRedirectIds: function(requestId, queryString) {
+ if (queryString) {
+ var re = /mp-r-id=([^&]+)/;
+ var matches = re.exec(queryString);
+ if (matches && matches.length) {
+ var sRedirectIds = matches[1];
+ var list = sRedirectIds.split(",");
+ list[list.length] = requestId;
+ return list;
+ }
+ }
+
+ return [requestId];
},
- fetch: function(requestId, fShowImmediately) {
- $.get(
+ fetch: function(requestId, queryString, fShowImmediately) {
+ var requestIds = this.appendRedirectIds(requestId, queryString);
+
+ jQuery.get(
"/gae_mini_profiler/request",
- { "request_id": requestId },
+ { "request_ids": requestIds.join(",") },
function(data) {
GaeMiniProfilerTemplate.init(function() { GaeMiniProfiler.finishFetch(data, fShowImmediately); });
},
@@ -28,24 +53,30 @@ var GaeMiniProfiler = {
},
finishFetch: function(data, fShowImmediately) {
- var jCorner = this.renderCorner(data);
-
- if (!jCorner.data("attached")) {
- $('body')
- .append(jCorner)
- .click(function(e) { return GaeMiniProfiler.collapse(e); });
- jCorner
- .data("attached", true);
- }
+ if (!data || !data.length) return;
- if (fShowImmediately)
- jCorner.find(".entry").first().click();
+ for (var ix = 0; ix < data.length; ix++) {
+
+ var jCorner = this.renderCorner(data[ix]);
+
+ if (!jCorner.data("attached")) {
+ jQuery('body')
+ .append(jCorner)
+ .click(function(e) { return GaeMiniProfiler.collapse(e); });
+ jCorner
+ .data("attached", true);
+ }
+
+ if (fShowImmediately)
+ jCorner.find(".entry").first().click();
+
+ }
},
collapse: function(e) {
- if ($(".g-m-p").is(":visible")) {
- $(".g-m-p").slideUp("fast");
- $(".g-m-p-corner").slideDown("fast")
+ if (jQuery(".g-m-p").is(":visible")) {
+ jQuery(".g-m-p").slideUp("fast");
+ jQuery(".g-m-p-corner").slideDown("fast")
.find(".expanded").removeClass("expanded");
return false;
}
@@ -54,73 +85,122 @@ var GaeMiniProfiler = {
},
expand: function(elEntry, data) {
- var jPopup = $(".g-m-p");
-
+ var jPopup = jQuery(".g-m-p");
+
if (jPopup.length)
jPopup.remove();
else
- $(document).keyup(function(e) { if (e.which == 27) GaeMiniProfiler.collapse() });
+ jQuery(document).keyup(function(e) { if (e.which == 27) GaeMiniProfiler.collapse() });
jPopup = this.renderPopup(data);
- $('body').append(jPopup);
+ jQuery('body').append(jPopup);
- var jCorner = $(".g-m-p-corner");
+ var jCorner = jQuery(".g-m-p-corner");
jCorner.find(".expanded").removeClass("expanded");
- $(elEntry).addClass("expanded");
+ jQuery(elEntry).addClass("expanded");
jPopup
.find(".profile-link")
.click(function() { GaeMiniProfiler.toggleSection(this, ".profiler-details"); return false; }).end()
.find(".rpc-link")
.click(function() { GaeMiniProfiler.toggleSection(this, ".rpc-details"); return false; }).end()
+ .find(".logs-link")
+ .click(function() { GaeMiniProfiler.toggleSection(this, ".logs-details"); return false; }).end()
.find(".callers-link")
- .click(function() { $(this).parents("td").find(".callers").slideToggle("fast"); return false; }).end()
+ .click(function() { jQuery(this).parents("td").find(".callers").slideToggle("fast"); return false; }).end()
+ .find(".toggle-enabled")
+ .click(function() { GaeMiniProfiler.toggleEnabled(this); return false; }).end()
.click(function(e) { e.stopPropagation(); })
.css("left", jCorner.offset().left + jCorner.width() + 18)
.slideDown("fast");
+
+ var toggleLogRows = function(level) {
+ var names = {10:'Debug', 20:'Info', 30:'Warning', 40:'Error', 50:'Critical'};
+ jQuery('#slider .minlevel-text').text(names[level]);
+ jQuery('#slider .loglevel').attr('class', 'loglevel ll'+level);
+ for (var i = 10; i<=50; i += 10) {
+ var rows = jQuery('tr.ll'+i);
+ if (i<level)
+ rows.hide();
+ else
+ rows.show();
+ }
+ };
+
+ var initLevel = 10;
+
+ if (jQuery('#slider .control').slider) {
+ initLevel = 30;
+ jQuery('#slider .control').slider({
+ value: initLevel,
+ min: 10,
+ max: 50,
+ step: 10,
+ range: 'min',
+ slide: function( event, ui ) {
+ toggleLogRows(ui.value);
+ }
+ });
+ }
+
+ toggleLogRows(initLevel);
},
toggleSection: function(elLink, selector) {
- var fWasVisible = $(selector).is(":visible");
+ var fWasVisible = jQuery(".g-m-p " + selector).is(":visible");
- $(".expand").removeClass("expanded");
- $(".details:visible").slideUp(50)
+ jQuery(".g-m-p .expand").removeClass("expanded");
+ jQuery(".g-m-p .details:visible").slideUp(50);
if (!fWasVisible) {
- $(elLink).parents(".expand").addClass("expanded");
- $(selector).slideDown("fast", function() {
- if (!GaeMiniProfiler.toggleSection["called_" + selector]) {
- $(selector + " table").tablesorter();
- GaeMiniProfiler.toggleSection["called_" + selector] = true;
+ jQuery(elLink).parents(".expand").addClass("expanded");
+ jQuery(selector).slideDown("fast", function() {
+
+ var jTable = jQuery(this).find("table");
+
+ if (jTable.length && !jTable.data("table-sorted")) {
+ jTable
+ .tablesorter()
+ .data("table-sorted", true);
}
+
});
}
},
renderPopup: function(data) {
- return $("#profilerTemplate").tmpl(data);
+ if (data.logs) {
+ var counts = {}
+ jQuery.each(data.logs, function(i, log) {
+ var c = counts[log[0]] || 0;
+ counts[log[0]] = c + 1;
+ });
+ data.log_count = counts;
+ }
+
+ return jQuery("#profilerTemplate").tmplPlugin(data);
},
renderCorner: function(data) {
if (data && data.profiler_results) {
- var jCorner = $(".g-m-p-corner");
+ var jCorner = jQuery(".g-m-p-corner");
var fFirst = false;
if (!jCorner.length) {
- jCorner = $("#profilerCornerTemplate").tmpl();
+ jCorner = jQuery("#profilerCornerTemplate").tmplPlugin();
fFirst = true;
}
return jCorner.append(
- $("#profilerCornerEntryTemplate")
- .tmpl(data)
+ jQuery("#profilerCornerEntryTemplate")
+ .tmplPlugin(data)
.addClass(fFirst ? "" : "ajax")
.click(function() { GaeMiniProfiler.expand(this, data); return false; })
);
}
return null;
- },
+ }
};
var GaeMiniProfilerTemplate = {
@@ -128,9 +208,9 @@ var GaeMiniProfilerTemplate = {
template: null,
init: function(callback) {
- $.get("/gae_mini_profiler/static/js/template.tmpl", function (data) {
+ jQuery.get("/gae_mini_profiler/static/js/template.tmpl", function (data) {
if (data) {
- $('body').append(data);
+ jQuery('body').append(data);
callback();
}
});
@@ -147,7 +227,7 @@ var GaeMiniProfilerTemplate = {
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*/
-(function(a){var r=a.fn.domManip,d="_tmplitem",q=/^[^<]*(<[\w\W]+>)[^>]*$|\{\{\! /,b={},f={},e,p={key:0,data:{}},i=0,c=0,l=[];function g(g,d,h,e){var c={data:e||(e===0||e===false)?e:d?d.data:{},_wrap:d?d._wrap:null,tmpl:null,parent:d||null,nodes:[],calls:u,nest:w,wrap:x,html:v,update:t};g&&a.extend(c,g,{nodes:[],parent:d});if(h){c.tmpl=h;c._ctnt=c._ctnt||c.tmpl(a,c);c.key=++i;(l.length?f:b)[i]=c}return c}a.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(f,d){a.fn[f]=function(n){var g=[],i=a(n),k,h,m,l,j=this.length===1&&this[0].parentNode;e=b||{};if(j&&j.nodeType===11&&j.childNodes.length===1&&i.length===1){i[d](this[0]);g=this}else{for(h=0,m=i.length;h<m;h++){c=h;k=(h>0?this.clone(true):this).get();a(i[h])[d](k);g=g.concat(k)}c=0;g=this.pushStack(g,f,i.selector)}l=e;e=null;a.tmpl.complete(l);return g}});a.fn.extend({tmpl:function(d,c,b){return a.tmpl(this[0],d,c,b)},tmplItem:function(){return a.tmplItem(this[0])},template:function(b){return a.template(b,this[0])},domManip:function(d,m,k){if(d[0]&&a.isArray(d[0])){var g=a.makeArray(arguments),h=d[0],j=h.length,i=0,f;while(i<j&&!(f=a.data(h[i++],"tmplItem")));if(f&&c)g[2]=function(b){a.tmpl.afterManip(this,b,k)};r.apply(this,g)}else r.apply(this,arguments);c=0;!e&&a.tmpl.complete(b);return this}});a.extend({tmpl:function(d,h,e,c){var i,k=!c;if(k){c=p;d=a.template[d]||a.template(null,d);f={}}else if(!d){d=c.tmpl;b[c.key]=c;c.nodes=[];c.wrapped&&n(c,c.wrapped);return a(j(c,null,c.tmpl(a,c)))}if(!d)return[];if(typeof h==="function")h=h.call(c||{});e&&e.wrapped&&n(e,e.wrapped);i=a.isArray(h)?a.map(h,function(a){return a?g(e,c,d,a):null}):[g(e,c,d,h)];return k?a(j(c,null,i)):i},tmplItem:function(b){var c;if(b instanceof a)b=b[0];while(b&&b.nodeType===1&&!(c=a.data(b,"tmplItem"))&&(b=b.parentNode));return c||p},template:function(c,b){if(b){if(typeof b==="string")b=o(b);else if(b instanceof a)b=b[0]||{};if(b.nodeType)b=a.data(b,"tmpl")||a.data(b,"tmpl",o(b.innerHTML));return typeof c==="string"?(a.template[c]=b):b}return c?typeof c!=="string"?a.template(null,c):a.template[c]||a.template(null,q.test(c)?c:a(c)):null},encode:function(a){return(""+a).split("<").join("&lt;").split(">").join("&gt;").split('"').join("&#34;").split("'").join("&#39;")}});a.extend(a.tmpl,{tag:{tmpl:{_default:{$2:"null"},open:"if($notnull_1){__=__.concat($item.nest($1,$2));}"},wrap:{_default:{$2:"null"},open:"$item.calls(__,$1,$2);__=[];",close:"call=$item.calls();__=call._.concat($item.wrap(call,__));"},each:{_default:{$2:"$index, $value"},open:"if($notnull_1){$.each($1a,function($2){with(this){",close:"}});}"},"if":{open:"if(($notnull_1) && $1a){",close:"}"},"else":{_default:{$1:"true"},open:"}else if(($notnull_1) && $1a){"},html:{open:"if($notnull_1){__.push($1a);}"},"=":{_default:{$1:"$data"},open:"if($notnull_1){__.push($.encode($1a));}"},"!":{open:""}},complete:function(){b={}},afterManip:function(f,b,d){var e=b.nodeType===11?a.makeArray(b.childNodes):b.nodeType===1?[b]:[];d.call(f,b);m(e);c++}});function j(e,g,f){var b,c=f?a.map(f,function(a){return typeof a==="string"?e.key?a.replace(/(<\w+)(?=[\s>])(?![^>]*_tmplitem)([^>]*)/g,"$1 "+d+'="'+e.key+'" $2'):a:j(a,e,a._ctnt)}):e;if(g)return c;c=c.join("");c.replace(/^\s*([^<\s][^<]*)?(<[\w\W]+>)([^>]*[^>\s])?\s*$/,function(f,c,e,d){b=a(e).get();m(b);if(c)b=k(c).concat(b);if(d)b=b.concat(k(d))});return b?b:k(c)}function k(c){var b=document.createElement("div");b.innerHTML=c;return a.makeArray(b.childNodes)}function o(b){return new Function("jQuery","$item","var $=jQuery,call,__=[],$data=$item.data;with($data){__.push('"+a.trim(b).replace(/([\\'])/g,"\\$1").replace(/[\r\t\n]/g," ").replace(/\$\{([^\}]*)\}/g,"{{= $1}}").replace(/\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g,function(m,l,k,g,b,c,d){var j=a.tmpl.tag[k],i,e,f;if(!j)throw"Unknown template tag: "+k;i=j._default||[];if(c&&!/\w$/.test(b)){b+=c;c=""}if(b){b=h(b);d=d?","+h(d)+")":c?")":"";e=c?b.indexOf(".")>-1?b+h(c):"("+b+").call($item"+d:b;f=c?e:"(typeof("+b+")==='function'?("+b+").call($item):("+b+"))"}else f=e=i.$1||"null";g=h(g);return"');"+j[l?"close":"open"].split("$notnull_1").join(b?"typeof("+b+")!=='undefined' && ("+b+")!=null":"true").split("$1a").join(f).split("$1").join(e).split("$2").join(g||i.$2||"")+"__.push('"})+"');}return __;")}function n(c,b){c._wrap=j(c,true,a.isArray(b)?b:[q.test(b)?b:a(b).html()]).join("")}function h(a){return a?a.replace(/\\'/g,"'").replace(/\\\\/g,"\\"):null}function s(b){var a=document.createElement("div");a.appendChild(b.cloneNode(true));return a.innerHTML}function m(o){var n="_"+c,k,j,l={},e,p,h;for(e=0,p=o.length;e<p;e++){if((k=o[e]).nodeType!==1)continue;j=k.getElementsByTagName("*");for(h=j.length-1;h>=0;h--)m(j[h]);m(k)}function m(j){var p,h=j,k,e,m;if(m=j.getAttribute(d)){while(h.parentNode&&(h=h.parentNode).nodeType===1&&!(p=h.getAttribute(d)));if(p!==m){h=h.parentNode?h.nodeType===11?0:h.getAttribute(d)||0:0;if(!(e=b[m])){e=f[m];e=g(e,b[h]||f[h]);e.key=++i;b[i]=e}c&&o(m)}j.removeAttribute(d)}else if(c&&(e=a.data(j,"tmplItem"))){o(e.key);b[e.key]=e;h=a.data(j.parentNode,"tmplItem");h=h?h.key:0}if(e){k=e;while(k&&k.key!=h){k.nodes.push(j);k=k.parent}delete e._ctnt;delete e._wrap;a.data(j,"tmplItem",e)}function o(a){a=a+n;e=l[a]=l[a]||g(e,b[e.parent.key+n]||e.parent)}}}function u(a,d,c,b){if(!a)return l.pop();l.push({_:a,tmpl:d,item:this,data:c,options:b})}function w(d,c,b){return a.tmpl(a.template(d),c,b,this)}function x(b,d){var c=b.options||{};c.wrapped=d;return a.tmpl(a.template(b.tmpl),b.data,c,b.item)}function v(d,c){var b=this._wrap;return a.map(a(a.isArray(b)?b.join(""):b).filter(d||"*"),function(a){return c?a.innerText||a.textContent:a.outerHTML||s(a)})}function t(){var b=this.nodes;a.tmpl(null,null,null,this).insertBefore(b[0]);a(b).remove()}})(jQuery);
+(function(a){var r=a.fn.domManip,d="_tmplitem",q=/^[^<]*(<[\w\W]+>)[^>]*$|\{\{\! /,b={},f={},e,p={key:0,data:{}},i=0,c=0,l=[];function g(g,d,h,e){var c={data:e||(e===0||e===false)?e:d?d.data:{},_wrap:d?d._wrap:null,tmplPlugin:null,parent:d||null,nodes:[],calls:u,nest:w,wrap:x,html:v,update:t};g&&a.extend(c,g,{nodes:[],parent:d});if(h){c.tmplPlugin=h;c._ctnt=c._ctnt||c.tmplPlugin(a,c);c.key=++i;(l.length?f:b)[i]=c}return c}a.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(f,d){a.fn[f]=function(n){var g=[],i=a(n),k,h,m,l,j=this.length===1&&this[0].parentNode;e=b||{};if(j&&j.nodeType===11&&j.childNodes.length===1&&i.length===1){i[d](this[0]);g=this}else{for(h=0,m=i.length;h<m;h++){c=h;k=(h>0?this.clone(true):this).get();a(i[h])[d](k);g=g.concat(k)}c=0;g=this.pushStack(g,f,i.selector)}l=e;e=null;a.tmplPlugin.complete(l);return g}});a.fn.extend({tmplPlugin:function(d,c,b){return a.tmplPlugin(this[0],d,c,b)},tmplItem:function(){return a.tmplItem(this[0])},template:function(b){return a.template(b,this[0])},domManip:function(d,m,k){if(d[0]&&a.isArray(d[0])){var g=a.makeArray(arguments),h=d[0],j=h.length,i=0,f;while(i<j&&!(f=a.data(h[i++],"tmplItem")));if(f&&c)g[2]=function(b){a.tmplPlugin.afterManip(this,b,k)};r.apply(this,g)}else r.apply(this,arguments);c=0;!e&&a.tmplPlugin.complete(b);return this}});a.extend({tmplPlugin:function(d,h,e,c){var i,k=!c;if(k){c=p;d=a.template[d]||a.template(null,d);f={}}else if(!d){d=c.tmplPlugin;b[c.key]=c;c.nodes=[];c.wrapped&&n(c,c.wrapped);return a(j(c,null,c.tmplPlugin(a,c)))}if(!d)return[];if(typeof h==="function")h=h.call(c||{});e&&e.wrapped&&n(e,e.wrapped);i=a.isArray(h)?a.map(h,function(a){return a?g(e,c,d,a):null}):[g(e,c,d,h)];return k?a(j(c,null,i)):i},tmplItem:function(b){var c;if(b instanceof a)b=b[0];while(b&&b.nodeType===1&&!(c=a.data(b,"tmplItem"))&&(b=b.parentNode));return c||p},template:function(c,b){if(b){if(typeof b==="string")b=o(b);else if(b instanceof a)b=b[0]||{};if(b.nodeType)b=a.data(b,"tmplPlugin")||a.data(b,"tmplPlugin",o(b.innerHTML));return typeof c==="string"?(a.template[c]=b):b}return c?typeof c!=="string"?a.template(null,c):a.template[c]||a.template(null,q.test(c)?c:a(c)):null},encode:function(a){return(""+a).split("<").join("&lt;").split(">").join("&gt;").split('"').join("&#34;").split("'").join("&#39;")}});a.extend(a.tmplPlugin,{tag:{tmplPlugin:{_default:{$2:"null"},open:"if($notnull_1){__=__.concat($item.nest($1,$2));}"},wrap:{_default:{$2:"null"},open:"$item.calls(__,$1,$2);__=[];",close:"call=$item.calls();__=call._.concat($item.wrap(call,__));"},each:{_default:{$2:"$index, $value"},open:"if($notnull_1){$.each($1a,function($2){with(this){",close:"}});}"},"if":{open:"if(($notnull_1) && $1a){",close:"}"},"else":{_default:{$1:"true"},open:"}else if(($notnull_1) && $1a){"},html:{open:"if($notnull_1){__.push($1a);}"},"=":{_default:{$1:"$data"},open:"if($notnull_1){__.push($.encode($1a));}"},"!":{open:""}},complete:function(){b={}},afterManip:function(f,b,d){var e=b.nodeType===11?a.makeArray(b.childNodes):b.nodeType===1?[b]:[];d.call(f,b);m(e);c++}});function j(e,g,f){var b,c=f?a.map(f,function(a){return typeof a==="string"?e.key?a.replace(/(<\w+)(?=[\s>])(?![^>]*_tmplitem)([^>]*)/g,"$1 "+d+'="'+e.key+'" $2'):a:j(a,e,a._ctnt)}):e;if(g)return c;c=c.join("");c.replace(/^\s*([^<\s][^<]*)?(<[\w\W]+>)([^>]*[^>\s])?\s*$/,function(f,c,e,d){b=a(e).get();m(b);if(c)b=k(c).concat(b);if(d)b=b.concat(k(d))});return b?b:k(c)}function k(c){var b=document.createElement("div");b.innerHTML=c;return a.makeArray(b.childNodes)}function o(b){return new Function("jQuery","$item","var $=jQuery,call,__=[],$data=$item.data;with($data){__.push('"+a.trim(b).replace(/([\\'])/g,"\\$1").replace(/[\r\t\n]/g," ").replace(/\$\{([^\}]*)\}/g,"{{= $1}}").replace(/\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g,function(m,l,k,g,b,c,d){var j=a.tmplPlugin.tag[k],i,e,f;if(!j)throw"Unknown template tag: "+k;i=j._default||[];if(c&&!/\w$/.test(b)){b+=c;c=""}if(b){b=h(b);d=d?","+h(d)+")":c?")":"";e=c?b.indexOf(".")>-1?b+h(c):"("+b+").call($item"+d:b;f=c?e:"(typeof("+b+")==='function'?("+b+").call($item):("+b+"))"}else f=e=i.$1||"null";g=h(g);return"');"+j[l?"close":"open"].split("$notnull_1").join(b?"typeof("+b+")!=='undefined' && ("+b+")!=null":"true").split("$1a").join(f).split("$1").join(e).split("$2").join(g||i.$2||"")+"__.push('"})+"');}return __;")}function n(c,b){c._wrap=j(c,true,a.isArray(b)?b:[q.test(b)?b:a(b).html()]).join("")}function h(a){return a?a.replace(/\\'/g,"'").replace(/\\\\/g,"\\"):null}function s(b){var a=document.createElement("div");a.appendChild(b.cloneNode(true));return a.innerHTML}function m(o){var n="_"+c,k,j,l={},e,p,h;for(e=0,p=o.length;e<p;e++){if((k=o[e]).nodeType!==1)continue;j=k.getElementsByTagName("*");for(h=j.length-1;h>=0;h--)m(j[h]);m(k)}function m(j){var p,h=j,k,e,m;if(m=j.getAttribute(d)){while(h.parentNode&&(h=h.parentNode).nodeType===1&&!(p=h.getAttribute(d)));if(p!==m){h=h.parentNode?h.nodeType===11?0:h.getAttribute(d)||0:0;if(!(e=b[m])){e=f[m];e=g(e,b[h]||f[h]);e.key=++i;b[i]=e}c&&o(m)}j.removeAttribute(d)}else if(c&&(e=a.data(j,"tmplItem"))){o(e.key);b[e.key]=e;h=a.data(j.parentNode,"tmplItem");h=h?h.key:0}if(e){k=e;while(k&&k.key!=h){k.nodes.push(j);k=k.parent}delete e._ctnt;delete e._wrap;a.data(j,"tmplItem",e)}function o(a){a=a+n;e=l[a]=l[a]||g(e,b[e.parent.key+n]||e.parent)}}}function u(a,d,c,b){if(!a)return l.pop();l.push({_:a,tmplPlugin:d,item:this,data:c,options:b})}function w(d,c,b){return a.tmplPlugin(a.template(d),c,b,this)}function x(b,d){var c=b.options||{};c.wrapped=d;return a.tmplPlugin(a.template(b.tmplPlugin),b.data,c,b.item)}function v(d,c){var b=this._wrap;return a.map(a(a.isArray(b)?b.join(""):b).filter(d||"*"),function(a){return c?a.innerText||a.textContent:a.outerHTML||s(a)})}function t(){var b=this.nodes;a.tmplPlugin(null,null,null,this).insertBefore(b[0]);a(b).remove()}})(jQuery);
/*
* jQuery TableSorter plugin
@@ -158,3 +238,44 @@ var GaeMiniProfilerTemplate = {
function(){var parsers=[],widgets=[];this.defaults={cssHeader:"header",cssAsc:"headerSortUp",cssDesc:"headerSortDown",cssChildRow:"expand-child",sortInitialOrder:"asc",sortMultiSortKey:"shiftKey",sortForce:null,sortAppend:null,sortLocaleCompare:true,textExtraction:"simple",parsers:{},widgets:[],widgetZebra:{css:["even","odd"]},headers:{},widthFixed:false,cancelSelection:true,sortList:[],headerList:[],dateFormat:"us",decimal:'/\.|\,/g',onRenderHeader:null,selectorHeaders:'thead th',debug:false};function benchmark(s,d){log(s+","+(new Date().getTime()-d.getTime())+"ms");}this.benchmark=benchmark;function log(s){if(typeof console!="undefined"&&typeof console.debug!="undefined"){console.log(s);}else{alert(s);}}function buildParserCache(table,$headers){if(table.config.debug){var parsersDebug="";}if(table.tBodies.length==0)return;var rows=table.tBodies[0].rows;if(rows[0]){var list=[],cells=rows[0].cells,l=cells.length;for(var i=0;i<l;i++){var p=false;if($.metadata&&($($headers[i]).metadata()&&$($headers[i]).metadata().sorter)){p=getParserById($($headers[i]).metadata().sorter);}else if((table.config.headers[i]&&table.config.headers[i].sorter)){p=getParserById(table.config.headers[i].sorter);}if(!p){p=detectParserForColumn(table,rows,-1,i);}if(table.config.debug){parsersDebug+="column:"+i+" parser:"+p.id+"\n";}list.push(p);}}if(table.config.debug){log(parsersDebug);}return list;};function detectParserForColumn(table,rows,rowIndex,cellIndex){var l=parsers.length,node=false,nodeValue=false,keepLooking=true;while(nodeValue==''&&keepLooking){rowIndex++;if(rows[rowIndex]){node=getNodeFromRowAndCellIndex(rows,rowIndex,cellIndex);nodeValue=trimAndGetNodeText(table.config,node);if(table.config.debug){log('Checking if value was empty on row:'+rowIndex);}}else{keepLooking=false;}}for(var i=1;i<l;i++){if(parsers[i].is(nodeValue,table,node)){return parsers[i];}}return parsers[0];}function getNodeFromRowAndCellIndex(rows,rowIndex,cellIndex){return rows[rowIndex].cells[cellIndex];}function trimAndGetNodeText(config,node){return $.trim(getElementText(config,node));}function getParserById(name){var l=parsers.length;for(var i=0;i<l;i++){if(parsers[i].id.toLowerCase()==name.toLowerCase()){return parsers[i];}}return false;}function buildCache(table){if(table.config.debug){var cacheTime=new Date();}var totalRows=(table.tBodies[0]&&table.tBodies[0].rows.length)||0,totalCells=(table.tBodies[0].rows[0]&&table.tBodies[0].rows[0].cells.length)||0,parsers=table.config.parsers,cache={row:[],normalized:[]};for(var i=0;i<totalRows;++i){var c=$(table.tBodies[0].rows[i]),cols=[];if(c.hasClass(table.config.cssChildRow)){cache.row[cache.row.length-1]=cache.row[cache.row.length-1].add(c);continue;}cache.row.push(c);for(var j=0;j<totalCells;++j){cols.push(parsers[j].format(getElementText(table.config,c[0].cells[j]),table,c[0].cells[j]));}cols.push(cache.normalized.length);cache.normalized.push(cols);cols=null;};if(table.config.debug){benchmark("Building cache for "+totalRows+" rows:",cacheTime);}return cache;};function getElementText(config,node){var text="";if(!node)return"";if(!config.supportsTextContent)config.supportsTextContent=node.textContent||false;if(config.textExtraction=="simple"){if(config.supportsTextContent){text=node.textContent;}else{if(node.childNodes[0]&&node.childNodes[0].hasChildNodes()){text=node.childNodes[0].innerHTML;}else{text=node.innerHTML;}}}else{if(typeof(config.textExtraction)=="function"){text=config.textExtraction(node);}else{text=$(node).text();}}return text;}function appendToTable(table,cache){if(table.config.debug){var appendTime=new Date()}var c=cache,r=c.row,n=c.normalized,totalRows=n.length,checkCell=(n[0].length-1),tableBody=$(table.tBodies[0]),rows=[];for(var i=0;i<totalRows;i++){var pos=n[i][checkCell];rows.push(r[pos]);if(!table.config.appender){var l=r[pos].length;for(var j=0;j<l;j++){tableBody[0].appendChild(r[pos][j]);}}}if(table.config.appender){table.config.appender(table,rows);}rows=null;if(table.config.debug){benchmark("Rebuilt table:",appendTime);}applyWidget(table);setTimeout(function(){$(table).trigger("sortEnd");},0);};function buildHeaders(table){if(table.config.debug){var time=new Date();}var meta=($.metadata)?true:false;var header_index=computeTableHeaderCellIndexes(table);$tableHeaders=$(table.config.selectorHeaders,table).each(function(index){this.column=header_index[this.parentNode.rowIndex+"-"+this.cellIndex];this.order=formatSortingOrder(table.config.sortInitialOrder);this.count=this.order;if(checkHeaderMetadata(this)||checkHeaderOptions(table,index))this.sortDisabled=true;if(checkHeaderOptionsSortingLocked(table,index))this.order=this.lockedOrder=checkHeaderOptionsSortingLocked(table,index);if(!this.sortDisabled){var $th=$(this).addClass(table.config.cssHeader);if(table.config.onRenderHeader)table.config.onRenderHeader.apply($th);}table.config.headerList[index]=this;});if(table.config.debug){benchmark("Built headers:",time);log($tableHeaders);}return $tableHeaders;};function computeTableHeaderCellIndexes(t){var matrix=[];var lookup={};var thead=t.getElementsByTagName('THEAD')[0];var trs=thead.getElementsByTagName('TR');for(var i=0;i<trs.length;i++){var cells=trs[i].cells;for(var j=0;j<cells.length;j++){var c=cells[j];var rowIndex=c.parentNode.rowIndex;var cellId=rowIndex+"-"+c.cellIndex;var rowSpan=c.rowSpan||1;var colSpan=c.colSpan||1
var firstAvailCol;if(typeof(matrix[rowIndex])=="undefined"){matrix[rowIndex]=[];}for(var k=0;k<matrix[rowIndex].length+1;k++){if(typeof(matrix[rowIndex][k])=="undefined"){firstAvailCol=k;break;}}lookup[cellId]=firstAvailCol;for(var k=rowIndex;k<rowIndex+rowSpan;k++){if(typeof(matrix[k])=="undefined"){matrix[k]=[];}var matrixrow=matrix[k];for(var l=firstAvailCol;l<firstAvailCol+colSpan;l++){matrixrow[l]="x";}}}}return lookup;}function checkCellColSpan(table,rows,row){var arr=[],r=table.tHead.rows,c=r[row].cells;for(var i=0;i<c.length;i++){var cell=c[i];if(cell.colSpan>1){arr=arr.concat(checkCellColSpan(table,headerArr,row++));}else{if(table.tHead.length==1||(cell.rowSpan>1||!r[row+1])){arr.push(cell);}}}return arr;};function checkHeaderMetadata(cell){if(($.metadata)&&($(cell).metadata().sorter===false)){return true;};return false;}function checkHeaderOptions(table,i){if((table.config.headers[i])&&(table.config.headers[i].sorter===false)){return true;};return false;}function checkHeaderOptionsSortingLocked(table,i){if((table.config.headers[i])&&(table.config.headers[i].lockedOrder))return table.config.headers[i].lockedOrder;return false;}function applyWidget(table){var c=table.config.widgets;var l=c.length;for(var i=0;i<l;i++){getWidgetById(c[i]).format(table);}}function getWidgetById(name){var l=widgets.length;for(var i=0;i<l;i++){if(widgets[i].id.toLowerCase()==name.toLowerCase()){return widgets[i];}}};function formatSortingOrder(v){if(typeof(v)!="Number"){return(v.toLowerCase()=="desc")?1:0;}else{return(v==1)?1:0;}}function isValueInArray(v,a){var l=a.length;for(var i=0;i<l;i++){if(a[i][0]==v){return true;}}return false;}function setHeadersCss(table,$headers,list,css){$headers.removeClass(css[0]).removeClass(css[1]);var h=[];$headers.each(function(offset){if(!this.sortDisabled){h[this.column]=$(this);}});var l=list.length;for(var i=0;i<l;i++){h[list[i][0]].addClass(css[list[i][1]]);}}function fixColumnWidth(table,$headers){var c=table.config;if(c.widthFixed){var colgroup=$('<colgroup>');$("tr:first td",table.tBodies[0]).each(function(){colgroup.append($('<col>').css('width',$(this).width()));});$(table).prepend(colgroup);};}function updateHeaderSortCount(table,sortList){var c=table.config,l=sortList.length;for(var i=0;i<l;i++){var s=sortList[i],o=c.headerList[s[0]];o.count=s[1];o.count++;}}function multisort(table,sortList,cache){if(table.config.debug){var sortTime=new Date();}var dynamicExp="var sortWrapper = function(a,b) {",l=sortList.length;for(var i=0;i<l;i++){var c=sortList[i][0];var order=sortList[i][1];var s=(table.config.parsers[c].type=="text")?((order==0)?makeSortFunction("text","asc",c):makeSortFunction("text","desc",c)):((order==0)?makeSortFunction("numeric","asc",c):makeSortFunction("numeric","desc",c));var e="e"+i;dynamicExp+="var "+e+" = "+s;dynamicExp+="if("+e+") { return "+e+"; } ";dynamicExp+="else { ";}var orgOrderCol=cache.normalized[0].length-1;dynamicExp+="return a["+orgOrderCol+"]-b["+orgOrderCol+"];";for(var i=0;i<l;i++){dynamicExp+="}; ";}dynamicExp+="return 0; ";dynamicExp+="}; ";if(table.config.debug){benchmark("Evaling expression:"+dynamicExp,new Date());}eval(dynamicExp);cache.normalized.sort(sortWrapper);if(table.config.debug){benchmark("Sorting on "+sortList.toString()+" and dir "+order+" time:",sortTime);}return cache;};function makeSortFunction(type,direction,index){var a="a["+index+"]",b="b["+index+"]";if(type=='text'&&direction=='asc'){return"("+a+" == "+b+" ? 0 : ("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : ("+a+" < "+b+") ? -1 : 1 )));";}else if(type=='text'&&direction=='desc'){return"("+a+" == "+b+" ? 0 : ("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : ("+b+" < "+a+") ? -1 : 1 )));";}else if(type=='numeric'&&direction=='asc'){return"("+a+" === null && "+b+" === null) ? 0 :("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : "+a+" - "+b+"));";}else if(type=='numeric'&&direction=='desc'){return"("+a+" === null && "+b+" === null) ? 0 :("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : "+b+" - "+a+"));";}};function makeSortText(i){return"((a["+i+"] < b["+i+"]) ? -1 : ((a["+i+"] > b["+i+"]) ? 1 : 0));";};function makeSortTextDesc(i){return"((b["+i+"] < a["+i+"]) ? -1 : ((b["+i+"] > a["+i+"]) ? 1 : 0));";};function makeSortNumeric(i){return"a["+i+"]-b["+i+"];";};function makeSortNumericDesc(i){return"b["+i+"]-a["+i+"];";};function sortText(a,b){if(table.config.sortLocaleCompare)return a.localeCompare(b);return((a<b)?-1:((a>b)?1:0));};function sortTextDesc(a,b){if(table.config.sortLocaleCompare)return b.localeCompare(a);return((b<a)?-1:((b>a)?1:0));};function sortNumeric(a,b){return a-b;};function sortNumericDesc(a,b){return b-a;};function getCachedSortType(parsers,i){return parsers[i].type;};this.construct=function(settings){return this.each(function(){if(!this.tHead||!this.tBodies)return;var $this,$document,$headers,cache,config,shiftDown=0,sortOrder;this.config={};config=$.extend(this.config,$.tablesorter.defaults,settings);$this=$(this);$.data(this,"tablesorter",config);$headers=buildHeaders(this);this.config.parsers=buildParserCache(this,$headers);cache=buildCache(this);var sortCSS=[config.cssDesc,config.cssAsc];fixColumnWidth(this);$headers.click(function(e){var totalRows=($this[0].tBodies[0]&&$this[0].tBodies[0].rows.length)||0;if(!this.sortDisabled&&totalRows>0){$this.trigger("sortStart");var $cell=$(this);var i=this.column;this.order=this.count++%2;if(this.lockedOrder)this.order=this.lockedOrder;if(!e[config.sortMultiSortKey]){config.sortList=[];if(config.sortForce!=null){var a=config.sortForce;for(var j=0;j<a.length;j++){if(a[j][0]!=i){config.sortList.push(a[j]);}}}config.sortList.push([i,this.order]);}else{if(isValueInArray(i,config.sortList)){for(var j=0;j<config.sortList.length;j++){var s=config.sortList[j],o=config.headerList[s[0]];if(s[0]==i){o.count=s[1];o.count++;s[1]=o.count%2;}}}else{config.sortList.push([i,this.order]);}};setTimeout(function(){setHeadersCss($this[0],$headers,config.sortList,sortCSS);appendToTable($this[0],multisort($this[0],config.sortList,cache));},1);return false;}}).mousedown(function(){if(config.cancelSelection){this.onselectstart=function(){return false};return false;}});$this.bind("update",function(){var me=this;setTimeout(function(){me.config.parsers=buildParserCache(me,$headers);cache=buildCache(me);},1);}).bind("updateCell",function(e,cell){var config=this.config;var pos=[(cell.parentNode.rowIndex-1),cell.cellIndex];cache.normalized[pos[0]][pos[1]]=config.parsers[pos[1]].format(getElementText(config,cell),cell);}).bind("sorton",function(e,list){$(this).trigger("sortStart");config.sortList=list;var sortList=config.sortList;updateHeaderSortCount(this,sortList);setHeadersCss(this,$headers,sortList,sortCSS);appendToTable(this,multisort(this,sortList,cache));}).bind("appendCache",function(){appendToTable(this,cache);}).bind("applyWidgetId",function(e,id){getWidgetById(id).format(this);}).bind("applyWidgets",function(){applyWidget(this);});if($.metadata&&($(this).metadata()&&$(this).metadata().sortlist)){config.sortList=$(this).metadata().sortlist;}if(config.sortList.length>0){$this.trigger("sorton",[config.sortList]);}applyWidget(this);});};this.addParser=function(parser){var l=parsers.length,a=true;for(var i=0;i<l;i++){if(parsers[i].id.toLowerCase()==parser.id.toLowerCase()){a=false;}}if(a){parsers.push(parser);};};this.addWidget=function(widget){widgets.push(widget);};this.formatFloat=function(s){var i=parseFloat(s);return(isNaN(i))?0:i;};this.formatInt=function(s){var i=parseInt(s);return(isNaN(i))?0:i;};this.isDigit=function(s,config){return/^[-+]?\d*$/.test($.trim(s.replace(/[,.']/g,'')));};this.clearTableBody=function(table){if($.browser.msie){function empty(){while(this.firstChild)this.removeChild(this.firstChild);}empty.apply(table.tBodies[0]);}else{table.tBodies[0].innerHTML="";}};}});$.fn.extend({tablesorter:$.tablesorter.construct});var ts=$.tablesorter;ts.addParser({id:"text",is:function(s){return true;},format:function(s){return $.trim(s.toLocaleLowerCase());},type:"text"});ts.addParser({id:"digit",is:function(s,table){var c=table.config;return $.tablesorter.isDigit(s,c);},format:function(s){return $.tablesorter.formatFloat(s);},type:"numeric"});ts.addParser({id:"currency",is:function(s){return/^[£$€?.]/.test(s);},format:function(s){return $.tablesorter.formatFloat(s.replace(new RegExp(/[£$€]/g),""));},type:"numeric"});ts.addParser({id:"ipAddress",is:function(s){return/^\d{2,3}[\.]\d{2,3}[\.]\d{2,3}[\.]\d{2,3}$/.test(s);},format:function(s){var a=s.split("."),r="",l=a.length;for(var i=0;i<l;i++){var item=a[i];if(item.length==2){r+="0"+item;}else{r+=item;}}return $.tablesorter.formatFloat(r);},type:"numeric"});ts.addParser({id:"url",is:function(s){return/^(https?|ftp|file):\/\/$/.test(s);},format:function(s){return jQuery.trim(s.replace(new RegExp(/(https?|ftp|file):\/\//),''));},type:"text"});ts.addParser({id:"isoDate",is:function(s){return/^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(s);},format:function(s){return $.tablesorter.formatFloat((s!="")?new Date(s.replace(new RegExp(/-/g),"/")).getTime():"0");},type:"numeric"});ts.addParser({id:"percent",is:function(s){return/\%$/.test($.trim(s));},format:function(s){return $.tablesorter.formatFloat(s.replace(new RegExp(/%/g),""));},type:"numeric"});ts.addParser({id:"usLongDate",is:function(s){return s.match(new RegExp(/^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/));},format:function(s){return $.tablesorter.formatFloat(new Date(s).getTime());},type:"numeric"});ts.addParser({id:"shortDate",is:function(s){return/\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}/.test(s);},format:function(s,table){var c=table.config;s=s.replace(/\-/g,"/");if(c.dateFormat=="us"){s=s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/,"$3/$1/$2");}else if(c.dateFormat=="uk"){s=s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/,"$3/$2/$1");}else if(c.dateFormat=="dd/mm/yy"||c.dateFormat=="dd-mm-yy"){s=s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2})/,"$1/$2/$3");}return $.tablesorter.formatFloat(new Date(s).getTime());},type:"numeric"});ts.addParser({id:"time",is:function(s){return/^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/.test(s);},format:function(s){return $.tablesorter.formatFloat(new Date("2000/01/01 "+s).getTime());},type:"numeric"});ts.addParser({id:"metadata",is:function(s){return false;},format:function(s,table,cell){var c=table.config,p=(!c.parserMetadataName)?'sortValue':c.parserMetadataName;return $(cell).metadata()[p];},type:"numeric"});ts.addWidget({id:"zebra",format:function(table){if(table.config.debug){var time=new Date();}var $tr,row=-1,odd;$("tr:visible",table.tBodies[0]).each(function(i){$tr=$(this);if(!$tr.hasClass(table.config.cssChildRow))row++;odd=(row%2==0);$tr.removeClass(table.config.widgetZebra.css[odd?0:1]).addClass(table.config.widgetZebra.css[odd?1:0])});if(table.config.debug){$.tablesorter.benchmark("Applying Zebra widget",time);}}});})(jQuery);
+/**
+ * jQuery Cookie plugin
+ *
+ * Copyright (c) 2010 Klaus Hartl (stilbuero.de)
+ * Dual licensed under the MIT and GPL licenses:
+ * http://www.opensource.org/licenses/mit-license.php
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ */
+jQuery.cookiePlugin = function (key, value, options) {
+
+ // key and at least value given, set cookie...
+ if (arguments.length > 1 && String(value) !== "[object Object]") {
+ options = jQuery.extend({}, options);
+
+ if (value === null || value === undefined) {
+ options.expires = -1;
+ }
+
+ if (typeof options.expires === 'number') {
+ var days = options.expires, t = options.expires = new Date();
+ t.setDate(t.getDate() + days);
+ }
+
+ value = String(value);
+
+ return (document.cookie = [
+ encodeURIComponent(key), '=',
+ options.raw ? value : encodeURIComponent(value),
+ options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
+ options.path ? '; path=' + options.path : '',
+ options.domain ? '; domain=' + options.domain : '',
+ options.secure ? '; secure' : ''
+ ].join(''));
+ }
+
+ // key and possibly options given, get cookie...
+ options = value || {};
+ var result, decode = options.raw ? function (s) { return s; } : decodeURIComponent;
+ return (result = new RegExp('(?:^|; )' + encodeURIComponent(key) + '=([^;]*)').exec(document.cookie)) ? decode(result[1]) : null;
+};
View
89 libs/gae_mini_profiler/static/js/template.tmpl
@@ -4,6 +4,7 @@
<script id="profilerCornerEntryTemplate" type="text/x-jquery-tmpl">
<div class="entry">
+ {{if temporary_redirect}}&#8623;{{/if}}
${profiler_results.total_time} <span class="ms">ms</span>
</div>
</script>
@@ -18,10 +19,20 @@
${profiler_results.total_time} <span class="ms">ms</span>
</div>
</div>
+
+ {{if simple_timing}}
+
+ <div class="simple-timing">Detailed timing information is disabled. <a href="#" class="toggle-enabled">Enable</a>.</div>
+
+ {{else}}
+
<div class="date_and_share">
+ {{if temporary_redirect}}
+ <span class="redirect" title="Temporary redirect">&#8623; (302 redirect)</span>
+ {{/if}}
<span class="date">${s_dt}</span>
- -
- <a class="share" href="/gae_mini_profiler/shared?request_id=${encodeURIComponent(request_id)}">Share</a>
+ - <a class="share" href="/gae_mini_profiler/shared?request_id=${encodeURIComponent(request_id)}">Share</a>
+ - <a class="toggle-enabled" href="#">Off</a>
</div>
<div class="expand">
@@ -81,21 +92,30 @@
<div class="expand">
<a href="#rpc-link" class="rpc-link link uses_script">Remote Procedure Calls</a>
<div class="summary">
- ${appstats_results.total_time} <span class="ms">ms</span>
- spent in ${appstats_results.total_call_count} RPC{{if appstats_results.total_call_count != 1}}s{{/if}}
-
- {{if appstats_results.likely_dupes}}
- <span class="dupe">(likely dupes)</span>
- {{/if}}
+ {{if appstats_results.appstats_available}}
+ ${appstats_results.total_time} <span class="ms">ms</span>
+ spent in ${appstats_results.total_call_count} RPC{{if appstats_results.total_call_count != 1}}s{{/if}}
+
+ {{if appstats_results.likely_dupes}}
+ <span class="dupe">(likely dupes)</span>
+ {{/if}}
+ {{else}}
+ Appstats are unavailable
+ {{/if}}
</div>
</div>
<div class="rpc-details details fancy-scrollbar" style="display:none;">
+
+ <a class="appstats-link" target="_appstats" href="/_ah/stats/details?time=${appstats_results.appstats_key}">Full Appstats Details</a>
+
+ {{if appstats_results.appstats_available && appstats_results.calls.length}}
<table class="rpc-service-totals">
<thead>
<tr>
<th class="left"><nobr>service type</nobr></th>
<th class="right">calls</th>
+ <th class="right">misses</th>
<th class="right headerSortDown"><nobr>total ms</nobr></th>
</tr>
</thead>
@@ -103,6 +123,7 @@
<tr>
<td>${$value.service_prefix}</td>
<td class="right">${$value.total_call_count}</td>
+ <td class="right">${$value.total_misses}</td>
<td class="right">${$value.total_time}</td>
</tr>
{{/each}}
@@ -142,11 +163,61 @@
${$value.request_short}
</span>
</td>
- <td>${$value.response}</td>
+ <td>
+ <span title="${$value.response}">
+ ${$value.response_short}
+ </span>
+ </td>