| #!/usr/bin/env python |
| |
| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| |
| import argparse |
| import json |
| import logging |
| import random |
| import string |
| import csv |
| |
| from pylons import tmpl_context as c |
| from testfixtures import LogCapture |
| from mock import patch |
| from ming.odm import ThreadLocalODMSession |
| |
| from allura import model as M |
| from allura.lib.helpers import push_config |
| from allura.tests import TestController |
| from allura.tests.decorators import patch_middleware_config |
| |
| from forgewiki import model as WM |
| |
| |
| def parse_args(): |
| parser = argparse.ArgumentParser( |
| formatter_class=argparse.RawTextHelpFormatter, |
| description='Count number of expensive calls (mongo, markdown, etc) for a standard page.\n' |
| 'Currently its a _discuss URL with a few posts on it. This exercises core logic\n' |
| '(project & tool lookup, security, discussion thread, main template, etc) but\n' |
| 'intentionally avoids most tool-specific code.') |
| parser.add_argument('--verbose', '-v', action='store_true', default=False, |
| help='Show call details. Note that Timers with debug_each_call=False (like ming\'s Cursor.next) are not displayed in verbose mode (but they are counted).') |
| parser.add_argument('--debug-html', action='store_true', default=False, |
| help='Save HTML responses as local files') |
| parser.add_argument( |
| '--data-file', default='call_counts.csv', type=argparse.FileType('a'), |
| help='CSV file that is appended to') |
| parser.add_argument('--id', default='', |
| help='An identifier for this run. Examples:\n' |
| '`git rev-parse --short HEAD` for current hash\n' |
| '`git log -1 --oneline` for hash + message') |
| return parser.parse_args() |
| |
| |
| def main(args): |
| test = TestController() |
| setup(test) |
| |
| url = generate_wiki_thread(test) |
| # make sure ODM sessions won't get re-used |
| ThreadLocalODMSession.close_all() |
| |
| counts = count_page(test, url, verbose=args.verbose, |
| debug_html=args.debug_html) |
| print json.dumps(counts) |
| write_csv(counts, args.id, args.data_file) |
| test.tearDown() |
| |
| |
| def setup(test): |
| # includes setting up mim |
| with patch_middleware_config({'stats.sample_rate': 1, |
| 'stats.debug_line_length': 1000, |
| }), \ |
| patch('timermiddleware.log.isEnabledFor', return_value=True): # can't set this via logging configuration since setUp() will load a logging config and then start using it before we have a good place to tweak it |
| test.setUp() |
| |
| tmw_log = logging.getLogger('timermiddleware') |
| tmw_log.disabled = 0 # gets disabled when .ini file is loaded; dumb. |
| tmw_log.setLevel(logging.DEBUG) |
| |
| |
| def generate_wiki_thread(test): |
| # automagically instantiate the app |
| test.app.get('/wiki/') |
| |
| project = M.Project.query.get(shortname='test') |
| app = project.app_instance('wiki') |
| |
| page = WM.Page.query.get(app_config_id=app.config._id, title='Home') |
| thread = page.discussion_thread |
| # create a few posts by a few users |
| with push_config(c, user=M.User.query.get(username='test-admin'), |
| app=app, |
| project=project): |
| thread.add_post(text='This is very helpful') |
| thread.add_post(text="But it's not **super** helpful") |
| with push_config(c, user=M.User.query.get(username='test-user')): |
| thread.add_post(text='I disagree') |
| with push_config(c, user=M.User.query.get(username='test-user-1')): |
| thread.add_post(text='But what about foo?') |
| |
| ThreadLocalODMSession.flush_all() |
| |
| url = '/p/test/wiki/_discuss/thread/{}/'.format(thread._id) |
| return url |
| |
| |
| def count_page(test, url, verbose=False, debug_html=False): |
| |
| with LogCapture('stats') as stats, LogCapture('timermiddleware') as calls: |
| resp = test.app.get(url, extra_environ=dict(username='*anonymous')) |
| print url, resp.status |
| if debug_html: |
| debug_filename = 'call-{}.html'.format(''.join([random.choice(string.ascii_letters + string.digits) |
| for n in xrange(10)])) |
| with open(debug_filename, 'w') as out: |
| out.write(resp.body) |
| print debug_filename |
| |
| if verbose: |
| for r in calls.records: |
| print r.getMessage() |
| |
| assert len(stats.records) == 1 |
| timings = json.loads(stats.records[0].getMessage()) |
| # total is always 1, which is misleading |
| del timings['call_counts']['total'] |
| return timings['call_counts'] |
| |
| |
| def write_csv(counts, id, data_file): |
| cols = sorted(counts.keys()) |
| row = counts |
| if id: |
| cols = ['id'] + cols |
| row = dict(counts, id=id) |
| csv_out = csv.DictWriter(data_file, cols) |
| if data_file.tell() == 0: |
| csv_out.writeheader() |
| csv_out.writerow(row) |
| data_file.close() |
| |
| |
| if __name__ == '__main__': |
| main(parse_args()) |