├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── conftest.py ├── drf_bench ├── __init__.py ├── core │ ├── __init__.py │ ├── admin.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ └── views.py ├── settings.py ├── urls.py └── wsgi.py ├── manage.py ├── setup.py ├── tests └── test_serializers.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Xavier Ordoquy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | 4 | recursive-include django-rest-framework-benchmark * 5 | recursive-exclude * __pycache__ 6 | recursive-exclude * *.py[co] 7 | 8 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | drf_bench 2 | ========= 3 | 4 | A few benchs to understand how playing on various degrees play on the 5 | Django REST framework performance: 6 | 7 | - size of the data to (de)serialize 8 | - ModelSerializer vs Serializer 9 | - Serialize vs Deserialize 10 | - number of fields to process 11 | 12 | 13 | How to run them 14 | --------------- 15 | 16 | Prior to the run, create / activate a virtualenv. 17 | 18 | git clone https://github.com/xordoquy/django-rest-framework-benchmark.git 19 | cd django-rest-framework-benchmark 20 | pip install -e .[tests] 21 | py.test 22 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | 2 | def pytest_configure(config): 3 | from django.conf import settings 4 | if not settings.configured: 5 | settings.configure( 6 | DATABASE_ENGINE='sqlite3', 7 | DATABASES={ 8 | 'default': { 9 | 'NAME': ':memory:', 10 | 'ENGINE': 'django.db.backends.sqlite3', 11 | 'TEST_NAME': ':memory:', 12 | }, 13 | }, 14 | DATABASE_NAME=':memory:', 15 | TEST_DATABASE_NAME=':memory:', 16 | INSTALLED_APPS=[ 17 | 'django.contrib.admin', 18 | 'django.contrib.auth', 19 | 'django.contrib.contenttypes', 20 | 'django.contrib.sessions', 21 | 'django.contrib.messages', 22 | 'django.contrib.staticfiles', 23 | 24 | # Project's apps 25 | 26 | # 3rd parties 27 | ], 28 | ROOT_URLCONF='django-rest-framework-benchmark.urls', 29 | DEBUG=False, 30 | SITE_ID=1, 31 | TEMPLATE_DEBUG=True, 32 | ALLOWED_HOSTS=['*'], 33 | LOGGING={ 34 | 'version': 1, 35 | 'disable_existing_loggers': False, 36 | 'formatters': { 37 | 'standard': { 38 | 'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s' 39 | }, 40 | }, 41 | 'handlers': { 42 | 'default': { 43 | 'level': 'DEBUG', 44 | 'class': 'logging.StreamHandler', 45 | }, 46 | }, 47 | 'loggers': { 48 | '': { 49 | 'handlers': ['default'], 50 | 'level': 'DEBUG', 51 | 'propagate': True 52 | }, 53 | }, 54 | }, 55 | ) 56 | -------------------------------------------------------------------------------- /drf_bench/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | -------------------------------------------------------------------------------- /drf_bench/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xordoquy/django-rest-framework-benchmark/c84cd636302704558c176d418611b6357ccc8bde/drf_bench/core/__init__.py -------------------------------------------------------------------------------- /drf_bench/core/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /drf_bench/core/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xordoquy/django-rest-framework-benchmark/c84cd636302704558c176d418611b6357ccc8bde/drf_bench/core/migrations/__init__.py -------------------------------------------------------------------------------- /drf_bench/core/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | def generate_model( 5 | field_name='CharField', 6 | field_quantity=1, 7 | field_type=models.CharField, 8 | field_arguments=None): 9 | 10 | class_name = 'Model%s%06i' % (field_name, field_quantity) 11 | 12 | class Meta: 13 | db_table = 'model_%s_%06i' % (field_type, field_quantity) 14 | 15 | if not field_arguments: 16 | field_arguments = {} 17 | 18 | attrs = dict(( 19 | ('field%06i' % i, field_type(**field_arguments)) 20 | for i in range(field_quantity))) 21 | 22 | attrs.update({'__module__': 'drf_bench.core', 'Meta': Meta}) 23 | 24 | model = type(class_name, (models.Model,), attrs) 25 | return model 26 | 27 | 28 | charfield_kwargs = { 29 | 'max_length': 256, 30 | } 31 | 32 | ModelCharField000001 = generate_model('CharField', 1, models.CharField, charfield_kwargs) 33 | ModelCharField000010 = generate_model('CharField', 10, models.CharField, charfield_kwargs) 34 | ModelCharField000100 = generate_model('CharField', 100, models.CharField, charfield_kwargs) 35 | ModelCharField001000 = generate_model('CharField', 1000, models.CharField, charfield_kwargs) 36 | -------------------------------------------------------------------------------- /drf_bench/core/serializers.py: -------------------------------------------------------------------------------- 1 | from django.core import validators 2 | from django.contrib.auth import models as auth_models 3 | from rest_framework import serializers 4 | from . import models 5 | 6 | 7 | class NullMixin(object): 8 | def create(self, validated_data): 9 | return None 10 | 11 | def udpate(self, instance, validated_data): 12 | return None 13 | 14 | 15 | class UserModel(NullMixin, serializers.ModelSerializer): 16 | class Meta: 17 | model = auth_models.User 18 | fields = ( 19 | 'id', 'password', 'last_login', 'is_superuser', 'username', 20 | 'first_name', 'last_name', 'email', 'is_staff', 'is_active', 21 | 'date_joined', 22 | ) 23 | extra_kwargs = { 24 | 'username': { 25 | 'validators': [ 26 | validators.RegexValidator( 27 | r'^[\w.@+-]+$', 28 | ('Enter a valid username. This value may contain only ' 29 | 'letters, numbers ' 'and @/./+/-/_ characters.') 30 | ), 31 | ], 32 | }, 33 | } 34 | 35 | 36 | class User(NullMixin, serializers.Serializer): 37 | id = serializers.IntegerField( 38 | label='ID', read_only=True) 39 | password = serializers.CharField( 40 | max_length=128) 41 | last_login = serializers.DateTimeField( 42 | allow_null=True, required=False) 43 | is_superuser = serializers.BooleanField( 44 | help_text='Designates that this user has all permissions without explicitly assigning them.', 45 | label='Superuser status', required=False) 46 | username = serializers.CharField( 47 | help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', 48 | max_length=30, 49 | validators=[ 50 | validators.RegexValidator( 51 | r'^[\w.@+-]+$', 52 | ('Enter a valid username. This value may contain only ' 53 | 'letters, numbers ' 'and @/./+/-/_ characters.') 54 | ), 55 | ]) 56 | first_name = serializers.CharField( 57 | allow_blank=True, max_length=30, required=False) 58 | last_name = serializers.CharField( 59 | allow_blank=True, max_length=30, required=False) 60 | email = serializers.EmailField( 61 | allow_blank=True, label='Email address', 62 | max_length=254, required=False) 63 | is_staff = serializers.BooleanField( 64 | help_text='Designates whether the user can log into this admin site.', 65 | label='Staff status', required=False) 66 | is_active = serializers.BooleanField( 67 | help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', 68 | label='Active', required=False) 69 | date_joined = serializers.DateTimeField( 70 | required=False) 71 | 72 | 73 | class ModelCharField000001(NullMixin, serializers.ModelSerializer): 74 | class Meta: 75 | model = models.ModelCharField000001 76 | 77 | 78 | class ModelCharField000010(NullMixin, serializers.ModelSerializer): 79 | class Meta: 80 | model = models.ModelCharField000010 81 | 82 | 83 | class ModelCharField000100(NullMixin, serializers.ModelSerializer): 84 | class Meta: 85 | model = models.ModelCharField000100 86 | 87 | 88 | class ModelCharField001000(NullMixin, serializers.ModelSerializer): 89 | class Meta: 90 | model = models.ModelCharField001000 91 | -------------------------------------------------------------------------------- /drf_bench/core/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /drf_bench/core/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /drf_bench/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for drf_bench project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.8. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.8/ref/settings/ 11 | """ 12 | 13 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 14 | import os 15 | 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'CHANGE THIS!!!' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = ( 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 41 | 'drf_bench.core', 42 | ) 43 | 44 | MIDDLEWARE_CLASSES = ( 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | 'django.middleware.security.SecurityMiddleware', 53 | ) 54 | 55 | ROOT_URLCONF = 'drf_bench.urls' 56 | 57 | TEMPLATES = [ 58 | { 59 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 60 | 'DIRS': [], 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.template.context_processors.debug', 65 | 'django.template.context_processors.request', 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.contrib.messages.context_processors.messages', 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | WSGI_APPLICATION = 'drf_bench.wsgi.application' 74 | 75 | 76 | # Database 77 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases 78 | 79 | DATABASES = { 80 | 'default': { 81 | 'ENGINE': 'django.db.backends.sqlite3', 82 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 83 | } 84 | } 85 | 86 | 87 | # Internationalization 88 | # https://docs.djangoproject.com/en/1.8/topics/i18n/ 89 | 90 | LANGUAGE_CODE = 'en-us' 91 | 92 | TIME_ZONE = 'UTC' 93 | 94 | USE_I18N = True 95 | 96 | USE_L10N = True 97 | 98 | USE_TZ = True 99 | 100 | 101 | # Static files (CSS, JavaScript, Images) 102 | # https://docs.djangoproject.com/en/1.8/howto/static-files/ 103 | 104 | STATIC_URL = '/static/' 105 | -------------------------------------------------------------------------------- /drf_bench/urls.py: -------------------------------------------------------------------------------- 1 | """drf_bench URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.8/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Add an import: from blog import urls as blog_urls 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) 15 | """ 16 | from django.conf.urls import include, url 17 | from django.contrib import admin 18 | 19 | urlpatterns = [ 20 | url(r'^admin/', include(admin.site.urls)), 21 | ] 22 | -------------------------------------------------------------------------------- /drf_bench/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for drf_bench project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault( 15 | "DJANGO_SETTINGS_MODULE", 16 | "drf_bench.settings" 17 | ) 18 | 19 | application = get_wsgi_application() 20 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "drf_bench.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | drf_bench 4 | ===== 5 | 6 | """ 7 | 8 | import drf_bench 9 | from setuptools import setup, find_packages 10 | import sys 11 | 12 | install_requires = [ 13 | "Django==1.8.4", 14 | "djangorestframework==3.2.3", 15 | ] 16 | 17 | dev_requires = [ 18 | 'django-debug-toolbar', 19 | ] 20 | 21 | test_requires = [ 22 | "pytest", 23 | "pytest-cov", 24 | "pytest-django", 25 | "pytest-benchmark", 26 | ] 27 | 28 | 29 | setup( 30 | name="drf_bench", 31 | version=drf_bench.__version__, 32 | author="Xavier Ordoquy", 33 | author_email="xordoquy@linovia.com", 34 | url="", 35 | description="A benchmark for Django REST framework.", 36 | 37 | # Packages and data to take into account 38 | packages=find_packages(exclude=("tests", "tests.*")), 39 | include_package_data=True, 40 | 41 | zip_safe=False, # Avoid eggs 42 | 43 | # Allow pip install .[tests,dev] to install the dev and test dependencies 44 | extras_require={ 45 | "tests": test_requires, 46 | "dev": dev_requires, 47 | }, 48 | install_requires=install_requires, 49 | tests_require=test_requires, 50 | ) 51 | -------------------------------------------------------------------------------- /tests/test_serializers.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.utils import timezone 3 | from django.contrib.auth import models 4 | from drf_bench.core import serializers 5 | 6 | 7 | def run_serializer(Serializer, data, many=True): 8 | serializer = Serializer(data, many=many) 9 | return serializer.data 10 | 11 | 12 | def run_deserializer(Serializer, data, many=True): 13 | serializer = Serializer(data=data, many=many) 14 | validity = serializer.is_valid() 15 | print(serializer.errors) 16 | return validity 17 | 18 | 19 | def generate_data(sample_size, base=False): 20 | if base: 21 | now = '2013-01-29T12:01' 22 | else: 23 | now = timezone.now() 24 | data = [{ 25 | "id": i, 26 | "password": 'zmrjegar', 27 | "is_superuser": False, 28 | "username": 'user%i' % i, 29 | "first_name": 'john', 30 | "last_name": 'doe', 31 | "email": 'john.doe%i@local.host' % i, 32 | "is_staff": False, 33 | "is_active": False, 34 | "date_joined": now, 35 | } for i in range(sample_size)] 36 | return data 37 | 38 | 39 | def generate_sample_data(sample_size=1, field_qt=1, default_data='test'): 40 | return [ 41 | dict((('field%06i' % j, default_data) for j in range(field_qt))) 42 | for i in range(sample_size) 43 | ] 44 | 45 | 46 | @pytest.mark.parametrize("sample_size", [1, 10, 100, 1000, 10000]) 47 | def test_model_serializer(benchmark, sample_size): 48 | data = [models.User(**kwargs) for kwargs in generate_data(sample_size)] 49 | result = benchmark(run_serializer, serializers.UserModel, data) 50 | assert result is not None 51 | 52 | 53 | @pytest.mark.parametrize("sample_size", [1, 10, 100, 1000, 10000]) 54 | def test_serializer(benchmark, sample_size): 55 | data = generate_data(sample_size) 56 | result = benchmark(run_serializer, serializers.User, data) 57 | assert result is not None 58 | 59 | 60 | @pytest.mark.parametrize("sample_size", [1, 10, 100, 1000, 10000]) 61 | def test_model_deserializer(benchmark, sample_size): 62 | data = generate_data(sample_size, base=True) 63 | result = benchmark(run_deserializer, serializers.UserModel, data) 64 | assert result is True 65 | 66 | 67 | @pytest.mark.parametrize("sample_size", [1, 10, 100, 1000, 10000]) 68 | def test_deserializer(benchmark, sample_size): 69 | data = generate_data(sample_size, base=True) 70 | result = benchmark(run_deserializer, serializers.User, data) 71 | assert result is True 72 | 73 | 74 | @pytest.mark.parametrize("field_qt", [1, 10, 100, 1000]) 75 | def test_model_serializer_field_qt(benchmark, field_qt, sample_size=1): 76 | data = generate_sample_data( 77 | sample_size=sample_size, 78 | field_qt=field_qt) 79 | result = benchmark( 80 | run_serializer, 81 | getattr(serializers, 'ModelCharField%06i' % field_qt), 82 | data) 83 | assert result is not None 84 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py{27,34} 8 | 9 | [pytest] 10 | python_files=test*.py 11 | addopts=--tb=short 12 | 13 | [testenv] 14 | commands = 15 | pip install -e .[tests] 16 | py.test 17 | 18 | --------------------------------------------------------------------------------