From 59659835e837aeb34c587999576047113c0bc448 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Tue, 3 Feb 2015 10:00:42 +0200 Subject: [PATCH 1/7] Added a basic benchmark suite for serializers. --- requirements.txt | 1 + runtests.py | 4 +- tests/fields.py | 10 +++ tests/models.py | 60 +++++++++++++++ tests/test_model_serializer.py | 40 +--------- tests/test_serializers_benchmarks.py | 107 +++++++++++++++++++++++++++ tox.ini | 1 + 7 files changed, 185 insertions(+), 38 deletions(-) create mode 100644 tests/fields.py create mode 100644 tests/test_serializers_benchmarks.py diff --git a/requirements.txt b/requirements.txt index 00d973cdf..10ef9790b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ Django>=1.4.11 pytest-django==2.8.0 pytest==2.6.4 pytest-cov==1.6 +pytest-bench==0.3.0 flake8==2.2.2 # Optional packages diff --git a/runtests.py b/runtests.py index 0008bfae5..040adacdc 100755 --- a/runtests.py +++ b/runtests.py @@ -8,8 +8,8 @@ import subprocess PYTEST_ARGS = { - 'default': ['tests', '--tb=short'], - 'fast': ['tests', '--tb=short', '-q'], + 'default': ['tests', '--tb=short', '--bench'], + 'fast': ['tests', '--tb=short', '-q', '--bench'], } FLAKE8_ARGS = ['rest_framework', 'tests', '--ignore=E501'] diff --git a/tests/fields.py b/tests/fields.py new file mode 100644 index 000000000..dafb21fce --- /dev/null +++ b/tests/fields.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +# coding: utf-8 +from django.db import models + + +class CustomField(models.Field): + """ + A custom model field simply for testing purposes. + """ + pass diff --git a/tests/models.py b/tests/models.py index 456b0a0bb..b07664349 100644 --- a/tests/models.py +++ b/tests/models.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from django.db import models from django.utils.translation import ugettext_lazy as _ +from .fields import CustomField class RESTFrameworkModel(models.Model): @@ -68,3 +69,62 @@ class NullableOneToOneSource(RESTFrameworkModel): name = models.CharField(max_length=100) target = models.OneToOneField(OneToOneTarget, null=True, blank=True, related_name='nullable_source') + + +class RegularFieldsModel(models.Model): + """ + A model class for testing regular flat fields. + """ + auto_field = models.AutoField(primary_key=True) + big_integer_field = models.BigIntegerField() + boolean_field = models.BooleanField(default=False) + char_field = models.CharField(max_length=100) + comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=100) + date_field = models.DateField() + datetime_field = models.DateTimeField() + decimal_field = models.DecimalField(max_digits=3, decimal_places=1) + email_field = models.EmailField(max_length=100) + float_field = models.FloatField() + integer_field = models.IntegerField() + null_boolean_field = models.NullBooleanField() + positive_integer_field = models.PositiveIntegerField() + positive_small_integer_field = models.PositiveSmallIntegerField() + slug_field = models.SlugField(max_length=100) + small_integer_field = models.SmallIntegerField() + text_field = models.TextField() + time_field = models.TimeField() + url_field = models.URLField(max_length=100) + custom_field = CustomField() + + def method(self): + return 'method' + + +class RegularFieldsAndFKModel(models.Model): + """ + A model class for testing regular flat fields. + """ + auto_field = models.AutoField(primary_key=True) + big_integer_field = models.BigIntegerField() + boolean_field = models.BooleanField(default=False) + char_field = models.CharField(max_length=100) + comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=100) + date_field = models.DateField() + datetime_field = models.DateTimeField() + decimal_field = models.DecimalField(max_digits=3, decimal_places=1) + email_field = models.EmailField(max_length=100) + float_field = models.FloatField() + integer_field = models.IntegerField() + null_boolean_field = models.NullBooleanField() + positive_integer_field = models.PositiveIntegerField() + positive_small_integer_field = models.PositiveSmallIntegerField() + slug_field = models.SlugField(max_length=100) + small_integer_field = models.SmallIntegerField() + text_field = models.TextField() + time_field = models.TimeField() + url_field = models.URLField(max_length=100) + custom_field = CustomField() + fk = models.ForeignKey(RegularFieldsModel) + + def method(self): + return 'method' diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 247b309a1..37b578840 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -6,13 +6,16 @@ These tests deal with ensuring that we correctly map the model fields onto an appropriate set of serializer fields for each case. """ from __future__ import unicode_literals + from django.core.exceptions import ImproperlyConfigured from django.core.validators import MaxValueValidator, MinValueValidator, MinLengthValidator from django.db import models from django.test import TestCase from django.utils import six + from rest_framework import serializers from rest_framework.compat import unicode_repr +from .models import RegularFieldsModel def dedent(blocktext): @@ -22,46 +25,11 @@ def dedent(blocktext): # Tests for regular field mappings. # --------------------------------- -class CustomField(models.Field): - """ - A custom model field simply for testing purposes. - """ - pass - class OneFieldModel(models.Model): char_field = models.CharField(max_length=100) -class RegularFieldsModel(models.Model): - """ - A model class for testing regular flat fields. - """ - auto_field = models.AutoField(primary_key=True) - big_integer_field = models.BigIntegerField() - boolean_field = models.BooleanField(default=False) - char_field = models.CharField(max_length=100) - comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=100) - date_field = models.DateField() - datetime_field = models.DateTimeField() - decimal_field = models.DecimalField(max_digits=3, decimal_places=1) - email_field = models.EmailField(max_length=100) - float_field = models.FloatField() - integer_field = models.IntegerField() - null_boolean_field = models.NullBooleanField() - positive_integer_field = models.PositiveIntegerField() - positive_small_integer_field = models.PositiveSmallIntegerField() - slug_field = models.SlugField(max_length=100) - small_integer_field = models.SmallIntegerField() - text_field = models.TextField() - time_field = models.TimeField() - url_field = models.URLField(max_length=100) - custom_field = CustomField() - - def method(self): - return 'method' - - COLOR_CHOICES = (('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')) @@ -125,7 +93,7 @@ class TestRegularFieldMappings(TestCase): text_field = CharField(style={'base_template': 'textarea.html'}) time_field = TimeField() url_field = URLField(max_length=100) - custom_field = ModelField(model_field=) + custom_field = ModelField(model_field=) """) self.assertEqual(unicode_repr(TestSerializer()), expected) diff --git a/tests/test_serializers_benchmarks.py b/tests/test_serializers_benchmarks.py new file mode 100644 index 000000000..63af030f3 --- /dev/null +++ b/tests/test_serializers_benchmarks.py @@ -0,0 +1,107 @@ +from decimal import Decimal + +from pytest import mark + +from datetime import datetime + +from rest_framework import serializers +from .models import RegularFieldsModel, RegularFieldsAndFKModel + + +data = { + 'big_integer_field': 100000, + 'char_field': 'a', + 'comma_separated_integer_field': '1,2', + 'date_field': datetime.now().date(), + 'datetime_field': datetime.now(), + 'decimal_field': Decimal('1.5'), + 'email_field': 'somewhere@overtherainbow.com', + 'float_field': 0.443, + 'integer_field': 55, + 'null_boolean_field': True, + 'positive_integer_field': 1, + 'positive_small_integer_field': 1, + 'slug_field': 'slug-friendly-text', + 'small_integer_field': 1, + 'text_field': 'lorem ipsum', + 'time_field': datetime.now().time(), + 'url_field': 'https://overtherainbow.com' +} + + +class TestSerializer(serializers.ModelSerializer): + class Meta: + model = RegularFieldsModel + fields = data.keys() + ['method'] + + +class TestNestedSerializer(serializers.ModelSerializer): + fk = TestSerializer() + + class Meta: + model = RegularFieldsAndFKModel + fields = data.keys() + ['method', 'fk'] + + +@mark.bench('serializers.ModelSerializer.to_representation') +def test_object_serialization(): + instance = RegularFieldsModel(**data) + serializer = TestSerializer(instance=instance) + + assert serializer.data, serializer.errors + + +@mark.bench('serializers.ModelSerializer.to_representation') +def test_nested_object_serialization(): + nested_instance = RegularFieldsModel(**data) + instance = RegularFieldsAndFKModel(fk=nested_instance, **data) + serializer = TestSerializer(instance=instance) + + assert serializer.data, serializer.errors + + +@mark.bench('serializers.ModelSerializer.to_representation') +def test_object_list_serialization(): + instance = [RegularFieldsModel(**data) for _ in range(100)] + serializer = TestSerializer(instance=instance, many=True) + + assert serializer.data, serializer.errors + + +@mark.bench('serializers.ModelSerializer.to_representation') +def test_object_serialization_with_partial_update(): + instance = RegularFieldsModel(**data) + serializer = TestSerializer(instance=instance, data={'char_field': 'b'}, partial=True) + + assert serializer.is_valid(), serializer.errors + assert serializer.data, serializer.errors + + +@mark.bench('serializers.ModelSerializer.to_representation') +def test_object_serialization_with_update(): + instance = RegularFieldsModel(**data) + new_data = data.copy() + new_data['char_field'] = 'b' + serializer = TestSerializer(instance=instance, data=new_data) + + assert serializer.is_valid(), serializer.errors + assert serializer.data, serializer.errors + + +@mark.bench('serializers.ModelSerializer.to_internal_value') +def test_object_deserialization(): + serializer = TestSerializer(data=data) + + assert serializer.is_valid(), serializer.errors + + +@mark.bench('serializers.ModelSerializer.to_internal_value') +def test_object_list_deserialization(): + serializer = TestSerializer(data=[data for _ in range(100)], many=True) + + assert serializer.is_valid(), serializer.errors + + +@mark.bench('serializers.ModelSerializer.__init__') +def test_serializer_initialization(): + TestSerializer(data=data) diff --git a/tox.ini b/tox.ini index 8e0369643..f620faeb7 100644 --- a/tox.ini +++ b/tox.ini @@ -22,6 +22,7 @@ deps = {py26,py27}-django{14,15}: django-oauth2-provider==0.2.3 {py26,py27}-django16: django-oauth2-provider==0.2.4 pytest-django==2.8.0 + pytest-bench==0.3.0 django-filter==0.9.2 defusedxml==0.3 markdown>=2.1.0 From 5f49f97c958261423e7eec12d60582a1ddc6301e Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Tue, 3 Feb 2015 10:05:42 +0200 Subject: [PATCH 2/7] Added a benchmark to the serializer's get_fields() method. --- tests/test_serializers_benchmarks.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_serializers_benchmarks.py b/tests/test_serializers_benchmarks.py index 63af030f3..fb04bbba4 100644 --- a/tests/test_serializers_benchmarks.py +++ b/tests/test_serializers_benchmarks.py @@ -43,6 +43,14 @@ class TestNestedSerializer(serializers.ModelSerializer): fields = data.keys() + ['method', 'fk'] +@mark.bench('serializers.ModelSerializer.get_fields') +def test_get_fields(): + instance = RegularFieldsModel(**data) + serializer = TestSerializer(instance=instance) + + assert serializer.get_fields() + + @mark.bench('serializers.ModelSerializer.to_representation') def test_object_serialization(): instance = RegularFieldsModel(**data) From 6118c69530569ec890419ddd33485c04bbac8cdd Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Tue, 3 Feb 2015 10:11:11 +0200 Subject: [PATCH 3/7] Deactivated running benchmarks in travis for now. --- runtests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtests.py b/runtests.py index 040adacdc..c8236ff25 100755 --- a/runtests.py +++ b/runtests.py @@ -9,7 +9,7 @@ import subprocess PYTEST_ARGS = { 'default': ['tests', '--tb=short', '--bench'], - 'fast': ['tests', '--tb=short', '-q', '--bench'], + 'fast': ['tests', '--tb=short', '-q'], } FLAKE8_ARGS = ['rest_framework', 'tests', '--ignore=E501'] From 677362ac67cf3c6cb1f61573c18240505ed2fe02 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Tue, 3 Feb 2015 11:08:44 +0200 Subject: [PATCH 4/7] The benchmarks are now Python 3 compatibility. --- tests/test_serializers_benchmarks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_serializers_benchmarks.py b/tests/test_serializers_benchmarks.py index fb04bbba4..45c1fdd58 100644 --- a/tests/test_serializers_benchmarks.py +++ b/tests/test_serializers_benchmarks.py @@ -32,7 +32,7 @@ data = { class TestSerializer(serializers.ModelSerializer): class Meta: model = RegularFieldsModel - fields = data.keys() + ['method'] + fields = list(data.keys()) + ['method'] class TestNestedSerializer(serializers.ModelSerializer): @@ -40,7 +40,7 @@ class TestNestedSerializer(serializers.ModelSerializer): class Meta: model = RegularFieldsAndFKModel - fields = data.keys() + ['method', 'fk'] + fields = list(data.keys()) + ['method', 'fk'] @mark.bench('serializers.ModelSerializer.get_fields') From acdb4548a8eff5f5741aba376f540d7a0f53c0c7 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Tue, 3 Feb 2015 11:11:31 +0200 Subject: [PATCH 5/7] Added a benchmark that will verify that the serializer fields are cached. --- tests/test_serializers_benchmarks.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_serializers_benchmarks.py b/tests/test_serializers_benchmarks.py index 45c1fdd58..3320ab6cb 100644 --- a/tests/test_serializers_benchmarks.py +++ b/tests/test_serializers_benchmarks.py @@ -51,6 +51,15 @@ def test_get_fields(): assert serializer.get_fields() +@mark.bench('serializers.ModelSerializer.get_fields') +def test_get_fields_twice(): + instance = RegularFieldsModel(**data) + serializer = TestSerializer(instance=instance) + + assert serializer.get_fields() + assert serializer.get_fields() + + @mark.bench('serializers.ModelSerializer.to_representation') def test_object_serialization(): instance = RegularFieldsModel(**data) From 6994cc2bb32f2ebca775221153c1ed1a8cef4492 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Tue, 3 Feb 2015 15:15:42 +0200 Subject: [PATCH 6/7] Added a benchmark that measures rendering a dictionary to JSON. --- tests/test_renderers_benchmarks.py | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/test_renderers_benchmarks.py diff --git a/tests/test_renderers_benchmarks.py b/tests/test_renderers_benchmarks.py new file mode 100644 index 000000000..e92f9babf --- /dev/null +++ b/tests/test_renderers_benchmarks.py @@ -0,0 +1,33 @@ +from decimal import Decimal +from datetime import datetime + +from pytest import mark + +from rest_framework import renderers + + +data = { + 'big_integer_field': 100000, + 'char_field': 'a', + 'comma_separated_integer_field': '1,2', + 'date_field': datetime.now().date(), + 'datetime_field': datetime.now(), + 'decimal_field': Decimal('1.5'), + 'email_field': 'somewhere@overtherainbow.com', + 'float_field': 0.443, + 'integer_field': 55, + 'null_boolean_field': True, + 'positive_integer_field': 1, + 'positive_small_integer_field': 1, + 'slug_field': 'slug-friendly-text', + 'small_integer_field': 1, + 'text_field': 'lorem ipsum', + 'time_field': datetime.now().time(), + 'url_field': 'https://overtherainbow.com' +} + + +@mark.bench('renderers.JSONRenderer.render') +def test_json_renderer(): + renderer = renderers.JSONRenderer() + renderer.render(data) From 866d41f26399618a3c69098cb3ba18963eab82e9 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Tue, 3 Feb 2015 17:24:32 +0200 Subject: [PATCH 7/7] Increased the number of iterations for more accurate results. --- tests/test_renderers_benchmarks.py | 2 +- tests/test_serializers_benchmarks.py | 29 +++++++++++++++------------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/tests/test_renderers_benchmarks.py b/tests/test_renderers_benchmarks.py index e92f9babf..ea9e91b1f 100644 --- a/tests/test_renderers_benchmarks.py +++ b/tests/test_renderers_benchmarks.py @@ -27,7 +27,7 @@ data = { } -@mark.bench('renderers.JSONRenderer.render') +@mark.bench('renderers.JSONRenderer.render', iterations=10000) def test_json_renderer(): renderer = renderers.JSONRenderer() renderer.render(data) diff --git a/tests/test_serializers_benchmarks.py b/tests/test_serializers_benchmarks.py index 3320ab6cb..506a658f3 100644 --- a/tests/test_serializers_benchmarks.py +++ b/tests/test_serializers_benchmarks.py @@ -28,6 +28,10 @@ data = { 'url_field': 'https://overtherainbow.com' } +data_list = [data for _ in range(100)] + +instances_list = [RegularFieldsModel(**data) for _ in range(100)] + class TestSerializer(serializers.ModelSerializer): class Meta: @@ -43,7 +47,7 @@ class TestNestedSerializer(serializers.ModelSerializer): fields = list(data.keys()) + ['method', 'fk'] -@mark.bench('serializers.ModelSerializer.get_fields') +@mark.bench('serializers.ModelSerializer.get_fields', iterations=1000) def test_get_fields(): instance = RegularFieldsModel(**data) serializer = TestSerializer(instance=instance) @@ -51,7 +55,7 @@ def test_get_fields(): assert serializer.get_fields() -@mark.bench('serializers.ModelSerializer.get_fields') +@mark.bench('serializers.ModelSerializer.get_fields', iterations=1000) def test_get_fields_twice(): instance = RegularFieldsModel(**data) serializer = TestSerializer(instance=instance) @@ -60,7 +64,7 @@ def test_get_fields_twice(): assert serializer.get_fields() -@mark.bench('serializers.ModelSerializer.to_representation') +@mark.bench('serializers.ModelSerializer.to_representation', iterations=1000) def test_object_serialization(): instance = RegularFieldsModel(**data) serializer = TestSerializer(instance=instance) @@ -68,7 +72,7 @@ def test_object_serialization(): assert serializer.data, serializer.errors -@mark.bench('serializers.ModelSerializer.to_representation') +@mark.bench('serializers.ModelSerializer.to_representation', iterations=1000) def test_nested_object_serialization(): nested_instance = RegularFieldsModel(**data) instance = RegularFieldsAndFKModel(fk=nested_instance, **data) @@ -77,15 +81,14 @@ def test_nested_object_serialization(): assert serializer.data, serializer.errors -@mark.bench('serializers.ModelSerializer.to_representation') +@mark.bench('serializers.ListSerializer.to_representation', iterations=1000) def test_object_list_serialization(): - instance = [RegularFieldsModel(**data) for _ in range(100)] - serializer = TestSerializer(instance=instance, many=True) + serializer = TestSerializer(instance=instances_list, many=True) assert serializer.data, serializer.errors -@mark.bench('serializers.ModelSerializer.to_representation') +@mark.bench('serializers.ModelSerializer.to_representation', iterations=10000) def test_object_serialization_with_partial_update(): instance = RegularFieldsModel(**data) serializer = TestSerializer(instance=instance, data={'char_field': 'b'}, partial=True) @@ -94,7 +97,7 @@ def test_object_serialization_with_partial_update(): assert serializer.data, serializer.errors -@mark.bench('serializers.ModelSerializer.to_representation') +@mark.bench('serializers.ModelSerializer.to_representation', iterations=10000) def test_object_serialization_with_update(): instance = RegularFieldsModel(**data) new_data = data.copy() @@ -105,20 +108,20 @@ def test_object_serialization_with_update(): assert serializer.data, serializer.errors -@mark.bench('serializers.ModelSerializer.to_internal_value') +@mark.bench('serializers.ModelSerializer.to_internal_value', iterations=1000) def test_object_deserialization(): serializer = TestSerializer(data=data) assert serializer.is_valid(), serializer.errors -@mark.bench('serializers.ModelSerializer.to_internal_value') +@mark.bench('serializers.ListSerializer.to_internal_value', iterations=1000) def test_object_list_deserialization(): - serializer = TestSerializer(data=[data for _ in range(100)], many=True) + serializer = TestSerializer(data=data_list, many=True) assert serializer.is_valid(), serializer.errors -@mark.bench('serializers.ModelSerializer.__init__') +@mark.bench('serializers.ModelSerializer.__init__', iterations=10000) def test_serializer_initialization(): TestSerializer(data=data)