[#6734] Add script for timing/profiling discussion-thread rendering

Signed-off-by: Tim Van Steenburgh <tvansteenburgh@gmail.com>
diff --git a/Allura/allura/scripts/md_perf.py b/Allura/allura/scripts/md_perf.py
new file mode 100644
index 0000000..503c70d
--- /dev/null
+++ b/Allura/allura/scripts/md_perf.py
@@ -0,0 +1,113 @@
+"""
+A script for timing/profiling the Markdown conversion of an artifact discussion thread.
+
+Example usage:
+
+(env-allura)root@h1v1024:/var/local/allura/Allura(master)$ time paster script production.ini allura/scripts/md_perf.py -- --converter=forge
+      Conversion Time (s)  Text Size Post._id
+   1 0.021863937377929688         20 7d3ce86bb2c3afaee21f28547ed542d0c8c483b8
+   2 0.002238035202026367         12 96b1283c6db619cd66ee092551758dc1ad5b958f
+   3 0.001832962036132812          9 42c44cd72815d29069378353745bef953bdb2b98
+   4 0.011796951293945312        901 cadbea52291c58bb190597c1df94527369b405fa
+   5 0.143218040466308594      11018 362bee7ef1ba4e062712bda22cb96ae9fe459b95
+   6 0.029596090316772461       1481 46b78785b849f55d24cf32ee1f4e4c916cb7a6fe
+   7 0.045151948928833008       4144 4e2f62188080baba9b88c045ba1f7c26903bfcf9
+   8 0.637056827545166016      14553 51c87f2b1d55c08147bb2353e20361236232b432
+   9 4.510329008102416992      23016 f298ad88584f3dc9b5c37ffde61391bf03d5bae6
+  10 3.614714860916137695      23016 8775a230b19c231daa1608bb4cea1e17aab54550
+  11 0.393079042434692383      19386 ed79e0d2f89366043a007e0837e98945453508c9
+  12 0.000739097595214844      43659 56ac2437ec1bd886a03d23e45aa1c947729760ec
+  13 0.258469104766845703       8520 57152cefe0424b7fc9dff7ab4d21a6ef6e90bf82
+  14 0.264052867889404297       8518 6788b13c90c9719ba46196a0a773f4c228df9f2a
+  15 0.995774984359741211      14553 3f5cce06d2400bcd56d7b90d12c569935f0099a4
+Total time: 10.930670023
+
+real    0m14.256s
+user    0m12.749s
+sys     0m1.112s
+
+"""
+
+import argparse
+import cProfile
+import re
+import sys
+import time
+
+from mock import patch
+
+try:
+    import re2
+    re2.set_fallback_notification(re2.FALLBACK_WARNING)
+    RE2_INSTALLED = True
+except ImportError:
+    RE2_INSTALLED = False
+
+from pylons import app_globals as g
+
+MAX_OUTPUT = 99999
+DUMMYTEXT = None
+
+
+def get_artifact():
+    from forgeblog import model as BM
+    return BM.BlogPost.query.get(
+            slug='2013/09/watch-breaking-bad-season-5-episode-16-felina-live-streaming')
+
+
+def main(opts):
+    import markdown
+    if opts.re2 and RE2_INSTALLED:
+        markdown.inlinepatterns.re = re2
+    converters = {
+        'markdown': lambda: markdown.Markdown(),
+        'markdown_safe': lambda: markdown.Markdown(safe_mode=True),
+        'markdown_escape': lambda: markdown.Markdown(safe_mode='escape'),
+        'forge': lambda: g.markdown,
+        }
+    md = converters[opts.converter]()
+    artifact = get_artifact()
+    return render(artifact, md, opts)
+
+
+def render(artifact, md, opts):
+    start = begin = time.time()
+    print "%4s %20s %10s %s" % ('', 'Conversion Time (s)', 'Text Size', 'Post._id')
+    for i, p in enumerate(artifact.discussion_thread.posts):
+        text = DUMMYTEXT or p.text
+        if opts.n and i + 1 not in opts.n:
+            print 'Skipping post %s' % str(i + 1)
+            continue
+        if opts.profile:
+            print 'Profiling post %s' % str(i + 1)
+            cProfile.runctx('output = md.convert(text)', globals(), locals())
+        else:
+            output = md.convert(text)
+        elapsed = time.time() - start
+        print "%4s %1.18f %10s %s" % (i + 1, elapsed, len(text), p._id)
+        if opts.output:
+            print 'Input:', text[:min(300, len(text))]
+            print 'Output:', output[:min(MAX_OUTPUT, len(output))]
+        start = time.time()
+    print "Total time:", start - begin
+    return output
+
+
+def parse_options():
+    parser = argparse.ArgumentParser()
+    parser.add_argument('--converter', default='markdown')
+    parser.add_argument('--profile', action='store_true', help='Run profiler and output timings')
+    parser.add_argument('--output', action='store_true', help='Print result of markdown conversion')
+    parser.add_argument('--re2', action='store_true', help='Run with re2 instead of re')
+    parser.add_argument('--compare', action='store_true', help='Run with re and re2, and compare results')
+    parser.add_argument('-n', '--n', nargs='+', type=int, help='Only convert nth post(s) in thread')
+    return parser.parse_args()
+
+
+if __name__ == '__main__':
+    opts = parse_options()
+    out1 = main(opts)
+    if opts.compare:
+        opts.re2 = not opts.re2
+        out2 = main(opts)
+        print 're/re2 outputs match: ', out1 == out2