Skip to content

Commit 261511e

Browse files
author
Ivan Dlugos
committed
Merge branch 'dev' into main
2 parents 2021c0e + 9de5471 commit 261511e

File tree

10 files changed

+293
-82
lines changed

10 files changed

+293
-82
lines changed

Makefile

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ export PATH := ${VIRTUAL_ENV}/bin:${PATH}
44

55
################################
66

7+
build: ${VENV} clean
8+
python3 setup.py bdist_wheel
9+
ls -lh dist
10+
711
${VENV}: ${VENV}/bin/activate
812

913
${VENV}/bin/activate: requirements.txt
@@ -20,13 +24,12 @@ init: ${VENV}
2024
test: ${VENV}
2125
python3 -m pytest -s
2226

23-
build: ${VENV} clean
24-
python3 setup.py bdist_wheel
25-
ls -lh dist
27+
benchmark: ${VENV}
28+
python3 -m benchmark
2629

2730
clean:
2831
rm -rf build/
2932
rm -rf dist/
3033
rm -rf *.egg-info
3134

32-
.PHONY: init test build
35+
.PHONY: init test build benchmark

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ cp lib/*objectbox* [/path/to/your/git/objectbox/checkout]/objectbox/lib/$(uname
125125
```
126126

127127
You can run `make test` to make sure everything works as expected.
128+
You can also try `make benchmark` to measure the CRUD performance on your machine.
128129

129130
License
130131
-------

benchmark.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import objectbox
2+
import time
3+
from tests.model import TestEntity
4+
from tests.common import remove_test_dir, load_empty_test_objectbox
5+
6+
7+
class ObjectBoxPerf:
8+
"""
9+
Performance executable
10+
"""
11+
12+
def __init__(self):
13+
self.ob = load_empty_test_objectbox()
14+
self.box = objectbox.Box(self.ob, TestEntity)
15+
16+
def remove_all(self):
17+
self.box.remove_all()
18+
19+
def put_many(self, items):
20+
self.box.put(items)
21+
22+
def read_all(self):
23+
return self.box.get_all()
24+
25+
26+
class Timer:
27+
"""
28+
Context manager to time a block of code.
29+
Appends the resulting runtime in milliseconds to a list of floats.
30+
"""
31+
32+
def __init__(self, out_list: [float]):
33+
self.start = time.time_ns()
34+
self.list = out_list
35+
36+
def __enter__(self):
37+
return self
38+
39+
def __exit__(self, exc_type, exc_val, exc_tb):
40+
self.list.append((time.time_ns() - self.start) / 1000 / 1000)
41+
42+
43+
class PerfExecutor:
44+
"""
45+
Performance executable
46+
"""
47+
48+
def __init__(self, executable):
49+
self.perf = executable
50+
51+
def run(self, count=1000, runs=10):
52+
inserts = self.__prepare_data(count)
53+
54+
# function => vector of runtimes in milliseconds
55+
from collections import defaultdict
56+
times = defaultdict(list)
57+
58+
progress_text = "Measuring performance"
59+
for i in range(runs):
60+
self.__progress_bar(progress_text, i, runs)
61+
62+
with Timer(times["insert-many"]):
63+
self.perf.put_many(inserts)
64+
65+
with Timer(times["read-all"]):
66+
items = self.perf.read_all()
67+
68+
with Timer(times["update-many"]):
69+
self.perf.put_many(items)
70+
71+
with Timer(times["remove-all"]):
72+
self.perf.remove_all()
73+
74+
self.__progress_bar(progress_text, runs, runs)
75+
76+
# print the results
77+
print()
78+
print('=' * 80)
79+
print("runs\t%d\t\tcount\t%d\t\tunit\tms" % (runs, count))
80+
print("Function\tMedian\tMean\tStdDev")
81+
import statistics
82+
for key in times.keys():
83+
print("%s\t%d\t%d\t%d" % (
84+
key,
85+
statistics.median(times[key]),
86+
statistics.mean(times[key]),
87+
statistics.stdev(times[key])
88+
))
89+
90+
@staticmethod
91+
def __prepare_data(count: int) -> [TestEntity]:
92+
result = []
93+
for i in range(count):
94+
object = TestEntity()
95+
object.str = "Entity no. %d" % i
96+
object.float = i * 1.1
97+
object.int = i
98+
result.append(object)
99+
return result
100+
101+
@staticmethod
102+
def __progress_bar(text, value, endvalue, bar_length=20):
103+
import sys
104+
percent = float(value) / endvalue
105+
arrow = '-' * int(round(percent * bar_length) - 1) + '>'
106+
spaces = ' ' * (bar_length - len(arrow))
107+
108+
sys.stdout.write("\r{0}: [{1}] {2}%".format(text, arrow + spaces, int(round(percent * 100))))
109+
sys.stdout.flush()
110+
111+
112+
if __name__ == "__main__":
113+
remove_test_dir()
114+
115+
obPerf = ObjectBoxPerf()
116+
executor = PerfExecutor(obPerf)
117+
executor.run(count=10000, runs=20)
118+
119+
remove_test_dir()

objectbox/box.py

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -105,31 +105,33 @@ def _put_many(self, objects) -> None:
105105
self._entity.set_object_id(objects[k], ids[k])
106106

107107
def get(self, id: int):
108-
c_data = ctypes.c_void_p()
109-
c_size = ctypes.c_size_t()
110-
obx_box_get(self._c_box, id, ctypes.byref(c_data), ctypes.byref(c_size))
108+
with self._ob.read_tx():
109+
c_data = ctypes.c_void_p()
110+
c_size = ctypes.c_size_t()
111+
obx_box_get(self._c_box, id, ctypes.byref(c_data), ctypes.byref(c_size))
111112

112-
data = c_voidp_as_bytes(c_data, c_size.value)
113-
return self._entity.unmarshal(data)
113+
data = c_voidp_as_bytes(c_data, c_size.value)
114+
return self._entity.unmarshal(data)
114115

115116
def get_all(self) -> list:
116-
# OBX_bytes_array*
117-
c_bytes_array_p = obx_box_get_all(self._c_box)
118-
119-
try:
120-
# OBX_bytes_array
121-
c_bytes_array = c_bytes_array_p.contents
122-
123-
result = list()
124-
for i in range(c_bytes_array.count):
125-
# OBX_bytes
126-
c_bytes = c_bytes_array.data[i]
127-
data = c_voidp_as_bytes(c_bytes.data, c_bytes.size)
128-
result.append(self._entity.unmarshal(data))
129-
130-
return result
131-
finally:
132-
obx_bytes_array_free(c_bytes_array_p)
117+
with self._ob.read_tx():
118+
# OBX_bytes_array*
119+
c_bytes_array_p = obx_box_get_all(self._c_box)
120+
121+
try:
122+
# OBX_bytes_array
123+
c_bytes_array = c_bytes_array_p.contents
124+
125+
result = list()
126+
for i in range(c_bytes_array.count):
127+
# OBX_bytes
128+
c_bytes = c_bytes_array.data[i]
129+
data = c_voidp_as_bytes(c_bytes.data, c_bytes.size)
130+
result.append(self._entity.unmarshal(data))
131+
132+
return result
133+
finally:
134+
obx_bytes_array_free(c_bytes_array_p)
133135

134136
def remove(self, id_or_object):
135137
if isinstance(id_or_object, self._entity.cls):

objectbox/builder.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,16 @@ def model(self, model: Model) -> 'Builder':
3333
return self
3434

3535
def build(self) -> 'ObjectBox':
36-
c_options = OBX_store_options()
37-
if len(self._directory) > 0:
38-
c_options.directory = c_str(self._directory)
36+
c_options = obx_opt()
3937

40-
c_store = obx_store_open(self._model._c_model, c_options.p())
38+
try:
39+
if len(self._directory) > 0:
40+
obx_opt_directory(c_options, c_str(self._directory))
41+
42+
obx_opt_model(c_options, self._model._c_model)
43+
except CoreException:
44+
obx_opt_free(c_options)
45+
raise
46+
47+
c_store = obx_store_open(c_options)
4148
return ObjectBox(c_store)

objectbox/c.py

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
import platform
1919
from objectbox.version import Version
2020

21-
2221
# This file contains C-API bindings based on the objectbox.h, linking to the 'objectbox' shared library
22+
required_version = "0.5.104"
2323

2424

2525
def shlib_name(library: str) -> str:
@@ -48,7 +48,6 @@ def shlib_name(library: str) -> str:
4848
# C-api (core library) version
4949
version_core = Version(__major.value, __minor.value, __patch.value)
5050

51-
required_version = "0.5.103"
5251
assert str(version_core) == required_version, \
5352
"Incorrect ObjectBox version loaded: %s instead of expected %s " % (str(version_core), required_version)
5453

@@ -82,15 +81,7 @@ class OBX_store(ctypes.Structure):
8281

8382

8483
class OBX_store_options(ctypes.Structure):
85-
_fields_ = [
86-
('directory', ctypes.c_char_p),
87-
('maxDbSizeInKByte', ctypes.c_uint64),
88-
('fileMode', ctypes.c_uint),
89-
('maxReaders', ctypes.c_uint)
90-
]
91-
92-
def p(self) -> 'ctypes.POINTER(OBX_store_options)':
93-
return ctypes.byref(self)
84+
pass
9485

9586

9687
OBX_store_options_p = ctypes.POINTER(OBX_store_options)
@@ -282,12 +273,48 @@ def c_voidp_as_bytes(voidp, size):
282273
obx_model_entity_last_property_id = fn('obx_model_entity_last_property_id', obx_err,
283274
[OBX_model_p, obx_schema_id, obx_uid])
284275

285-
# OBX_store* (OBX_model* model, const OBX_store_options* options);
286-
obx_store_open = fn('obx_store_open', OBX_store_p, [OBX_model_p, OBX_store_options_p])
276+
# OBX_store_options* ();
277+
obx_opt = fn('obx_opt', OBX_store_options_p, [])
278+
279+
# obx_err (OBX_store_options* opt, const char* dir);
280+
obx_opt_directory = fn('obx_opt_directory', obx_err, [OBX_store_options_p, ctypes.c_char_p])
281+
282+
# void (OBX_store_options* opt, size_t size_in_kb);
283+
obx_opt_max_db_size_in_kb = fn('obx_opt_max_db_size_in_kb', None, [OBX_store_options_p, ctypes.c_size_t])
284+
285+
# void (OBX_store_options* opt, int file_mode);
286+
obx_opt_file_mode = fn('obx_opt_file_mode', None, [OBX_store_options_p, ctypes.c_int])
287+
288+
# void (OBX_store_options* opt, int max_readers);
289+
obx_opt_max_readers = fn('obx_opt_max_readers', None, [OBX_store_options_p, ctypes.c_int])
290+
291+
# obx_err (OBX_store_options* opt, OBX_model* model);
292+
obx_opt_model = fn('obx_opt_model', obx_err, [OBX_store_options_p, OBX_model_p])
293+
294+
# void (OBX_store_options* opt);
295+
obx_opt_free = fn('obx_opt_free', None, [OBX_store_options_p])
296+
297+
# OBX_store* (const OBX_store_options* options);
298+
obx_store_open = fn('obx_store_open', OBX_store_p, [OBX_store_options_p])
287299

288300
# obx_err (OBX_store* store);
289301
obx_store_close = fn('obx_store_close', obx_err, [OBX_store_p])
290302

303+
# OBX_txn* (OBX_store* store);
304+
obx_txn_begin = fn('obx_txn_begin', OBX_txn_p, [OBX_store_p])
305+
306+
# OBX_txn* (OBX_store* store);
307+
obx_txn_begin_read = fn('obx_txn_begin_read', OBX_txn_p, [OBX_store_p])
308+
309+
# obx_err (OBX_txn* txn)
310+
obx_txn_close = fn('obx_txn_close', obx_err, [OBX_txn_p])
311+
312+
# obx_err (OBX_txn* txn);
313+
obx_txn_abort = fn('obx_txn_abort', obx_err, [OBX_txn_p])
314+
315+
# obx_err (OBX_txn* txn);
316+
obx_txn_commit = fn('obx_txn_commit', obx_err, [OBX_txn_p])
317+
291318
# OBX_box* (OBX_store* store, obx_schema_id entity_id);
292319
obx_box = fn('obx_box', OBX_box_p, [OBX_store_p, obx_schema_id])
293320

objectbox/objectbox.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,16 @@
1414

1515

1616
from objectbox.c import *
17-
17+
import objectbox.transaction
1818

1919
class ObjectBox:
2020
def __init__(self, c_store: OBX_store_p):
2121
self._c_store = c_store
2222

2323
def __del__(self):
2424
obx_store_close(self._c_store)
25+
26+
def read_tx(self):
27+
return objectbox.transaction.read(self)
28+
29+

objectbox/transaction.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Copyright 2019 ObjectBox Ltd. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from objectbox.c import *
16+
from contextlib import contextmanager
17+
18+
19+
@contextmanager
20+
def read(ob: 'ObjectBox'):
21+
tx = obx_txn_begin_read(ob._c_store)
22+
try:
23+
yield
24+
finally:
25+
obx_txn_close(tx)
26+
27+
28+
@contextmanager
29+
def write(ob: 'ObjectBox'):
30+
tx = obx_txn_begin(ob._c_store)
31+
successful = False
32+
try:
33+
yield
34+
successful = True
35+
finally:
36+
# this is better than bare `except:` because it doesn't catch stuff like KeyboardInterrupt
37+
if successful:
38+
obx_txn_commit(tx)
39+
else:
40+
obx_txn_abort(tx)
41+
42+
obx_txn_close(tx)

0 commit comments

Comments
 (0)