Skip to content

Commit

Permalink
Add simple benchmark (#73)
Browse files Browse the repository at this point in the history
* add simple benchmark

* reorganize bench into separate folder

* add benchmarking setup

* remove --benchmark-autosave

* black

* tune benchmarks to have lower stddev

* made the benchmark about 2-3x faster

* litn

* add another case to benchmark

* Fix linting

* Don't run the benchmark in pre-commit hook

---------

Co-authored-by: Berend Klein Haneveld <[email protected]>
  • Loading branch information
Korijn and berendkleinhaneveld authored Nov 8, 2023
1 parent c295c76 commit 95756af
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 19 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
__pycache__
.vscode
.coverage
dist
dist
.benchmarks
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ repos:
hooks:
- id: pytest
name: Tests
entry: poetry run pytest
entry: poetry run pytest tests
language: system
types: [python]
pass_filenames: false
28 changes: 28 additions & 0 deletions bench/profiling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from observ import reactive, watch


def noop():
pass


# @profile
def main():
obj = reactive({})

watch(obj, callback=noop, deep=True, sync=True)

obj["bar"] = "baz"
obj["quux"] = "quuz"
obj.update(
{
"bar": "foo",
"quazi": "var",
}
)
del obj["bar"]
_ = obj["quux"]
obj.clear()


if __name__ == "__main__":
main()
84 changes: 84 additions & 0 deletions bench/test_bench.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from functools import partial

import pytest

from observ import reactive, watch


def noop():
pass


N = 10000


def bench_dict(plain, add_watcher):
for _ in range(N):
obj = {} if plain else reactive({})
if add_watcher:
watcher = watch(obj, callback=noop, deep=True, sync=True) # noqa: F841
obj["bar"] = "baz"
obj["quux"] = "quuz"
obj.update(
{
"bar": "foo",
"quazi": "var",
}
)
del obj["bar"]
_ = obj["quux"] # read something
obj.clear()


@pytest.mark.timeout(timeout=0)
@pytest.mark.benchmark(
group="dict_plain_vs_reactive",
)
@pytest.mark.parametrize("name", ["plain", "reactive", "reactive+watcher"])
def test_dict_plain_vs_reactive(benchmark, name):
bench_fn = partial(bench_dict, name == "plain", name.endswith("+watcher"))
benchmark(bench_fn)


def bench_list(plain, add_watcher):
for _ in range(N):
obj = [] if plain else reactive([])
if add_watcher:
watcher = watch(obj, callback=noop, deep=True, sync=True) # noqa: F841
obj.append("bar")
obj.extend(["quux", "quuz"])
obj[1] = "foo"
obj.pop(0)
_ = obj[0] # read something
obj.clear()


@pytest.mark.timeout(timeout=0)
@pytest.mark.benchmark(
group="list_plain_vs_reactive",
)
@pytest.mark.parametrize("name", ["plain", "reactive", "reactive+watcher"])
def test_list_plain_vs_reactive(benchmark, name):
bench_fn = partial(bench_list, name == "plain", name.endswith("+watcher"))
benchmark(bench_fn)


def bench_set(plain, add_watcher):
for _ in range(N):
obj = set() if plain else reactive(set())
if add_watcher:
watcher = watch(obj, callback=noop, deep=True, sync=True) # noqa: F841
obj.add("bar")
obj.update({"quux", "quuz"})
_ = list(iter(obj)) # read something
obj.clear()


@pytest.mark.timeout(timeout=0)
@pytest.mark.benchmark(
group="set_plain_vs_reactive",
)
@pytest.mark.parametrize("name", ["plain", "reactive", "reactive+watcher"])
def test_set_plain_vs_reactive(benchmark, name):
bench_fn = partial(bench_set, name == "plain", name.endswith("+watcher"))
benchmark(bench_fn)
17 changes: 13 additions & 4 deletions observ/dep.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,27 @@ class Dep:
stack: List["Watcher"] = [] # noqa: F821

def __init__(self) -> None:
self._subs: WeakSet["Watcher"] = WeakSet() # noqa: F821
self._subs: WeakSet["Watcher"] = None # noqa: F821

def add_sub(self, sub: "Watcher") -> None: # noqa: F821
if self._subs is None:
self._subs = WeakSet()
self._subs.add(sub)

def remove_sub(self, sub: "Watcher") -> None: # noqa: F821
self._subs.remove(sub)
if self._subs:
self._subs.remove(sub)

def depend(self) -> None:
if self.stack:
self.stack[-1].add_dep(self)

def notify(self) -> None:
for sub in sorted(self._subs, key=lambda s: s.id):
sub.update()
# just iterating over self._subs even if
# it is empty is 10x slower
# than putting this if-statement in front of it
# because a weakset must acquire a lock on its
# weak references before iterating
if self._subs:
for sub in sorted(self._subs, key=lambda s: s.id):
sub.update()
5 changes: 3 additions & 2 deletions observ/proxy_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,10 @@ def get_proxy(self, target, readonly=False, shallow=False):
Returns a proxy from the collection for the given object and configuration.
Will return None if there is no proxy for the object's id.
"""
if id(target) not in self.db:
try:
return self.db[id(target)]["proxies"].get((readonly, shallow))
except KeyError:
return None
return self.db[id(target)]["proxies"].get((readonly, shallow))


# Create a global proxy collection
Expand Down
49 changes: 41 additions & 8 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.9"
patchdiff = "~0.3.4"
pytest-benchmark = "^4.0.0"

[tool.poetry.group.dev.dependencies]
black = "*"
Expand All @@ -30,3 +31,7 @@ urllib3 = { version = "*", python = "<4"}
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

[tool.pytest.ini_options]
addopts = "--benchmark-columns='mean, stddev, rounds'"
timeout = 3
4 changes: 1 addition & 3 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,5 @@ application-import-names = observ
import-order-style = google
per-file-ignores =
observ/__init__.py:F401,F403
bench/*:F821
exclude = .venv

[tool:pytest]
timeout = 3

0 comments on commit 95756af

Please sign in to comment.