From a728045cc3873bc4edd2fd35f9ef6fa4ff694991 Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Sat, 17 Oct 2026 17:41:38 +0200 Subject: [PATCH 1/6] Update Python dependencies --- .python-version | 1 + uv.lock | 145 +++++++++++++++++++++++++++++++++++------------- 2 files changed, 108 insertions(+), 38 deletions(-) create mode 100644 .python-version diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..6324d40 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/uv.lock b/uv.lock index 585a860..ee6d953 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,10 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.10, <4.0" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version < '3.11'", +] [[package]] name = "cfgv" @@ -13,38 +17,53 @@ wheels = [ [[package]] name = "distlib" -version = "0.4.3" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/d36966de7304047a1001a7559fbf266c464bb5d8cc85464536ef8c9bcc32/distlib-0.4.4.tar.gz", hash = "sha256:ca4175b4ac7f71223cc30ed7248c45d2b38c2439d8ee6e498ab67ed63cf3508d", size = 621304, upload-time = "2026-10-16T13:56:34.203Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/2b/238f02a4927fa2e27aa995371d240a468b52e42eae44a678434aff736b38/distlib-0.4.4-py2.py3-none-any.whl", hash = "sha256:cf09d55b1e85a89576b036aeb71550bfd68320f3f21761ca329eadadad918638", size = 472099, upload-time = "2026-10-16T13:56:32.053Z" }, +] + +[[package]] +name = "filelock" +version = "4.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/58/6fd434bec86eff7c38a3168454cb132b762b2bea9b3ac094101a2f7bc32a/filelock-4.1.0.tar.gz", hash = "sha256:ad7f724afef953e731b1cc39bcd3a09166d72ed7fcdf29e6e88b1c3235c6715d", size = 561277, upload-time = "2026-10-09T19:57:20.34Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/ee/86/032133892a5de43b5a98200b01aadcad68cc255e274a762f08b8a76d2912/filelock-4.1.0-py3-none-any.whl", hash = "sha256:2ce9818e3e2d8f284c1a964414447ef148d42a5fd5e2a477a7118e574b293ec1", size = 133003, upload-time = "2026-10-09T19:57:18.716Z" }, ] [[package]] name = "filelock" -version = "3.29.4" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e6/dc/be6cbe99670cd6e4ad387123647cb08e0c32975e223f82551e914c5568a6/filelock-3.29.4.tar.gz", hash = "sha256:10cdb3656fc44541cdf30652a93fb10ec6b05325620eb316bd26893e4201538a", size = 63028, upload-time = "2026-06-13T16:12:00.744Z" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/a9/1af41b37c3279712b22cdc63aac78a52432202b6fe1f9666a2a3d2831fb4/filelock-4.2.0.tar.gz", hash = "sha256:7a60906c75227cf04d0c273afadc8219400f11aeb13cc69591d4f6cdc6c8036e", size = 569667, upload-time = "2026-10-14T20:57:13.11Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl", hash = "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767", size = 42757, upload-time = "2026-06-13T16:11:59.582Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a3/9bc26acff301fe1aaea1cc3d82a1d57e0a34df3e1cadbfa91ac2dbcdde5c/filelock-4.2.0-py3-none-any.whl", hash = "sha256:2ff5690882e8cdb00ef31fb3d01a3094c29f30985426c59495afb1733f3b7238", size = 135032, upload-time = "2026-10-14T20:57:11.349Z" }, ] [[package]] name = "identify" -version = "2.6.19" +version = "2.6.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/35/d70c0006c7cee65999ea94a6273e60b2094f600a3d8b71b04318253fc643/identify-2.6.20.tar.gz", hash = "sha256:ad729860a923858d26917c2f4fb0a1d83d27a75b1e090c06440c573f048f3285", size = 99681, upload-time = "2026-09-26T20:29:07.187Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, + { url = "https://files.pythonhosted.org/packages/fa/70/fffc9613501877c0a10a1ef73a165e6ded3e53f6e7502227fef56d973933/identify-2.6.20-py2.py3-none-any.whl", hash = "sha256:6a16b69b93187244e0548cbfd25b3e4a6f9a7a2ad784625c3bec2b8d27b81aaa", size = 99506, upload-time = "2026-09-26T20:29:06.054Z" }, ] [[package]] name = "nodeenv" -version = "1.10.0" +version = "1.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/105de02c1322cfada6d9710d9146ef8026419d433c9d08359a2d35805811/nodeenv-1.11.0.tar.gz", hash = "sha256:3ce8fe5b71d16e8af7039ca65257354100bc772965d6bc549070649e53b1b146", size = 99301, upload-time = "2026-09-26T11:29:21.367Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, + { url = "https://files.pythonhosted.org/packages/54/c8/12811c9b48fde162bb72b6f2e78fada9a247a09d7bb5be2050a5d099c77b/nodeenv-1.11.0-py2.py3-none-any.whl", hash = "sha256:edaa16e6c14d7cf395d75d4bbd5a26390f4dc06501a33b4e76282b02cc688a25", size = 34277, upload-time = "2026-09-26T11:29:19.933Z" }, ] [[package]] @@ -52,29 +71,76 @@ name = "ordered" version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "pre-commit" }, + { name = "pre-commit", version = "4.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "pre-commit", version = "4.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "python-dotenv" }, ] [package.metadata] requires-dist = [ - { name = "pre-commit", specifier = ">=4.2.0,<5.0.0" }, - { name = "python-dotenv", specifier = ">=1.1.0,<2.0.0" }, + { name = "pre-commit", specifier = ">=4.2.0" }, + { name = "python-dotenv", specifier = ">=1.1.0" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, ] [[package]] name = "platformdirs" -version = "4.10.0" +version = "4.12.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/90/a1/d5f9002a70298c64a789779077d8dd90c10aa1f47fe40c86802df874f2a6/platformdirs-4.12.4.tar.gz", hash = "sha256:63743c02414e755de4e31b8f68125c1407495b86c5a006e203c01ff8b9924250", size = 60010, upload-time = "2026-10-07T23:30:32.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ba/223e00b885e960edd5d4b4d178c88acce019bd5b83786093681b8c393492/platformdirs-4.12.4-py3-none-any.whl", hash = "sha256:78bfb9db2a8471ed7eebe3c3c932da413911042994e699b384fbb4493fa872d7", size = 32596, upload-time = "2026-10-07T23:30:30.825Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.13.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/da/91/4a03cfdb03314cfca262921797fff04ff4054bd86a160aed76eddf8aa12b/platformdirs-4.13.3.tar.gz", hash = "sha256:5e567f664eb087ab8521c0179cd8d1bd60857d271136567a39719e28e2d383ce", size = 61559, upload-time = "2026-10-16T01:16:16.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/b8/fd1af06b079af236f5423f7c1821264419cc8f6b4803f79353acbb8bfa53/platformdirs-4.13.3-py3-none-any.whl", hash = "sha256:f6ad7f447f24f8a3b82cce5976387428bff894a0eca6c3488f4a17f153c130c4", size = 32762, upload-time = "2026-10-16T01:16:15.051Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.6.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/89/1f3e8e1fc3e97de0fa963495832f581f025f29471602a309e48808244292/pre_commit-4.6.2.tar.gz", hash = "sha256:8f5d7bfb021ecdbcd9d49d89847082dd24172ccde534390081a679ad046e2441", size = 198670, upload-time = "2026-08-10T22:07:18.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/e2/bbb7129c9e7999a6b8ee9cca3b66486c25c423ab5a75f34071798b74ce94/pre_commit-4.6.2-py2.py3-none-any.whl", hash = "sha256:e2dde9a75d3bce11bd3831c26d134df00a2803c1d818be6a0383c3dcda25dc4e", size = 226202, upload-time = "2026-08-10T22:07:16.942Z" }, ] [[package]] name = "pre-commit" -version = "4.6.0" +version = "4.7.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] dependencies = [ { name = "cfgv" }, { name = "identify" }, @@ -82,31 +148,31 @@ dependencies = [ { name = "pyyaml" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/c3/6965c2690ddce885ec75121572d065a5b94c5a5d8d3e657927ecda9fd715/pre_commit-4.7.0.tar.gz", hash = "sha256:1f3deee12b914c8fa184bd2b5953391c2e4f36763e403d4a23dc9983fa7c6e86", size = 198120, upload-time = "2026-10-12T20:45:40.32Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, + { url = "https://files.pythonhosted.org/packages/eb/83/ce0a196e4f7b20b11ce3aa1fc72fb973f64b7628d51c1b8aebad37fb7f81/pre_commit-4.7.0-py2.py3-none-any.whl", hash = "sha256:2e229038ad3656081b70c27913157e4b636ef306f643f024a8a8e3f0f7b2f877", size = 226261, upload-time = "2026-10-12T20:45:38.858Z" }, ] [[package]] name = "python-discovery" -version = "1.4.2" +version = "1.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "filelock" }, - { name = "platformdirs" }, + { name = "filelock", version = "4.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "filelock", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0b/1a/cbbaf13b730abb0a16b964d984e19f2fe520c21a4dc664051359a3f5a9e7/python_discovery-1.4.2.tar.gz", hash = "sha256:8f3746c4b4968d22afbb97d36e1a0e5b66e6c0f297290f2e95f05b9b8bf18690", size = 70277, upload-time = "2026-06-11T16:10:42.383Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/54/fad125f5c48f7821644ee6f223ebfcedc4e23597c1a16eeb13d33aa4a91a/python_discovery-1.6.3.tar.gz", hash = "sha256:a62b301d96cf5489cb96ab023b32e068421867fdd5df2e42eece877869047fa2", size = 88998, upload-time = "2026-10-16T03:54:29.507Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/82/a70006589557f267f15bd384c0642ad49f0d97b690c3a05b166b9dcbad3b/python_discovery-1.4.2-py3-none-any.whl", hash = "sha256:475803f53b7b2ed6e490e27373f9d8340f7d2eebf9acdaf645d7d714c97bb500", size = 33886, upload-time = "2026-06-11T16:10:41.192Z" }, + { url = "https://files.pythonhosted.org/packages/de/ec/643826e4d69b7bdce4213f221e8f5ed06d0a9dd69fa9b33342578d2cc8c8/python_discovery-1.6.3-py3-none-any.whl", hash = "sha256:cb7654125e3dcb594269a6feb56ac693f1f13b7c654493a93eba72dc22d18034", size = 39386, upload-time = "2026-10-16T03:54:28.237Z" }, ] [[package]] name = "python-dotenv" -version = "1.2.2" +version = "1.2.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/26/2fbeedb218a787a5eea551c7532cac4e009f83d689dd2faa0d0353473f86/python_dotenv-1.2.4.tar.gz", hash = "sha256:f0d53e69935a851c0dcc78f3ab7aaccd8cabef0b92382b576b824212902873c0", size = 60824, upload-time = "2026-10-01T05:36:10Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, + { url = "https://files.pythonhosted.org/packages/60/d1/38f3a3405989a89ac18390803e70c6ad7c7760da4f9b83cbeca0c44a0c72/python_dotenv-1.2.4-py3-none-any.whl", hash = "sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc", size = 23266, upload-time = "2026-10-01T05:36:08.633Z" }, ] [[package]] @@ -175,25 +241,28 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[package]] name = "virtualenv" -version = "21.5.1" +version = "21.14.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, - { name = "filelock" }, - { name = "platformdirs" }, + { name = "filelock", version = "4.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "filelock", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "platformdirs", version = "4.12.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "platformdirs", version = "4.13.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "python-discovery" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/a5/81f987504738e6defeed61ec1c47e2aefab3c35d8eeb87e1b3f38cf28254/virtualenv-21.5.1.tar.gz", hash = "sha256:dca3bf98275a59c652b69d68e73433e597d977c2da9198882479d1a7188009c8", size = 4578798, upload-time = "2026-06-16T16:23:58.603Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/6a/fbdeea5c07d5674139639d42a97a41d26e9b9b8cb2a1265fa936f655ee20/virtualenv-21.14.8.tar.gz", hash = "sha256:394e45ed610ceffb5018e1a179e3a8012c02fd87fe792ab4aae67ce5e7e3afbd", size = 5474561, upload-time = "2026-10-16T00:09:09.672Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl", hash = "sha256:55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783", size = 4558820, upload-time = "2026-06-16T16:23:56.963Z" }, + { url = "https://files.pythonhosted.org/packages/14/01/cd63b97dd556293e1c74d76053936d9cef11a28eac605c735ae9f0ee324f/virtualenv-21.14.8-py3-none-any.whl", hash = "sha256:d2a347fdbc1a65e33c2c5e84dc767b4a3538a25749536a752744d4fe1f71c37a", size = 5487606, upload-time = "2026-10-16T00:09:07.441Z" }, ] From 7489265f949d8ad862529835a5f64e435822d9d4 Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Sat, 17 Oct 2026 17:41:55 +0200 Subject: [PATCH 2/6] WIP --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index aa85766..519269c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "ordered" version = "0.1.0" description = "The Python environment for the Ordered project" -requires-python = ">=3.10,<4.0" +requires-python = ">=3.14,<4.0" dependencies = [ "python-dotenv>=1.1.0", "pre-commit>=4.2.0" From 23e3f07a21e30657867329dc63a2914291c5d523 Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Sat, 17 Oct 2026 18:13:47 +0200 Subject: [PATCH 3/6] WIP --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5c915d8..a76f5ce 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ [![Benchmarks](https://img.shields.io/github/actions/workflow/status/CogitatorTech/ordered/benches.yml?label=benchmarks&style=flat&labelColor=282c34&logo=github)](https://github.com/CogitatorTech/ordered/actions/workflows/benches.yml) [![Zig](https://img.shields.io/badge/zig-0.17.0-F7A41D?style=flat&labelColor=282c34&logo=zig)](https://ziglang.org/download/) [![Release](https://img.shields.io/github/release/CogitatorTech/ordered.svg?label=release&style=flat&labelColor=282c34&logo=github)](https://github.com/CogitatorTech/ordered/releases/latest) -[![Docs](https://img.shields.io/badge/docs-read-blue?style=flat&labelColor=282c34&logo=read-the-docs)](https://CogitatorTech.github.io/ordered/) -[![Examples](https://img.shields.io/badge/examples-view-green?style=flat&labelColor=282c34&logo=zig)](https://github.com/CogitatorTech/ordered/tree/main/examples) +[![Docs](https://img.shields.io/badge/docs-read-507ec6?style=flat&labelColor=282c34&logo=read-the-docs)](https://CogitatorTech.github.io/ordered/) +[![Examples](https://img.shields.io/badge/examples-view-507ec6?style=flat&labelColor=282c34&logo=zig)](https://github.com/CogitatorTech/ordered/tree/main/examples) [![License](https://img.shields.io/badge/license-MIT-007ec6?label=license&style=flat&labelColor=282c34&logo=open-source-initiative)](https://github.com/CogitatorTech/ordered/blob/main/LICENSE) A sorted collection library for Zig From afa33e3d99467dc46ba69e93dd8d35230316d2e8 Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Sun, 25 Oct 2026 14:15:48 +0100 Subject: [PATCH 4/6] Add a simple benchmark between `std.{Treap,AutoHashMap}` and `ordered` data structures --- benches/README.md | 47 +++++++- benches/b7_std_comparison.zig | 206 ++++++++++++++++++++++++++++++++++ build.zig | 7 +- uv.lock | 142 +++-------------------- 4 files changed, 274 insertions(+), 128 deletions(-) create mode 100644 benches/b7_std_comparison.zig diff --git a/benches/README.md b/benches/README.md index 616cf5f..b26d9ec 100644 --- a/benches/README.md +++ b/benches/README.md @@ -10,6 +10,7 @@ | 4 | [b4_skip_list_map.zig](b4_skip_list_map.zig) | Benchmarks for Skip list map implementation | | 5 | [b5_trie_map.zig](b5_trie_map.zig) | Benchmarks for Trie map implementation | | 6 | [b6_cartesian_tree_map.zig](b6_cartesian_tree_map.zig) | Benchmarks for Cartesian tree map implementation | +| 7 | [b7_std_comparison.zig](b7_std_comparison.zig) | Integer maps compared with Zig standard-library containers | #### Running Benchmarks @@ -26,14 +27,56 @@ zig build bench-b1_btree_map ``` > [!NOTE] -> Each benchmark measures three core operations across multiple data sizes: +> The individual container benchmarks measure core operations across multiple data sizes: > 1. **Insert and Put**: measures the time to insert elements sequentially into an empty data structure > 2. **Lookup**: measures the time to search for all elements in a pre-populated structure > 3. **Delete**: measures the time to remove all elements from a pre-populated structure > > **Test Sizes**: benchmarks run with 1,000, 10,000, 100,000, and 1,000,000 elements to show performance scaling. > -> **Timing Method**: uses `std.time.Timer` for high-precision nanosecond-level timing. Each operation is timed in bulk, +> **Timing Method**: uses the local timer backed by `std.Io.Timestamp` on the monotonic `.real` clock. Each operation is timed in bulk, > then divided by the number of operations to get per-operation timing. > > **Compilation**: benchmarks are compiled with `ReleaseFast` optimization (see [build.zig](../build.zig)). + +#### Standard-Library Comparison + +```sh +make bench BENCHMARK=b7_std_comparison +``` + +This benchmark compares `BTreeMap` (branching factor 16), `CartesianTreeMap`, and `SkipListMap` (maximum level 16) +with `std.Treap` and `std.AutoHashMap`. The hash map does not provide sorted iteration, so its results are a baseline +for workloads that do not need ordering. The intrusive `std.Treap` has a local adapter that allocates a node and +stores a value for each key, including allocation and freeing in the measured insertion and removal times. + +All containers use `u32` keys and values, `std.heap.smp_allocator`, and ReleaseFast optimization for both the runner +and the imported library. Each case reports the median of five runs after one warmup, at 1,000, 10,000, and 100,000 +elements. Keys are unique integers from zero to the size minus one. Insertion, lookup, and removal use either +ascending keys or the same shuffled permutation, generated with a fixed seed. Container priorities and skip-list +levels use their normal initialization and can differ between runs. + +Successful lookups and iteration consume the stored values and check their sums after timing. Unsuccessful lookups +use keys above the maximum stored key, which favors trees that quickly reject that range. Removal counts and final +container sizes are also checked. Duplicate updates, missing removals, empty iteration, and cleanup are checked with +the debug allocator before timing. Iteration includes iterator creation and cleanup; container initialization and +final cleanup are excluded from the operation timings. CSV rows are written to standard error. + +##### Sample Results + +The following results were measured on Linux x86_64 with an AMD Ryzen 9 9950X and Zig 0.17.0 on October 25, 2026, +using the native target. Values are nanoseconds per operation for 100,000 shuffled keys, except iteration, which is +nanoseconds per item. + +| Container | Insert | Lookup Hit | Lookup Miss | Iteration | Remove | +|-----------|-------:|-----------:|------------:|----------:|-------:| +| `BTreeMap` | 66.63 | 105.15 | 29.90 | 2.11 | 108.39 | +| `CartesianTreeMap` | 191.65 | 122.15 | 7.08 | 8.11 | 123.50 | +| `SkipListMap` | 172.16 | 177.46 | 10.74 | 16.48 | 168.18 | +| `std.Treap` | 117.01 | 80.66 | 12.44 | 14.25 | 99.12 | +| `std.AutoHashMap` (unordered) | 20.28 | 8.06 | 13.56 | 2.01 | 6.86 | + +These are local measurements, with no CPU pinning or frequency control. The phases run consecutively on the same +container, so lookup and iteration measure a populated container with recently accessed data. This suite does not +measure memory usage, string keys, interleaved mutations, or prefix and range queries. It adds no external +dependencies; C and third-party Zig libraries are not included. diff --git a/benches/b7_std_comparison.zig b/benches/b7_std_comparison.zig new file mode 100644 index 0000000..50502e0 --- /dev/null +++ b/benches/b7_std_comparison.zig @@ -0,0 +1,206 @@ +const std = @import("std"); +const ordered = @import("ordered"); +const Timer = @import("util/timer.zig").Timer; + +const BTree = ordered.BTreeMap(u32, u32, compare, 16); +const CartesianTree = ordered.CartesianTreeMap(u32, u32, compare); +const SkipList = ordered.SkipListMap(u32, u32, compare, 16); +const HashMap = std.AutoHashMap(u32, u32); +const MAPS = .{ BTree, CartesianTree, SkipList, TreapMap, HashMap }; +const NAMES = .{ "BTreeMap", "CartesianTreeMap", "SkipListMap", "std.Treap", "std.AutoHashMap (unordered)" }; +const REPEATS = 5; + +fn compare(lhs: u32, rhs: u32) std.math.Order { + return std.math.order(lhs, rhs); +} + +// std.Treap is intrusive, so include node allocation and value storage in its cost. +const TreapMap = struct { + const Tree = std.Treap(u32, std.math.order); + const Node = struct { tree_node: Tree.Node, value: u32 }; + + allocator: std.mem.Allocator, + tree: Tree = .{}, + len: usize = 0, + + fn init(allocator: std.mem.Allocator) TreapMap { + return .{ .allocator = allocator }; + } + + fn deinit(self: *TreapMap) void { + while (self.tree.getMin()) |node| _ = self.remove(node.key); + } + + fn count(self: *const TreapMap) usize { + return self.len; + } + + fn put(self: *TreapMap, key: u32, value: u32) !void { + var entry = self.tree.getEntryFor(key); + if (entry.node) |node| { + const parent: *Node = @fieldParentPtr("tree_node", node); + parent.value = value; + } else { + const node = try self.allocator.create(Node); + node.value = value; + entry.set(&node.tree_node); + self.len += 1; + } + } + + fn get(self: *TreapMap, key: u32) ?u32 { + const node = self.tree.getEntryFor(key).node orelse return null; + const parent: *Node = @fieldParentPtr("tree_node", node); + return parent.value; + } + + fn remove(self: *TreapMap, key: u32) ?u32 { + var entry = self.tree.getEntryFor(key); + const node = entry.node orelse return null; + const parent: *Node = @fieldParentPtr("tree_node", node); + const value = parent.value; + entry.set(null); + self.allocator.destroy(parent); + self.len -= 1; + return value; + } +}; + +fn initMap(comptime Map: type, allocator: std.mem.Allocator) !Map { + if (Map == SkipList) return try Map.init(allocator); + return Map.init(allocator); +} + +fn getValue(comptime Map: type, map: *Map, key: u32) ?u32 { + if (Map == BTree or Map == SkipList) { + return if (map.get(key)) |value| value.* else null; + } + return map.get(key); +} + +fn iterationSum(comptime Map: type, map: *Map, allocator: std.mem.Allocator) !u64 { + var sum: u64 = 0; + if (Map == TreapMap) { + var it = map.tree.inorderIterator(); + while (it.next()) |node| { + const parent: *TreapMap.Node = @fieldParentPtr("tree_node", node); + sum += parent.value; + } + } else if (Map == HashMap) { + var it = map.iterator(); + while (it.next()) |entry| sum += entry.value_ptr.*; + } else if (Map == SkipList) { + var it = map.iterator(); + while (it.next()) |entry| sum += entry.value; + } else { + var it = if (Map == CartesianTree) try map.iterator(allocator) else try map.iterator(); + defer it.deinit(); + while (try it.next()) |entry| sum += entry.value; + } + return sum; +} + +fn check(ok: bool) !void { + if (!ok) return error.BenchmarkMismatch; +} + +fn selfCheck(comptime Map: type, allocator: std.mem.Allocator) !void { + var map = try initMap(Map, allocator); + defer map.deinit(); + try check(map.count() == 0 and getValue(Map, &map, 7) == null); + try map.put(7, 10); + try map.put(7, 20); + try check(map.count() == 1 and getValue(Map, &map, 7) == 20); + try check(try iterationSum(Map, &map, allocator) == 20); + if (Map == HashMap) { + try check(map.remove(7)); + try check(!map.remove(7)); + } else { + try check(map.remove(7) == 20); + try check(map.remove(7) == null); + } + try check(map.count() == 0); + try check(try iterationSum(Map, &map, allocator) == 0); + // Leave a live node to exercise cleanup as well as removal. + try map.put(9, 30); +} + +fn measure(comptime Map: type, allocator: std.mem.Allocator, keys: []const u32) ![5]u64 { + var map = try initMap(Map, allocator); + defer map.deinit(); + var times: [5]u64 = undefined; + var timer = try Timer.start(); + for (keys) |key| try map.put(key, key + 1); + times[0] = timer.read(); + try check(map.count() == keys.len); + + const expected = @as(u64, keys.len) * (keys.len + 1) / 2; + var sum: u64 = 0; + _ = timer.lap(); + for (keys) |key| sum += getValue(Map, &map, key) orelse 0; + times[1] = timer.read(); + try check(sum == expected); + + var found: usize = 0; + _ = timer.lap(); + for (keys) |key| { + if (getValue(Map, &map, key + @as(u32, @intCast(keys.len))) != null) found += 1; + } + times[2] = timer.read(); + try check(found == 0); + + _ = timer.lap(); + sum = try iterationSum(Map, &map, allocator); + times[3] = timer.read(); + try check(sum == expected); + + var removed: usize = 0; + _ = timer.lap(); + for (keys) |key| { + if (Map == HashMap) { + if (map.remove(key)) removed += 1; + } else { + if (map.remove(key) != null) removed += 1; + } + } + times[4] = timer.read(); + try check(removed == keys.len and map.count() == 0); + return times; +} + +pub fn main() !void { + var debug_allocator = std.heap.DebugAllocator(.{}){}; + { + defer if (debug_allocator.deinit() == .leak) @panic("benchmark self-check leaked"); + inline for (MAPS) |Map| try selfCheck(Map, debug_allocator.allocator()); + } + const allocator = std.heap.smp_allocator; + std.debug.print("ReleaseFast; u32 keys and values; smp_allocator; median of {d} runs after one warmup\n", .{REPEATS}); + std.debug.print("container,items,input,insert_ns/op,hit_ns/op,miss_ns/op,iterate_ns/item,remove_ns/op\n", .{}); + for ([_]usize{ 1_000, 10_000, 100_000 }) |size| { + const keys = try allocator.alloc(u32, size); + defer allocator.free(keys); + for (keys, 0..) |*key, i| key.* = @intCast(i); + for ([_][]const u8{ "sequential", "shuffled" }, 0..) |input, input_index| { + if (input_index == 1) { + var prng = std.Random.DefaultPrng.init(0x6f726465726564); + prng.random().shuffle(u32, keys); + } + inline for (MAPS, NAMES) |Map, name| { + _ = try measure(Map, allocator, keys); + var samples: [5][REPEATS]u64 = undefined; + for (0..REPEATS) |repeat| { + const times = try measure(Map, allocator, keys); + for (times, 0..) |time, operation| samples[operation][repeat] = time; + } + std.debug.print("{s},{d},{s}", .{ name, size, input }); + for (&samples) |*operation| { + std.mem.sort(u64, operation, {}, std.sort.asc(u64)); + const ns_per_op = @as(f64, @floatFromInt(operation[REPEATS / 2])) / @as(f64, @floatFromInt(size)); + std.debug.print(",{d:.2}", .{ns_per_op}); + } + std.debug.print("\n", .{}); + } + } + } +} diff --git a/build.zig b/build.zig index 9196a2f..f734fce 100644 --- a/build.zig +++ b/build.zig @@ -74,6 +74,11 @@ pub fn build(b: *std.Build) void { // --- Benchmarks --- const benches_path = "benches"; if (b.root.openDir(io, benches_path, .{ .iterate = true })) |benches_dir| { + const bench_lib_module = b.createModule(.{ + .root_source_file = lib_source, + .target = target, + .optimize = .ReleaseFast, + }); b.dependOnDirectoryContents(b.path(benches_path)); var dir = benches_dir; defer dir.close(io); @@ -89,7 +94,7 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = .ReleaseFast, // Use ReleaseFast for benchmarks }); - bench_module.addImport("ordered", lib_module); + bench_module.addImport("ordered", bench_lib_module); const bench_exe = b.addExecutable(.{ .name = bench_name, .root_module = bench_module, diff --git a/uv.lock b/uv.lock index ee6d953..33287fc 100644 --- a/uv.lock +++ b/uv.lock @@ -1,10 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.10, <4.0" -resolution-markers = [ - "python_full_version >= '3.11'", - "python_full_version < '3.11'", -] +requires-python = ">=3.14, <4.0" [[package]] name = "cfgv" @@ -26,26 +22,11 @@ wheels = [ [[package]] name = "filelock" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -sdist = { url = "https://files.pythonhosted.org/packages/4c/58/6fd434bec86eff7c38a3168454cb132b762b2bea9b3ac094101a2f7bc32a/filelock-4.1.0.tar.gz", hash = "sha256:ad7f724afef953e731b1cc39bcd3a09166d72ed7fcdf29e6e88b1c3235c6715d", size = 561277, upload-time = "2026-10-09T19:57:20.34Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/86/032133892a5de43b5a98200b01aadcad68cc255e274a762f08b8a76d2912/filelock-4.1.0-py3-none-any.whl", hash = "sha256:2ce9818e3e2d8f284c1a964414447ef148d42a5fd5e2a477a7118e574b293ec1", size = 133003, upload-time = "2026-10-09T19:57:18.716Z" }, -] - -[[package]] -name = "filelock" -version = "4.2.0" +version = "4.2.4" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11'", -] -sdist = { url = "https://files.pythonhosted.org/packages/f4/a9/1af41b37c3279712b22cdc63aac78a52432202b6fe1f9666a2a3d2831fb4/filelock-4.2.0.tar.gz", hash = "sha256:7a60906c75227cf04d0c273afadc8219400f11aeb13cc69591d4f6cdc6c8036e", size = 569667, upload-time = "2026-10-14T20:57:13.11Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/28/3893324bcadf8f89b1616637468d766af3f625008c0735bf704fdfa86617/filelock-4.2.4.tar.gz", hash = "sha256:c6d8663535e1650ee430507b4ffbac7b464dd64b7647ea0112a9f406499d163d", size = 652444, upload-time = "2026-10-19T01:57:51.001Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/a3/9bc26acff301fe1aaea1cc3d82a1d57e0a34df3e1cadbfa91ac2dbcdde5c/filelock-4.2.0-py3-none-any.whl", hash = "sha256:2ff5690882e8cdb00ef31fb3d01a3094c29f30985426c59495afb1733f3b7238", size = 135032, upload-time = "2026-10-14T20:57:11.349Z" }, + { url = "https://files.pythonhosted.org/packages/e0/1b/5e74289a1878e666bb4b65fe5805a04cafcac2aa5f519ef1ead67ef91c56/filelock-4.2.4-py3-none-any.whl", hash = "sha256:301241438c57936ef93f4243a94f2559022268bfcf9b375a9a72eb0f8db8358e", size = 162475, upload-time = "2026-10-19T01:57:49.419Z" }, ] [[package]] @@ -71,8 +52,7 @@ name = "ordered" version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "pre-commit", version = "4.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pre-commit", version = "4.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pre-commit" }, { name = "python-dotenv" }, ] @@ -93,54 +73,17 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.12.4" +version = "4.13.5" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -sdist = { url = "https://files.pythonhosted.org/packages/90/a1/d5f9002a70298c64a789779077d8dd90c10aa1f47fe40c86802df874f2a6/platformdirs-4.12.4.tar.gz", hash = "sha256:63743c02414e755de4e31b8f68125c1407495b86c5a006e203c01ff8b9924250", size = 60010, upload-time = "2026-10-07T23:30:32.426Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/e6/d0abbb256b44e6a05748c6942511ff67db8dbb068de0c35b24bf1b395b09/platformdirs-4.13.5.tar.gz", hash = "sha256:05831adf87d392760f4b5c3a0a95579ab5a10b18b9b9bd43680dcb47544788a0", size = 62362, upload-time = "2026-10-19T01:02:11.778Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/ba/223e00b885e960edd5d4b4d178c88acce019bd5b83786093681b8c393492/platformdirs-4.12.4-py3-none-any.whl", hash = "sha256:78bfb9db2a8471ed7eebe3c3c932da413911042994e699b384fbb4493fa872d7", size = 32596, upload-time = "2026-10-07T23:30:30.825Z" }, -] - -[[package]] -name = "platformdirs" -version = "4.13.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11'", -] -sdist = { url = "https://files.pythonhosted.org/packages/da/91/4a03cfdb03314cfca262921797fff04ff4054bd86a160aed76eddf8aa12b/platformdirs-4.13.3.tar.gz", hash = "sha256:5e567f664eb087ab8521c0179cd8d1bd60857d271136567a39719e28e2d383ce", size = 61559, upload-time = "2026-10-16T01:16:16.573Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/45/b8/fd1af06b079af236f5423f7c1821264419cc8f6b4803f79353acbb8bfa53/platformdirs-4.13.3-py3-none-any.whl", hash = "sha256:f6ad7f447f24f8a3b82cce5976387428bff894a0eca6c3488f4a17f153c130c4", size = 32762, upload-time = "2026-10-16T01:16:15.051Z" }, -] - -[[package]] -name = "pre-commit" -version = "4.6.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -dependencies = [ - { name = "cfgv" }, - { name = "identify" }, - { name = "nodeenv" }, - { name = "pyyaml" }, - { name = "virtualenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/74/89/1f3e8e1fc3e97de0fa963495832f581f025f29471602a309e48808244292/pre_commit-4.6.2.tar.gz", hash = "sha256:8f5d7bfb021ecdbcd9d49d89847082dd24172ccde534390081a679ad046e2441", size = 198670, upload-time = "2026-08-10T22:07:18.421Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/45/e2/bbb7129c9e7999a6b8ee9cca3b66486c25c423ab5a75f34071798b74ce94/pre_commit-4.6.2-py2.py3-none-any.whl", hash = "sha256:e2dde9a75d3bce11bd3831c26d134df00a2803c1d818be6a0383c3dcda25dc4e", size = 226202, upload-time = "2026-08-10T22:07:16.942Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8a/1d8cb3842909dc193a73835adedf412e32e18f8ffd59f43983949622fe6f/platformdirs-4.13.5-py3-none-any.whl", hash = "sha256:53d3ec4280b815c1e0b02edad7f9ee8fda1ba9c0865dd9c2e6e63849ce82fc1e", size = 32779, upload-time = "2026-10-19T01:02:10.17Z" }, ] [[package]] name = "pre-commit" version = "4.7.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11'", -] dependencies = [ { name = "cfgv" }, { name = "identify" }, @@ -155,15 +98,14 @@ wheels = [ [[package]] name = "python-discovery" -version = "1.6.3" +version = "1.6.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "filelock", version = "4.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "filelock", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "filelock" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/54/fad125f5c48f7821644ee6f223ebfcedc4e23597c1a16eeb13d33aa4a91a/python_discovery-1.6.3.tar.gz", hash = "sha256:a62b301d96cf5489cb96ab023b32e068421867fdd5df2e42eece877869047fa2", size = 88998, upload-time = "2026-10-16T03:54:29.507Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/ee/cf608372c88ce2c136ee13c69ab6f667dbc0d0c1176af188b4ddc8853e66/python_discovery-1.6.4.tar.gz", hash = "sha256:bc70d31f0ec5de11eda13d4d64c16e24b3506a65984684f2b6c98cb83b2ea9df", size = 89883, upload-time = "2026-10-19T01:45:14.855Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/ec/643826e4d69b7bdce4213f221e8f5ed06d0a9dd69fa9b33342578d2cc8c8/python_discovery-1.6.3-py3-none-any.whl", hash = "sha256:cb7654125e3dcb594269a6feb56ac693f1f13b7c654493a93eba72dc22d18034", size = 39386, upload-time = "2026-10-16T03:54:28.237Z" }, + { url = "https://files.pythonhosted.org/packages/60/91/1aa70d3dfcab0dbcf88a2cbc3bce7f7c69562287a90216e617eaa4937e25/python_discovery-1.6.4-py3-none-any.whl", hash = "sha256:530179cc51a1816d512b15635b3dc931295ae12efb183783ad31190a5d425d2b", size = 39385, upload-time = "2026-10-19T01:45:13.668Z" }, ] [[package]] @@ -181,44 +123,6 @@ version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, - { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, - { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, - { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, - { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, @@ -239,30 +143,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] -[[package]] -name = "typing-extensions" -version = "4.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, -] - [[package]] name = "virtualenv" -version = "21.14.8" +version = "21.14.13" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, - { name = "filelock", version = "4.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "filelock", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "filelock" }, { name = "packaging" }, - { name = "platformdirs", version = "4.12.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "platformdirs", version = "4.13.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "platformdirs" }, { name = "python-discovery" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0b/6a/fbdeea5c07d5674139639d42a97a41d26e9b9b8cb2a1265fa936f655ee20/virtualenv-21.14.8.tar.gz", hash = "sha256:394e45ed610ceffb5018e1a179e3a8012c02fd87fe792ab4aae67ce5e7e3afbd", size = 5474561, upload-time = "2026-10-16T00:09:09.672Z" } +sdist = { url = "https://files.pythonhosted.org/packages/70/aa/bff50daee0488e16d611258a9b6c5a1d0c9e128f8ca424e9f4e1c0444ef2/virtualenv-21.14.13.tar.gz", hash = "sha256:de5307ba4b9a0001d9c8ea02832d38f63ca432880f416c1bface61c4323fd905", size = 5570301, upload-time = "2026-10-22T20:10:16.952Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/01/cd63b97dd556293e1c74d76053936d9cef11a28eac605c735ae9f0ee324f/virtualenv-21.14.8-py3-none-any.whl", hash = "sha256:d2a347fdbc1a65e33c2c5e84dc767b4a3538a25749536a752744d4fe1f71c37a", size = 5487606, upload-time = "2026-10-16T00:09:07.441Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a9/c10d8a44a29fb94a18b43fdf11f38caca53ae7c9406f11a72b1cb8f624f1/virtualenv-21.14.13-py3-none-any.whl", hash = "sha256:5fbb04138a4861580b78ba9110d529a26bc48fcc93c8ec36bc0202eeed96a788", size = 5557094, upload-time = "2026-10-22T20:10:15.096Z" }, ] From 550fd2dfc403a4972d8c096d254d12506589efb4 Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Sun, 25 Oct 2026 14:36:34 +0100 Subject: [PATCH 5/6] Improve the benchmarks --- Makefile | 7 +- benches/README.md | 121 +++++--- benches/b7_std_comparison.zig | 206 +------------- benches/b8_memory_comparison.zig | 6 + benches/util/comparison.zig | 410 ++++++++++++++++++++++++++++ benches/util/counting_allocator.zig | 87 ++++++ build.zig | 10 + src/ordered/skip_list_map.zig | 13 + 8 files changed, 617 insertions(+), 243 deletions(-) create mode 100644 benches/b8_memory_comparison.zig create mode 100644 benches/util/comparison.zig create mode 100644 benches/util/counting_allocator.zig diff --git a/Makefile b/Makefile index c38d2a6..f496157 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,7 @@ EXAMPLE ?= all # Automatically find all benchmark names BENCHMARKS := $(patsubst %.zig,%,$(notdir $(wildcard benches/*.zig))) BENCHMARK ?= all +BENCH_ARGS ?= SHELL := /usr/bin/env bash .SHELLFLAGS := -eu -o pipefail -c @@ -68,11 +69,11 @@ bench: ## Run a benchmark (like 'make bench BENCHMARK=b1_btree_map' or 'make run for ex in $(BENCHMARKS); do \ echo ""; \ echo "--> Running '$$ex'"; \ - $(ZIG) build bench-$$ex $(BUILD_OPTS); \ + $(ZIG) build bench-$$ex $(BUILD_OPTS) -- $(BENCH_ARGS); \ done; \ else \ echo "--> Running benchmark: $(BENCHMARK)"; \ - $(ZIG) build bench-$(BENCHMARK) $(BUILD_OPTS); \ + $(ZIG) build bench-$(BENCHMARK) $(BUILD_OPTS) -- $(BENCH_ARGS); \ fi test: ## Run tests @@ -89,7 +90,7 @@ clean: ## Remove docs, build artifacts, and cache directories lint: ## Check code style and formatting of Zig files @echo "Running code style checks..." - @$(ZIG) fmt --check $(SRC_DIR) $(EXAMPLES_DIR) + @$(ZIG) fmt --check $(SRC_DIR) $(EXAMPLES_DIR) $(BENCHMARKS_DIR) build.zig format: ## Format Zig files @echo "Formatting Zig files..." diff --git a/benches/README.md b/benches/README.md index b26d9ec..c9fa139 100644 --- a/benches/README.md +++ b/benches/README.md @@ -10,7 +10,8 @@ | 4 | [b4_skip_list_map.zig](b4_skip_list_map.zig) | Benchmarks for Skip list map implementation | | 5 | [b5_trie_map.zig](b5_trie_map.zig) | Benchmarks for Trie map implementation | | 6 | [b6_cartesian_tree_map.zig](b6_cartesian_tree_map.zig) | Benchmarks for Cartesian tree map implementation | -| 7 | [b7_std_comparison.zig](b7_std_comparison.zig) | Integer maps compared with Zig standard-library containers | +| 7 | [b7_std_comparison.zig](b7_std_comparison.zig) | Timing comparisons with Zig standard-library containers | +| 8 | [b8_memory_comparison.zig](b8_memory_comparison.zig) | Memory comparisons with Zig standard-library containers | #### Running Benchmarks @@ -39,44 +40,90 @@ zig build bench-b1_btree_map > > **Compilation**: benchmarks are compiled with `ReleaseFast` optimization (see [build.zig](../build.zig)). -#### Standard-Library Comparison +#### Standard-Library Comparisons + +The timing and memory runners compare `BTreeMap` (branching factor 16), `CartesianTreeMap`, and `SkipListMap` +(maximum level 16) with Zig's `std.Treap` and hash maps. Integer keys use `std.AutoHashMap`; string keys use +`std.StringHashMap`. Hash maps have unordered iteration and serve as baselines for workloads that do not require +ordering. The intrusive `std.Treap` uses a local adapter that allocates a node and stores a value for each key. +Allocation and freeing are included in insertion and removal timings. ```sh +# The complete default workloads, including batches of tiny maps. make bench BENCHMARK=b7_std_comparison +make bench BENCHMARK=b8_memory_comparison + +# A short validation run of every key and value combination. +make bench BENCHMARK=b7_std_comparison BENCH_ARGS=--quick +make bench BENCHMARK=b8_memory_comparison BENCH_ARGS=--quick + +# Separate runs with 1M and 10M integer entries. +make bench BENCHMARK=b7_std_comparison BENCH_ARGS=--large +make bench BENCHMARK=b8_memory_comparison BENCH_ARGS=--large + +# Equivalent direct Zig invocation. +zig build bench-b7_std_comparison -- --large ``` -This benchmark compares `BTreeMap` (branching factor 16), `CartesianTreeMap`, and `SkipListMap` (maximum level 16) -with `std.Treap` and `std.AutoHashMap`. The hash map does not provide sorted iteration, so its results are a baseline -for workloads that do not need ordering. The intrusive `std.Treap` has a local adapter that allocates a node and -stores a value for each key, including allocation and freeing in the measured insertion and removal times. - -All containers use `u32` keys and values, `std.heap.smp_allocator`, and ReleaseFast optimization for both the runner -and the imported library. Each case reports the median of five runs after one warmup, at 1,000, 10,000, and 100,000 -elements. Keys are unique integers from zero to the size minus one. Insertion, lookup, and removal use either -ascending keys or the same shuffled permutation, generated with a fixed seed. Container priorities and skip-list -levels use their normal initialization and can differ between runs. - -Successful lookups and iteration consume the stored values and check their sums after timing. Unsuccessful lookups -use keys above the maximum stored key, which favors trees that quickly reject that range. Removal counts and final -container sizes are also checked. Duplicate updates, missing removals, empty iteration, and cleanup are checked with -the debug allocator before timing. Iteration includes iterator creation and cleanup; container initialization and -final cleanup are excluded from the operation timings. CSV rows are written to standard error. - -##### Sample Results - -The following results were measured on Linux x86_64 with an AMD Ryzen 9 9950X and Zig 0.17.0 on October 25, 2026, -using the native target. Values are nanoseconds per operation for 100,000 shuffled keys, except iteration, which is -nanoseconds per item. - -| Container | Insert | Lookup Hit | Lookup Miss | Iteration | Remove | -|-----------|-------:|-----------:|------------:|----------:|-------:| -| `BTreeMap` | 66.63 | 105.15 | 29.90 | 2.11 | 108.39 | -| `CartesianTreeMap` | 191.65 | 122.15 | 7.08 | 8.11 | 123.50 | -| `SkipListMap` | 172.16 | 177.46 | 10.74 | 16.48 | 168.18 | -| `std.Treap` | 117.01 | 80.66 | 12.44 | 14.25 | 99.12 | -| `std.AutoHashMap` (unordered) | 20.28 | 8.06 | 13.56 | 2.01 | 6.86 | - -These are local measurements, with no CPU pinning or frequency control. The phases run consecutively on the same -container, so lookup and iteration measure a populated container with recently accessed data. This suite does not -measure memory usage, string keys, interleaved mutations, or prefix and range queries. It adds no external -dependencies; C and third-party Zig libraries are not included. +##### Workloads + +| Mode | Entries per Map | Keys | Values | Tiny Maps | +|------|-----------------|------|--------|-----------| +| Default | 1,000, 10,000, and 100,000 | `u64`, 16-byte strings, and 100-byte strings | `u64` and 32-byte records | 10,000 maps each of 4 and 32 entries | +| `--quick` | 1,000 | Same as default | Same as default | 128 maps each of 4 and 32 entries | +| `--large` | 1,000,000 and 10,000,000 | `u64` | `u64` | None | + +All containers use `std.heap.smp_allocator` and ReleaseFast optimization for both the runner and imported library. +The large mode runs separately so routine benchmarks do not construct 10M-entry maps. String and larger-value +cases remain in the default workload. Tiny maps use 16-byte string keys and 32-byte records. + +Present integer keys are even numbers, and absent keys are odd numbers interleaved among them; the final absent +key is just above the maximum present key. String keys encode those same numbers as fixed-width hexadecimal text. +The 100-byte case pads that text with a shared prefix, exercising comparisons of long common prefixes. These are +synthetic keys, rather than samples from real applications. + +Each single-map workload runs with ascending and shuffled insertion orders. Lookup and removal each use a separate +permutation generated with a different fixed seed, regardless of insertion order. Every container receives the +same workload. Container priorities and skip-list levels use normal initialization and may vary between runs. +Key buffers and operation permutations are generated before measurement and kept alive until all containers finish. +Strings are borrowed from shared buffers; key allocation and copying are excluded. + +##### Timing Measurements + +The timing runner measures insertion, successful lookup, unsuccessful lookup, full iteration, and removal. Lookups +use `getPtr` consistently, and successful lookups and iteration consume every word of the stored value. Result sums, +hit counts, removal counts, and container sizes are checked outside timed regions. Each phase runs once per map; +lookup, iteration, and removal follow insertion on the same populated container. Iterator creation and cleanup are +included in iteration time. Container initialization and final cleanup are excluded for single-map workloads. + +Tiny-map workloads measure construction and destruction in nanoseconds per map, and iteration in nanoseconds per +item, while all maps coexist. Construction includes the map array allocation, initialization, and insertion; +destruction includes freeing all containers and the array. Maps share the same key buffers and insertion order. + +Default and large timing runs report the median of five samples after one warmup. Quick runs use one sample after +one warmup and are intended for validation. CSV output goes to standard error, preceded by a metadata line. Tiny +results form a second section with their own header and timing columns. + +##### Memory Measurements + +The memory runner performs no timing. A counting allocator wraps `smp_allocator` during construction and records +live requested bytes after construction, peak requested bytes during construction, and live bytes per entry. +It includes each container's inline storage by allocating an array of containers, including for a single map. +Each workload is built once, and every measured allocation must be released after destruction. + +Shared string buffers, operation permutations, allocator metadata, size-class rounding, reserved pages, and process +RSS are excluded. The string payload cost is shared equally by all containers, so string length alone does not +change reported container memory. Peak bytes describe allocations visible through Zig's allocator interface, not +internal temporary allocations made by the underlying allocator. Memory instrumentation is never used by the timing +runner. Repeated runs may differ for randomized containers. + +##### Validation and Limits + +`make test` includes benchmark tests for adapters, both value sizes, both string lengths, independent permutations, +interleaved misses, duplicate updates, missing removals, empty iteration, allocator accounting, and cleanup after +allocation failures. `make lint` includes benchmark sources and `build.zig`. + +Measurements are single-threaded, with no CPU pinning or frequency control. This suite does not measure concurrent +access, interleaved mutations, prefix queries, or range scans. It adds no external dependencies and includes no Rust +or C libraries. Results from the earlier `u32` comparison are not comparable with these workloads and have been +removed from this document. diff --git a/benches/b7_std_comparison.zig b/benches/b7_std_comparison.zig index 50502e0..e84dbf1 100644 --- a/benches/b7_std_comparison.zig +++ b/benches/b7_std_comparison.zig @@ -1,206 +1,6 @@ const std = @import("std"); -const ordered = @import("ordered"); -const Timer = @import("util/timer.zig").Timer; +const comparison = @import("util/comparison.zig"); -const BTree = ordered.BTreeMap(u32, u32, compare, 16); -const CartesianTree = ordered.CartesianTreeMap(u32, u32, compare); -const SkipList = ordered.SkipListMap(u32, u32, compare, 16); -const HashMap = std.AutoHashMap(u32, u32); -const MAPS = .{ BTree, CartesianTree, SkipList, TreapMap, HashMap }; -const NAMES = .{ "BTreeMap", "CartesianTreeMap", "SkipListMap", "std.Treap", "std.AutoHashMap (unordered)" }; -const REPEATS = 5; - -fn compare(lhs: u32, rhs: u32) std.math.Order { - return std.math.order(lhs, rhs); -} - -// std.Treap is intrusive, so include node allocation and value storage in its cost. -const TreapMap = struct { - const Tree = std.Treap(u32, std.math.order); - const Node = struct { tree_node: Tree.Node, value: u32 }; - - allocator: std.mem.Allocator, - tree: Tree = .{}, - len: usize = 0, - - fn init(allocator: std.mem.Allocator) TreapMap { - return .{ .allocator = allocator }; - } - - fn deinit(self: *TreapMap) void { - while (self.tree.getMin()) |node| _ = self.remove(node.key); - } - - fn count(self: *const TreapMap) usize { - return self.len; - } - - fn put(self: *TreapMap, key: u32, value: u32) !void { - var entry = self.tree.getEntryFor(key); - if (entry.node) |node| { - const parent: *Node = @fieldParentPtr("tree_node", node); - parent.value = value; - } else { - const node = try self.allocator.create(Node); - node.value = value; - entry.set(&node.tree_node); - self.len += 1; - } - } - - fn get(self: *TreapMap, key: u32) ?u32 { - const node = self.tree.getEntryFor(key).node orelse return null; - const parent: *Node = @fieldParentPtr("tree_node", node); - return parent.value; - } - - fn remove(self: *TreapMap, key: u32) ?u32 { - var entry = self.tree.getEntryFor(key); - const node = entry.node orelse return null; - const parent: *Node = @fieldParentPtr("tree_node", node); - const value = parent.value; - entry.set(null); - self.allocator.destroy(parent); - self.len -= 1; - return value; - } -}; - -fn initMap(comptime Map: type, allocator: std.mem.Allocator) !Map { - if (Map == SkipList) return try Map.init(allocator); - return Map.init(allocator); -} - -fn getValue(comptime Map: type, map: *Map, key: u32) ?u32 { - if (Map == BTree or Map == SkipList) { - return if (map.get(key)) |value| value.* else null; - } - return map.get(key); -} - -fn iterationSum(comptime Map: type, map: *Map, allocator: std.mem.Allocator) !u64 { - var sum: u64 = 0; - if (Map == TreapMap) { - var it = map.tree.inorderIterator(); - while (it.next()) |node| { - const parent: *TreapMap.Node = @fieldParentPtr("tree_node", node); - sum += parent.value; - } - } else if (Map == HashMap) { - var it = map.iterator(); - while (it.next()) |entry| sum += entry.value_ptr.*; - } else if (Map == SkipList) { - var it = map.iterator(); - while (it.next()) |entry| sum += entry.value; - } else { - var it = if (Map == CartesianTree) try map.iterator(allocator) else try map.iterator(); - defer it.deinit(); - while (try it.next()) |entry| sum += entry.value; - } - return sum; -} - -fn check(ok: bool) !void { - if (!ok) return error.BenchmarkMismatch; -} - -fn selfCheck(comptime Map: type, allocator: std.mem.Allocator) !void { - var map = try initMap(Map, allocator); - defer map.deinit(); - try check(map.count() == 0 and getValue(Map, &map, 7) == null); - try map.put(7, 10); - try map.put(7, 20); - try check(map.count() == 1 and getValue(Map, &map, 7) == 20); - try check(try iterationSum(Map, &map, allocator) == 20); - if (Map == HashMap) { - try check(map.remove(7)); - try check(!map.remove(7)); - } else { - try check(map.remove(7) == 20); - try check(map.remove(7) == null); - } - try check(map.count() == 0); - try check(try iterationSum(Map, &map, allocator) == 0); - // Leave a live node to exercise cleanup as well as removal. - try map.put(9, 30); -} - -fn measure(comptime Map: type, allocator: std.mem.Allocator, keys: []const u32) ![5]u64 { - var map = try initMap(Map, allocator); - defer map.deinit(); - var times: [5]u64 = undefined; - var timer = try Timer.start(); - for (keys) |key| try map.put(key, key + 1); - times[0] = timer.read(); - try check(map.count() == keys.len); - - const expected = @as(u64, keys.len) * (keys.len + 1) / 2; - var sum: u64 = 0; - _ = timer.lap(); - for (keys) |key| sum += getValue(Map, &map, key) orelse 0; - times[1] = timer.read(); - try check(sum == expected); - - var found: usize = 0; - _ = timer.lap(); - for (keys) |key| { - if (getValue(Map, &map, key + @as(u32, @intCast(keys.len))) != null) found += 1; - } - times[2] = timer.read(); - try check(found == 0); - - _ = timer.lap(); - sum = try iterationSum(Map, &map, allocator); - times[3] = timer.read(); - try check(sum == expected); - - var removed: usize = 0; - _ = timer.lap(); - for (keys) |key| { - if (Map == HashMap) { - if (map.remove(key)) removed += 1; - } else { - if (map.remove(key) != null) removed += 1; - } - } - times[4] = timer.read(); - try check(removed == keys.len and map.count() == 0); - return times; -} - -pub fn main() !void { - var debug_allocator = std.heap.DebugAllocator(.{}){}; - { - defer if (debug_allocator.deinit() == .leak) @panic("benchmark self-check leaked"); - inline for (MAPS) |Map| try selfCheck(Map, debug_allocator.allocator()); - } - const allocator = std.heap.smp_allocator; - std.debug.print("ReleaseFast; u32 keys and values; smp_allocator; median of {d} runs after one warmup\n", .{REPEATS}); - std.debug.print("container,items,input,insert_ns/op,hit_ns/op,miss_ns/op,iterate_ns/item,remove_ns/op\n", .{}); - for ([_]usize{ 1_000, 10_000, 100_000 }) |size| { - const keys = try allocator.alloc(u32, size); - defer allocator.free(keys); - for (keys, 0..) |*key, i| key.* = @intCast(i); - for ([_][]const u8{ "sequential", "shuffled" }, 0..) |input, input_index| { - if (input_index == 1) { - var prng = std.Random.DefaultPrng.init(0x6f726465726564); - prng.random().shuffle(u32, keys); - } - inline for (MAPS, NAMES) |Map, name| { - _ = try measure(Map, allocator, keys); - var samples: [5][REPEATS]u64 = undefined; - for (0..REPEATS) |repeat| { - const times = try measure(Map, allocator, keys); - for (times, 0..) |time, operation| samples[operation][repeat] = time; - } - std.debug.print("{s},{d},{s}", .{ name, size, input }); - for (&samples) |*operation| { - std.mem.sort(u64, operation, {}, std.sort.asc(u64)); - const ns_per_op = @as(f64, @floatFromInt(operation[REPEATS / 2])) / @as(f64, @floatFromInt(size)); - std.debug.print(",{d:.2}", .{ns_per_op}); - } - std.debug.print("\n", .{}); - } - } - } +pub fn main(init: std.process.Init) !void { + try comparison.run(init, false); } diff --git a/benches/b8_memory_comparison.zig b/benches/b8_memory_comparison.zig new file mode 100644 index 0000000..4e8b3bd --- /dev/null +++ b/benches/b8_memory_comparison.zig @@ -0,0 +1,6 @@ +const std = @import("std"); +const comparison = @import("util/comparison.zig"); + +pub fn main(init: std.process.Init) !void { + try comparison.run(init, true); +} diff --git a/benches/util/comparison.zig b/benches/util/comparison.zig new file mode 100644 index 0000000..ffe2db9 --- /dev/null +++ b/benches/util/comparison.zig @@ -0,0 +1,410 @@ +const std = @import("std"); +const ordered = @import("ordered"); +const Timer = @import("timer.zig").Timer; +const CountingAllocator = @import("counting_allocator.zig").CountingAllocator; + +const Mode = enum { normal, quick, large }; +const NAMES = .{ "BTreeMap", "CartesianTreeMap", "SkipListMap", "std.Treap", "std.HashMap (unordered)" }; + +fn check(ok: bool) !void { + if (!ok) return error.BenchmarkMismatch; +} + +fn shuffle(items: []u32, seed: u64) void { + var prng = std.Random.DefaultPrng.init(seed); + prng.random().shuffle(u32, items); +} + +fn Suite(comptime K: type, comptime V: type) type { + return struct { + const BTree = ordered.BTreeMap(K, V, compare, 16); + const CartesianTree = ordered.CartesianTreeMap(K, V, compare); + const SkipList = ordered.SkipListMap(K, V, compare, 16); + const HashMap = if (K == u64) std.AutoHashMap(K, V) else std.StringHashMap(V); + const MAPS = .{ BTree, CartesianTree, SkipList, TreapMap, HashMap }; + + fn compare(a: K, b: K) std.math.Order { + return if (K == u64) std.math.order(a, b) else std.mem.order(u8, a, b); + } + + fn value(id: u64) V { + return if (V == u64) id else .{ id, id * 3, id * 5, id * 7 }; + } + + fn fingerprint(v: V) u64 { + return if (V == u64) v else v[0] + v[1] + v[2] + v[3]; + } + + fn expectedSum(size: usize) u64 { + return @as(u64, size) * (size + 1) / 2 * (if (V == u64) @as(u64, 1) else 16); + } + + // std.Treap is intrusive; include allocation and value storage in its cost. + const TreapMap = struct { + const Tree = std.Treap(K, compare); + const Node = struct { tree_node: Tree.Node, value: V }; + allocator: std.mem.Allocator, + tree: Tree = .{}, + len: usize = 0, + + fn init(allocator: std.mem.Allocator) TreapMap { + return .{ .allocator = allocator }; + } + + fn deinit(self: *TreapMap) void { + while (self.tree.getMin()) |node| _ = self.remove(node.key); + } + + fn count(self: *const TreapMap) usize { + return self.len; + } + + fn put(self: *TreapMap, key: K, v: V) !void { + var entry = self.tree.getEntryFor(key); + if (entry.node) |node| { + const parent: *Node = @fieldParentPtr("tree_node", node); + parent.value = v; + } else { + const node = try self.allocator.create(Node); + node.value = v; + entry.set(&node.tree_node); + self.len += 1; + } + } + + fn getPtr(self: *TreapMap, key: K) ?*V { + const node = self.tree.getEntryFor(key).node orelse return null; + const parent: *Node = @fieldParentPtr("tree_node", node); + return &parent.value; + } + + fn remove(self: *TreapMap, key: K) ?V { + var entry = self.tree.getEntryFor(key); + const node = entry.node orelse return null; + const parent: *Node = @fieldParentPtr("tree_node", node); + const v = parent.value; + entry.set(null); + self.allocator.destroy(parent); + self.len -= 1; + return v; + } + }; + + const Dataset = struct { + present: []K, + absent: []K, + bytes: []u8, + insertion: []u32, + lookup: []u32, + removal: []u32, + + fn init(allocator: std.mem.Allocator, size: usize, key_len: usize) !Dataset { + const present = try allocator.alloc(K, size); + errdefer allocator.free(present); + const absent = try allocator.alloc(K, size); + errdefer allocator.free(absent); + const bytes = try allocator.alloc(u8, if (K == u64) 0 else size * 2 * key_len); + errdefer allocator.free(bytes); + for (0..size) |i| { + if (K == u64) { + present[i] = 2 * i; + absent[i] = 2 * i + 1; + } else { + for (0..2) |which| { + const key = bytes[(2 * i + which) * key_len ..][0..key_len]; + @memset(key, 'a'); + _ = try std.fmt.bufPrint(key[key_len - 16 ..], "{x:0>16}", .{2 * i + which}); + if (which == 0) present[i] = key else absent[i] = key; + } + } + } + const insertion = try allocator.alloc(u32, size); + errdefer allocator.free(insertion); + const lookup = try allocator.alloc(u32, size); + errdefer allocator.free(lookup); + const removal = try allocator.alloc(u32, size); + errdefer allocator.free(removal); + for (0..size) |i| { + insertion[i] = @intCast(i); + lookup[i] = @intCast(i); + removal[i] = @intCast(i); + } + shuffle(lookup, 0x10_0c); + shuffle(removal, 0xde_1e); + return .{ .present = present, .absent = absent, .bytes = bytes, .insertion = insertion, .lookup = lookup, .removal = removal }; + } + + fn deinit(self: Dataset, allocator: std.mem.Allocator) void { + allocator.free(self.removal); + allocator.free(self.lookup); + allocator.free(self.insertion); + allocator.free(self.bytes); + allocator.free(self.absent); + allocator.free(self.present); + } + }; + + fn initMap(comptime Map: type, allocator: std.mem.Allocator) !Map { + return if (Map == SkipList) try Map.init(allocator) else Map.init(allocator); + } + + fn remove(comptime Map: type, map: *Map, key: K) bool { + return if (Map == HashMap) map.remove(key) else map.remove(key) != null; + } + + fn iterationSum(comptime Map: type, map: *Map, allocator: std.mem.Allocator) !u64 { + var sum: u64 = 0; + if (Map == TreapMap) { + var it = map.tree.inorderIterator(); + while (it.next()) |node| { + const parent: *TreapMap.Node = @fieldParentPtr("tree_node", node); + sum += fingerprint(parent.value); + } + } else if (Map == HashMap) { + var it = map.iterator(); + while (it.next()) |entry| sum += fingerprint(entry.value_ptr.*); + } else if (Map == SkipList) { + var it = map.iterator(); + while (it.next()) |entry| sum += fingerprint(entry.value); + } else { + var it = if (Map == CartesianTree) try map.iterator(allocator) else try map.iterator(); + defer it.deinit(); + while (try it.next()) |entry| sum += fingerprint(entry.value); + } + return sum; + } + + fn measure(comptime Map: type, allocator: std.mem.Allocator, data: Dataset) ![5]u64 { + var map = try initMap(Map, allocator); + defer map.deinit(); + var times: [5]u64 = undefined; + var timer = try Timer.start(); + for (data.insertion) |i| try map.put(data.present[i], value(@as(u64, i) + 1)); + times[0] = timer.read(); + try check(map.count() == data.present.len); + var sum: u64 = 0; + var hits: usize = 0; + _ = timer.lap(); + for (data.lookup) |i| { + if (map.getPtr(data.present[i])) |v| { + sum += fingerprint(v.*); + hits += 1; + } + } + times[1] = timer.read(); + try check(hits == data.present.len and sum == expectedSum(data.present.len)); + hits = 0; + _ = timer.lap(); + for (data.lookup) |i| { + if (map.getPtr(data.absent[i]) != null) hits += 1; + } + times[2] = timer.read(); + try check(hits == 0); + _ = timer.lap(); + sum = try iterationSum(Map, &map, allocator); + times[3] = timer.read(); + try check(sum == expectedSum(data.present.len)); + var removed: usize = 0; + _ = timer.lap(); + for (data.removal) |i| { + if (remove(Map, &map, data.present[i])) removed += 1; + } + times[4] = timer.read(); + try check(removed == data.present.len and map.count() == 0); + return times; + } + + fn buildMany(comptime Map: type, allocator: std.mem.Allocator, data: Dataset, count: usize) ![]Map { + const maps = try allocator.alloc(Map, count); + errdefer allocator.free(maps); + var initialized: usize = 0; + errdefer for (maps[0..initialized]) |*map| map.deinit(); + for (maps) |*map| { + map.* = try initMap(Map, allocator); + initialized += 1; + for (data.insertion) |i| try map.put(data.present[i], value(@as(u64, i) + 1)); + } + return maps; + } + + fn destroyMany(comptime Map: type, allocator: std.mem.Allocator, maps: []Map) void { + for (maps) |*map| map.deinit(); + allocator.free(maps); + } + + fn measureTiny(comptime Map: type, allocator: std.mem.Allocator, data: Dataset, count: usize) ![3]u64 { + var timer = try Timer.start(); + const maps = try buildMany(Map, allocator, data, count); + const build_ns = timer.read(); + errdefer destroyMany(Map, allocator, maps); + var sum: u64 = 0; + _ = timer.lap(); + for (maps) |*map| sum += try iterationSum(Map, map, allocator); + const iteration_ns = timer.read(); + try check(sum == expectedSum(data.present.len) * count); + _ = timer.lap(); + destroyMany(Map, allocator, maps); + const destroy_ns = timer.read(); + return .{ build_ns, iteration_ns, destroy_ns }; + } + + fn selfCheck(allocator: std.mem.Allocator, key_len: usize) !void { + const data = try Dataset.init(allocator, 17, key_len); + defer data.deinit(allocator); + inline for (MAPS) |Map| { + var map = try initMap(Map, allocator); + defer map.deinit(); + try check(map.count() == 0 and map.getPtr(data.present[0]) == null); + try check(try iterationSum(Map, &map, allocator) == 0); + try check(!remove(Map, &map, data.present[0])); + try map.put(data.present[0], value(1)); + try map.put(data.present[0], value(2)); + try check(map.count() == 1 and fingerprint(map.getPtr(data.present[0]).?.*) == fingerprint(value(2))); + try check(remove(Map, &map, data.present[0]) and map.count() == 0); + // Leave a live node to exercise cleanup as well as removal. + try map.put(data.present[1], value(3)); + _ = try measure(Map, allocator, data); + _ = try measureTiny(Map, allocator, data, 3); + } + } + + fn report(allocator: std.mem.Allocator, size: usize, key_len: usize, repeats: usize, comptime memory: bool, comptime tiny: bool, tiny_count: usize) !void { + const data = try Dataset.init(allocator, size, key_len); + defer data.deinit(allocator); + for (0..if (tiny) @as(usize, 1) else 2) |input_index| { + if (input_index == 1 or tiny) shuffle(data.insertion, 0x1a_5e); + inline for (MAPS, NAMES) |Map, name| { + const input = if (input_index == 1 or tiny) "shuffled" else "sequential"; + const key_kind = if (K == u64) "u64" else if (key_len == 16) "string16" else "string100"; + std.debug.print("{s},{s},{d},{d},{s},{d}", .{ name, key_kind, @sizeOf(V), size, input, @max(tiny_count, 1) }); + if (memory) { + var counting: CountingAllocator = .{ .child = allocator }; + var live: usize = 0; + var peak: usize = 0; + { + const maps = try buildMany(Map, counting.allocator(), data, @max(tiny_count, 1)); + defer destroyMany(Map, counting.allocator(), maps); + live = counting.live_bytes; + peak = counting.peak_bytes; + for (maps) |map| try check(map.count() == size); + } + try check(counting.live_bytes == 0); + const per_entry = @as(f64, @floatFromInt(live)) / @as(f64, @floatFromInt(size * @max(tiny_count, 1))); + std.debug.print(",{d},{d},{d:.2}\n", .{ live, peak, per_entry }); + } else { + const operation_count = if (tiny) 3 else 5; + var samples: [operation_count][5]u64 = undefined; + for (0..repeats + 1) |repeat| { + const times = if (tiny) try measureTiny(Map, allocator, data, tiny_count) else try measure(Map, allocator, data); + if (repeat > 0) for (times, 0..) |time, operation| { + samples[operation][repeat - 1] = time; + }; + } + for (&samples, 0..) |*operation, i| { + std.mem.sort(u64, operation[0..repeats], {}, std.sort.asc(u64)); + const divisor = if (!tiny) size else if (i == 1) size * tiny_count else tiny_count; + const per_op = @as(f64, @floatFromInt(operation[repeats / 2])) / @as(f64, @floatFromInt(divisor)); + std.debug.print(",{d:.2}", .{per_op}); + } + std.debug.print("\n", .{}); + } + } + } + } + }; +} + +pub fn run(init: std.process.Init, comptime memory: bool) !void { + const args = try init.minimal.args.toSlice(init.arena.allocator()); + var mode: Mode = .normal; + if (args.len > 2) return error.InvalidArguments; + if (args.len == 2) { + mode = if (std.mem.eql(u8, args[1], "--quick")) .quick else if (std.mem.eql(u8, args[1], "--large")) .large else { + std.debug.print("Usage: {s} [--quick|--large]\n", .{args[0]}); + return error.InvalidArguments; + }; + } + const allocator = std.heap.smp_allocator; + const repeats: usize = if (mode == .quick) 1 else 5; + const sizes: []const usize = switch (mode) { + .normal => &.{ 1_000, 10_000, 100_000 }, + .quick => &.{1_000}, + .large => &.{ 1_000_000, 10_000_000 }, + }; + std.debug.print("ReleaseFast; smp_allocator; {s}; {d} measured runs after one warmup for timings\n", .{ if (memory) "requested live and peak bytes, excluding shared key buffers" else "median timings", repeats }); + std.debug.print("container,keys,value_bytes,items,input,maps,{s}\n", .{if (memory) "live_bytes,peak_bytes,bytes/item" else "insert_ns/op,hit_ns/op,miss_ns/op,iterate_ns/item,remove_ns/op"}); + for (sizes) |size| { + try Suite(u64, u64).report(allocator, size, 0, repeats, memory, false, 0); + if (mode != .large) { + try Suite(u64, [4]u64).report(allocator, size, 0, repeats, memory, false, 0); + inline for (.{ u64, [4]u64 }) |V| { + for ([_]usize{ 16, 100 }) |key_len| try Suite([]const u8, V).report(allocator, size, key_len, repeats, memory, false, 0); + } + } + } + if (mode != .large) { + std.debug.print("container,keys,value_bytes,items,input,maps,{s}\n", .{if (memory) "live_bytes,peak_bytes,bytes/item" else "build_ns/map,iterate_ns/item,destroy_ns/map"}); + for ([_]usize{ 4, 32 }) |size| try Suite([]const u8, [4]u64).report(allocator, size, 16, repeats, memory, true, if (mode == .quick) 128 else 10_000); + } +} + +test "comparison workloads cover map adapters, value sizes, and cleanup" { + inline for (.{ u64, [4]u64 }) |V| { + try Suite(u64, V).selfCheck(std.testing.allocator, 0); + try Suite([]const u8, V).selfCheck(std.testing.allocator, 16); + try Suite([]const u8, V).selfCheck(std.testing.allocator, 100); + } +} + +test "missing keys are interleaved and operation orders are independent permutations" { + const S = Suite(u64, u64); + const data = try S.Dataset.init(std.testing.allocator, 100, 0); + defer data.deinit(std.testing.allocator); + var seen: [100]bool = @splat(false); + for (data.present, data.absent, 0..) |present, absent, i| { + try std.testing.expectEqual(2 * i, present); + try std.testing.expectEqual(present + 1, absent); + } + for (data.lookup) |i| { + try std.testing.expect(!seen[i]); + seen[i] = true; + } + @memset(&seen, false); + for (data.removal) |i| { + try std.testing.expect(!seen[i]); + seen[i] = true; + } + try std.testing.expect(!std.mem.eql(u32, data.insertion, data.lookup)); + shuffle(data.insertion, 0x1a_5e); + try std.testing.expect(!std.mem.eql(u32, data.insertion, data.lookup)); + try std.testing.expect(!std.mem.eql(u32, data.insertion, data.removal)); + try std.testing.expect(!std.mem.eql(u32, data.lookup, data.removal)); +} + +fn allocationCheck(allocator: std.mem.Allocator) !void { + const S = Suite([]const u8, [4]u64); + const data = try S.Dataset.init(allocator, 4, 16); + defer data.deinit(allocator); + inline for (S.MAPS) |Map| { + const maps = try S.buildMany(Map, allocator, data, 3); + S.destroyMany(Map, allocator, maps); + } +} + +test "dataset and partially built maps release allocations on failure" { + try std.testing.checkAllAllocationFailures(std.testing.allocator, allocationCheck, .{}); +} + +test "memory workload releases all measured allocations" { + const S = Suite([]const u8, [4]u64); + const data = try S.Dataset.init(std.testing.allocator, 32, 16); + defer data.deinit(std.testing.allocator); + inline for (S.MAPS) |Map| { + var counting: CountingAllocator = .{ .child = std.testing.allocator }; + const maps = try S.buildMany(Map, counting.allocator(), data, 4); + try std.testing.expect(counting.live_bytes >= @sizeOf(Map) * 4); + try std.testing.expect(counting.peak_bytes >= counting.live_bytes); + S.destroyMany(Map, counting.allocator(), maps); + try std.testing.expectEqual(0, counting.live_bytes); + } +} diff --git a/benches/util/counting_allocator.zig b/benches/util/counting_allocator.zig new file mode 100644 index 0000000..47238bb --- /dev/null +++ b/benches/util/counting_allocator.zig @@ -0,0 +1,87 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Alignment = std.mem.Alignment; + +pub const CountingAllocator = struct { + child: Allocator, + live_bytes: usize = 0, + peak_bytes: usize = 0, + + pub fn allocator(self: *CountingAllocator) Allocator { + return .{ .ptr = self, .vtable = &.{ .alloc = alloc, .resize = resize, .remap = remap, .free = free } }; + } + + fn account(self: *CountingAllocator, old_len: usize, new_len: usize) void { + self.live_bytes = self.live_bytes - old_len + new_len; + self.peak_bytes = @max(self.peak_bytes, self.live_bytes); + } + + fn alloc(ctx: *anyopaque, len: usize, alignment: Alignment, ret_addr: usize) ?[*]u8 { + const self: *CountingAllocator = @ptrCast(@alignCast(ctx)); + const ptr = self.child.rawAlloc(len, alignment, ret_addr) orelse return null; + self.account(0, len); + return ptr; + } + + fn resize(ctx: *anyopaque, memory: []u8, alignment: Alignment, new_len: usize, ret_addr: usize) bool { + const self: *CountingAllocator = @ptrCast(@alignCast(ctx)); + if (!self.child.rawResize(memory, alignment, new_len, ret_addr)) return false; + self.account(memory.len, new_len); + return true; + } + + fn remap(ctx: *anyopaque, memory: []u8, alignment: Alignment, new_len: usize, ret_addr: usize) ?[*]u8 { + const self: *CountingAllocator = @ptrCast(@alignCast(ctx)); + const ptr = self.child.rawRemap(memory, alignment, new_len, ret_addr) orelse return null; + self.account(memory.len, new_len); + return ptr; + } + + fn free(ctx: *anyopaque, memory: []u8, alignment: Alignment, ret_addr: usize) void { + const self: *CountingAllocator = @ptrCast(@alignCast(ctx)); + self.child.rawFree(memory, alignment, ret_addr); + self.account(memory.len, 0); + } +}; + +test "counting allocator tracks allocations, resizing, remapping, and freeing" { + var counting: CountingAllocator = .{ .child = std.testing.allocator }; + const allocator = counting.allocator(); + var bytes = try allocator.alloc(u8, 100); + try std.testing.expectEqual(100, counting.live_bytes); + if (allocator.resize(bytes, 50)) bytes = bytes[0..50]; + try std.testing.expectEqual(bytes.len, counting.live_bytes); + if (allocator.remap(bytes, 200)) |moved| bytes = moved; + try std.testing.expectEqual(bytes.len, counting.live_bytes); + bytes = try allocator.realloc(bytes, 400); + try std.testing.expectEqual(400, counting.live_bytes); + try std.testing.expect(counting.peak_bytes >= 400); + allocator.free(bytes); + try std.testing.expectEqual(0, counting.live_bytes); +} + +test "failed allocation does not change counts" { + var counting: CountingAllocator = .{ .child = std.testing.failing_allocator }; + try std.testing.expectError(error.OutOfMemory, counting.allocator().alloc(u8, 1)); + try std.testing.expectEqual(0, counting.live_bytes); + try std.testing.expectEqual(0, counting.peak_bytes); +} + +test "successful and failed size changes preserve accounting" { + var buffer: [512]u8 = undefined; + var fixed = std.heap.FixedBufferAllocator.init(&buffer); + var counting: CountingAllocator = .{ .child = fixed.allocator() }; + const allocator = counting.allocator(); + var bytes = try allocator.alloc(u8, 256); + try std.testing.expect(allocator.resize(bytes, 128)); + bytes = bytes[0..128]; + try std.testing.expectEqual(128, counting.live_bytes); + bytes = allocator.remap(bytes, 200) orelse return error.TestUnexpectedResult; + try std.testing.expectEqual(200, counting.live_bytes); + try std.testing.expect(!allocator.resize(bytes, 1024)); + try std.testing.expect(allocator.remap(bytes, 1024) == null); + try std.testing.expectEqual(200, counting.live_bytes); + try std.testing.expectEqual(256, counting.peak_bytes); + allocator.free(bytes); + try std.testing.expectEqual(0, counting.live_bytes); +} diff --git a/build.zig b/build.zig index f734fce..d6583b6 100644 --- a/build.zig +++ b/build.zig @@ -74,6 +74,15 @@ pub fn build(b: *std.Build) void { // --- Benchmarks --- const benches_path = "benches"; if (b.root.openDir(io, benches_path, .{ .iterate = true })) |benches_dir| { + const bench_test_module = b.createModule(.{ + .root_source_file = b.path("benches/util/comparison.zig"), + .target = target, + .optimize = optimize, + }); + bench_test_module.addImport("ordered", lib_module); + const bench_tests = b.addTest(.{ .root_module = bench_test_module }); + const run_bench_tests = b.addRunArtifact(bench_tests); + test_step.dependOn(&run_bench_tests.step); const bench_lib_module = b.createModule(.{ .root_source_file = lib_source, .target = target, @@ -101,6 +110,7 @@ pub fn build(b: *std.Build) void { }); b.installArtifact(bench_exe); const run_bench_cmd = b.addRunArtifact(bench_exe); + run_bench_cmd.addPassthruArgs(); const bench_step_name = b.fmt("bench-{s}", .{bench_name}); const bench_step_desc = b.fmt("Run the {s} benchmark", .{bench_name}); const bench_step = b.step(bench_step_name, bench_step_desc); diff --git a/src/ordered/skip_list_map.zig b/src/ordered/skip_list_map.zig index b13dd1c..6fee965 100644 --- a/src/ordered/skip_list_map.zig +++ b/src/ordered/skip_list_map.zig @@ -50,6 +50,7 @@ pub fn SkipListMap( fn init(allocator: std.mem.Allocator, key: K, value: V, level: u8) !*Node { const node = try allocator.create(Node); + errdefer allocator.destroy(node); node.key = key; node.value = value; node.forward = try allocator.alloc(?*Node, level + 1); @@ -82,6 +83,7 @@ pub fn SkipListMap( /// Returns `error.OutOfMemory` if allocation fails. pub fn init(allocator: std.mem.Allocator) !Self { const header = try allocator.create(Node); + errdefer allocator.destroy(header); header.key = undefined; header.value = undefined; header.forward = try allocator.alloc(?*Node, MAX_LEVEL); @@ -565,3 +567,14 @@ test "SkipListMap: minimum max level" { try std.testing.expectEqual(@as(i32, 2), list.get(2).?.*); } + +fn allocationFailureCheck(allocator: std.mem.Allocator) !void { + var list = try SkipListMap(i32, i32, i32Compare, 16).init(allocator); + defer list.deinit(); + try list.put(1, 10); + try list.put(2, 20); +} + +test "SkipListMap: allocation failures release partial nodes" { + try std.testing.checkAllAllocationFailures(std.testing.allocator, allocationFailureCheck, .{}); +} From c7dd2ff5490421dfd7e1f9f7493db8b3163ddae3 Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Sun, 25 Oct 2026 17:07:35 +0100 Subject: [PATCH 6/6] WIP --- benches/README.md | 34 +++++++++-- benches/util/comparison.zig | 110 +++++++++++++++++++++++++----------- 2 files changed, 106 insertions(+), 38 deletions(-) diff --git a/benches/README.md b/benches/README.md index c9fa139..9e423b4 100644 --- a/benches/README.md +++ b/benches/README.md @@ -61,6 +61,9 @@ make bench BENCHMARK=b8_memory_comparison BENCH_ARGS=--quick make bench BENCHMARK=b7_std_comparison BENCH_ARGS=--large make bench BENCHMARK=b8_memory_comparison BENCH_ARGS=--large +# Every large timing case with one sample after one warmup. +make bench BENCHMARK=b7_std_comparison BENCH_ARGS='--large --quick' + # Equivalent direct Zig invocation. zig build bench-b7_std_comparison -- --large ``` @@ -72,6 +75,7 @@ zig build bench-b7_std_comparison -- --large | Default | 1,000, 10,000, and 100,000 | `u64`, 16-byte strings, and 100-byte strings | `u64` and 32-byte records | 10,000 maps each of 4 and 32 entries | | `--quick` | 1,000 | Same as default | Same as default | 128 maps each of 4 and 32 entries | | `--large` | 1,000,000 and 10,000,000 | `u64` | `u64` | None | +| `--large --quick` | Same as large | `u64` | `u64` | None | All containers use `std.heap.smp_allocator` and ReleaseFast optimization for both the runner and imported library. The large mode runs separately so routine benchmarks do not construct 10M-entry maps. String and larger-value @@ -86,7 +90,9 @@ Each single-map workload runs with ascending and shuffled insertion orders. Look permutation generated with a different fixed seed, regardless of insertion order. Every container receives the same workload. Container priorities and skip-list levels use normal initialization and may vary between runs. Key buffers and operation permutations are generated before measurement and kept alive until all containers finish. -Strings are borrowed from shared buffers; key allocation and copying are excluded. +Strings are borrowed from shared buffers; string payload allocation and copying are excluded. +Successful lookup and removal use separately stored, byte-identical string keys, so pointer equality cannot bypass +content comparison. ##### Timing Measurements @@ -95,13 +101,17 @@ use `getPtr` consistently, and successful lookups and iteration consume every wo hit counts, removal counts, and container sizes are checked outside timed regions. Each phase runs once per map; lookup, iteration, and removal follow insertion on the same populated container. Iterator creation and cleanup are included in iteration time. Container initialization and final cleanup are excluded for single-map workloads. +Insertion and iteration visit every entry. Lookup and removal each visit at most 100,000 keys from their independent +permutations. Their timings are divided by the number of sampled keys, and the remaining entries are freed outside +the removal timer. This keeps large-map queries bounded while preserving the full working set. Default and quick +workloads visit every key because their maps fit within that limit. Tiny-map workloads measure construction and destruction in nanoseconds per map, and iteration in nanoseconds per item, while all maps coexist. Construction includes the map array allocation, initialization, and insertion; destruction includes freeing all containers and the array. Maps share the same key buffers and insertion order. -Default and large timing runs report the median of five samples after one warmup. Quick runs use one sample after -one warmup and are intended for validation. CSV output goes to standard error, preceded by a metadata line. Tiny +Default and large timing runs report the median of five samples after one warmup. Adding `--quick`, including with +`--large`, uses one sample after one warmup for validation. CSV output goes to standard error, preceded by a metadata line. Tiny results form a second section with their own header and timing columns. ##### Memory Measurements @@ -117,11 +127,27 @@ change reported container memory. Peak bytes describe allocations visible throug internal temporary allocations made by the underlying allocator. Memory instrumentation is never used by the timing runner. Repeated runs may differ for randomized containers. +##### Sample Results + +The following results were measured on Linux x86_64 with an AMD Ryzen 9 9950X and Zig 0.17.0 on October 25, 2026, +using the native target. Each map held 100,000 shuffled `u64` keys and `u64` values. Timings are nanoseconds per +operation, except iteration, which is nanoseconds per item. Memory is live requested bytes per item from a separate +construction run, including inline container storage and excluding shared workload buffers. + +| Container | Insert | Lookup Hit | Lookup Miss | Iteration | Remove | Bytes per Item | +|-----------|-------:|-----------:|------------:|----------:|-------:|---------------:| +| `BTreeMap` | 86.30 | 123.21 | 134.28 | 2.52 | 130.85 | 36.83 | +| `CartesianTreeMap` | 222.54 | 141.05 | 154.62 | 9.08 | 159.50 | 40.00 | +| `SkipListMap` | 195.68 | 202.16 | 197.31 | 18.43 | 194.13 | 47.99 | +| `std.Treap` | 137.92 | 102.40 | 102.92 | 15.26 | 129.29 | 48.00 | +| `std.AutoHashMap` (unordered) | 29.11 | 10.61 | 19.91 | 2.43 | 11.60 | 22.28 | + ##### Validation and Limits `make test` includes benchmark tests for adapters, both value sizes, both string lengths, independent permutations, interleaved misses, duplicate updates, missing removals, empty iteration, allocator accounting, and cleanup after -allocation failures. `make lint` includes benchmark sources and `build.zig`. +allocation failures. A workload above the query limit checks sampled lookups and partial removal. `make lint` +includes benchmark sources and `build.zig`. Measurements are single-threaded, with no CPU pinning or frequency control. This suite does not measure concurrent access, interleaved mutations, prefix queries, or range scans. It adds no external dependencies and includes no Rust diff --git a/benches/util/comparison.zig b/benches/util/comparison.zig index ffe2db9..3435cba 100644 --- a/benches/util/comparison.zig +++ b/benches/util/comparison.zig @@ -3,7 +3,7 @@ const ordered = @import("ordered"); const Timer = @import("timer.zig").Timer; const CountingAllocator = @import("counting_allocator.zig").CountingAllocator; -const Mode = enum { normal, quick, large }; +const PROBE_LIMIT = 100_000; const NAMES = .{ "BTreeMap", "CartesianTreeMap", "SkipListMap", "std.Treap", "std.HashMap (unordered)" }; fn check(ok: bool) !void { @@ -93,6 +93,7 @@ fn Suite(comptime K: type, comptime V: type) type { const Dataset = struct { present: []K, absent: []K, + queries: []K, bytes: []u8, insertion: []u32, lookup: []u32, @@ -103,18 +104,26 @@ fn Suite(comptime K: type, comptime V: type) type { errdefer allocator.free(present); const absent = try allocator.alloc(K, size); errdefer allocator.free(absent); - const bytes = try allocator.alloc(u8, if (K == u64) 0 else size * 2 * key_len); + const queries = try allocator.alloc(K, size); + errdefer allocator.free(queries); + const bytes = try allocator.alloc(u8, if (K == u64) 0 else size * 3 * key_len); errdefer allocator.free(bytes); for (0..size) |i| { if (K == u64) { present[i] = 2 * i; absent[i] = 2 * i + 1; + queries[i] = present[i]; } else { - for (0..2) |which| { - const key = bytes[(2 * i + which) * key_len ..][0..key_len]; + for (0..3) |which| { + const key = bytes[(3 * i + which) * key_len ..][0..key_len]; @memset(key, 'a'); - _ = try std.fmt.bufPrint(key[key_len - 16 ..], "{x:0>16}", .{2 * i + which}); - if (which == 0) present[i] = key else absent[i] = key; + _ = try std.fmt.bufPrint(key[key_len - 16 ..], "{x:0>16}", .{2 * i + which % 2}); + switch (which) { + 0 => present[i] = key, + 1 => absent[i] = key, + 2 => queries[i] = key, + else => unreachable, + } } } } @@ -131,7 +140,7 @@ fn Suite(comptime K: type, comptime V: type) type { } shuffle(lookup, 0x10_0c); shuffle(removal, 0xde_1e); - return .{ .present = present, .absent = absent, .bytes = bytes, .insertion = insertion, .lookup = lookup, .removal = removal }; + return .{ .present = present, .absent = absent, .queries = queries, .bytes = bytes, .insertion = insertion, .lookup = lookup, .removal = removal }; } fn deinit(self: Dataset, allocator: std.mem.Allocator) void { @@ -139,6 +148,7 @@ fn Suite(comptime K: type, comptime V: type) type { allocator.free(self.lookup); allocator.free(self.insertion); allocator.free(self.bytes); + allocator.free(self.queries); allocator.free(self.absent); allocator.free(self.present); } @@ -182,20 +192,23 @@ fn Suite(comptime K: type, comptime V: type) type { for (data.insertion) |i| try map.put(data.present[i], value(@as(u64, i) + 1)); times[0] = timer.read(); try check(map.count() == data.present.len); + const probes = data.lookup[0..@min(data.lookup.len, PROBE_LIMIT)]; + var expected_hits: u64 = 0; + for (probes) |i| expected_hits += fingerprint(value(@as(u64, i) + 1)); var sum: u64 = 0; var hits: usize = 0; _ = timer.lap(); - for (data.lookup) |i| { - if (map.getPtr(data.present[i])) |v| { + for (probes) |i| { + if (map.getPtr(data.queries[i])) |v| { sum += fingerprint(v.*); hits += 1; } } times[1] = timer.read(); - try check(hits == data.present.len and sum == expectedSum(data.present.len)); + try check(hits == probes.len and sum == expected_hits); hits = 0; _ = timer.lap(); - for (data.lookup) |i| { + for (probes) |i| { if (map.getPtr(data.absent[i]) != null) hits += 1; } times[2] = timer.read(); @@ -206,11 +219,11 @@ fn Suite(comptime K: type, comptime V: type) type { try check(sum == expectedSum(data.present.len)); var removed: usize = 0; _ = timer.lap(); - for (data.removal) |i| { - if (remove(Map, &map, data.present[i])) removed += 1; + for (data.removal[0..probes.len]) |i| { + if (remove(Map, &map, data.queries[i])) removed += 1; } times[4] = timer.read(); - try check(removed == data.present.len and map.count() == 0); + try check(removed == probes.len and map.count() == data.present.len - probes.len); return times; } @@ -258,9 +271,9 @@ fn Suite(comptime K: type, comptime V: type) type { try check(try iterationSum(Map, &map, allocator) == 0); try check(!remove(Map, &map, data.present[0])); try map.put(data.present[0], value(1)); - try map.put(data.present[0], value(2)); - try check(map.count() == 1 and fingerprint(map.getPtr(data.present[0]).?.*) == fingerprint(value(2))); - try check(remove(Map, &map, data.present[0]) and map.count() == 0); + try map.put(data.queries[0], value(2)); + try check(map.count() == 1 and fingerprint(map.getPtr(data.queries[0]).?.*) == fingerprint(value(2))); + try check(remove(Map, &map, data.queries[0]) and map.count() == 0); // Leave a live node to exercise cleanup as well as removal. try map.put(data.present[1], value(3)); _ = try measure(Map, allocator, data); @@ -302,7 +315,9 @@ fn Suite(comptime K: type, comptime V: type) type { } for (&samples, 0..) |*operation, i| { std.mem.sort(u64, operation[0..repeats], {}, std.sort.asc(u64)); - const divisor = if (!tiny) size else if (i == 1) size * tiny_count else tiny_count; + const divisor = if (!tiny) + (if (i == 1 or i == 2 or i == 4) @min(size, PROBE_LIMIT) else size) + else if (i == 1) size * tiny_count else tiny_count; const per_op = @as(f64, @floatFromInt(operation[repeats / 2])) / @as(f64, @floatFromInt(divisor)); std.debug.print(",{d:.2}", .{per_op}); } @@ -316,35 +331,39 @@ fn Suite(comptime K: type, comptime V: type) type { pub fn run(init: std.process.Init, comptime memory: bool) !void { const args = try init.minimal.args.toSlice(init.arena.allocator()); - var mode: Mode = .normal; - if (args.len > 2) return error.InvalidArguments; - if (args.len == 2) { - mode = if (std.mem.eql(u8, args[1], "--quick")) .quick else if (std.mem.eql(u8, args[1], "--large")) .large else { - std.debug.print("Usage: {s} [--quick|--large]\n", .{args[0]}); + var quick = false; + var large = false; + for (args[1..]) |arg| { + if (std.mem.eql(u8, arg, "--quick")) { + quick = true; + } else if (std.mem.eql(u8, arg, "--large")) { + large = true; + } else { + std.debug.print("Usage: {s} [--quick] [--large]\n", .{args[0]}); return error.InvalidArguments; - }; + } } const allocator = std.heap.smp_allocator; - const repeats: usize = if (mode == .quick) 1 else 5; - const sizes: []const usize = switch (mode) { - .normal => &.{ 1_000, 10_000, 100_000 }, - .quick => &.{1_000}, - .large => &.{ 1_000_000, 10_000_000 }, - }; - std.debug.print("ReleaseFast; smp_allocator; {s}; {d} measured runs after one warmup for timings\n", .{ if (memory) "requested live and peak bytes, excluding shared key buffers" else "median timings", repeats }); + const repeats: usize = if (quick) 1 else 5; + const sizes: []const usize = if (large) &.{ 1_000_000, 10_000_000 } else if (quick) &.{1_000} else &.{ 1_000, 10_000, 100_000 }; + if (memory) { + std.debug.print("ReleaseFast; smp_allocator; requested live and peak bytes; shared key buffers excluded\n", .{}); + } else { + std.debug.print("ReleaseFast; smp_allocator; median timings; samples={d}; warmups=1; query_limit={d}\n", .{ repeats, PROBE_LIMIT }); + } std.debug.print("container,keys,value_bytes,items,input,maps,{s}\n", .{if (memory) "live_bytes,peak_bytes,bytes/item" else "insert_ns/op,hit_ns/op,miss_ns/op,iterate_ns/item,remove_ns/op"}); for (sizes) |size| { try Suite(u64, u64).report(allocator, size, 0, repeats, memory, false, 0); - if (mode != .large) { + if (!large) { try Suite(u64, [4]u64).report(allocator, size, 0, repeats, memory, false, 0); inline for (.{ u64, [4]u64 }) |V| { for ([_]usize{ 16, 100 }) |key_len| try Suite([]const u8, V).report(allocator, size, key_len, repeats, memory, false, 0); } } } - if (mode != .large) { + if (!large) { std.debug.print("container,keys,value_bytes,items,input,maps,{s}\n", .{if (memory) "live_bytes,peak_bytes,bytes/item" else "build_ns/map,iterate_ns/item,destroy_ns/map"}); - for ([_]usize{ 4, 32 }) |size| try Suite([]const u8, [4]u64).report(allocator, size, 16, repeats, memory, true, if (mode == .quick) 128 else 10_000); + for ([_]usize{ 4, 32 }) |size| try Suite([]const u8, [4]u64).report(allocator, size, 16, repeats, memory, true, if (quick) 128 else 10_000); } } @@ -395,6 +414,14 @@ test "dataset and partially built maps release allocations on failure" { try std.testing.checkAllAllocationFailures(std.testing.allocator, allocationCheck, .{}); } +test "large workloads check sampled lookups and partial removal" { + const S = Suite(u64, u64); + const data = try S.Dataset.init(std.testing.allocator, PROBE_LIMIT + 3, 0); + defer data.deinit(std.testing.allocator); + _ = try S.measure(S.BTree, std.testing.allocator, data); + _ = try S.measure(S.HashMap, std.testing.allocator, data); +} + test "memory workload releases all measured allocations" { const S = Suite([]const u8, [4]u64); const data = try S.Dataset.init(std.testing.allocator, 32, 16); @@ -408,3 +435,18 @@ test "memory workload releases all measured allocations" { try std.testing.expectEqual(0, counting.live_bytes); } } + +test "string probes have equal contents at different addresses and misses sort between present keys" { + const S = Suite([]const u8, u64); + for ([_]usize{ 16, 100 }) |key_len| { + const data = try S.Dataset.init(std.testing.allocator, 17, key_len); + defer data.deinit(std.testing.allocator); + for (data.present, data.queries, data.absent, 0..) |present, query, absent, i| { + try std.testing.expectEqual(key_len, present.len); + try std.testing.expect(present.ptr != query.ptr); + try std.testing.expectEqualStrings(present, query); + try std.testing.expect(std.mem.order(u8, present, absent) == .lt); + if (i + 1 < data.present.len) try std.testing.expect(std.mem.order(u8, absent, data.present[i + 1]) == .lt); + } + } +}