PageRenderTime 40ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/dozer/profile.py

https://bitbucket.org/bbangert/dozer/
Python | 339 lines | 326 code | 8 blank | 5 comment | 10 complexity | f095777fda7888086ddc78c8d67fccc1 MD5 | raw file
  1. try:
  2. import cProfile
  3. except ImportError:
  4. # python2.4
  5. import profile as cProfile
  6. import cPickle
  7. import errno
  8. import time
  9. import os
  10. import re
  11. import thread
  12. from datetime import datetime
  13. from pkg_resources import resource_filename
  14. from mako.lookup import TemplateLookup
  15. from paste import urlparser
  16. from webob import Request, Response
  17. from webob import exc
  18. here_dir = os.path.dirname(os.path.abspath(__file__))
  19. DEFAULT_IGNORED_PATHS = [r'/favicon\.ico$', r'^/error/document']
  20. class Profiler(object):
  21. def __init__(self, app, global_conf=None, profile_path=None,
  22. ignored_paths=DEFAULT_IGNORED_PATHS,
  23. dot_graph_cutoff=0.2, **kwargs):
  24. """Profiles an application and saves the pickled version to a
  25. file
  26. """
  27. assert profile_path, "need profile_path"
  28. assert os.path.isdir(profile_path), "%r: no such directory" % profile_path
  29. self.app = app
  30. self.conf = global_conf
  31. self.dot_graph_cutoff = float(dot_graph_cutoff)
  32. self.profile_path = profile_path
  33. self.ignored_paths = map(re.compile, ignored_paths)
  34. tmpl_dir = os.path.join(here_dir, 'templates')
  35. self.mako = TemplateLookup(directories=[tmpl_dir])
  36. def __call__(self, environ, start_response):
  37. assert not environ['wsgi.multiprocess'], (
  38. "Dozer middleware is not usable in a "
  39. "multi-process environment")
  40. req = Request(environ)
  41. req.base_path = req.application_url + '/_profiler'
  42. if req.path_info_peek() == '_profiler':
  43. return self.profiler(req)(environ, start_response)
  44. for regex in self.ignored_paths:
  45. if regex.match(environ['PATH_INFO']) is not None:
  46. return self.app(environ, start_response)
  47. return self.run_profile(environ, start_response)
  48. def profiler(self, req):
  49. assert req.path_info_pop() == '_profiler'
  50. next_part = req.path_info_pop() or 'showall'
  51. method = getattr(self, next_part, None)
  52. if method is None:
  53. return exc.HTTPNotFound('Nothing could be found to match %r' % next_part)
  54. if not getattr(method, 'exposed', False):
  55. return exc.HTTPForbidden('Access to %r is forbidden' % next_part)
  56. return method(req)
  57. def media(self, req):
  58. """Static path where images and other files live"""
  59. path = resource_filename('dozer', 'media')
  60. app = urlparser.StaticURLParser(path)
  61. return app
  62. media.exposed = True
  63. def show(self, req):
  64. profile_id = req.path_info_pop()
  65. if not profile_id:
  66. return exc.HTTPNotFound('Missing profile id to view')
  67. dir_name = self.profile_path or ''
  68. fname = os.path.join(dir_name, profile_id) + '.pkl'
  69. data = cPickle.load(open(fname, 'rb'))
  70. top = [x for x in data['profile'].values() if not x.get('callers')]
  71. res = Response()
  72. res.body = self.render('/show_profile.mako', time=data['time'],
  73. profile=top, profile_data=data['profile'],
  74. environ=data['environ'], id=profile_id)
  75. return res
  76. show.exposed = True
  77. def showall(self, req):
  78. dir_name = self.profile_path
  79. profiles = []
  80. errors = []
  81. max_cost = 0
  82. for profile_file in os.listdir(dir_name):
  83. if profile_file.endswith('.pkl'):
  84. path = os.path.join(self.profile_path, profile_file)
  85. modified = os.stat(path).st_mtime
  86. try:
  87. data = cPickle.load(open(path, 'rb'))
  88. except Exception, e:
  89. errors.append((modified, '%s: %s' % (e.__class__.__name__, e), profile_file[:-4]))
  90. else:
  91. environ = data['environ']
  92. top = [x for x in data['profile'].values() if not x.get('callers')]
  93. if top:
  94. total_cost = max(float(x['cost']) for x in top)
  95. else:
  96. total_cost = 0
  97. max_cost = max(max_cost, total_cost)
  98. profiles.append((modified, environ, total_cost, profile_file[:-4]))
  99. profiles.sort(reverse=True)
  100. errors.sort(reverse=True)
  101. res = Response()
  102. if profiles:
  103. earliest = profiles[-1][0]
  104. else:
  105. earliest = None
  106. res.body = self.render('/list_profiles.mako', profiles=profiles,
  107. errors=errors, now=time.time(),
  108. earliest=earliest, max_cost=max_cost)
  109. return res
  110. showall.exposed = True
  111. def delete(self, req):
  112. profile_id = req.path_info_pop()
  113. if profile_id: # this prob a security risk
  114. try:
  115. for ext in ('.gv', '.pkl'):
  116. os.unlink(os.path.join(self.profile_path, profile_id + ext))
  117. except OSError, e:
  118. if e.errno == errno.ENOENT:
  119. pass # allow a file not found exception
  120. else:
  121. raise
  122. return Response('deleted %s' % profile_id)
  123. for filename in os.listdir(self.profile_path):
  124. if filename.endswith('.pkl') or filename.endswith('.gv'):
  125. os.unlink(os.path.join(self.profile_path, filename))
  126. res = Response()
  127. res.location = '/_profiler/showall'
  128. res.status_int = 302
  129. return res
  130. delete.exposed = True
  131. def render(self, name, **vars):
  132. tmpl = self.mako.get_template(name)
  133. return tmpl.render(**vars)
  134. def run_profile(self, environ, start_response):
  135. """Run the profile over the request and save it"""
  136. prof = cProfile.Profile()
  137. response_body = []
  138. def catching_start_response(status, headers, exc_info=None):
  139. start_response(status, headers, exc_info)
  140. return response_body.append
  141. def runapp():
  142. appiter = self.app(environ, catching_start_response)
  143. try:
  144. response_body.extend(appiter)
  145. finally:
  146. if hasattr(appiter, 'close'):
  147. appiter.close()
  148. prof.runcall(runapp)
  149. body = ''.join(response_body)
  150. results = prof.getstats()
  151. tree = buildtree(results)
  152. # Pull out 'safe' bits from environ
  153. safe_environ = {}
  154. for k, v in environ.iteritems():
  155. if k.startswith('HTTP_'):
  156. safe_environ[k] = v
  157. elif k in ['REQUEST_METHOD', 'SCRIPT_NAME', 'PATH_INFO',
  158. 'QUERY_STRING', 'CONTENT_TYPE', 'CONTENT_LENGTH',
  159. 'SERVER_NAME', 'SERVER_PORT', 'SERVER_PROTOCOL']:
  160. safe_environ[k] = v
  161. safe_environ['thread_id'] = str(thread.get_ident())
  162. profile_run = dict(time=datetime.now(), profile=tree,
  163. environ=safe_environ)
  164. fname_base = str(time.time()).replace('.', '_')
  165. prof_file = fname_base + '.pkl'
  166. dir_name = self.profile_path or ''
  167. cPickle.dump(profile_run, open(os.path.join(dir_name, prof_file), 'wb'))
  168. write_dot_graph(results, tree, os.path.join(dir_name, fname_base+'.gv'),
  169. cutoff=self.dot_graph_cutoff)
  170. del results, tree, profile_run
  171. return [body]
  172. def label(code):
  173. """Generate a friendlier version of the code function called"""
  174. if isinstance(code, str):
  175. return code
  176. else:
  177. return '%s %s:%d' % (code.co_name,
  178. code.co_filename,
  179. code.co_firstlineno)
  180. def graphlabel(code):
  181. lb = label(code)
  182. return lb.replace('"', "'").strip()
  183. def setup_time(t):
  184. """Takes a time generally assumed to be quite small and blows it
  185. up into millisecond time.
  186. For example:
  187. 0.004 seconds -> 4 ms
  188. 0.00025 seconds -> 0.25 ms
  189. The result is returned as a string.
  190. """
  191. t = t*1000
  192. t = '%0.2f' % t
  193. return t
  194. def color(w):
  195. # color scheme borrowed from
  196. # http://gprof2dot.jrfonseca.googlecode.com/hg/gprof2dot.py
  197. hmin, smin, lmin = 2/3., 0.8, .25
  198. hmax, smax, lmax = 0, 1, .5
  199. gamma = 2.2
  200. h = hmin + w * (hmax - hmin)
  201. s = smin + w * (smax - smin)
  202. l = lmin + w * (lmax - lmin)
  203. # http://www.w3.org/TR/css3-color/#hsl-color
  204. if l <= 0.5:
  205. m2 = l * (s + 1)
  206. else:
  207. m2 = l + s - l * s
  208. m1 = l * 2 - m2
  209. def h2rgb(m1, m2, h):
  210. if h < 0:
  211. h += 1.0
  212. elif h > 1:
  213. h -= 1.0
  214. if h * 6 < 1.0:
  215. return m1 + (m2 - m1) * h * 6
  216. elif h * 2 < 1:
  217. return m2
  218. elif h * 3 < 2:
  219. return m1 + (m2 - m1) * (2/3.0 - h) * 6
  220. else:
  221. return m1
  222. r = h2rgb(m1, m2, h + 1/3.0)
  223. g = h2rgb(m1, m2, h)
  224. b = h2rgb(m1, m2, h - 1/3.0)
  225. # gamma correction
  226. r **= gamma
  227. g **= gamma
  228. b **= gamma
  229. # graphvizification
  230. r = min(max(0, round(r * 0xff)), 0xff)
  231. g = min(max(0, round(g * 0xff)), 0xff)
  232. b = min(max(0, round(b * 0xff)), 0xff)
  233. return "#%02X%02X%02X" % (r, g, b)
  234. def write_dot_graph(data, tree, filename, cutoff=0.2):
  235. f = open(filename, 'w')
  236. f.write('digraph prof {\n')
  237. f.write('\tsize="11,9"; ratio = fill;\n')
  238. f.write('\tnode [style=filled];\n')
  239. # Find the largest time
  240. highest = 0.00
  241. for entry in tree.values():
  242. if float(entry['cost']) > highest:
  243. highest = float(entry['cost'])
  244. if highest == 0:
  245. highest = 1 # avoid division by zero
  246. for entry in data:
  247. code = entry.code
  248. entry_name = graphlabel(code)
  249. skip = float(setup_time(entry.totaltime)) < cutoff
  250. if isinstance(code, str) or skip:
  251. continue
  252. else:
  253. t = tree[label(code)]['cost']
  254. c = color(float(t) / highest)
  255. f.write('\t"%s" [label="%s\\n%sms",color="%s",fontcolor="white"]\n' % (entry_name, code.co_name, t, c))
  256. if entry.calls:
  257. for subentry in entry.calls:
  258. subcode = subentry.code
  259. skip = float(setup_time(subentry.totaltime)) < cutoff
  260. if isinstance(subcode, str) or skip:
  261. continue
  262. sub_name = graphlabel(subcode)
  263. f.write('\t"%s" -> "%s" [label="%s"]\n' % \
  264. (entry_name, sub_name, subentry.callcount))
  265. f.write('}\n')
  266. f.close()
  267. def buildtree(data):
  268. """Takes a pmstats object as returned by cProfile and constructs
  269. a call tree out of it"""
  270. functree = {}
  271. callregistry = {}
  272. for entry in data:
  273. node = {}
  274. code = entry.code
  275. # If its a builtin
  276. if isinstance(code, str):
  277. node['filename'] = '~'
  278. node['source_position'] = 0
  279. node['func_name'] = code
  280. else:
  281. node['filename'] = code.co_filename
  282. node['source_position'] = code.co_firstlineno
  283. node['func_name'] = code.co_name
  284. node['line_no'] = code.co_firstlineno
  285. node['cost'] = setup_time(entry.totaltime)
  286. node['function'] = label(code)
  287. if entry.calls:
  288. for subentry in entry.calls:
  289. subnode = {}
  290. subnode['builtin'] = isinstance(subentry.code, str)
  291. subnode['cost'] = setup_time(subentry.totaltime)
  292. subnode['function'] = label(subentry.code)
  293. subnode['callcount'] = subentry.callcount
  294. node.setdefault('calls', []).append(subnode)
  295. callregistry.setdefault(subnode['function'], []).append(node['function'])
  296. else:
  297. node['calls'] = []
  298. functree[node['function']] = node
  299. for key in callregistry:
  300. node = functree[key]
  301. node['callers'] = callregistry[key]
  302. return functree