Skip to content

Commit 2f58da6

Browse files
TheDistributorMartin Gallwey
andauthored
Add an initial set of performance benchmarks (#199)
* Add an initial set of performance benchmarks * make it work with pre-existing dbs too * Add whether cython is enabled --------- Co-authored-by: Martin Gallwey <mgy5@3ds.com>
1 parent 0ac4ee6 commit 2f58da6

8 files changed

Lines changed: 480 additions & 94 deletions

File tree

.circleci/config.yml

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,88 @@ jobs:
7373
- after_failure:
7474
when : "on_fail"
7575

76+
perf_bench:
77+
description: "Run insert/select performance benchmarks on master and on this branch, print the diff"
78+
docker:
79+
- image: nuodb/nuodb:latest
80+
user: root
81+
resource_class: xlarge
82+
environment:
83+
TZ : America/New_York
84+
NUO_SET_TLS : disable
85+
NUOCMD_CLIENT_KEY : ""
86+
NUOCMD_VERIFY_SERVER : ""
87+
NUOCMD_PLUGINS : ""
88+
steps:
89+
- checkout
90+
- run:
91+
name: Install build tools
92+
command: |
93+
PYVER=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
94+
dnf install -y git make gcc "python${PYVER}-devel"
95+
- run:
96+
name: Install pip
97+
command: |
98+
curl https://bootstrap.pypa.io/get-pip.py -o /tmp/get-pip.py
99+
python3 /tmp/get-pip.py --user
100+
- run:
101+
name: Make artifact directories
102+
command: mkdir -p artifacts results
103+
- run:
104+
name: Start NuoDB Admin
105+
command: |
106+
sudo -u nuodb /opt/nuodb/etc/nuoadmin tls $NUO_SET_TLS
107+
sudo -u nuodb /opt/nuodb/etc/nuoadmin tls status
108+
sudo -u nuodb /opt/nuodb/etc/nuoadmin start
109+
sudo -u nuodb /opt/nuodb/bin/nuocmd --show-json get effective-license
110+
# Run master then this branch on the same runner and let compare.py
111+
# print a delta table. Hardware noise mostly cancels because both
112+
# runs share the container; xlarge gives us dedicated cores.
113+
- run:
114+
name: Baseline benchmarks on master
115+
command: |
116+
GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
117+
git fetch --no-tags --depth=1 origin master
118+
git worktree add /tmp/base FETCH_HEAD
119+
# Copy the perf suite + conftest hooks from this branch so the
120+
# master worktree's driver is exercised by the same benchmarks.
121+
cp -a tests/perf /tmp/base/tests/
122+
cp tests/conftest.py /tmp/base/tests/conftest.py
123+
cd /tmp/base
124+
make PYTHON=python3 install
125+
$HOME/.local/bin/pip install pytest-benchmark
126+
python3 -m pytest tests/perf --run-perf --benchmark-only \
127+
--benchmark-json=/tmp/master.json \
128+
--benchmark-columns=min,mean,median,stddev,rounds
129+
- run:
130+
name: Reset DB
131+
command: |
132+
sudo -u nuodb /opt/nuodb/bin/nuocmd shutdown database \
133+
--db-name pynuodb_test 2>/dev/null || true
134+
- run:
135+
name: Branch benchmarks + diff vs master
136+
command: |
137+
make PYTHON=python3 install
138+
$HOME/.local/bin/pip install pytest-benchmark
139+
python3 -m pytest tests/perf --run-perf --benchmark-only \
140+
--benchmark-json=artifacts/branch.json \
141+
--benchmark-columns=min,mean,median,stddev,rounds
142+
python3 tests/perf/compare.py \
143+
/tmp/master.json artifacts/branch.json \
144+
| tee artifacts/perf_diff.txt
145+
- store_artifacts:
146+
path: artifacts
147+
- after_failure:
148+
when : "on_fail"
149+
76150
workflows:
77151
build-project:
78152
jobs:
79153
- build_n_run:
80154
name: "Build and run regression tests"
81155
context:
82156
- common-config
157+
- perf_bench:
158+
name: "Run performance benchmarks, comparing results to master"
159+
context:
160+
- common-config

test-performance/timesInsert.py

Lines changed: 0 additions & 82 deletions
This file was deleted.

test_requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mock>=1.0
22
nose>=1.3
33
pytest>=2.7
4+
pytest-benchmark>=4.0
45
coverage>=3.7
56
pytest-cov>=1.8.1
67
coveralls>=0.5

tests/conftest.py

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,52 @@
3030

3131
from . import nuocmd, cvtjson
3232

33+
34+
def pytest_addoption(parser):
35+
parser.addoption("--run-perf", action="store_true", default=False,
36+
help="run performance benchmarks under tests/perf")
37+
# Bypass the nuocmd-driven discovery/lifecycle fixtures and connect
38+
# straight to an already-running database. Handy for local perf runs
39+
# where you don't want (or can't) shell out to nuocmd.
40+
parser.addoption("--use-existing-db", action="store_true", default=False,
41+
help="skip nuocmd discovery; use --db-* options to connect")
42+
parser.addoption("--db-host", default="localhost:48004",
43+
help="SQL host:port when --use-existing-db is set")
44+
parser.addoption("--db-name", default=DATABASE_NAME,
45+
help="database name when --use-existing-db is set")
46+
parser.addoption("--db-user", default=DBA_USER,
47+
help="user when --use-existing-db is set")
48+
parser.addoption("--db-password", default=DBA_PASSWORD,
49+
help="password when --use-existing-db is set")
50+
parser.addoption("--db-schema", default="test",
51+
help="schema when --use-existing-db is set")
52+
53+
54+
def pytest_configure(config):
55+
config.addinivalue_line(
56+
"markers",
57+
"perf: performance benchmark; skipped unless --run-perf is passed")
58+
59+
60+
def pytest_report_header(config):
61+
import pynuodb
62+
try:
63+
from pynuodb import _fetch
64+
ext = "cython: %s" % _fetch.__file__
65+
except ImportError:
66+
ext = "cython: NOT loaded (pure-Python fallback)"
67+
return "pynuodb: %s (%s)\n%s" % (
68+
pynuodb.__version__, pynuodb.__file__, ext)
69+
70+
71+
def pytest_collection_modifyitems(config, items):
72+
if config.getoption("--run-perf"):
73+
return
74+
skip = pytest.mark.skip(reason="need --run-perf to run performance tests")
75+
for item in items:
76+
if "perf" in item.keywords:
77+
item.add_marker(skip)
78+
3379
_log = logging.getLogger("pynuodbtest")
3480

3581
DB_OPTIONS = [] # type: List[str]
@@ -286,19 +332,33 @@ def te(ap, db):
286332

287333

288334
@pytest.fixture(scope='session')
289-
def database(ap, db, te):
290-
# type: (AP_FIXTURE, DB_FIXTURE, TE_FIXTURE) -> DATABASE_FIXTURE
335+
def database(request):
336+
# type: (pytest.FixtureRequest) -> DATABASE_FIXTURE
291337
import pynuodb
338+
339+
if request.config.getoption("--use-existing-db"):
340+
connect_args = {
341+
'database': request.config.getoption("--db-name"),
342+
'host': request.config.getoption("--db-host"),
343+
'user': request.config.getoption("--db-user"),
344+
'password': request.config.getoption("--db-password"),
345+
'options': {'schema': request.config.getoption("--db-schema")},
346+
} # type: DATABASE_FIXTURE
347+
else:
348+
ap = request.getfixturevalue('ap')
349+
db = request.getfixturevalue('db')
350+
request.getfixturevalue('te')
351+
connect_args = {'database': db[0],
352+
'host': ap[1],
353+
'user': db[1],
354+
'password': db[2],
355+
'options': {'schema': 'test'}}
356+
292357
end = time.time() + 30
293358
conn = None
294-
_log.info("Creating a SQL connection to %s as user %s with schema 'test'",
295-
db[0], db[1])
296-
297-
connect_args = {'database': db[0],
298-
'host': ap[1],
299-
'user': db[1],
300-
'password': db[2],
301-
'options': {'schema': 'test'}} # type: DATABASE_FIXTURE
359+
_log.info("Creating a SQL connection to %s as user %s",
360+
connect_args['database'], connect_args['user'])
361+
302362
system_information = {'effective_version': 0}
303363

304364
try:
@@ -323,6 +383,6 @@ def database(ap, db, te):
323383
if conn:
324384
conn.close()
325385

326-
_log.info("Database %s is available", db[0])
386+
_log.info("Database %s is available", connect_args['database'])
327387

328388
return {'connect_args': connect_args, 'system_information': system_information}

tests/nuodb_base.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,17 @@ class NuoBase(object):
3131
lower_func = 'lower' # For stored procedure test
3232

3333
@pytest.fixture(autouse=True)
34-
def _setup(self, database):
34+
def _setup(self, database, request):
3535
# Preserve the options we'll need to create a connection to the DB
3636
self.connect_args = database['connect_args']
3737
self.system_information = database['system_information']
3838

39+
# In --use-existing-db mode we trust the caller and skip the
40+
# nuocmd process check: the successful connection made in the
41+
# `database` fixture is already proof the DB and a TE are up.
42+
if request.config.getoption("--use-existing-db"):
43+
return
44+
3945
# Verify the database is up and has a running TE
4046
dbname = self.connect_args['database']
4147
(ret, out) = nuocmd(['--show-json', 'get', 'processes',

tests/perf/__init__.py

Whitespace-only changes.

tests/perf/compare.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# -*- coding: utf-8 -*-
2+
"""Compare two pytest-benchmark JSON files (master vs branch).
3+
4+
Prints a table of master min, branch min, and the absolute + percentage
5+
delta per test. Exits non-zero if any test regressed by more than
6+
--fail-threshold (default 10%), so CI turns a real regression into a
7+
failed build. Improvements never fail the build.
8+
"""
9+
from __future__ import print_function
10+
11+
import argparse
12+
import json
13+
import sys
14+
15+
16+
def _load(path):
17+
with open(path) as f:
18+
data = json.load(f)
19+
return {b['name']: b['stats']['min'] for b in data['benchmarks']}
20+
21+
22+
def main(args):
23+
master = _load(args.master)
24+
branch = _load(args.branch)
25+
26+
print("%-40s %16s %16s %14s %10s" % ( "Test", "master min (ms)", "branch min (ms)", "delta (ms)", "delta %"))
27+
print("-" * 100)
28+
29+
regressed = []
30+
for name in sorted(set(master) & set(branch)):
31+
m = master[name] * 1000.0
32+
b = branch[name] * 1000.0
33+
d = b - m
34+
p = (d / m) * 100.0 if m else float('nan')
35+
print("%-40s %16.3f %16.3f %+14.3f %+9.2f%%" % (name, m, b, d, p))
36+
if p > args.fail_threshold:
37+
regressed.append((name, p))
38+
39+
only_master = sorted(set(master) - set(branch))
40+
only_branch = sorted(set(branch) - set(master))
41+
if only_master:
42+
print("\nOnly in master: %s" % ", ".join(only_master))
43+
if only_branch:
44+
print("Only in branch: %s" % ", ".join(only_branch))
45+
46+
print()
47+
if regressed:
48+
print("FAIL: %d test(s) regressed by more than %.2f%%:" % (len(regressed), args.fail_threshold))
49+
for name, p in regressed:
50+
print(" %s: %+.2f%%" % (name, p))
51+
sys.exit(1)
52+
print("OK: no test regressed by more than %.2f%%" % args.fail_threshold)
53+
54+
55+
def _parse_args():
56+
p = argparse.ArgumentParser()
57+
p.add_argument('master', help='pytest-benchmark JSON for master')
58+
p.add_argument('branch', help='pytest-benchmark JSON for this branch')
59+
p.add_argument('--fail-threshold', type=float, default=15.0,
60+
help='percent slowdown that fails the build (default 15)')
61+
return p.parse_args()
62+
63+
64+
if __name__ == '__main__':
65+
main(_parse_args())

0 commit comments

Comments
 (0)