diff options
author | drebs <drebs@riseup.net> | 2017-09-17 12:08:25 -0300 |
---|---|---|
committer | drebs <drebs@riseup.net> | 2017-09-17 15:50:55 -0300 |
commit | cfff46ff9becdbe5cf48816870e625ed253ecc57 (patch) | |
tree | 8d239e4499f559d86ed17ea3632008303b25d485 /tests/benchmarks | |
parent | f29abe28bd778838626d12fcabe3980a8ce4fa8c (diff) |
[refactor] move tests to root of repository
Tests entrypoint was in a testing/ subfolder in the root of the
repository. This was made mainly because we had some common files for
tests and we didn't want to ship them (files in testing/test_soledad,
which is itself a python package. This sometimes causes errors when
loading tests (it seems setuptools is confused with having one python
package in a subdirectory of another).
This commit moves the tests entrypoint to the root of the repository.
Closes: #8952
Diffstat (limited to 'tests/benchmarks')
-rw-r--r-- | tests/benchmarks/README.md | 51 | ||||
-rw-r--r-- | tests/benchmarks/assets/cert_default.conf | 15 | ||||
-rw-r--r-- | tests/benchmarks/conftest.py | 154 | ||||
-rw-r--r-- | tests/benchmarks/pytest.ini | 2 | ||||
-rw-r--r-- | tests/benchmarks/test_crypto.py | 109 | ||||
-rw-r--r-- | tests/benchmarks/test_legacy_vs_blobs.py | 305 | ||||
-rw-r--r-- | tests/benchmarks/test_misc.py | 9 | ||||
-rw-r--r-- | tests/benchmarks/test_resources.py | 50 | ||||
-rw-r--r-- | tests/benchmarks/test_sqlcipher.py | 47 | ||||
-rw-r--r-- | tests/benchmarks/test_sqlite_blobs_backend.py | 82 | ||||
-rw-r--r-- | tests/benchmarks/test_sync.py | 92 |
11 files changed, 916 insertions, 0 deletions
diff --git a/tests/benchmarks/README.md b/tests/benchmarks/README.md new file mode 100644 index 00000000..b2465a78 --- /dev/null +++ b/tests/benchmarks/README.md @@ -0,0 +1,51 @@ +Benchmark tests +=============== + +This folder contains benchmark tests for Soledad. It aims to provide a fair +account on the time and resources taken to perform some actions. + +These benchmarks are built on top of `pytest-benchmark`, a `pytest` fixture that +provides means for running test functions multiple times and generating +reports. The results are printed to screen and also posted to elasticsearch. + +`pytest-benchmark` runs tests multiple times so it can provide meaningful +statistics for the time taken for a tipical run of a test function. The number +of times that the test is run can be manually or automatically configured. When +automatically configured, the number of runs is decided by taking into account +multiple `pytest-benchmark` configuration parameters. See the following page +for more details on how `pytest-benchmark` works: + + https://pytest-benchmark.readthedocs.io/en/stable/calibration.html + +Some graphs and analysis resulting from these tests can be seen on: + + https://benchmarks.leap.se/ + + +Resource consumption +-------------------- + +For each test, CPU and memory usage statistics are also collected, by querying +`cpu_percent()` and `memory_percent()` from `psutil.Process` for the current +test process. Some notes about the current resource consumption estimation process: + +* Currently, resources are measured for the whole set of rounds that a test + function is run. That means that the CPU and memory percentage include the + `pytest` and `pytest-benchmark` machinery overhead. Anyway, for now this might + provide a fair approximation of per-run test function resource usage. + +* CPU is measured before and after the run of the benchmark function and + returns the percentage that the currnet process occupied of the CPU time + between the two calls. + +* Memory is sampled during the benchmark run by a separate thread. Sampling + interval might have to be configured on a per-test basis, as different tests + take different times to execute (from milliseconds to tens of seconds). For + now, an interval of 0.1s seems to cover all tests. + + +Benchmarks website +------------------ + +To update the benchmarks website, see the documentation in +``../../../docs/misc/benchmarks-website.rst``. diff --git a/tests/benchmarks/assets/cert_default.conf b/tests/benchmarks/assets/cert_default.conf new file mode 100644 index 00000000..8043cea3 --- /dev/null +++ b/tests/benchmarks/assets/cert_default.conf @@ -0,0 +1,15 @@ +[ req ] +default_bits = 1024 +default_keyfile = keyfile.pem +distinguished_name = req_distinguished_name +prompt = no +output_password = mypass + +[ req_distinguished_name ] +C = GB +ST = Test State or Province +L = Test Locality +O = Organization Name +OU = Organizational Unit Name +CN = localhost +emailAddress = test@email.address diff --git a/tests/benchmarks/conftest.py b/tests/benchmarks/conftest.py new file mode 100644 index 00000000..80eccb08 --- /dev/null +++ b/tests/benchmarks/conftest.py @@ -0,0 +1,154 @@ +import functools +import numpy +import os +import psutil +import pytest +import threading +import time + +from twisted.internet import threads, reactor + + +# +# pytest customizations +# + +# mark benchmark tests using their group names (thanks ionelmc! :) +def pytest_collection_modifyitems(items, config): + for item in items: + bench = item.get_marker("benchmark") + if bench and bench.kwargs.get('group'): + group = bench.kwargs['group'] + marker = getattr(pytest.mark, 'benchmark_' + group) + item.add_marker(marker) + + subdir = config.getoption('subdir') + if subdir == 'benchmarks': + # we have to manually setup the events server in order to be able to + # signal events. This is usually done by the enclosing application + # using soledad client (i.e. bitmask client). + from leap.common.events import server + server.ensure_server() + + +# +# benchmark fixtures +# + +@pytest.fixture() +def txbenchmark(monitored_benchmark): + def blockOnThread(*args, **kwargs): + return threads.deferToThread( + monitored_benchmark, threads.blockingCallFromThread, + reactor, *args, **kwargs) + return blockOnThread + + +@pytest.fixture() +def txbenchmark_with_setup(monitored_benchmark_with_setup): + def blockOnThreadWithSetup(setup, f, *args, **kwargs): + def blocking_runner(*args, **kwargs): + return threads.blockingCallFromThread(reactor, f, *args, **kwargs) + + def blocking_setup(): + args = threads.blockingCallFromThread(reactor, setup) + try: + return tuple(arg for arg in args), {} + except TypeError: + return ((args,), {}) if args else None + + def bench(): + return monitored_benchmark_with_setup( + blocking_runner, setup=blocking_setup, + rounds=4, warmup_rounds=1, iterations=1, + args=args, kwargs=kwargs) + return threads.deferToThread(bench) + return blockOnThreadWithSetup + + +# +# resource monitoring +# + +class ResourceWatcher(threading.Thread): + + sampling_interval = 0.1 + + def __init__(self, watch_memory): + threading.Thread.__init__(self) + self.process = psutil.Process(os.getpid()) + self.running = False + # monitored resources + self.cpu_percent = None + self.watch_memory = watch_memory + self.memory_samples = [] + self.memory_percent = None + + def run(self): + self.running = True + self.process.cpu_percent() + # decide how long to sleep based on need to sample memory + sleep = self.sampling_interval if not self.watch_memory else 1 + while self.running: + if self.watch_memory: + sample = self.process.memory_percent(memtype='rss') + self.memory_samples.append(sample) + time.sleep(sleep) + + def stop(self): + self.running = False + self.join() + # save cpu usage info + self.cpu_percent = self.process.cpu_percent() + # save memory usage info + if self.watch_memory: + memory_percent = { + 'sampling_interval': self.sampling_interval, + 'samples': self.memory_samples, + 'stats': {}, + } + for stat in 'max', 'min', 'mean', 'std': + fun = getattr(numpy, stat) + memory_percent['stats'][stat] = fun(self.memory_samples) + self.memory_percent = memory_percent + + +def _monitored_benchmark(benchmark_fixture, benchmark_function, request, + *args, **kwargs): + # setup resource monitoring + watch_memory = _watch_memory(request) + watcher = ResourceWatcher(watch_memory) + watcher.start() + # run benchmarking function + benchmark_function(*args, **kwargs) + # store results + watcher.stop() + benchmark_fixture.extra_info.update({ + 'cpu_percent': watcher.cpu_percent + }) + if watch_memory: + benchmark_fixture.extra_info.update({ + 'memory_percent': watcher.memory_percent, + }) + # add docstring info + if request.scope == 'function': + fun = request.function + doc = fun.__doc__ or '' + benchmark_fixture.extra_info.update({'doc': doc.strip()}) + + +def _watch_memory(request): + return request.config.getoption('--watch-memory') + + +@pytest.fixture +def monitored_benchmark(benchmark, request): + return functools.partial( + _monitored_benchmark, benchmark, benchmark, request) + + +@pytest.fixture +def monitored_benchmark_with_setup(benchmark, request, *args, **kwargs): + return functools.partial( + _monitored_benchmark, benchmark, benchmark.pedantic, request, + *args, **kwargs) diff --git a/tests/benchmarks/pytest.ini b/tests/benchmarks/pytest.ini new file mode 100644 index 00000000..7a0508ce --- /dev/null +++ b/tests/benchmarks/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +twisted = yes diff --git a/tests/benchmarks/test_crypto.py b/tests/benchmarks/test_crypto.py new file mode 100644 index 00000000..3be447a5 --- /dev/null +++ b/tests/benchmarks/test_crypto.py @@ -0,0 +1,109 @@ +""" +Benchmarks for crypto operations. +If you don't want to stress your local machine too much, you can pass the +SIZE_LIMT environment variable. + +For instance, to keep the maximum payload at 1MB: + +SIZE_LIMIT=1E6 py.test -s tests/perf/test_crypto.py +""" +import pytest +import os +import json +from uuid import uuid4 + +from leap.soledad.common.document import SoledadDocument +from leap.soledad.client import _crypto + +LIMIT = int(float(os.environ.get('SIZE_LIMIT', 50 * 1000 * 1000))) + + +def create_doc_encryption(size): + @pytest.mark.benchmark(group="test_crypto_encrypt_doc") + @pytest.inlineCallbacks + def test_doc_encryption(soledad_client, txbenchmark, payload): + """ + Encrypt a document of a given size. + """ + crypto = soledad_client()._crypto + + DOC_CONTENT = {'payload': payload(size)} + doc = SoledadDocument( + doc_id=uuid4().hex, rev='rev', + json=json.dumps(DOC_CONTENT)) + + yield txbenchmark(crypto.encrypt_doc, doc) + return test_doc_encryption + + +# TODO this test is really bullshit, because it's still including +# the json serialization. + +def create_doc_decryption(size): + @pytest.inlineCallbacks + @pytest.mark.benchmark(group="test_crypto_decrypt_doc") + def test_doc_decryption(soledad_client, txbenchmark, payload): + """ + Decrypt a document of a given size. + """ + crypto = soledad_client()._crypto + + DOC_CONTENT = {'payload': payload(size)} + doc = SoledadDocument( + doc_id=uuid4().hex, rev='rev', + json=json.dumps(DOC_CONTENT)) + + encrypted_doc = yield crypto.encrypt_doc(doc) + doc.set_json(encrypted_doc) + + yield txbenchmark(crypto.decrypt_doc, doc) + return test_doc_decryption + + +def create_raw_encryption(size): + @pytest.mark.benchmark(group="test_crypto_raw_encrypt") + def test_raw_encrypt(monitored_benchmark, payload): + """ + Encrypt raw payload using default mode from crypto module. + """ + key = payload(32) + monitored_benchmark(_crypto.encrypt_sym, payload(size), key) + return test_raw_encrypt + + +def create_raw_decryption(size): + @pytest.mark.benchmark(group="test_crypto_raw_decrypt") + def test_raw_decrypt(monitored_benchmark, payload): + """ + Decrypt raw payload using default mode from crypto module. + """ + key = payload(32) + iv, ciphertext = _crypto.encrypt_sym(payload(size), key) + monitored_benchmark(_crypto.decrypt_sym, ciphertext, key, iv) + return test_raw_decrypt + + +# Create the TESTS in the global namespace, they'll be picked by the benchmark +# plugin. + +encryption_tests = [ + ('10k', 1E4), + ('100k', 1E5), + ('500k', 5E5), + ('1M', 1E6), + ('10M', 1E7), + ('50M', 5E7), +] + +for name, size in encryption_tests: + if size < LIMIT: + sz = int(size) + globals()['test_encrypt_doc_' + name] = create_doc_encryption(sz) + globals()['test_decrypt_doc_' + name] = create_doc_decryption(sz) + + +for name, size in encryption_tests: + if size < LIMIT: + sz = int(size) + globals()['test_encrypt_raw_' + name] = create_raw_encryption(sz) + globals()['test_decrypt_raw_' + name] = create_raw_decryption(sz) diff --git a/tests/benchmarks/test_legacy_vs_blobs.py b/tests/benchmarks/test_legacy_vs_blobs.py new file mode 100644 index 00000000..47d6482c --- /dev/null +++ b/tests/benchmarks/test_legacy_vs_blobs.py @@ -0,0 +1,305 @@ +# "Legacy" versus "Incoming blobs" pipeline comparison +# ==================================================== +# +# This benchmarking aims to compare the legacy and new mail incoming pipeline, +# to asses performance improvements brought by the introduction of blobs. +# +# We use the following sizes in these tests: +# +# - headers: 4 KB +# - metadata: 0.1 KB +# - flags: 0.5 KB +# - content: variable +# +# "Legacy" incoming mail pipeline: +# +# - email arrives at MX. +# - MX encrypts to public key and puts into couch. +# - pubkey encrypted doc is synced to soledad client as "incoming". +# - bitmask mail processes "incoming" and generates 3 metadocs + 1 payload +# doc per message. +# - soledad client syncs 4 documents back to server. +# +# "Incoming blobs" mail pipeline: +# +# - email arrives at MX. +# - MX encyrpts to public key and puts into soledad server. +# - soledad server writes a blob to filesystem. +# - soledad client gets the incoming blob from server and generates 3 +# metadocs + 1 blob. +# - soledad client syncs 3 meta documents and 1 blob back to server. +# +# Some notes about the tests in this file: +# +# - This is a simulation of the legacy and new incoming mail pipelines. +# There is no actual mail processing operation done (i.e. no pubkey crypto, +# no mail parsing), only usual soledad document manipulation and sync (with +# local 1network and crypto). +# +# - Each test simulates a whole incoming mail pipeline, including get new +# incoming messages from server, create new documents that represent the +# parsed message, and synchronize those back to the server. +# +# - These tests are disabled by default because it doesn't make much sense to +# have them run automatically for all commits in the repository. Instead, +# we will run them manually for specific releases and store results and +# analisys in a subfolder. + +import base64 +import pytest +import random +import sys +import treq +import uuid + +from io import BytesIO + +from twisted.internet.defer import gatherResults +from twisted.internet.defer import returnValue +from twisted.internet.defer import DeferredSemaphore + +from leap.soledad.common.blobs import Flags +from leap.soledad.client._db.blobs import BlobDoc + + +def payload(size): + random.seed(1337) # same seed to avoid different bench results + payload_bytes = bytearray(random.getrandbits(8) for _ in xrange(size)) + # encode as base64 to avoid ascii encode/decode errors + return base64.b64encode(payload_bytes)[:size] # remove b64 overhead + + +PARTS = { + 'headers': payload(4000), + 'metadata': payload(100), + 'flags': payload(500), +} + + +# +# "Legacy" incoming mail pipeline. +# + +@pytest.inlineCallbacks +def load_up_legacy(client, amount, content): + # make sure there are no document from previous runs + yield client.sync() + _, docs = yield client.get_all_docs() + deferreds = [] + for doc in docs: + d = client.delete_doc(doc) + deferreds.append(d) + yield gatherResults(deferreds) + yield client.sync() + + # create a bunch of local documents representing email messages + deferreds = [] + for i in xrange(amount): + deferreds.append(client.create_doc(content)) + yield gatherResults(deferreds) + yield client.sync() + + +@pytest.inlineCallbacks +def process_incoming_docs(client, docs): + deferreds = [] + for doc in docs: + + # create fake documents that represent message + for name in PARTS.keys(): + d = client.create_doc({name: doc.content[name]}) + deferreds.append(d) + + # create one document with content + key = 'content' + d = client.create_doc({key: doc.content[key]}) + deferreds.append(d) + + # delete the old incoming document + d = client.delete_doc(doc) + deferreds.append(d) + + # wait for all operatios to succeed + yield gatherResults(deferreds) + + +def create_legacy_test(amount, size): + group = 'test_legacy_vs_blobs_%d_%dk' % (amount, (size / 1000)) + + @pytest.inlineCallbacks + @pytest.mark.skip(reason="avoid running for all commits") + @pytest.mark.benchmark(group=group) + def test(soledad_client, txbenchmark_with_setup): + client = soledad_client() + + # setup the content of initial documents representing incoming emails + content = {'content': payload(size), 'incoming': True} + for name, data in PARTS.items(): + content[name] = data + + @pytest.inlineCallbacks + def setup(): + yield load_up_legacy(client, amount, content) + clean_client = soledad_client(force_fresh_db=True) + yield clean_client.create_index('incoming', 'bool(incoming)') + returnValue(clean_client) + + @pytest.inlineCallbacks + def legacy_pipeline(client): + yield client.sync() + docs = yield client.get_from_index('incoming', '1') + yield process_incoming_docs(client, docs) + yield client.sync() + + yield txbenchmark_with_setup(setup, legacy_pipeline) + return test + + +# ATTENTION: update the documentation in ../docs/benchmarks.rst if you change +# the number of docs or the doc sizes for the tests below. +test_legacy_10_1000k = create_legacy_test(10, 1000 * 1000) +test_legacy_100_100k = create_legacy_test(100, 100 * 1000) +test_legacy_1000_10k = create_legacy_test(1000, 10 * 1000) + + +# +# "Incoming blobs" mail pipeline: +# + +# used to limit the amount of concurrent accesses to the blob manager +semaphore = DeferredSemaphore(2) + + +# deliver data to a user by using the incoming api at given url. +def deliver_using_incoming_api(url, user_uuid, token, data): + auth = 'Token %s' % base64.b64encode('%s:%s' % (user_uuid, token)) + uri = "%s/incoming/%s/%s?namespace=MX" % (url, user_uuid, uuid.uuid4().hex) + return treq.put(uri, headers={'Authorization': auth}, data=BytesIO(data)) + + +# deliver data to a user by faking incoming using blobs +@pytest.inlineCallbacks +def deliver_using_blobs(client, fd): + # put + blob_id = uuid.uuid4().hex + doc = BlobDoc(fd, blob_id=blob_id) + size = sys.getsizeof(fd) + yield client.blobmanager.put(doc, size, namespace='MX') + # and flag + flags = [Flags.PENDING] + yield client.blobmanager.set_flags(blob_id, flags, namespace='MX') + + +def reclaim_free_space(client): + return client.blobmanager.local.dbpool.runQuery("VACUUM") + + +@pytest.inlineCallbacks +def load_up_blobs(client, amount, data): + # make sure there are no document from previous runs + yield client.sync() + _, docs = yield client.get_all_docs() + deferreds = [] + for doc in docs: + d = client.delete_doc(doc) + deferreds.append(d) + yield gatherResults(deferreds) + yield client.sync() + + # delete all payload from blobs db and server + for namespace in ['MX', 'payload']: + ids = yield client.blobmanager.remote_list(namespace=namespace) + deferreds = [] + for blob_id in ids: + d = semaphore.run( + client.blobmanager.delete, blob_id, namespace=namespace) + deferreds.append(d) + yield gatherResults(deferreds) + + # create a bunch of incoming blobs + deferreds = [] + for i in xrange(amount): + # choose method of delivery based in test being local or remote + if '127.0.0.1' in client.server_url: + fun = deliver_using_incoming_api + args = (client.server_url, client.uuid, client.token, data) + else: + fun = deliver_using_blobs + args = (client, BytesIO(data)) + d = semaphore.run(fun, *args) + deferreds.append(d) + yield gatherResults(deferreds) + + # empty local blobs db + yield client.blobmanager.local.dbpool.runQuery( + "DELETE FROM blobs WHERE 1;") + yield reclaim_free_space(client) + + +@pytest.inlineCallbacks +def process_incoming_blobs(client, pending): + # process items + deferreds = [] + for item in pending: + d = process_one_incoming_blob(client, item) + deferreds.append(d) + yield gatherResults(deferreds) + + +@pytest.inlineCallbacks +def process_one_incoming_blob(client, item): + fd = yield semaphore.run( + client.blobmanager.get, item, namespace='MX') + + # create metadata docs + deferreds = [] + for name, data in PARTS.items(): + d = client.create_doc({name: data}) + deferreds.append(d) + + # put the incoming blob as it would be done after mail processing + doc = BlobDoc(fd, blob_id=uuid.uuid4().hex) + size = sys.getsizeof(fd) + d = semaphore.run( + client.blobmanager.put, doc, size, namespace='payload') + deferreds.append(d) + yield gatherResults(deferreds) + + # delete incoming blob + yield semaphore.run( + client.blobmanager.delete, item, namespace='MX') + + +def create_blobs_test(amount, size): + group = 'test_legacy_vs_blobs_%d_%dk' % (amount, (size / 1000)) + + @pytest.inlineCallbacks + @pytest.mark.skip(reason="avoid running for all commits") + @pytest.mark.benchmark(group=group) + def test(soledad_client, txbenchmark_with_setup): + client = soledad_client() + blob_payload = payload(size) + + @pytest.inlineCallbacks + def setup(): + yield load_up_blobs(client, amount, blob_payload) + returnValue(soledad_client(force_fresh_db=True)) + + @pytest.inlineCallbacks + def blobs_pipeline(client): + pending = yield client.blobmanager.remote_list( + namespace='MX', filter_flags=Flags.PENDING) + yield process_incoming_blobs(client, pending) + # reclaim_free_space(client) + yield client.sync() + yield client.blobmanager.send_missing(namespace='payload') + + yield txbenchmark_with_setup(setup, blobs_pipeline) + return test + + +# ATTENTION: update the documentation in ../docs/benchmarks.rst if you change +# the number of docs or the doc sizes for the tests below. +test_blobs_10_1000k = create_blobs_test(10, 1000 * 1000) +test_blobs_100_100k = create_blobs_test(100, 100 * 1000) +test_blobs_1000_10k = create_blobs_test(1000, 10 * 1000) diff --git a/tests/benchmarks/test_misc.py b/tests/benchmarks/test_misc.py new file mode 100644 index 00000000..8b2178b9 --- /dev/null +++ b/tests/benchmarks/test_misc.py @@ -0,0 +1,9 @@ +import pytest + + +@pytest.mark.benchmark(group="test_instance") +def test_initialization(soledad_client, monitored_benchmark): + """ + Soledad client object initialization. + """ + monitored_benchmark(soledad_client) diff --git a/tests/benchmarks/test_resources.py b/tests/benchmarks/test_resources.py new file mode 100644 index 00000000..173edbd1 --- /dev/null +++ b/tests/benchmarks/test_resources.py @@ -0,0 +1,50 @@ +import pytest +import random +import time + +from decimal import Decimal + + +def bellardBig(n): + # http://en.wikipedia.org/wiki/Bellard%27s_formula + pi = Decimal(0) + k = 0 + while k < n: + pi += (Decimal(-1) ** k / (1024 ** k)) * ( + Decimal(256) / (10 * k + 1) + + Decimal(1) / (10 * k + 9) - + Decimal(64) / (10 * k + 3) - + Decimal(32) / (4 * k + 1) - + Decimal(4) / (10 * k + 5) - + Decimal(4) / (10 * k + 7) - + Decimal(1) / (4 * k + 3)) + k += 1 + pi = pi * 1 / (2 ** 6) + return pi + + +@pytest.mark.skip(reason='not a real use case, used only for instrumentation') +def test_cpu_intensive(monitored_benchmark): + + def _cpu_intensive(): + sleep = [random.uniform(0.5, 1.5) for _ in xrange(3)] + while sleep: + t = sleep.pop() + time.sleep(t) + bellardBig(int((10 ** 3) * t)) + + monitored_benchmark(_cpu_intensive) + + +@pytest.mark.skip(reason='not a real use case, used only for instrumentation') +def test_memory_intensive(monitored_benchmark): + + def _memory_intensive(): + sleep = [random.uniform(0.5, 1.5) for _ in xrange(3)] + bigdata = "" + while sleep: + t = sleep.pop() + bigdata += "b" * 10 * int(10E6) + time.sleep(t) + + monitored_benchmark(_memory_intensive) diff --git a/tests/benchmarks/test_sqlcipher.py b/tests/benchmarks/test_sqlcipher.py new file mode 100644 index 00000000..9108084c --- /dev/null +++ b/tests/benchmarks/test_sqlcipher.py @@ -0,0 +1,47 @@ +''' +Tests SoledadClient/SQLCipher interaction +''' +import pytest + +from twisted.internet.defer import gatherResults + + +def load_up(client, amount, payload, defer=True): + results = [client.create_doc({'content': payload}) for _ in xrange(amount)] + if defer: + return gatherResults(results) + + +def build_test_sqlcipher_async_create(amount, size): + @pytest.inlineCallbacks + @pytest.mark.benchmark(group="test_sqlcipher_async_create") + def test(soledad_client, txbenchmark_with_setup, payload): + """ + Create many documents of a given size concurrently. + """ + client = soledad_client() + yield txbenchmark_with_setup( + lambda: None, load_up, client, amount, payload(size)) + return test + + +def build_test_sqlcipher_create(amount, size): + @pytest.mark.skip(reason="this test is lengthy and not a real use case") + @pytest.mark.benchmark(group="test_sqlcipher_create") + def test(soledad_client, monitored_benchmark, payload): + """ + Create many documents of a given size serially. + """ + client = soledad_client()._dbsyncer + monitored_benchmark( + load_up, client, amount, payload(size), defer=False) + return test + + +test_async_create_10_1000k = build_test_sqlcipher_async_create(10, 1000 * 1000) +test_async_create_100_100k = build_test_sqlcipher_async_create(100, 100 * 1000) +test_async_create_1000_10k = build_test_sqlcipher_async_create(1000, 10 * 1000) +# synchronous +test_create_10_1000k = build_test_sqlcipher_create(10, 1000 * 1000) +test_create_100_100k = build_test_sqlcipher_create(100, 100 * 1000) +test_create_1000_10k = build_test_sqlcipher_create(1000, 10 * 1000) diff --git a/tests/benchmarks/test_sqlite_blobs_backend.py b/tests/benchmarks/test_sqlite_blobs_backend.py new file mode 100644 index 00000000..e02cacad --- /dev/null +++ b/tests/benchmarks/test_sqlite_blobs_backend.py @@ -0,0 +1,82 @@ +import pytest +import os + +from uuid import uuid4 +from io import BytesIO + +from twisted.internet.defer import gatherResults +from twisted.internet.defer import DeferredSemaphore + +from leap.soledad.client._db.blobs import SQLiteBlobBackend + + +semaphore = DeferredSemaphore(2) + + +# +# put +# + +def put(backend, amount, data): + deferreds = [] + for _ in xrange(amount): + blob_id = uuid4().hex + fd = BytesIO(data) + size = len(data) + d = semaphore.run(backend.put, blob_id, fd, size) + deferreds.append(d) + return gatherResults(deferreds) + + +def create_put_test(amount, size): + + @pytest.inlineCallbacks + @pytest.mark.sqlite_blobs_backend_put + def test(txbenchmark, payload, tmpdir): + dbpath = os.path.join(tmpdir.strpath, 'blobs.db') + backend = SQLiteBlobBackend(dbpath, key='123') + data = payload(size) + yield txbenchmark(put, backend, amount, data) + + return test + + +test_sqlite_blobs_backend_put_1_10000k = create_put_test(1, 10000 * 1000) +test_sqlite_blobs_backend_put_10_1000k = create_put_test(10, 1000 * 1000) +test_sqlite_blobs_backend_put_100_100k = create_put_test(100, 100 * 1000) +test_sqlite_blobs_backend_put_1000_10k = create_put_test(1000, 10 * 1000) + + +# +# put +# + +@pytest.inlineCallbacks +def get(backend): + local = yield backend.list() + deferreds = [] + for blob_id in local: + d = backend.get(blob_id) + deferreds.append(d) + yield gatherResults(deferreds) + + +def create_get_test(amount, size): + + @pytest.inlineCallbacks + @pytest.mark.sqlite_blobs_backend_get + def test(txbenchmark, payload, tmpdir): + dbpath = os.path.join(tmpdir.strpath, 'blobs.db') + backend = SQLiteBlobBackend(dbpath, key='123') + data = payload(size) + + yield put(backend, amount, data) + yield txbenchmark(get, backend) + + return test + + +test_sqlite_blobs_backend_get_1_10000k = create_get_test(1, 10000 * 1000) +test_sqlite_blobs_backend_get_10_1000k = create_get_test(10, 1000 * 1000) +test_sqlite_blobs_backend_get_100_100k = create_get_test(100, 100 * 1000) +test_sqlite_blobs_backend_get_1000_10k = create_get_test(1000, 10 * 1000) diff --git a/tests/benchmarks/test_sync.py b/tests/benchmarks/test_sync.py new file mode 100644 index 00000000..45506d77 --- /dev/null +++ b/tests/benchmarks/test_sync.py @@ -0,0 +1,92 @@ +import pytest +from twisted.internet.defer import gatherResults + + +@pytest.inlineCallbacks +def load_up(client, amount, payload): + # create a bunch of local documents + deferreds = [] + for i in xrange(amount): + deferreds.append(client.create_doc({'content': payload})) + yield gatherResults(deferreds) + + +# Each test created with this function will: +# +# - get a fresh client. +# - iterate: +# - setup: create N docs of a certain size +# - benchmark: sync() -- uploads N docs. +def create_upload(uploads, size): + @pytest.inlineCallbacks + @pytest.mark.benchmark(group="test_upload") + def test(soledad_client, txbenchmark_with_setup, payload): + """ + Upload many documents of a given size. + """ + client = soledad_client() + + def setup(): + return load_up(client, uploads, payload(size)) + + yield txbenchmark_with_setup(setup, client.sync) + return test + + +# ATTENTION: update the documentation in ../docs/benchmarks.rst if you change +# the number of docs or the doc sizes for the tests below. +test_upload_10_1000k = create_upload(10, 1000 * 1000) +test_upload_100_100k = create_upload(100, 100 * 1000) +test_upload_1000_10k = create_upload(1000, 10 * 1000) + + +# Each test created with this function will: +# +# - get a fresh client. +# - create N docs of a certain size +# - sync (uploads those docs) +# - iterate: +# - setup: get a fresh client with empty local db +# - benchmark: sync() -- downloads N docs. +def create_download(downloads, size): + @pytest.inlineCallbacks + @pytest.mark.benchmark(group="test_download") + def test(soledad_client, txbenchmark_with_setup, payload): + """ + Download many documents of the same size. + """ + client = soledad_client() + + yield load_up(client, downloads, payload(size)) + yield client.sync() + # We could create them directly on couch, but sending them + # ensures we are dealing with properly encrypted docs + + def setup(): + return soledad_client(force_fresh_db=True) + + def sync(clean_client): + return clean_client.sync() + yield txbenchmark_with_setup(setup, sync) + return test + + +# ATTENTION: update the documentation in ../docs/benchmarks.rst if you change +# the number of docs or the doc sizes for the tests below. +test_download_10_1000k = create_download(10, 1000 * 1000) +test_download_100_100k = create_download(100, 100 * 1000) +test_download_1000_10k = create_download(1000, 10 * 1000) + + +@pytest.inlineCallbacks +@pytest.mark.benchmark(group="test_nothing_to_sync") +def test_nothing_to_sync(soledad_client, txbenchmark_with_setup): + """ + Sync two replicas that are already in sync. + """ + def setup(): + return soledad_client() + + def sync(clean_client): + return clean_client.sync() + yield txbenchmark_with_setup(setup, sync) |