diff --git a/.gitignore b/.gitignore index ba74660..0d41387 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ coverage.xml # Django stuff: *.log +db.sqlite3 # Sphinx documentation docs/_build/ diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..52a360b --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +recursive-include django_perf_project *.html diff --git a/README.md b/README.md new file mode 100644 index 0000000..0c1e617 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# django-perf-tester + +The goal of this project is to benchmark pypy against python 2.7 + +## Installation + +* Create a virtualenv `virtualenv -p /usr/bin/pypy pypy_venv` +* cd into this new virtualenv and activate it`. bin/activate` +* `pip install -e git+https://github.com/yml/django-perf-tester#egg=django-perf-tester` +* pip install the requirements `pip install -r src/django-perf-tester/requirements.txt` +* run `tox -c src/django-perf-tester/tox.ini` diff --git a/benchmark_vm.sh b/benchmark_vm.sh new file mode 100644 index 0000000..e6ef36c --- /dev/null +++ b/benchmark_vm.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -ex + +docker rm django-perf; docker run -t --name=django-perf -v "$PWD":/usr/src/django-perf -w /usr/src/django-perf pypy:2-5.1.1 bash -c "ln -sf /usr/local/bin/pypy /usr/local/bin/python; bash test.sh" > result_test_pypy.txt +docker rm django-perf; docker run -t --name=django-perf -v "$PWD":/usr/src/django-perf -w /usr/src/django-perf python:2.7.11 bash test.sh > result_test_python2.7.11.txt +docker rm django-perf; docker run -t --name=django-perf -v "$PWD":/usr/src/django-perf -w /usr/src/django-perf pyston/pyston bash test.sh > result_test_pyston.txt diff --git a/django_perf_test/__init__.py b/django_perf_project/__init__.py similarity index 100% rename from django_perf_test/__init__.py rename to django_perf_project/__init__.py diff --git a/fooapp/__init__.py b/django_perf_project/apps/__init__.py similarity index 100% rename from fooapp/__init__.py rename to django_perf_project/apps/__init__.py diff --git a/fooapp/migrations/__init__.py b/django_perf_project/apps/botbotperf/__init__.py similarity index 100% rename from fooapp/migrations/__init__.py rename to django_perf_project/apps/botbotperf/__init__.py diff --git a/fooapp/admin.py b/django_perf_project/apps/botbotperf/admin.py similarity index 100% rename from fooapp/admin.py rename to django_perf_project/apps/botbotperf/admin.py diff --git a/django_perf_project/apps/botbotperf/migrations/0001_initial.py b/django_perf_project/apps/botbotperf/migrations/0001_initial.py new file mode 100644 index 0000000..1fc5a4f --- /dev/null +++ b/django_perf_project/apps/botbotperf/migrations/0001_initial.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='ActivePlugin', + fields=[ + ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, help_text='', auto_created=True)), + ('configuration', models.TextField(blank=True, default={}, help_text=b'User-specified attributes for this plugin {"username": "joe", "api-key": "foo"}')), + ], + ), + migrations.CreateModel( + name='Channel', + fields=[ + ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, help_text='', auto_created=True)), + ('created', models.DateTimeField(help_text='', auto_now_add=True)), + ('updated', models.DateTimeField(help_text='', auto_now=True)), + ('name', models.CharField(max_length=250, help_text=b'IRC expects room name: #django')), + ('slug', models.SlugField(help_text='')), + ('private_slug', models.SlugField(unique=True, blank=True, null=True, help_text=b'Slug used for private rooms')), + ('password', models.CharField(max_length=250, blank=True, null=True, help_text=b'Password (mode +k) if the channel requires one')), + ('status', models.CharField(max_length=20, default=b'PENDING', choices=[(b'PENDING', b'Pending'), (b'ACTIVE', b'Active'), (b'ARCHIVED', b'Archived'), (b'BANNED', b'Banned')], help_text='')), + ('is_public', models.BooleanField(default=False, help_text='')), + ('is_featured', models.BooleanField(default=False, help_text='')), + ('public_kudos', models.BooleanField(default=True, help_text='')), + ('fingerprint', models.CharField(max_length=36, blank=True, null=True, help_text='')), + ('notes', models.TextField(blank=True, help_text='')), + ], + options={ + 'ordering': ('name',), + }, + ), + migrations.CreateModel( + name='ChatBot', + fields=[ + ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, help_text='', auto_created=True)), + ('is_active', models.BooleanField(default=False, help_text='')), + ('server', models.CharField(max_length=100, help_text=b'Format: irc.example.net:6697')), + ('server_password', models.CharField(max_length=100, blank=True, null=True, help_text=b'IRC server password - PASS command. Optional')), + ('server_identifier', models.CharField(max_length=164, help_text='')), + ('nick', models.CharField(max_length=64, help_text='')), + ('password', models.CharField(max_length=100, blank=True, null=True, help_text=b'Password to identify with NickServ. Optional.')), + ('real_name', models.CharField(max_length=250, help_text=b'Usually a URL with information about this bot.')), + ('slug', models.CharField(max_length=50, db_index=True, help_text='')), + ('max_channels', models.IntegerField(default=200, help_text='')), + ], + ), + migrations.CreateModel( + name='Plugin', + fields=[ + ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, help_text='', auto_created=True)), + ('name', models.CharField(max_length=100, help_text='')), + ('slug', models.SlugField(help_text='')), + ], + ), + migrations.AddField( + model_name='channel', + name='chatbot', + field=models.ForeignKey(help_text='', to='botbotperf.ChatBot'), + ), + migrations.AddField( + model_name='channel', + name='plugins', + field=models.ManyToManyField(help_text='', to='botbotperf.Plugin', through='botbotperf.ActivePlugin'), + ), + migrations.AddField( + model_name='activeplugin', + name='channel', + field=models.ForeignKey(help_text='', to='botbotperf.Channel'), + ), + migrations.AddField( + model_name='activeplugin', + name='plugin', + field=models.ForeignKey(help_text='', to='botbotperf.Plugin'), + ), + migrations.AlterUniqueTogether( + name='channel', + unique_together=set([('slug', 'chatbot'), ('name', 'chatbot')]), + ), + ] diff --git a/django_perf_project/apps/botbotperf/migrations/0002_usercount.py b/django_perf_project/apps/botbotperf/migrations/0002_usercount.py new file mode 100644 index 0000000..c26b85f --- /dev/null +++ b/django_perf_project/apps/botbotperf/migrations/0002_usercount.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('botbotperf', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='UserCount', + fields=[ + ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, help_text='', auto_created=True)), + ('dt', models.DateField(help_text='')), + ('counts', models.IntegerField(help_text='')), + ('channel', models.ForeignKey(help_text='', to='botbotperf.Channel')), + ], + ), + ] diff --git a/django_perf_project/apps/botbotperf/migrations/0003_auto_20151125_1111.py b/django_perf_project/apps/botbotperf/migrations/0003_auto_20151125_1111.py new file mode 100644 index 0000000..f065375 --- /dev/null +++ b/django_perf_project/apps/botbotperf/migrations/0003_auto_20151125_1111.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('botbotperf', '0002_usercount'), + ] + + operations = [ + migrations.CreateModel( + name='Log', + fields=[ + ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, help_text='', auto_created=True)), + ('timestamp', models.DateTimeField(db_index=True, help_text='')), + ('nick', models.CharField(max_length=255, help_text='')), + ('text', models.TextField(help_text='')), + ('action', models.BooleanField(default=False, help_text='')), + ('command', models.CharField(max_length=50, blank=True, null=True, help_text='')), + ('host', models.TextField(blank=True, null=True, help_text='')), + ('raw', models.TextField(blank=True, null=True, help_text='')), + ('room', models.CharField(max_length=100, blank=True, null=True, help_text='')), + ('bot', models.ForeignKey(null=True, help_text='', to='botbotperf.ChatBot')), + ('channel', models.ForeignKey(null=True, help_text='', to='botbotperf.Channel')), + ], + options={ + 'ordering': ('-timestamp',), + }, + ), + migrations.AlterIndexTogether( + name='log', + index_together=set([('channel', 'timestamp')]), + ), + ] diff --git a/django_perf_project/apps/botbotperf/migrations/__init__.py b/django_perf_project/apps/botbotperf/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_perf_project/apps/botbotperf/models.py b/django_perf_project/apps/botbotperf/models.py new file mode 100644 index 0000000..bb9bc20 --- /dev/null +++ b/django_perf_project/apps/botbotperf/models.py @@ -0,0 +1,462 @@ +import datetime +import random +import string +import uuid +from collections import OrderedDict +from importlib import import_module + +from django.conf import settings +from django.template.loader import render_to_string +from django.core.cache import cache +from django.db import models +from django.db.models import Max, Min +from django.db.models.aggregates import Count +from django.utils.text import slugify +from django.contrib.admindocs.utils import trim_docstring + + +class TimeStampedModel(models.Model): + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True + + +def pretty_slug(server): + parts = server.split('.') + if len(parts) == 3: + return parts[1] + return server + + +class ChatBotManager(models.Manager): + def get_active_slugs(self): + return [i[0] for i in + self.get_queryset().filter(is_active=True).distinct( + 'slug').values_list('slug')] + + +class NoAvailableChatBots(Exception): + """ + Raised we we don't have any chat bots aviable that can be used on the network. + """ + + +class ChatBot(models.Model): + is_active = models.BooleanField(default=False) + + server = models.CharField( + max_length=100, help_text="Format: irc.example.net:6697") + server_password = models.CharField( + max_length=100, + blank=True, + null=True, + help_text="IRC server password - PASS command. Optional") + server_identifier = models.CharField(max_length=164) + + nick = models.CharField(max_length=64) + password = models.CharField( + max_length=100, + blank=True, + null=True, + help_text="Password to identify with NickServ. Optional.") + real_name = models.CharField( + max_length=250, + help_text="Usually a URL with information about this bot.") + + slug = models.CharField(max_length=50, db_index=True) + max_channels = models.IntegerField(default=200) + + objects = ChatBotManager() + + @property + def legacy_slug(self): + return self.server.split(':')[0] + + def __unicode__(self): + return u'{server} ({nick})'.format(server=self.server, nick=self.nick) + + @property + def date_cache_key(self): + return 'dc:{0}'.format(self.pk) + + def save(self, *args, **kwargs): + self.server_identifier = u"%s.%s" % ( + slugify(unicode(self.server.replace(":", " ").replace(".", " "))), + slugify(unicode(self.nick)) + ) + + if not self.slug: + server = self.server.split(':')[0] + self.slug = pretty_slug(server) + + return super(ChatBot, self).save(*args, **kwargs) + + @classmethod + def allocate_bot(cls, slug): + bots = cls.objects.filter(slug='freenode', is_active=True).annotate( + Count('channel')).order_by('channel__count') + + for bot in bots: + if bot.max_channels > bot.channel__count: + return bot + else: + continue + + raise NoAvailableChatBots(slug) + + +class ChannelQuerySet(models.query.QuerySet): + def active(self): + return self.filter(status=Channel.ACTIVE) + + +class ChannelManager(models.Manager): + + def get_queryset(self): + return ChannelQuerySet(self.model, using=self._db) + + def public(self): + return self.get_queryset().filter(is_public=True) + + def active(self): + self.get_queryset().active() + + +class Channel(TimeStampedModel): + PENDING = 'PENDING' + ACTIVE = 'ACTIVE' + BANNED = 'BANNED' + ARCHIVED = 'ARCHIVED' + + STATUS_CHOICES = ( + (PENDING, 'Pending'), + (ACTIVE, 'Active'), + (ARCHIVED, 'Archived'), + (BANNED, 'Banned') + ) + + # These are the default plugin slugs. + DEFAULT_PLUGINS = ["logger", "ping", "last_seen", "help", "bangmotivate"] + + chatbot = models.ForeignKey(ChatBot) + name = models.CharField(max_length=250, + help_text="IRC expects room name: #django") + slug = models.SlugField() + private_slug = models.SlugField(unique=True, blank=True, null=True, + help_text="Slug used for private rooms") + + password = models.CharField(max_length=250, blank=True, null=True, + help_text="Password (mode +k) if the channel requires one") + + status = models.CharField(choices=STATUS_CHOICES, default=PENDING, max_length=20) + + # Flags + is_public = models.BooleanField(default=False) + is_featured = models.BooleanField(default=False) + public_kudos = models.BooleanField(default=True) + + plugins = models.ManyToManyField('Plugin', + through='ActivePlugin') + + fingerprint = models.CharField(max_length=36, blank=True, null=True) + + notes = models.TextField(blank=True) + + objects = ChannelManager() + + def __unicode__(self): + return self.name + + class Meta: + ordering = ('name',) + unique_together = ( + ('slug', 'chatbot'), + ('name', 'chatbot') + ) + + @classmethod + def generate_private_slug(cls): + return "".join([random.choice(string.ascii_letters) for _ in xrange(8)]) + + def get_absolute_url(self): + from botbot.apps.bots.utils import reverse_channel + return reverse_channel(self, 'log_current') + + def get_eventsource_url(self): + from botbot.apps.bots.utils import reverse_channel + return reverse_channel(self, 'log_stream') + + def create_default_plugins(self): + """ + Adds our default plugins to the channel. + :return: + """ + for plugin in self.DEFAULT_PLUGINS: + pobj = Plugin.objects.get(slug=plugin) + active = ActivePlugin() + active.plugin = pobj + active.channel = self + active.save() + + @property + def active_plugin_slugs_cache_key(self): + return 'channel:{0}:plugins'.format(self.name) + + def plugin_config_cache_key(self, slug): + return 'channel:{0}:{1}:config'.format(self.name, slug) + + @property + def active_plugin_slugs(self): + """A cached set of the active plugins for the channel""" + cache_key = self.active_plugin_slugs_cache_key + cached_plugins = cache.get(cache_key) + if not cached_plugins: + plugins = self.activeplugin_set.all().select_related('plugin') + slug_set = set([actv.plugin.slug for actv in plugins]) + cache.set(cache_key, slug_set) + cached_plugins = slug_set + return cached_plugins + + def plugin_config(self, plugin_slug): + """A cached configuration for an active plugin""" + cache_key = self.plugin_config_cache_key(plugin_slug) + cached_config = cache.get(cache_key) + if not cached_config: + try: + active_plugin = self.activeplugin_set.get( + plugin__slug=plugin_slug) + cached_config = active_plugin.configuration + except ActivePlugin.DoesNotExist: + cached_config = {} + cache.set(cache_key, cached_config) + return cached_config + + def user_can_access_kudos(self, user): + if self.public_kudos: + return True + return ( + user.is_authenticated() + ) + + @property + def visible_commands_filter(self): + """ + Provide Q object useful for limiting the logs to those that matter. + Limits to certain IRC commands (including some more for + private channels). + """ + return models.Q( + command__in=['PRIVMSG', + 'NICK', + 'NOTICE', + 'TOPIC', + 'ACTION', + 'SHUTDOWN', + 'JOIN', + 'QUIT', + 'PART', + 'AWAY' + ]) + + def filtered_logs(self): + return (self.log_set.filter(self.visible_commands_filter) + .exclude(command="NOTICE", nick="NickServ") + .exclude(command="NOTICE", + text__startswith="*** ")) + + def get_months_active(self): + """ + Creates a OrderedDict of the format: + { + ... + '2010': { + first_day_of_month_datetime: pk_of_first_log, + ... + }, + } + """ + current_month = datetime.datetime.today().month + # Added the current month to the key to automatically update + minmax_dict_key = "minmax_dict_%s_%s" % (self.id, current_month) + minmax_dict = cache.get(minmax_dict_key, None) + if minmax_dict is None: + minmax_dict = self.log_set.all().aggregate( + last_log=Max("timestamp"), + first_log=Min("timestamp")) + if not minmax_dict['first_log']: + return OrderedDict() + # cache for 10 days + cache.set(minmax_dict_key, minmax_dict, 864000) + first_log = minmax_dict['first_log'].date() + last_log = minmax_dict['last_log'].date() + last_log = datetime.date(last_log.year, last_log.month, 1) + current = datetime.date(first_log.year, first_log.month, 1) + months_active = OrderedDict() + while current <= last_log: + months_active.setdefault(current.year, []).append(current) + if current.month == 12: + current = datetime.date(current.year + 1, 1, 1) + else: + current = datetime.date(current.year, current.month + 1, 1) + return months_active + + def current_size(self): + """Number of users in this channel. + We only log hourly, so can be a bit off. + None if we don't have a record yet. + """ + try: + usercount = UserCount.objects.get(channel=self, + dt=datetime.date.today()) + except UserCount.DoesNotExist: + return None + + hour = datetime.datetime.now().hour + + try: + # Postgres arrays are 1 based, but here become 0 based, so shift + count = usercount.counts[hour - 2] + if not count: + # Try one hour ago in case not logged this hour yet + count = usercount.counts[hour - 3] + except IndexError: + return None + return count + + def save(self, *args, **kwargs): + """ + Ensure that an empty slug is converted to a null slug so that it + doesn't trip up on multiple slugs being empty. + Update the 'fingerprint' on every save, its a UUID indicating the + botbot-bot application that something has changed in this channel. + """ + if not self.is_public and not self.private_slug: + self.private_slug = self.generate_private_slug() + + self.fingerprint = uuid.uuid4() + + super(Channel, self).save(*args, **kwargs) + + +class Plugin(models.Model): + """A global plugin registered in botbot""" + name = models.CharField(max_length=100) + slug = models.SlugField() + + @property + def user_docs(self): + for mod_prefix in ('botbot_plugins.plugins.', + 'botbot.apps.plugins.core.'): + try: + docs = import_module(mod_prefix + self.slug).Plugin.__doc__ + return trim_docstring(docs) + except (ImportError, AttributeError): + continue + return '' + + def __unicode__(self): + return self.name + + +class ActivePlugin(models.Model): + """An active plugin for a ChatBot""" + plugin = models.ForeignKey('Plugin') + channel = models.ForeignKey('Channel') + configuration = models.TextField( + blank=True, default={}, + help_text="User-specified attributes for this plugin " + + '{"username": "joe", "api-key": "foo"}') + + def save(self, *args, **kwargs): + obj = super(ActivePlugin, self).save(*args, **kwargs) + # Let the plugin_runner auto-reload the new values + cache.delete(self.channel.plugin_config_cache_key(self.plugin.slug)) + cache.delete(self.channel.active_plugin_slugs_cache_key) + return obj + + def __unicode__(self): + return u'{0} for {1}'.format(self.plugin.name, self.channel.name) + + +class UserCount(models.Model): + """Number of users in a channel, per hour.""" + + channel = models.ForeignKey(Channel) + dt = models.DateField() + counts = models.IntegerField() + + def __unicode__(self): + return "{} on {}: {}".format(self.channel, self.dt, self.counts) + +REDACTED_TEXT = '[redacted]' + +MSG_TMPL = { + u"JOIN": u"{nick} joined the channel", + u"NICK": u"{nick} is now known as {text}", + u"QUIT": u"{nick} has quit", + u"PART": u"{nick} has left the channel", + u"ACTION": u"{nick} {text}", + u"SHUTDOWN": u"-- BotBot disconnected, possible missing messages --", + } + + +class Log(models.Model): + bot = models.ForeignKey('ChatBot', null=True) + channel = models.ForeignKey('Channel', null=True) + timestamp = models.DateTimeField(db_index=True) + nick = models.CharField(max_length=255) + text = models.TextField() + action = models.BooleanField(default=False) + + command = models.CharField(max_length=50, null=True, blank=True) + host = models.TextField(null=True, blank=True) + raw = models.TextField(null=True, blank=True) + + # freenode chan name length limit is 50 chars, Campfire room ids are ints, + # so 100 should be enough + room = models.CharField(max_length=100, null=True, blank=True) + + class Meta: + ordering = ('-timestamp',) + index_together = [ + ['channel', 'timestamp'], + ] + + def get_absolute_url(self): + return "TODO" + + def as_html(self): + return render_to_string("logs/log_display.html", + {'message_list': [self]}) + def get_cleaned_host(self): + if self.host: + if '@' in self.host: + return self.host.split('@')[1] + else: + return self.host + + def get_nick_color(self): + return hash(self.nick) % 32 + + def __unicode__(self): + if self.command == u"PRIVMSG": + text = u'' + if self.nick: + text += u'{0}: '.format(self.nick) + text += self.text[:20] + else: + try: + text = MSG_TMPL[self.command].format(nick=self.nick, text=self.text) + except KeyError: + text = u"{}: {}".format(self.command, self.text) + + return text + + def save(self, *args, **kwargs): + if self.nick in settings.EXCLUDE_NICKS: + self.text = REDACTED_TEXT + + obj = super(Log, self).save(*args, **kwargs) + return obj diff --git a/django_perf_project/apps/botbotperf/tests.py b/django_perf_project/apps/botbotperf/tests.py new file mode 100644 index 0000000..063d32e --- /dev/null +++ b/django_perf_project/apps/botbotperf/tests.py @@ -0,0 +1,81 @@ +import inspect +import datetime + +import populate +from django_perf_project.apps.core.tests import PerfoTestCase + +from .models import Channel, ChatBot + + +def simulate_botbot_queries(slug, start_date): + next_day = start_date + datetime.timedelta(days=1) + previous_day = start_date - datetime.timedelta(days=1) + + channel = (Channel.objects.filter(status=Channel.ACTIVE, + slug=slug, + is_public=True) + .select_related('chatbot'))[0] + size = channel.current_size() + # current logs + logs = (channel.filtered_logs() + .filter(timestamp__gte=start_date, + timestamp__lt=next_day) + .order_by('timestamp')) + # determine page number of previous logs + prev_count = (channel.filtered_logs() + .filter(timestamp__gte=previous_day, + timestamp__lt=start_date) + .order_by('timestamp').count()) + + # determine next page number + next_count = logs.count() + if size: + print("Members: {}".format(size)) + return { + 'logs': list(logs), + 'prev_count': prev_count, + 'next_count': next_count} + + +class EmptyBotbotPerfoTest(PerfoTestCase): + + def setUp(self): + super(EmptyBotbotPerfoTest, self).setUp() + + self.start_date = datetime.date(2015, 4, 1) + self.slug = 'pypy' + + chatbot = ChatBot.objects.create( + is_active=True, server="irc.example.net:6697", + nick="foo", password="bar", slug="thechatbot") + Channel.objects.create( + chatbot=chatbot, + status=Channel.ACTIVE, + slug=self.slug, + is_public=True) + + def test_reduced_botbot_logs(self): + name = inspect.currentframe().f_code.co_name[5:] + + def f(): + simulate_botbot_queries(self.slug, self.start_date) + + self.run_benchmark(name, self.small_iterations, f) + + +class LoadedBotbotPerfoTest(PerfoTestCase): + + def setUp(self): + super(LoadedBotbotPerfoTest, self).setUp() + + self.start_date = datetime.date(2015, 4, 1) + self.slug = 'pypy' + populate.populate_botbot(self.slug, self.start_date) + + def test_reduced_botbot_logs(self): + name = inspect.currentframe().f_code.co_name[5:] + + def f(): + simulate_botbot_queries(self.slug, self.start_date) + + self.run_benchmark(name, self.xsmall_iterations, f) diff --git a/fooapp/views.py b/django_perf_project/apps/botbotperf/views.py similarity index 100% rename from fooapp/views.py rename to django_perf_project/apps/botbotperf/views.py diff --git a/django_perf_project/apps/core/__init__.py b/django_perf_project/apps/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_perf_project/apps/core/admin.py b/django_perf_project/apps/core/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/django_perf_project/apps/core/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/django_perf_project/apps/core/migrations/__init__.py b/django_perf_project/apps/core/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_perf_project/apps/core/models.py b/django_perf_project/apps/core/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/django_perf_project/apps/core/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/django_perf_project/apps/core/tests.py b/django_perf_project/apps/core/tests.py new file mode 100644 index 0000000..637ea08 --- /dev/null +++ b/django_perf_project/apps/core/tests.py @@ -0,0 +1,28 @@ +import time +from django.test import TestCase + + +class PerfoTestCase(TestCase): + + def setUp(self): + self.iterations = [10, 1e2-10, 1e3-1e2, + 1e4-1e3, 1e5-1e4, 1e6-1e5, + 1e7-1e6] + self.small_iterations = self.iterations[0:5] + self.xsmall_iterations = self.iterations[0:4] + + def report_run(self, name, nb_iteration, + total_duration, last_iteration_duration): + print u"{0:e}s avg per iteration for {1} (iterations={2}) -- last operation: {3:e}s".format((total_duration + last_iteration_duration)/nb_iteration, name, nb_iteration, last_iteration_duration) + + def run_benchmark(self, name, iterations, func): + print + for n in iterations: + start = time.time() + for i in xrange(int(n)-1): + func() + duration = time.time() - start + start = time.time() + func() + last_operation = time.time() - start + self.report_run(name, n, duration, last_operation) diff --git a/django_perf_project/apps/core/views.py b/django_perf_project/apps/core/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/django_perf_project/apps/core/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/django_perf_project/apps/ormperf/__init__.py b/django_perf_project/apps/ormperf/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_perf_project/apps/ormperf/admin.py b/django_perf_project/apps/ormperf/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/django_perf_project/apps/ormperf/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/fooapp/apps.py b/django_perf_project/apps/ormperf/apps.py similarity index 76% rename from fooapp/apps.py rename to django_perf_project/apps/ormperf/apps.py index 61d65c4..6b58935 100644 --- a/fooapp/apps.py +++ b/django_perf_project/apps/ormperf/apps.py @@ -2,4 +2,4 @@ class FooappConfig(AppConfig): - name = 'fooapp' + name = 'ormperf' diff --git a/fooapp/migrations/0001_initial.py b/django_perf_project/apps/ormperf/migrations/0001_initial.py similarity index 95% rename from fooapp/migrations/0001_initial.py rename to django_perf_project/apps/ormperf/migrations/0001_initial.py index 0f62d4d..dd2d113 100644 --- a/fooapp/migrations/0001_initial.py +++ b/django_perf_project/apps/ormperf/migrations/0001_initial.py @@ -33,6 +33,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='choice', name='question', - field=models.ForeignKey(help_text='', on_delete=django.db.models.deletion.CASCADE, to='fooapp.Question'), + field=models.ForeignKey(help_text='', on_delete=django.db.models.deletion.CASCADE, to='ormperf.Question'), ), ] diff --git a/django_perf_project/apps/ormperf/migrations/__init__.py b/django_perf_project/apps/ormperf/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fooapp/models.py b/django_perf_project/apps/ormperf/models.py similarity index 100% rename from fooapp/models.py rename to django_perf_project/apps/ormperf/models.py diff --git a/django_perf_project/apps/ormperf/tests.py b/django_perf_project/apps/ormperf/tests.py new file mode 100644 index 0000000..d6681eb --- /dev/null +++ b/django_perf_project/apps/ormperf/tests.py @@ -0,0 +1,98 @@ +import inspect +from datetime import datetime, timedelta + +from django.db.models import Count, Q +from django.utils.timezone import utc + +from .models import Question +from django_perf_project.apps.core.tests import PerfoTestCase + + +def add(x, y): + return x + y + + +class OrmPerfoTest(PerfoTestCase): + + def setUp(self): + super(OrmPerfoTest, self).setUp() + + # creating 100 questions + for i in range(1, 11): + for j in range(10): + Question.objects.create( + question_text="q {}".format(i), + pub_date=datetime(2015, 10, i, j, 0, 15, tzinfo=utc)) + + def test_is_pypyjit_available(self): + try: + import pypyjit + snp=pypyjit.get_stats_snapshot() + print u"pypyjit.get_stats_snapshot().counter_times: {}".format(snp.counter_times) + except ImportError: + self.assertEqual(True, False, "pypyjit is not available") + + + def test_int_addition(self): + name = inspect.currentframe().f_code.co_name[5:] + + def f(): + return add(1, 2) + self.run_benchmark(name, self.iterations, f) + + def test_orm_datetime_filtering(self): + name = inspect.currentframe().f_code.co_name[5:] + d0 = datetime(2015, 10, 1, 0, 0, 0, tzinfo=utc) + d1 = datetime(2015, 10, 12, 10, 0, 0, tzinfo=utc) + + def f(): + return [x for x in Question.objects.filter(pub_date__gte=d0, pub_date__lt=d1).all()] + self.run_benchmark(name, self.small_iterations, f) + + def test_orm_first_ten(self): + name = inspect.currentframe().f_code.co_name[5:] + + def f(): + return [x for x in Question.objects.all()[:10]] + self.run_benchmark(name, self.small_iterations, f) + + def test_orm_annotation(self): + name = inspect.currentframe().f_code.co_name[5:] + + def f (): + return [q for q in Question.objects.extra({'pub_day': "date(pub_date)"}).values('pub_day').annotate(count=Count('id'))] + self.run_benchmark(name, self.small_iterations, f) + + def test_orm_clone(self): + name = inspect.currentframe().f_code.co_name[5:] + d0 = datetime(2015, 10, 1, 0, 0, 0, tzinfo=utc) + d1 = d0 + timedelta(days=1) + + def f(): + qs = Question.objects.all() + qs = qs.filter(pub_date__gte=d0) + qs = qs.filter(pub_date__lt=d1) + return [x for x in qs] + self.run_benchmark(name, self.small_iterations, f) + + def test_orm_or_q(self): + name = inspect.currentframe().f_code.co_name[5:] + d0 = datetime(2015, 10, 1, 0, 0, 0, tzinfo=utc) + d1 = d0 + timedelta(days=1) + d2 = d1 + timedelta(days=1) + d3 = d2 + timedelta(days=1) + d4 = d3 + timedelta(days=1) + d5 = d4 + timedelta(days=1) + d6 = d5 + timedelta(days=1) + d7 = d6 + timedelta(days=1) + + q1 = Q(pub_date__gte=d0, pub_date__lt=d1) + q2 = Q(pub_date__gte=d1, pub_date__lt=d2) + q3 = Q(pub_date__gte=d3, pub_date__lt=d4) + q4 = Q(pub_date__gte=d4, pub_date__lt=d5) + q5 = Q(pub_date__gte=d6, pub_date__lt=d7) + + def f(): + qs = Question.objects.filter(q1 | q2 | q3 | q4 | q5) + return [x for x in qs] + self.run_benchmark(name, self.small_iterations, f) diff --git a/django_perf_project/apps/ormperf/views.py b/django_perf_project/apps/ormperf/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/django_perf_project/apps/ormperf/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/django_perf_project/apps/templateperf/__init__.py b/django_perf_project/apps/templateperf/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_perf_project/apps/templateperf/admin.py b/django_perf_project/apps/templateperf/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/django_perf_project/apps/templateperf/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/django_perf_project/apps/templateperf/migrations/__init__.py b/django_perf_project/apps/templateperf/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_perf_project/apps/templateperf/models.py b/django_perf_project/apps/templateperf/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/django_perf_project/apps/templateperf/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/django_perf_project/apps/templateperf/templates/base.html b/django_perf_project/apps/templateperf/templates/base.html new file mode 100644 index 0000000..6a41234 --- /dev/null +++ b/django_perf_project/apps/templateperf/templates/base.html @@ -0,0 +1 @@ +{% for i in range %}b{{ i }}{% include "child.html" %}{% endfor %} diff --git a/django_perf_project/apps/templateperf/templates/base_no_extends.html b/django_perf_project/apps/templateperf/templates/base_no_extends.html new file mode 100644 index 0000000..7fd647f --- /dev/null +++ b/django_perf_project/apps/templateperf/templates/base_no_extends.html @@ -0,0 +1 @@ +{% for i in range %}b{{ i }}{% include "child_no_extends.html" %}{% endfor %} diff --git a/django_perf_project/apps/templateperf/templates/child.html b/django_perf_project/apps/templateperf/templates/child.html new file mode 100644 index 0000000..ae0fd01 --- /dev/null +++ b/django_perf_project/apps/templateperf/templates/child.html @@ -0,0 +1,2 @@ +{% extends "top.html" %} +{% block "child" %}{{ block.super }}c {{ foo }} {% endblock %} diff --git a/django_perf_project/apps/templateperf/templates/child_no_extends.html b/django_perf_project/apps/templateperf/templates/child_no_extends.html new file mode 100644 index 0000000..af41ecb --- /dev/null +++ b/django_perf_project/apps/templateperf/templates/child_no_extends.html @@ -0,0 +1 @@ +{% block "child" %}c {{ foo }} {% endblock %} diff --git a/django_perf_project/apps/templateperf/templates/top.html b/django_perf_project/apps/templateperf/templates/top.html new file mode 100644 index 0000000..232ba3c --- /dev/null +++ b/django_perf_project/apps/templateperf/templates/top.html @@ -0,0 +1 @@ +{% block "child" %}t{% endblock %} diff --git a/django_perf_project/apps/templateperf/tests.py b/django_perf_project/apps/templateperf/tests.py new file mode 100644 index 0000000..523acad --- /dev/null +++ b/django_perf_project/apps/templateperf/tests.py @@ -0,0 +1,44 @@ +import inspect + +from django.template import loader, RequestContext +from django_perf_project.apps.core.tests import PerfoTestCase + + +class TemplatePerfTest(PerfoTestCase): + + def setUp(self): + super(TemplatePerfTest, self).setUp() + self.params = {'title': 'title', 'range': range(100)} + + def render_template(self, template_name): + tmpl = loader.get_template(template_name) + ctx = RequestContext({}, self.params) + return tmpl.render(ctx) + + def test_render_child(self): + name = inspect.currentframe().f_code.co_name[5:] + + def f(): + self.render_template("child.html") + self.run_benchmark(name, self.xsmall_iterations, f) + + def test_render_child_no_extends(self): + name = inspect.currentframe().f_code.co_name[5:] + + def f(): + self.render_template("child_no_extends.html") + self.run_benchmark(name, self.xsmall_iterations, f) + + def test_render_base(self): + name = inspect.currentframe().f_code.co_name[5:] + + def f(): + self.render_template("base.html") + self.run_benchmark(name, self.xsmall_iterations, f) + + def test_render_base_no_extends(self): + name = inspect.currentframe().f_code.co_name[5:] + + def f(): + self.render_template("base_no_extends.html") + self.run_benchmark(name, self.xsmall_iterations, f) diff --git a/django_perf_project/apps/templateperf/views.py b/django_perf_project/apps/templateperf/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/django_perf_project/apps/templateperf/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/django_perf_test/settings.py b/django_perf_project/settings.py similarity index 90% rename from django_perf_test/settings.py rename to django_perf_project/settings.py index 0acaa49..1d26d2a 100644 --- a/django_perf_test/settings.py +++ b/django_perf_project/settings.py @@ -1,5 +1,5 @@ """ -Django settings for django_perf_test project. +Django settings for django_perf_project project. Generated by 'django-admin startproject' using Django 1.10.dev20151118003232. @@ -37,7 +37,9 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'fooapp', + 'django_perf_project.apps.ormperf', + 'django_perf_project.apps.templateperf', + 'django_perf_project.apps.botbotperf', ] MIDDLEWARE_CLASSES = [ @@ -50,7 +52,7 @@ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] -ROOT_URLCONF = 'django_perf_test.urls' +ROOT_URLCONF = 'django_perf_project.urls' TEMPLATES = [ { @@ -68,7 +70,7 @@ }, ] -WSGI_APPLICATION = 'django_perf_test.wsgi.application' +WSGI_APPLICATION = 'django_project.wsgi.application' # Database @@ -119,3 +121,7 @@ # https://docs.djangoproject.com/en/dev/howto/static-files/ STATIC_URL = '/static/' + +# botbot specific + +EXCLUDE_NICKS = [] diff --git a/django_perf_test/urls.py b/django_perf_project/urls.py similarity index 95% rename from django_perf_test/urls.py rename to django_perf_project/urls.py index ae6f494..041fb80 100644 --- a/django_perf_test/urls.py +++ b/django_perf_project/urls.py @@ -1,4 +1,4 @@ -"""django_perf_test URL Configuration +"""django_perf_project URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/dev/topics/http/urls/ diff --git a/django_perf_test/wsgi.py b/django_perf_project/wsgi.py similarity index 69% rename from django_perf_test/wsgi.py rename to django_perf_project/wsgi.py index 5ce0d21..b7c3dd7 100644 --- a/django_perf_test/wsgi.py +++ b/django_perf_project/wsgi.py @@ -1,5 +1,5 @@ """ -WSGI config for django_perf_test project. +WSGI config for django_perf_project project. It exposes the WSGI callable as a module-level variable named ``application``. @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_perf_test.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_perf_project.settings") application = get_wsgi_application() diff --git a/fooapp/tests.py b/fooapp/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/fooapp/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/manage.py b/manage.py index 6e67e42..886e0ce 100755 --- a/manage.py +++ b/manage.py @@ -1,9 +1,10 @@ #!/usr/bin/env python + import os import sys if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_perf_test.settings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_perf_project.settings") from django.core.management import execute_from_command_line diff --git a/populate.py b/populate.py index dad2df4..3d28ef9 100644 --- a/populate.py +++ b/populate.py @@ -1,19 +1,49 @@ - import os +from datetime import timedelta, date -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_perf_test.settings') +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_perf_project.settings') import django django.setup() from django.utils import timezone -from fooapp.models import Question, Choice -from datetime import datetime, timedelta -if __name__ == '__main__': +from django_perf_project.apps.ormperf.models import Question, Choice +from django_perf_project.apps.botbotperf.models import ChatBot, Channel, Log + + +def create_questions_and_choices(): d0 = timezone.datetime(2015, 10, 21, tzinfo=timezone.utc) for i in range(1000): - q = Question(question_text="foo bar %d" % i, pub_date=d0 + timedelta(seconds=i)) + q = Question( + question_text="foo bar %d" % i, pub_date=d0 + timedelta(seconds=i)) q.save() for k in range(3): - c = Choice(question=q, choice_text="aaaaaaaaaaaa %d %d" % (i, k), votes=i) + c = Choice( + question=q, choice_text="aaaaaaaaaaaa %d %d" % (i, k), votes=i) c.save() + +def populate_botbot(slug, start_date): + chatbot = ChatBot.objects.create( + is_active=True, server="irc.example.net:6697", + nick="foo", password="bar",slug="thechatbot") + channel = Channel.objects.create( + chatbot=chatbot, + status=Channel.ACTIVE, + slug=slug, + is_public=True) + + for d in [start_date - timedelta(days=1), + start_date, + start_date + timedelta(days=1)]: + for i in range(500): + Log.objects.create( + bot=chatbot, channel=channel, + timestamp=d + timedelta(seconds=i*10), + nick=u"foo{0}".format(i % 2), + text=u"message {0}".format(i), + command="PRIVMSG",) + + +if __name__ == '__main__': + create_questions_and_choices() + populate_botbot("pypy", date(2015, 4, 1)) diff --git a/query.py b/query.py index fce702f..b9e7b86 100644 --- a/query.py +++ b/query.py @@ -1,11 +1,12 @@ +#!/usr/bin/env python import os, sys -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_perf_test.settings') +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_perf_project.settings') import django django.setup() -from fooapp.models import Question, Choice +from ormperf.models import Question, Choice from datetime import datetime, timedelta from django.utils.timezone import utc diff --git a/query2.py b/query2.py new file mode 100644 index 0000000..55f02bc --- /dev/null +++ b/query2.py @@ -0,0 +1,57 @@ + +import os, sys + +#import tracer + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_perf_project.settings') +import django +django.setup() + +import datetime +from django.utils.timezone import utc +from django_perf_project.apps.botbotperf.models import Channel, ChatBot + +chatbot = ChatBot.objects.create( + is_active=True, server="irc.example.net:6697", + nick="foo", password="bar",slug="thechatbot") +Channel.objects.create( + chatbot=chatbot, + status=Channel.ACTIVE, + slug='pypy', + is_public=True) +def f(): + channel = (Channel.objects.filter(status=Channel.ACTIVE, + slug='pypy', + is_public=True) + .select_related('chatbot'))[0] + size = channel.current_size() + # current logs + start_date = datetime.datetime(2015, 4, 1, tzinfo=utc) + next_day = start_date + datetime.timedelta(days=1) + previous_day = start_date - datetime.timedelta(days=1) + + logs = (channel.filtered_logs() + .filter(timestamp__gte=start_date, + timestamp__lt=next_day) + .order_by('timestamp')) + # determine page number of previous logs + prev_count = (channel.filtered_logs() + .filter( timestamp__gte=previous_day, + timestamp__lt=start_date) + .order_by('timestamp').count()) + + # determine next page number + next_count = logs.count() + if size: + print("Members: {}".format(size)) + return { + 'logs': list(logs), + 'prev_count': prev_count, + 'next_count': next_count} + +import time +for k in range(300): + t0 = time.time() + for k in range(100): + f() + print time.time() - t0 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..dbe4151 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Django==1.9.6 +# tox==2.2.1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..1ee289f --- /dev/null +++ b/setup.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +from setuptools import setup, find_packages + +packages = find_packages() + +setup( + name='django-perf-project', + version='0.0.1', + description="", + url='', + packages=find_packages(), + include_package_data=True, + scripts=['manage.py', 'query.py', 'populate.py'], +) diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..f4142af --- /dev/null +++ b/test.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +pip install . -r requirements.txt -q +#python manage.py test + +# python manage.py test django_perf_project.apps.ormperf.tests.OrmPerfoTest.test_int_addition +python manage.py test django_perf_project.apps.ormperf.tests.OrmPerfoTest.test_orm_datetime_filtering +# python manage.py test django_perf_project.apps.ormperf.tests.OrmPerfoTest.test_orm_first_ten +# python manage.py test django_perf_project.apps.ormperf.tests.OrmPerfoTest.test_orm_annotation +# python manage.py test django_perf_project.apps.ormperf.tests.OrmPerfoTest.test_orm_clone +# python manage.py test django_perf_project.apps.ormperf.tests.OrmPerfoTest.test_orm_or_q +# python manage.py test django_perf_project.apps.templateperf.tests.TemplatePerfTest.test_render_child +# python manage.py test django_perf_project.apps.templateperf.tests.TemplatePerfTest.test_render_child_no_extends +python manage.py test django_perf_project.apps.templateperf.tests.TemplatePerfTest.test_render_base +# python manage.py test django_perf_project.apps.templateperf.tests.TemplatePerfTest.test_render_base_no_extends +python manage.py test django_perf_project.apps.botbotperf.tests.EmptyBotbotPerfoTest.test_reduced_botbot_logs +# python manage.py test django_perf_project.apps.botbotperf.tests.LoadedBotbotPerfoTest.test_reduced_botbot_logs +# python manage.py test django_perf_project.apps.ormperf.tests.OrmPerfoTest.test_is_pypyjit_available + diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..0693e6a --- /dev/null +++ b/tox.ini @@ -0,0 +1,69 @@ +[tox] +toxworkdir=/tmp/tox/django-perf-tester +envlist= + py27-django-1.9, + pypy-django-1.9, + +[testenv] +commands= + #manage.py test django_perf_project.apps.ormperf.tests.OrmPerfoTest.test_orm_datetime_filtering + #manage.py test django_perf_project.apps.ormperf.tests.OrmPerfoTest.test_orm_first_ten + #manage.py test django_perf_project.apps.ormperf.tests.OrmPerfoTest.test_orm_annotation + #manage.py test django_perf_project.apps.ormperf.tests.OrmPerfoTest.test_orm_clone + #manage.py test django_perf_project.apps.ormperf.tests.OrmPerfoTest.test_orm_or_q + ##manage.py test django_perf_project.apps.ormperf.tests.OrmPerfoTest.test_int_addition + ##manage.py test django_perf_project.apps.ormperf.tests.OrmPerfoTest.test_is_pypyjit_available + #manage.py test django_perf_project.apps.templateperf.tests.TemplatePerfTest.test_render_child + #manage.py test django_perf_project.apps.templateperf.tests.TemplatePerfTest.test_render_child_no_extends + #manage.py test django_perf_project.apps.templateperf.tests.TemplatePerfTest.test_render_base + #manage.py test django_perf_project.apps.templateperf.tests.TemplatePerfTest.test_render_base_no_extends + manage.py test django_perf_project.apps.botbotperf.tests.EmptyBotbotPerfoTest.test_reduced_botbot_logs + manage.py test django_perf_project.apps.botbotperf.tests.LoadedBotbotPerfoTest.test_reduced_botbot_logs + +[testenv:py27-django-1.4] +basepython=/usr/bin/python2.7 +deps=django==1.4.22 + +[testenv:py27-django-1.5] +basepython=/usr/bin/python2.7 +deps=django==1.5.12 + +[testenv:py27-django-1.6] +basepython=/usr/bin/python2.7 +deps=django==1.6.11 + +[testenv:py27-django-1.7] +basepython=/usr/bin/python2.7 +deps=django==1.7.10 + +[testenv:py27-django-1.8] +basepython=/usr/bin/python2.7 +deps=django==1.8.6 + +[testenv:py27-django-1.9] +basepython=/usr/bin/python2.7 +deps=django==1.9.rc1 + +[testenv:pypy-django-1.4] +basepython=/usr/bin/pypy +deps=django==1.4.22 + +[testenv:pypy-django-1.5] +basepython=/usr/bin/pypy +deps=django==1.5.12 + +[testenv:pypy-django-1.6] +basepython=/usr/bin/pypy +deps=django==1.6.11 + +[testenv:pypy-django-1.7] +basepython=/usr/bin/pypy +deps=django==1.7.10 + +[testenv:pypy-django-1.8] +basepython=/usr/bin/pypy +deps=django==1.8.6 + +[testenv:pypy-django-1.9] +basepython=/usr/bin/pypy +deps=django==1.9.rc1