commit 3299cdb246d4f67699927b65b2387b037c53f09c Author: gmega Date: Wed Oct 30 20:22:09 2024 -0300 initial scaffolding diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b32d8aa --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pyc +.idea \ No newline at end of file diff --git a/benchmarks/__init__.py b/benchmarks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/benchmarks/core/__init__.py b/benchmarks/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/benchmarks/core/network.py b/benchmarks/core/network.py new file mode 100644 index 0000000..b19a7e4 --- /dev/null +++ b/benchmarks/core/network.py @@ -0,0 +1,40 @@ +from abc import abstractmethod, ABC +from pathlib import Path + +from typing_extensions import Generic, TypeVar, List, Optional + +TNode = TypeVar('TNode', bound='Node') +TFileHandle = TypeVar('TFileHandle') + + +class Node(ABC, Generic[TFileHandle]): + """A :class:`Node` represents a peer within a :class:`FileSharingNetwork`.""" + + @abstractmethod + def seed( + self, + file: Path, + handle: Optional[TFileHandle] + ) -> TFileHandle: + """ + Makes the current :class:`Node` a seeder for the specified file. + + :param file: path to the file to seed. + :param handle: an existing network handle to this file. If none is provided, a new one + will be generated. + """ + pass + + def leech(self, handle: TFileHandle): + """Makes the current node a leecher for the provided handle.""" + pass + + +class FileSharingNetwork(Generic[TNode], ABC): + """A :class:`FileSharingNetwork` is a set of :class:`Node`s that share + an interest in a given file.""" + + @property + @abstractmethod + def nodes(self) -> List[TNode]: + pass diff --git a/benchmarks/core/utils.py b/benchmarks/core/utils.py new file mode 100644 index 0000000..056e60b --- /dev/null +++ b/benchmarks/core/utils.py @@ -0,0 +1,19 @@ +import random +from pathlib import Path +from typing import Callable, Iterator + +# A Sampler samples without replacement from [0, ..., n]. +Sampler = Callable[[int], Iterator[int]] + +# A DataGenerator generates files for experiments. +DataGenerator = Callable[[], Path] + + +def sample(n: int) -> Iterator[int]: + """Samples without replacement using a Fisher-Yates shuffle.""" + p = list(range(0, n)) + for i in range(n - 1): + j = i + random.randint(0, n - i) + tmp = p[j] + p[j], p[j + 1] = p[j + 1], tmp + yield p[i] diff --git a/benchmarks/experiments/__init__.py b/benchmarks/experiments/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/benchmarks/experiments/static_experiment.py b/benchmarks/experiments/static_experiment.py new file mode 100644 index 0000000..4cadd47 --- /dev/null +++ b/benchmarks/experiments/static_experiment.py @@ -0,0 +1,35 @@ +from typing_extensions import Generic + +from benchmarks.core.network import FileSharingNetwork, TNode +from benchmarks.core.utils import Sampler, DataGenerator + + +class StaticDisseminationExperiment(Generic[TNode]): + def __init__( + self, + network: FileSharingNetwork[TNode], + seeders: int, + sampler: Sampler, + generator: DataGenerator + ): + self.network = network + self.sampler = sampler + self.generate_data = generator + self.seeders = seeders + + def run(self): + sample = self.sampler(len(self.network.nodes)) + seeder_idx = [next(sample) for _ in range(0, self.seeders)] + seeders, leechers = ( + [self.network.nodes[i] for i in seeder_idx], + [self.network.nodes[i] for i in range(0, len(self.network.nodes)) if i not in seeder_idx] + ) + + data = self.generate_data() + handle = None + + for node in seeders: + handle = node.seed(data, handle) + + for node in leechers: + node.leech(handle) diff --git a/benchmarks/experiments/tests/__init__.py b/benchmarks/experiments/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/benchmarks/experiments/tests/test_static_experiment.py b/benchmarks/experiments/tests/test_static_experiment.py new file mode 100644 index 0000000..b609902 --- /dev/null +++ b/benchmarks/experiments/tests/test_static_experiment.py @@ -0,0 +1,87 @@ +from dataclasses import dataclass +from pathlib import Path +from typing import Optional, List + +from benchmarks.core.network import FileSharingNetwork, TFileHandle, TNode, Node +from benchmarks.core.utils import Sampler +from benchmarks.experiments.static_experiment import StaticDisseminationExperiment + + +@dataclass +class MockHandle: + path: Path + + +def mock_sampler(elements: List[int]) -> Sampler: + return lambda _: iter(elements) + + +class MockNode(Node[MockHandle]): + + def __init__(self): + self.seeding: Optional[MockNode] = None + self.leeching: Optional[MockHandle] = None + + def seed(self, path: Path, handle: Optional[MockHandle] = None) -> MockHandle: + self.seeding = path + return MockHandle(path) + + def leech(self, handle: MockHandle): + self.leeching = handle + + +class MockFileSharingNetwork(FileSharingNetwork[MockNode]): + + def __init__(self, n: int): + self._nodes = [MockNode() for _ in range(n)] + + @property + def nodes(self) -> List[TNode]: + return self._nodes + + +def test_should_place_seeders(): + network = MockFileSharingNetwork(n=13) + file = Path('/path/to/data') + seeder_indexes = [9, 6, 3] + + experiment = StaticDisseminationExperiment( + seeders=3, + sampler=mock_sampler(seeder_indexes), + network=network, + generator=lambda: Path('/path/to/data'), + ) + + experiment.run() + + actual_seeders = set() + for index, node in enumerate(network.nodes): + if node.seeding is not None: + actual_seeders.add(index) + assert node.seeding == file + + assert actual_seeders == set(seeder_indexes) + + +def test_should_place_leechers(): + network = MockFileSharingNetwork(n=13) + file = Path('/path/to/data') + seeder_indexes = [9, 6, 3] + + experiment = StaticDisseminationExperiment( + seeders=3, + sampler=mock_sampler(seeder_indexes), + network=network, + generator=lambda: Path('/path/to/data'), + ) + + experiment.run() + + actual_leechers = set() + for index, node in enumerate(network.nodes): + if node.leeching is not None: + assert node.leeching.path == file + assert node.seeding is None + actual_leechers.add(index) + + assert actual_leechers == set(range(13)) - set(seeder_indexes) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..2458fe6 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,159 @@ +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "deluge-client" +version = "1.10.2" +description = "Simple Deluge Client" +optional = false +python-versions = "*" +files = [ + {file = "deluge-client-1.10.2.tar.gz", hash = "sha256:3881aee3c4e0ca9dd8a56b710047b837e1d087a83e421636a074771f92a9f1b5"}, + {file = "deluge_client-1.10.2-py3-none-any.whl", hash = "sha256:ea052663c0ed3a2247517d316c3784d70c83279dd9e088623fd06b164768f4f1"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "mypy" +version = "1.13.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, + {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, + {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, + {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, + {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, + {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, + {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, + {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, + {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, + {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, + {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, + {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, + {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, + {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, + {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, + {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, + {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, + {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, + {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, + {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, + {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +typing-extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pytest" +version = "8.3.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.12" +content-hash = "4c657d5b89f926722ec65f35124c9ac0e8138a4264be1860490ab5eaa8a1eb44" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..37da87d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[tool.poetry] +name = "bittorrent-benchmarks" +version = "0.1.0" +description = "Harness for benchmarking Codex against BitTorrent." +authors = ["Your Name "] +license = "MIT" +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.12" +deluge-client = "^1.10.2" + + +[tool.poetry.group.test.dependencies] +pytest = "^8.3.3" +mypy = "^1.13.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api"