Skip to content

Commit f3e8fc2

Browse files
authored
test: Performance Testing (#675)
* performance files * test_benchmark * performance testing changes * changes in benchmark performance for prod * changes to number of runs * adding comments * linting changes * changes for 3.2 * Revert "changes for 3.2" This reverts commit 488035c. * adding licence
1 parent 74f2269 commit f3e8fc2

File tree

4 files changed

+320
-0
lines changed

4 files changed

+320
-0
lines changed

tests/performance/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Copyright 2021 Google LLC
2+
#
3+
# Use of this source code is governed by a BSD-style
4+
# license that can be found in the LICENSE file or at
5+
# https://developers.google.com/open-source/licenses/bsd
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Copyright 2021 Google LLC
2+
#
3+
# Use of this source code is governed by a BSD-style
4+
# license that can be found in the LICENSE file or at
5+
# https://developers.google.com/open-source/licenses/bsd
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Copyright 2021 Google LLC
2+
#
3+
# Use of this source code is governed by a BSD-style
4+
# license that can be found in the LICENSE file or at
5+
# https://developers.google.com/open-source/licenses/bsd
6+
7+
from django.db import models
8+
9+
10+
class Author(models.Model):
11+
id = models.IntegerField(primary_key=True)
12+
first_name = models.CharField(max_length=20)
13+
last_name = models.CharField(max_length=20)
14+
rating = models.CharField(max_length=50)
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
# Copyright 2021 Google LLC
2+
#
3+
# Use of this source code is governed by a BSD-style
4+
# license that can be found in the LICENSE file or at
5+
# https://developers.google.com/open-source/licenses/bsd
6+
7+
import random
8+
import time
9+
import unittest
10+
11+
import pandas as pd
12+
import pytest
13+
from django.db import connection
14+
from google.api_core.exceptions import Aborted
15+
from google.cloud import spanner_dbapi
16+
from google.cloud.spanner_v1 import Client, KeySet
17+
18+
from tests.performance.django_spanner.models import Author
19+
from tests.settings import DATABASE_NAME, INSTANCE_ID
20+
from tests.system.django_spanner.utils import setup_database, setup_instance
21+
22+
23+
def measure_execution_time(function):
24+
"""Decorator to measure a wrapped method execution time."""
25+
26+
def wrapper(self, measures):
27+
"""Execute the wrapped method and measure its execution time.
28+
Args:
29+
measures (dict): Test cases and their execution time.
30+
"""
31+
t_start = time.time()
32+
try:
33+
function(self)
34+
measures[function.__name__] = round(time.time() - t_start, 4)
35+
except Aborted:
36+
measures[function.__name__] = 0
37+
38+
return wrapper
39+
40+
41+
def insert_one_row(transaction, one_row):
42+
"""A transaction-function for the original Spanner client.
43+
Inserts a single row into a database and then fetches it back.
44+
"""
45+
transaction.execute_update(
46+
"INSERT Author (id, first_name, last_name, rating) "
47+
" VALUES {}".format(str(one_row))
48+
)
49+
last_name = transaction.execute_sql(
50+
"SELECT last_name FROM Author WHERE id=1"
51+
).one()[0]
52+
if last_name != "Allison":
53+
raise ValueError("Received invalid last name: " + last_name)
54+
55+
56+
def insert_many_rows(transaction, many_rows):
57+
"""A transaction-function for the original Spanner client.
58+
Insert 100 rows into a database.
59+
"""
60+
statements = []
61+
for row in many_rows:
62+
statements.append(
63+
"INSERT Author (id, first_name, last_name, rating) "
64+
" VALUES {}".format(str(row))
65+
)
66+
_, count = transaction.batch_update(statements)
67+
if sum(count) != 99:
68+
raise ValueError("Wrong number of inserts: " + str(sum(count)))
69+
70+
71+
class DjangoBenchmarkTest:
72+
"""The Django performace testing class."""
73+
74+
def __init__(self):
75+
with connection.schema_editor() as editor:
76+
# Create the tables
77+
editor.create_model(Author)
78+
79+
self._many_rows = []
80+
self._many_rows2 = []
81+
for i in range(99):
82+
num = round(random.randint(0, 100000000))
83+
self._many_rows.append(Author(num, "Pete", "Allison", "2.1"))
84+
num2 = round(random.randint(0, 100000000))
85+
self._many_rows2.append(Author(num2, "Pete", "Allison", "2.1"))
86+
87+
@measure_execution_time
88+
def insert_one_row_with_fetch_after(self):
89+
author_kent = Author(
90+
id=2, first_name="Pete", last_name="Allison", rating="2.1",
91+
)
92+
author_kent.save()
93+
last_name = Author.objects.get(pk=author_kent.id).last_name
94+
if last_name != "Allison":
95+
raise ValueError("Received invalid last name: " + last_name)
96+
97+
@measure_execution_time
98+
def insert_many_rows(self):
99+
for row in self._many_rows:
100+
row.save()
101+
102+
@measure_execution_time
103+
def insert_many_rows_with_mutations(self):
104+
Author.objects.bulk_create(self._many_rows2)
105+
106+
@measure_execution_time
107+
def read_one_row(self):
108+
row = Author.objects.all().first()
109+
if row is None:
110+
raise ValueError("No rows read")
111+
112+
@measure_execution_time
113+
def select_many_rows(self):
114+
rows = Author.objects.all()
115+
if len(rows) != 100:
116+
raise ValueError("Wrong number of rows read")
117+
118+
def _cleanup(self):
119+
"""Drop the test table."""
120+
with connection.schema_editor() as editor:
121+
# delete the table
122+
editor.delete_model(Author)
123+
124+
def run(self):
125+
"""Execute every test case."""
126+
measures = {}
127+
for method in (
128+
self.insert_one_row_with_fetch_after,
129+
self.read_one_row,
130+
self.insert_many_rows,
131+
self.select_many_rows,
132+
self.insert_many_rows_with_mutations,
133+
):
134+
method(measures)
135+
self._cleanup()
136+
return measures
137+
138+
139+
class SpannerBenchmarkTest:
140+
"""The Spanner performace testing class."""
141+
142+
def __init__(self):
143+
self._create_table()
144+
self._one_row = (
145+
1,
146+
"Pete",
147+
"Allison",
148+
"2.1",
149+
)
150+
self._client = Client()
151+
self._instance = self._client.instance(INSTANCE_ID)
152+
self._database = self._instance.database(DATABASE_NAME)
153+
154+
self._many_rows = []
155+
self._many_rows2 = []
156+
for i in range(99):
157+
num = round(random.randint(0, 100000000))
158+
self._many_rows.append((num, "Pete", "Allison", "2.1"))
159+
num2 = round(random.randint(0, 100000000))
160+
self._many_rows2.append((num2, "Pete", "Allison", "2.1"))
161+
162+
# initiate a session
163+
with self._database.snapshot():
164+
pass
165+
166+
def _create_table(self):
167+
"""Create a table for performace testing."""
168+
conn = spanner_dbapi.connect(INSTANCE_ID, DATABASE_NAME)
169+
conn.database.update_ddl(
170+
[
171+
"""
172+
CREATE TABLE Author (
173+
id INT64,
174+
first_name STRING(20),
175+
last_name STRING(20),
176+
rating STRING(50),
177+
) PRIMARY KEY (id)
178+
"""
179+
]
180+
).result(120)
181+
182+
conn.close()
183+
184+
@measure_execution_time
185+
def insert_one_row_with_fetch_after(self):
186+
self._database.run_in_transaction(insert_one_row, self._one_row)
187+
188+
@measure_execution_time
189+
def insert_many_rows(self):
190+
self._database.run_in_transaction(insert_many_rows, self._many_rows)
191+
192+
@measure_execution_time
193+
def insert_many_rows_with_mutations(self):
194+
with self._database.batch() as batch:
195+
batch.insert(
196+
table="Author",
197+
columns=("id", "first_name", "last_name", "rating"),
198+
values=self._many_rows2,
199+
)
200+
201+
@measure_execution_time
202+
def read_one_row(self):
203+
with self._database.snapshot() as snapshot:
204+
keyset = KeySet(all_=True)
205+
snapshot.read(
206+
table="Author",
207+
columns=("id", "first_name", "last_name", "rating"),
208+
keyset=keyset,
209+
).one()
210+
211+
@measure_execution_time
212+
def select_many_rows(self):
213+
with self._database.snapshot() as snapshot:
214+
rows = list(
215+
snapshot.execute_sql("SELECT * FROM Author ORDER BY last_name")
216+
)
217+
if len(rows) != 100:
218+
raise ValueError("Wrong number of rows read")
219+
220+
def _cleanup(self):
221+
"""Drop the test table."""
222+
conn = spanner_dbapi.connect(INSTANCE_ID, DATABASE_NAME)
223+
conn.database.update_ddl(["DROP TABLE Author"])
224+
conn.close()
225+
226+
def run(self):
227+
"""Execute every test case."""
228+
measures = {}
229+
for method in (
230+
self.insert_one_row_with_fetch_after,
231+
self.read_one_row,
232+
self.insert_many_rows,
233+
self.select_many_rows,
234+
self.insert_many_rows_with_mutations,
235+
):
236+
method(measures)
237+
self._cleanup()
238+
return measures
239+
240+
241+
@pytest.mark.django_db()
242+
class BenchmarkTest(unittest.TestCase):
243+
def setUp(self):
244+
setup_instance()
245+
setup_database()
246+
247+
def test_run(self):
248+
django_obj = pd.DataFrame(
249+
columns=[
250+
"insert_one_row_with_fetch_after",
251+
"read_one_row",
252+
"insert_many_rows",
253+
"select_many_rows",
254+
"insert_many_rows_with_mutations",
255+
]
256+
)
257+
spanner_obj = pd.DataFrame(
258+
columns=[
259+
"insert_one_row_with_fetch_after",
260+
"read_one_row",
261+
"insert_many_rows",
262+
"select_many_rows",
263+
"insert_many_rows_with_mutations",
264+
]
265+
)
266+
267+
for _ in range(50):
268+
django_obj = django_obj.append(
269+
DjangoBenchmarkTest().run(), ignore_index=True
270+
)
271+
spanner_obj = spanner_obj.append(
272+
SpannerBenchmarkTest().run(), ignore_index=True
273+
)
274+
275+
avg = pd.concat(
276+
[django_obj.mean(axis=0), spanner_obj.mean(axis=0)], axis=1
277+
)
278+
avg.columns = ["Django", "Spanner"]
279+
std = pd.concat(
280+
[django_obj.std(axis=0), spanner_obj.std(axis=0)], axis=1
281+
)
282+
std.columns = ["Django", "Spanner"]
283+
err = pd.concat(
284+
[django_obj.sem(axis=0), spanner_obj.sem(axis=0)], axis=1
285+
)
286+
err.columns = ["Django", "Spanner"]
287+
288+
print(
289+
"Average: ",
290+
avg,
291+
"Standard Deviation: ",
292+
std,
293+
"Error:",
294+
err,
295+
sep="\n",
296+
)

0 commit comments

Comments
 (0)