├── requirements-dev.txt ├── pyproject.toml ├── Cargo.toml ├── benchmarks ├── test_bench.py └── histogram.svg ├── LICENSE-MIT ├── LICENSE ├── .gitignore ├── tests └── test_knockknock.py ├── Cargo.lock ├── README.md ├── .github └── workflows │ └── CI.yml └── src └── lib.rs /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest>=7.0.0,<8.0.0 2 | numpy>=2.1.0,<3.0.0; python_version >= "3.10" 3 | numpy>=1.0.0,<2.0.0; python_version < "3.10" 4 | pytest-benchmark[histogram]>=4.0.0,<=5.0.0 5 | pytest-rerunfailures==11.1.2 6 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "gilknocker" 3 | keywords = ["GIL"] 4 | requires-python = ">=3.9" 5 | 6 | [project.urls] 7 | homepage = "https://github.com/milesgranger/gilknocker" 8 | repository = "https://github.com/milesgranger/gilknocker" 9 | 10 | [build-system] 11 | requires = ["maturin>=1.3.0,<2.0.0"] 12 | build-backend = "maturin" 13 | 14 | [tool.pytest.ini_options] 15 | addopts = "-v --reruns 3" 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gil-knocker" 3 | version = "0.4.2" 4 | edition = "2021" 5 | authors = ["Miles Granger "] 6 | license = "MIT" 7 | description = "Knock on the Python GIL, determine how busy it is." 8 | readme = "README.md" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [lib] 13 | name = "gilknocker" 14 | crate-type = ["cdylib", "rlib"] 15 | 16 | [profile.release] 17 | lto = "fat" 18 | codegen-units = 1 19 | opt-level = 3 20 | 21 | [dependencies] 22 | pyo3 = { git = "https://github.com/PyO3/pyo3.git", rev="90cc69b", features = ["extension-module"] } 23 | parking_lot = "^0.12" 24 | -------------------------------------------------------------------------------- /benchmarks/test_bench.py: -------------------------------------------------------------------------------- 1 | import time 2 | import random 3 | import pytest 4 | import numpy as np 5 | import threading 6 | from gilknocker import KnockKnock 7 | 8 | 9 | def a_lotta_gil(): 10 | """Keep the GIL busy""" 11 | for i in range(100_000_000): 12 | pass 13 | 14 | 15 | def a_little_gil(): 16 | """Work which releases the GIL""" 17 | for i in range(4): 18 | x = np.random.randn(2048, 2048) 19 | x[:] = np.fft.fft2(x).real 20 | 21 | 22 | def some_gil(): 23 | for i in range(10_000): 24 | time.sleep(random.random() / 100_000) 25 | _ = b"1" * 2048**2 26 | 27 | 28 | @pytest.mark.parametrize("interval", (None, 1, 10, 100, 1_000, 10_000)) 29 | @pytest.mark.parametrize("target", (a_lotta_gil, some_gil, a_little_gil)) 30 | def test_bench(benchmark, interval: int, target): 31 | if interval: 32 | knocker = KnockKnock(interval) 33 | knocker.start() 34 | 35 | benchmark(target) 36 | 37 | if interval: 38 | knocker.stop() 39 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Miles Granger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | flaco/*.so 3 | flaco/*.c 4 | *.html 5 | *.dat 6 | heaptrack.* 7 | .idea/ 8 | __pycache__/ 9 | 10 | # Distribution / packaging 11 | wheelhouse/ 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | *.cpp 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | prof/ 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # dotenv 86 | .env 87 | 88 | # virtualenv 89 | .venv 90 | venv/ 91 | ENV/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site -------------------------------------------------------------------------------- /tests/test_knockknock.py: -------------------------------------------------------------------------------- 1 | import random 2 | import pytest 3 | import numpy as np 4 | import threading 5 | import time 6 | from gilknocker import KnockKnock 7 | 8 | 9 | N_THREADS = 4 10 | N_PTS = 2048 11 | 12 | 13 | def a_lotta_gil(): 14 | """Keep the GIL busy""" 15 | for i in range(100_000_000): 16 | pass 17 | 18 | 19 | def a_little_gil(): 20 | """Work which releases the GIL""" 21 | for i in range(5): 22 | x = np.random.randn(N_PTS, N_PTS) 23 | x[:] = np.fft.fft2(x).real 24 | 25 | 26 | def some_gil(): 27 | for i in range(10_000): 28 | time.sleep(random.random() / 100_000) 29 | _ = b"1" * 2048**2 30 | 31 | 32 | def _run(target): 33 | knocker = KnockKnock(polling_interval_micros=1000) 34 | knocker.start() 35 | threads = [] 36 | for i in range(N_THREADS): 37 | thread = threading.Thread(target=target, daemon=True) 38 | threads.append(thread) 39 | thread.start() 40 | 41 | for thread in threads: 42 | thread.join() 43 | print(f"Metric: {knocker.contention_metric}") 44 | return knocker 45 | 46 | 47 | def test_knockknock_busy(): 48 | knocker = _run(a_lotta_gil) 49 | 50 | try: 51 | # usually ~0.9, but sometimes ~0.6 on Mac 52 | assert knocker.contention_metric > 0.6 53 | 54 | # Now wait for it to 'cool' back down 55 | # by looping over some work which releases the GIL 56 | prev_cm = knocker.contention_metric 57 | for i in range(10): 58 | a_little_gil() 59 | assert knocker.contention_metric < prev_cm 60 | prev_cm = knocker.contention_metric 61 | finally: 62 | knocker.stop() 63 | 64 | 65 | def test_knockknock_available_gil(): 66 | knocker = _run(a_little_gil) 67 | 68 | try: 69 | # usually ~0.002, but can be up to ~0.15 on windows 70 | assert knocker.contention_metric < 0.2 71 | finally: 72 | knocker.stop() 73 | 74 | 75 | def test_knockknock_some_gil(): 76 | knocker = _run(some_gil) 77 | 78 | try: 79 | # usually ~0.75ish, but ~0.4 on mac 80 | assert 0.2 < knocker.contention_metric < 0.9 81 | finally: 82 | knocker.stop() 83 | 84 | 85 | def test_knockknock_reset_contention_metric(): 86 | knocker = _run(a_lotta_gil) 87 | 88 | try: 89 | assert knocker.contention_metric > 0.6 90 | knocker.reset_contention_metric() 91 | assert knocker.contention_metric < 0.001 92 | 93 | finally: 94 | knocker.stop() 95 | 96 | 97 | # Manual verification with py-spy 98 | # busy should give high GIL % 99 | if __name__ == "__main__": 100 | test_knockknock_busy() 101 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "autocfg" 7 | version = "1.1.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 10 | 11 | [[package]] 12 | name = "bitflags" 13 | version = "1.3.2" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 16 | 17 | [[package]] 18 | name = "cfg-if" 19 | version = "1.0.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 22 | 23 | [[package]] 24 | name = "gil-knocker" 25 | version = "0.4.2" 26 | dependencies = [ 27 | "parking_lot", 28 | "pyo3", 29 | ] 30 | 31 | [[package]] 32 | name = "heck" 33 | version = "0.4.1" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 36 | 37 | [[package]] 38 | name = "indoc" 39 | version = "2.0.4" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" 42 | 43 | [[package]] 44 | name = "libc" 45 | version = "0.2.148" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" 48 | 49 | [[package]] 50 | name = "lock_api" 51 | version = "0.4.10" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" 54 | dependencies = [ 55 | "autocfg", 56 | "scopeguard", 57 | ] 58 | 59 | [[package]] 60 | name = "memoffset" 61 | version = "0.9.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" 64 | dependencies = [ 65 | "autocfg", 66 | ] 67 | 68 | [[package]] 69 | name = "once_cell" 70 | version = "1.18.0" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 73 | 74 | [[package]] 75 | name = "parking_lot" 76 | version = "0.12.1" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 79 | dependencies = [ 80 | "lock_api", 81 | "parking_lot_core", 82 | ] 83 | 84 | [[package]] 85 | name = "parking_lot_core" 86 | version = "0.9.8" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" 89 | dependencies = [ 90 | "cfg-if", 91 | "libc", 92 | "redox_syscall", 93 | "smallvec", 94 | "windows-targets", 95 | ] 96 | 97 | [[package]] 98 | name = "proc-macro2" 99 | version = "1.0.67" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" 102 | dependencies = [ 103 | "unicode-ident", 104 | ] 105 | 106 | [[package]] 107 | name = "pyo3" 108 | version = "0.19.2" 109 | source = "git+https://github.com/PyO3/pyo3.git?rev=90cc69b#90cc69ba73eaad3567bd660db3f531bafaadf583" 110 | dependencies = [ 111 | "cfg-if", 112 | "indoc", 113 | "libc", 114 | "memoffset", 115 | "parking_lot", 116 | "pyo3-build-config", 117 | "pyo3-ffi", 118 | "pyo3-macros", 119 | "unindent", 120 | ] 121 | 122 | [[package]] 123 | name = "pyo3-build-config" 124 | version = "0.19.2" 125 | source = "git+https://github.com/PyO3/pyo3.git?rev=90cc69b#90cc69ba73eaad3567bd660db3f531bafaadf583" 126 | dependencies = [ 127 | "once_cell", 128 | "target-lexicon", 129 | ] 130 | 131 | [[package]] 132 | name = "pyo3-ffi" 133 | version = "0.19.2" 134 | source = "git+https://github.com/PyO3/pyo3.git?rev=90cc69b#90cc69ba73eaad3567bd660db3f531bafaadf583" 135 | dependencies = [ 136 | "libc", 137 | "pyo3-build-config", 138 | ] 139 | 140 | [[package]] 141 | name = "pyo3-macros" 142 | version = "0.19.2" 143 | source = "git+https://github.com/PyO3/pyo3.git?rev=90cc69b#90cc69ba73eaad3567bd660db3f531bafaadf583" 144 | dependencies = [ 145 | "proc-macro2", 146 | "pyo3-macros-backend", 147 | "quote", 148 | "syn", 149 | ] 150 | 151 | [[package]] 152 | name = "pyo3-macros-backend" 153 | version = "0.19.2" 154 | source = "git+https://github.com/PyO3/pyo3.git?rev=90cc69b#90cc69ba73eaad3567bd660db3f531bafaadf583" 155 | dependencies = [ 156 | "heck", 157 | "proc-macro2", 158 | "quote", 159 | "syn", 160 | ] 161 | 162 | [[package]] 163 | name = "quote" 164 | version = "1.0.33" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 167 | dependencies = [ 168 | "proc-macro2", 169 | ] 170 | 171 | [[package]] 172 | name = "redox_syscall" 173 | version = "0.3.5" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" 176 | dependencies = [ 177 | "bitflags", 178 | ] 179 | 180 | [[package]] 181 | name = "scopeguard" 182 | version = "1.2.0" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 185 | 186 | [[package]] 187 | name = "smallvec" 188 | version = "1.11.1" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" 191 | 192 | [[package]] 193 | name = "syn" 194 | version = "2.0.37" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" 197 | dependencies = [ 198 | "proc-macro2", 199 | "quote", 200 | "unicode-ident", 201 | ] 202 | 203 | [[package]] 204 | name = "target-lexicon" 205 | version = "0.12.11" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" 208 | 209 | [[package]] 210 | name = "unicode-ident" 211 | version = "1.0.12" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 214 | 215 | [[package]] 216 | name = "unindent" 217 | version = "0.2.3" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" 220 | 221 | [[package]] 222 | name = "windows-targets" 223 | version = "0.48.5" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 226 | dependencies = [ 227 | "windows_aarch64_gnullvm", 228 | "windows_aarch64_msvc", 229 | "windows_i686_gnu", 230 | "windows_i686_msvc", 231 | "windows_x86_64_gnu", 232 | "windows_x86_64_gnullvm", 233 | "windows_x86_64_msvc", 234 | ] 235 | 236 | [[package]] 237 | name = "windows_aarch64_gnullvm" 238 | version = "0.48.5" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 241 | 242 | [[package]] 243 | name = "windows_aarch64_msvc" 244 | version = "0.48.5" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 247 | 248 | [[package]] 249 | name = "windows_i686_gnu" 250 | version = "0.48.5" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 253 | 254 | [[package]] 255 | name = "windows_i686_msvc" 256 | version = "0.48.5" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 259 | 260 | [[package]] 261 | name = "windows_x86_64_gnu" 262 | version = "0.48.5" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 265 | 266 | [[package]] 267 | name = "windows_x86_64_gnullvm" 268 | version = "0.48.5" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 271 | 272 | [[package]] 273 | name = "windows_x86_64_msvc" 274 | version = "0.48.5" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 277 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## GIL Knocker 2 | 3 | 4 | `pip install gilknocker` 5 | 6 | 7 | [![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black) 8 | [![CI](https://github.com/milesgranger/gilknocker/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/milesgranger/gilknocker/actions/workflows/CI.yml) 9 | [![PyPI](https://img.shields.io/pypi/v/gilknocker.svg)](https://pypi.org/project/gilknocker) 10 | ![PyPI - Wheel](https://img.shields.io/pypi/wheel/gilknocker) 11 | [![Downloads](https://pepy.tech/badge/gilknocker/month)](https://pepy.tech/project/gilknocker) 12 | 13 | 14 | When you thought the GIL was available, and you find yourself suspecting it might be spending time 15 | with another. 16 | 17 | You probably want [py-spy](https://github.com/benfred/py-spy), however if you're 18 | looking for a quick-and-dirty way to slip in a GIL contention metric within a specific 19 | chunk of code, this might help you. 20 | 21 | ### How? 22 | 23 | Unfortunately, there doesn't appear to be any explicit C-API for checking how busy 24 | the GIL is. [PyGILState_Check](https://docs.python.org/3/c-api/init.html#c.PyGILState_Check) 25 | won't really work, that's limited to the current thread. 26 | [PyInterpreterState](https://docs.python.org/3/c-api/init.html#c.PyGILState_Check) 27 | is an opaque struct, and the [PyRuntimeState](https://github.com/python/cpython/blob/main/Include/internal/pycore_pystate.h) 28 | and other goodies are private in CPython. 29 | 30 | So, in ~200 lines of Rusty code, I've conjured up a basic metric that seems 31 | to align with what is reported by `py-spy` when running the same [test case](./tests/test_knockknock.py). 32 | This works by spawning a thread which, at regular intervals, re-acquires the GIL and checks 33 | how long it took for the GIL to answer. 34 | 35 | Note, the `polling_interval_micros`, `sampling_interval_micros`, and `sleeping_interval_micros` 36 | are configurable. 37 | 38 | - `polling_interval_micros` 39 | - How frequently to re-acquire the GIL and measure how long it took to acquire. The more frequent, the 40 | more likely the contention metric will represent accurate GIL contention. A good value for this is 1-1000. 41 | 42 | - `sampling_interval_micros` 43 | - How _long_ to run the polling routine. If this is 1ms, then for 1ms it will try to re-acquire the GIL 44 | at `polling_interval_micros` frequency. Defaults to 10x `polling_interval_micros` 45 | 46 | - `sleeping_interval_micros` 47 | - How long to sleep between sampling routines. Defaults to 100x `polling_interval_micros` 48 | 49 | 50 | ### Use 51 | 52 | Look at the [tests](./tests) 53 | 54 | ```python 55 | 56 | from gilknocker import KnockKnock 57 | 58 | # These two are equivalent. 59 | knocker = KnockKnock(1_000) 60 | knocker = KnockKnock( 61 | polling_interval_micros=1_000, 62 | sampling_interval_micros=10_000, 63 | sleeping_interval_micros=100_000 64 | ) 65 | knocker.start() 66 | 67 | ... smart code here ... 68 | 69 | knocker.contention_metric # float between 0-1 indicating roughly how busy the GIL was. 70 | knocker.reset_contention_metric() # reset timers and meteric calculation 71 | 72 | ... some more smart code ... 73 | 74 | knocker.stop() 75 | knocker.stop() # Idempodent stopping behavior 76 | 77 | knocker.contention_metric # will stay the same after `stop()` is called. 78 | 79 | knocker.is_running # If you're ever in doubt 80 | 81 | ``` 82 | 83 | ### How will this impact my program? 84 | 85 | Short answer, it depends, but probably not much. As stated above, the more frequent the 86 | polling and sampling interval, the more likely non-GIL bound programs will be affected, since there is 87 | more room for contention. In GIL heavy programs, the monitoring thread will spend most of its 88 | time simply waiting for a lock. This is demonstrated in the [benchmarks](./benchmarks) testing. 89 | 90 | In general, it appears that `polling_interval_micros=1_000` is a good tradeoff in terms of accurate 91 | GIL contention metric and the resulting `sampling_interval_micros=10_000` (defaults to 10x polling interval) 92 | is high enough to relax performance impact a bit when combined with `sleeping_interval_micros=100_000` 93 | (defaults to 100x polling interval); but feel free to play with these to conform to your needs. 94 | 95 | Below is a summary of benchmarking two different 96 | functions, one which uses the GIL, and one which releases it. For `interval=None` this means 97 | no polling was used, effectively just running the function without `gilknocker`. Otherwise, 98 | the interval represents the value passed to `KnockKnock(polling_interval_micros=interval)` 99 | 100 | `python -m pytest -v --benchmark-only benchmarks/ --benchmark-histogram` 101 | 102 | ``` 103 | ------------------------------------------------------------------------------------ benchmark: 18 tests ------------------------------------------------------------------------------------- 104 | Name (time in s) Min Max Mean StdDev Median IQR Outliers OPS Rounds Iterations 105 | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 106 | test_bench[a_little_gil-100000] 1.5368 (2.07) 1.6596 (2.23) 1.5968 (2.14) 0.0476 (130.12) 1.5943 (2.14) 0.0719 (140.14) 2;0 0.6262 (0.47) 5 1 107 | test_bench[a_little_gil-10000] 1.5321 (2.06) 1.5989 (2.14) 1.5618 (2.09) 0.0289 (78.95) 1.5610 (2.09) 0.0510 (99.52) 2;0 0.6403 (0.48) 5 1 108 | test_bench[a_little_gil-1000] 1.5246 (2.05) 1.5298 (2.05) 1.5271 (2.05) 0.0019 (5.12) 1.5269 (2.05) 0.0021 (4.00) 2;0 0.6549 (0.49) 5 1 109 | test_bench[a_little_gil-100] 1.5505 (2.09) 1.5543 (2.08) 1.5528 (2.08) 0.0014 (3.96) 1.5533 (2.08) 0.0018 (3.60) 2;0 0.6440 (0.48) 5 1 110 | test_bench[a_little_gil-10] 1.5863 (2.13) 1.6074 (2.16) 1.5928 (2.14) 0.0088 (23.94) 1.5896 (2.13) 0.0111 (21.60) 1;0 0.6278 (0.47) 5 1 111 | test_bench[a_little_gil-None] 1.5043 (2.02) 1.5067 (2.02) 1.5051 (2.02) 0.0011 (2.95) 1.5044 (2.02) 0.0016 (3.17) 1;0 0.6644 (0.50) 5 1 112 | test_bench[a_lotta_gil-100000] 0.7450 (1.00) 0.7458 (1.0) 0.7455 (1.0) 0.0004 (1.0) 0.7457 (1.0) 0.0005 (1.0) 1;0 1.3413 (1.0) 5 1 113 | test_bench[a_lotta_gil-10000] 0.7471 (1.00) 0.8104 (1.09) 0.7601 (1.02) 0.0281 (76.94) 0.7472 (1.00) 0.0168 (32.82) 1;1 1.3156 (0.98) 5 1 114 | test_bench[a_lotta_gil-1000] 0.7436 (1.0) 0.7472 (1.00) 0.7463 (1.00) 0.0015 (4.11) 0.7470 (1.00) 0.0013 (2.54) 1;1 1.3400 (1.00) 5 1 115 | test_bench[a_lotta_gil-100] 0.7558 (1.02) 0.7680 (1.03) 0.7640 (1.02) 0.0050 (13.56) 0.7644 (1.03) 0.0061 (11.97) 1;0 1.3089 (0.98) 5 1 116 | test_bench[a_lotta_gil-10] 0.7542 (1.01) 0.7734 (1.04) 0.7649 (1.03) 0.0084 (23.05) 0.7669 (1.03) 0.0151 (29.45) 2;0 1.3074 (0.97) 5 1 117 | test_bench[a_lotta_gil-None] 0.7437 (1.00) 0.8490 (1.14) 0.8006 (1.07) 0.0501 (137.15) 0.8074 (1.08) 0.0969 (189.03) 1;0 1.2490 (0.93) 5 1 118 | test_bench[some_gil-100000] 1.4114 (1.90) 1.4131 (1.89) 1.4122 (1.89) 0.0007 (1.81) 1.4121 (1.89) 0.0010 (2.00) 2;0 0.7081 (0.53) 5 1 119 | test_bench[some_gil-10000] 1.4115 (1.90) 1.4258 (1.91) 1.4167 (1.90) 0.0059 (16.03) 1.4141 (1.90) 0.0083 (16.19) 1;0 0.7058 (0.53) 5 1 120 | test_bench[some_gil-1000] 1.4169 (1.91) 1.5793 (2.12) 1.4618 (1.96) 0.0690 (188.82) 1.4232 (1.91) 0.0769 (150.04) 1;0 0.6841 (0.51) 5 1 121 | test_bench[some_gil-100] 1.4468 (1.95) 1.6261 (2.18) 1.5701 (2.11) 0.0752 (205.83) 1.5998 (2.15) 0.1004 (195.70) 1;0 0.6369 (0.47) 5 1 122 | test_bench[some_gil-10] 1.5269 (2.05) 1.9894 (2.67) 1.7037 (2.29) 0.1895 (518.49) 1.7301 (2.32) 0.2692 (524.96) 1;0 0.5870 (0.44) 5 1 123 | test_bench[some_gil-None] 1.4115 (1.90) 1.4267 (1.91) 1.4155 (1.90) 0.0063 (17.33) 1.4136 (1.90) 0.0053 (10.24) 1;1 0.7065 (0.53) 5 1 124 | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 125 | ``` 126 | 127 | ![](./benchmarks/histogram.svg) 128 | 129 | --- 130 | 131 | ### License 132 | 133 | [Unlicense](LICENSE) or [MIT](LICENSE-MIT), at your discretion. 134 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | release: 9 | types: 10 | - released 11 | - prereleased 12 | - edited 13 | 14 | jobs: 15 | macos: 16 | runs-on: macos-latest 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] 21 | steps: 22 | - uses: actions/checkout@v5 23 | - uses: actions/setup-python@v6 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install Rust toolchain 27 | uses: dtolnay/rust-toolchain@stable 28 | with: 29 | targets: aarch64-apple-darwin 30 | - name: Build wheels - universal2 31 | uses: PyO3/maturin-action@v1 32 | with: 33 | target: universal2-apple-darwin 34 | args: -i python --release --out dist 35 | - name: Install built wheel 36 | run: | 37 | pip install gilknocker --no-index --find-links dist --force-reinstall 38 | - name: Python UnitTest 39 | run: | 40 | pip install -r requirements-dev.txt 41 | python -m pytest tests 42 | - name: Upload wheels 43 | uses: actions/upload-artifact@v4 44 | with: 45 | name: wheels-macos-${{ matrix.python-version }} 46 | path: dist 47 | 48 | windows: 49 | runs-on: windows-latest 50 | strategy: 51 | fail-fast: false 52 | matrix: 53 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] 54 | target: [x64, x86] 55 | steps: 56 | - uses: actions/checkout@v5 57 | - uses: actions/setup-python@v6 58 | with: 59 | python-version: ${{ matrix.python-version }} 60 | architecture: ${{ matrix.target }} 61 | - name: Install Rust toolchain 62 | uses: dtolnay/rust-toolchain@stable 63 | - name: Build wheels 64 | uses: PyO3/maturin-action@v1 65 | with: 66 | target: ${{ matrix.target }} 67 | args: -i python --release --out dist 68 | - name: Install built wheel 69 | run: | 70 | pip install gilknocker --no-index --find-links dist --force-reinstall 71 | - name: Python UnitTest 72 | run: | 73 | python -m pip install -r requirements-dev.txt 74 | python -m pytest -vs tests 75 | - name: Upload wheels 76 | uses: actions/upload-artifact@v4 77 | with: 78 | name: wheels-windows-${{ matrix.python-version }}-${{ matrix.target }} 79 | path: dist 80 | 81 | linux: 82 | runs-on: ubuntu-latest 83 | strategy: 84 | fail-fast: false 85 | matrix: 86 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] 87 | target: [x86_64, i686] 88 | steps: 89 | - uses: actions/checkout@v5 90 | - name: Install Rust toolchain 91 | uses: dtolnay/rust-toolchain@stable 92 | - uses: actions/setup-python@v6 93 | with: 94 | python-version: ${{ matrix.python-version }} 95 | - name: Build Wheels 96 | uses: PyO3/maturin-action@v1 97 | with: 98 | target: ${{ matrix.target }} 99 | manylinux: auto 100 | args: -i ${{ matrix.python-version }} --release --out dist 101 | - name: Python UnitTest 102 | if: matrix.target == 'x86_64' 103 | run: | 104 | pip install gilknocker --no-index --find-links dist --force-reinstall 105 | python -m pip install -r requirements-dev.txt 106 | python -m pytest -vs tests 107 | - name: Upload wheels 108 | uses: actions/upload-artifact@v4 109 | with: 110 | name: wheels-linux-${{ matrix.python-version }}-${{ matrix.target }} 111 | path: dist 112 | 113 | pypy-linux: 114 | runs-on: ubuntu-latest 115 | strategy: 116 | fail-fast: false 117 | matrix: 118 | python-version: [pypy-3.9, pypy-3.10, pypy-3.11] 119 | steps: 120 | - uses: actions/checkout@v5 121 | - uses: actions/cache@v4 122 | with: 123 | path: | 124 | ~/.cargo/bin/ 125 | ~/.cargo/registry/index/ 126 | ~/.cargo/registry/cache/ 127 | ~/.cargo/git/db/ 128 | target/ 129 | key: ${{ runner.os }}-${{matrix.python-version}}-cargo-${{ hashFiles('**/Cargo.lock') }} 130 | - uses: actions/setup-python@v6 131 | with: 132 | python-version: ${{ matrix.python-version }} 133 | - name: Build Wheels - gilknocker 134 | uses: PyO3/maturin-action@v1 135 | with: 136 | manylinux: auto 137 | args: -i ${{ matrix.python-version }} --release --out dist 138 | - name: Python UnitTest - gilknocker 139 | run: | 140 | pip install gilknocker --no-index --find-links dist 141 | pypy -c "import gilknocker" 142 | - name: Upload wheels 143 | uses: actions/upload-artifact@v4 144 | with: 145 | name: wheels-pypy-linux-${{ matrix.python-version }} 146 | path: dist 147 | 148 | pypy-macos: 149 | runs-on: macos-latest 150 | strategy: 151 | fail-fast: false 152 | matrix: 153 | python-version: [pypy-3.9, pypy-3.10, pypy-3.11] 154 | steps: 155 | - uses: actions/checkout@v5 156 | - uses: actions/cache@v4 157 | with: 158 | path: | 159 | ~/.cargo/bin/ 160 | ~/.cargo/registry/index/ 161 | ~/.cargo/registry/cache/ 162 | ~/.cargo/git/db/ 163 | target/ 164 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 165 | - name: Install Rust toolchain 166 | uses: dtolnay/rust-toolchain@stable 167 | - uses: actions/setup-python@v6 168 | with: 169 | python-version: ${{ matrix.python-version }} 170 | - name: Install maturin 171 | run: pip install maturin 172 | - name: Build Wheels - gilknocker 173 | uses: PyO3/maturin-action@v1 174 | with: 175 | manylinux: auto 176 | args: -i ${{ matrix.python-version }} --release --out dist 177 | - name: Install wheel 178 | run: | 179 | pip install gilknocker --no-index --find-links dist 180 | - name: Python Import test 181 | run: pypy -c "import gilknocker" 182 | - name: Upload wheels 183 | uses: actions/upload-artifact@v4 184 | with: 185 | name: wheels-pypy-macos-${{ matrix.python-version }} 186 | path: dist 187 | 188 | linux-cross: 189 | runs-on: ubuntu-latest 190 | strategy: 191 | fail-fast: false 192 | matrix: 193 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] 194 | target: [aarch64, armv7, s390x, ppc64le] 195 | include: 196 | - python-version: pypy-3.9 197 | target: aarch64 198 | steps: 199 | - uses: actions/checkout@v5 200 | - uses: actions/cache@v4 201 | with: 202 | path: | 203 | ~/.cargo/bin/ 204 | ~/.cargo/registry/index/ 205 | ~/.cargo/registry/cache/ 206 | ~/.cargo/git/db/ 207 | target/ 208 | key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }} 209 | - name: Build Wheels 210 | uses: PyO3/maturin-action@v1 211 | with: 212 | target: ${{ matrix.target }} 213 | manylinux: auto 214 | args: -i ${{ matrix.python-version }} --release --out dist 215 | - uses: uraimo/run-on-arch-action@v3 216 | # skipped cross compiled pypy wheel tests for now 217 | if: ${{ !startsWith(matrix.python-version, 'pypy') }} 218 | name: Install built wheel 219 | with: 220 | arch: ${{ matrix.target }} 221 | distro: ubuntu22.04 222 | githubToken: ${{ github.token }} 223 | # Mount the dist directory as /artifacts in the container 224 | dockerRunArgs: | 225 | --volume "${PWD}/dist:/artifacts" 226 | install: | 227 | apt-get update 228 | apt-get install -y --no-install-recommends python3 python3-venv software-properties-common gpg-agent qemu-user-static binfmt-support 229 | add-apt-repository ppa:deadsnakes/ppa 230 | apt-get update 231 | apt-get install -y curl python3.9-venv python3.10-venv python3.11-venv python3.12-venv python3.13-venv python3.14-venv 232 | run: | 233 | ls -lrth /artifacts 234 | PYTHON=python${{ matrix.python-version }} 235 | $PYTHON -m venv venv 236 | venv/bin/pip install -U pip 237 | venv/bin/pip install gilknocker --no-index --find-links /artifacts --force-reinstall 238 | venv/bin/python -c 'import gilknocker' 239 | - name: Upload wheels 240 | uses: actions/upload-artifact@v4 241 | with: 242 | name: wheels-linux-cross-${{ matrix.python-version }}-${{ matrix.target }} 243 | path: dist 244 | 245 | release: 246 | name: Release 247 | runs-on: ubuntu-latest 248 | if: startsWith(github.ref, 'refs/tags/') 249 | needs: [macos, windows, linux] 250 | steps: 251 | - uses: actions/download-artifact@v5 252 | with: 253 | pattern: wheels-* 254 | merge-multiple: true 255 | - uses: actions/setup-python@v6 256 | with: 257 | python-version: "3.13" 258 | - name: Publish to PyPi 259 | env: 260 | TWINE_USERNAME: __token__ 261 | TWINE_PASSWORD: ${{ secrets.TWINE_TOKEN }} 262 | run: | 263 | pip install --upgrade wheel pip setuptools twine 264 | twine upload --skip-existing * 265 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[deny(missing_docs)] 2 | use parking_lot::{const_rwlock, RwLock}; 3 | use pyo3::ffi::{PyEval_InitThreads, PyEval_ThreadsInitialized}; 4 | use pyo3::prelude::*; 5 | use pyo3::PyResult; 6 | use std::ops::DerefMut; 7 | use std::{ 8 | mem::take, 9 | sync::{ 10 | mpsc::{channel, Receiver, RecvTimeoutError, Sender}, 11 | Arc, 12 | }, 13 | thread, 14 | time::{Duration, Instant}, 15 | }; 16 | 17 | #[pymodule] 18 | fn gilknocker(_py: Python, m: &PyModule) -> PyResult<()> { 19 | m.add("__version__", env!("CARGO_PKG_VERSION"))?; 20 | m.add_class::()?; 21 | Ok(()) 22 | } 23 | 24 | /// Possible messages to pass to the monitoring thread. 25 | enum Message { 26 | Stop, 27 | Reset, 28 | } 29 | 30 | /// Acknowledgement from monitoring thread 31 | struct Ack; 32 | 33 | /// Struct for polling, knocking on the GIL, 34 | /// checking if it's locked in the current thread 35 | /// 36 | /// Example 37 | /// ------- 38 | /// ```python 39 | /// from gilknocker import KnockKnock 40 | /// knocker = KnockKnock(100) # try to reacquire the gil every 100 microseconds 41 | /// knocker.start() 42 | /// ... some smart code ... 43 | /// knocker.stop() 44 | /// knocker.contention_metric # float between 0-1 indicating GIL contention 45 | /// ``` 46 | #[pyclass(name = "KnockKnock")] 47 | #[derive(Default)] 48 | pub struct KnockKnock { 49 | handle: Option>, 50 | tx: Option>, 51 | rx: Option>, 52 | contention_metric: Arc>, 53 | polling_interval: Duration, 54 | sampling_interval: Duration, 55 | sleeping_interval: Duration, 56 | timeout: Duration, 57 | } 58 | 59 | #[pymethods] 60 | impl KnockKnock { 61 | /// Initialize with ``polling_interval_micros``, as the time between trying to acquire the GIL, 62 | /// ``sampling_interval_micros`` as the time between the polling routine, and ``timeout_secs`` 63 | /// as time to wait for monitoring thread to exit. 64 | /// 65 | /// A more frequent polling interval will give a more accurate reflection of actual GIL contention, 66 | /// and a more frequent sampling interval will increase the 'real time' reflection of GIL contention. 67 | /// Alternatively a less frequent sampling interval will come to reflect an average GIL contention of 68 | /// the running program. 69 | /// 70 | /// polling_interval_micros: Optional[int] 71 | /// How frequently to ask to aquire the GIL, defaults to 1_000 microseconds (1ms) 72 | /// sampling_interval_micros: Optional[int] 73 | /// How long to sample the GIL contention at polling interval, 74 | /// defaults to 10x polling_interval_micros. 75 | /// sleeping_interval_micros: Optional[int] 76 | /// How long to sleep without sampling the GIL, defaults to 100x polling_interval_micros. 77 | /// timeout_micros: Optional[int] 78 | /// Timeout when attempting to stop or send messages to monitoring thread. Defaults to 79 | /// max(sleeping_interval_micros, sampling_interval_micros, polling_interval_micros) + 1ms 80 | #[new] 81 | pub fn __new__( 82 | polling_interval_micros: Option, 83 | sampling_interval_micros: Option, 84 | sleeping_interval_micros: Option, 85 | timeout_micros: Option, 86 | ) -> PyResult { 87 | let polling_interval = 88 | Duration::from_micros(polling_interval_micros.unwrap_or_else(|| 1000)); 89 | let sampling_interval = Duration::from_micros( 90 | sampling_interval_micros.unwrap_or_else(|| polling_interval.as_micros() as u64 * 10), 91 | ); 92 | let sleeping_interval = Duration::from_micros( 93 | sleeping_interval_micros.unwrap_or_else(|| polling_interval.as_micros() as u64 * 100), 94 | ); 95 | let timeout = match timeout_micros { 96 | Some(micros) => Duration::from_micros(micros), 97 | None => Duration::from_micros( 98 | std::cmp::max(sampling_interval.as_micros(), sleeping_interval.as_micros()) as u64 99 | + 1_000, 100 | ), 101 | }; 102 | Ok(KnockKnock { 103 | polling_interval, 104 | sampling_interval, 105 | sleeping_interval, 106 | timeout, 107 | ..Default::default() 108 | }) 109 | } 110 | 111 | /// Get the contention metric, not _specific_ meaning other than a higher 112 | /// value (closer to 1) indicates increased contention when acquiring the GIL. 113 | /// and lower indicates less contention, with 0 theoretically indicating zero 114 | /// contention. 115 | #[getter] 116 | pub fn contention_metric(&self) -> f32 { 117 | *(*self.contention_metric).read() 118 | } 119 | 120 | /// Reset the contention metric/monitoring state 121 | pub fn reset_contention_metric(&mut self, py: Python) -> PyResult<()> { 122 | if let Some(tx) = &self.tx { 123 | // notify thread to reset metric and timers 124 | if let Err(e) = tx.send(Message::Reset) { 125 | let warning = py.get_type::(); 126 | PyErr::warn(py, warning, &e.to_string(), 0)?; 127 | } 128 | 129 | // wait for ack 130 | if let Err(e) = self 131 | .rx 132 | .as_ref() 133 | .unwrap() // if tx is set, then rx is as well. 134 | .recv_timeout(self.timeout) 135 | { 136 | let warning = py.get_type::(); 137 | PyErr::warn(py, warning, &e.to_string(), 0)?; 138 | } 139 | } 140 | *(*self.contention_metric).write() = 0f32; 141 | Ok(()) 142 | } 143 | 144 | /// Start polling the GIL to check if it's locked. 145 | fn start(mut slf: PyRefMut<'_, Self>) -> PyResult<()> { 146 | unsafe { 147 | if PyEval_ThreadsInitialized() == 0 { 148 | PyEval_InitThreads(); 149 | } 150 | } 151 | 152 | // Register atexit function to stop gilknocker thread 153 | // which reduces the chance of odd 'no Python frame' core dumps 154 | // when trying to acquire the GIL when the process has exited. 155 | { 156 | let ptr = slf.as_ptr(); 157 | let py = slf.py(); 158 | let __knocker = unsafe { PyObject::from_borrowed_ptr(py, ptr) }; 159 | let atexit = py.import("atexit")?; 160 | let locals = pyo3::types::PyDict::new(py); 161 | locals.set_item("__knocker", __knocker)?; 162 | locals.set_item("atexit", atexit)?; 163 | py.run("atexit.register(__knocker.stop)", None, Some(locals))?; 164 | } 165 | 166 | let self_: &mut KnockKnock = slf.deref_mut(); 167 | 168 | // send messages to thread 169 | let (tx, recv) = channel(); 170 | self_.tx = Some(tx); 171 | 172 | // recieve messages from thread 173 | let (send, rx) = channel(); 174 | self_.rx = Some(rx); 175 | 176 | let contention_metric = Arc::new(const_rwlock(0_f32)); 177 | self_.contention_metric = contention_metric.clone(); 178 | 179 | let polling_interval = self_.polling_interval; 180 | let sampling_interval = self_.sampling_interval; 181 | let sleeping_interval = self_.sleeping_interval; 182 | 183 | let handle = { 184 | thread::spawn(move || { 185 | let mut total_time_waiting = Duration::from_millis(0); 186 | let mut total_time_sampling = Duration::from_millis(0); 187 | 188 | let sample_gil = || { 189 | thread::spawn(move || { 190 | let time_sampling = Instant::now(); 191 | let mut time_waiting = Duration::from_secs(0); 192 | 193 | // Begin polling gil for duration of sampling interval 194 | while time_sampling.elapsed() < sampling_interval { 195 | let start = Instant::now(); 196 | time_waiting += Python::with_gil(move |_| start.elapsed()); 197 | thread::sleep(polling_interval); 198 | } 199 | (time_waiting, time_sampling.elapsed()) 200 | }) 201 | }; 202 | 203 | let mut handle = Some(sample_gil()); 204 | loop { 205 | match recv.recv_timeout(sleeping_interval) { 206 | Ok(message) => match message { 207 | Message::Stop => break, 208 | Message::Reset => { 209 | total_time_waiting = Duration::from_millis(0); 210 | total_time_sampling = Duration::from_millis(0); 211 | *(*contention_metric).write() = 0_f32; 212 | send.send(Ack).unwrap(); // notify reset done 213 | } 214 | }, 215 | Err(RecvTimeoutError::Disconnected) => break, 216 | Err(RecvTimeoutError::Timeout) => { 217 | if handle 218 | .as_ref() 219 | .map(|hdl| hdl.is_finished()) 220 | .unwrap_or_else(|| false) 221 | { 222 | let (time_waiting, time_sampling) = 223 | take(&mut handle).unwrap().join().unwrap(); 224 | total_time_sampling += time_sampling; 225 | total_time_waiting += time_waiting; 226 | let mut cm = (*contention_metric).write(); 227 | *cm = total_time_waiting.as_micros() as f32 228 | / total_time_sampling.as_micros() as f32; 229 | debug_assert!(handle.is_none()); // handle reset when done 230 | } else if handle.is_none() { 231 | handle = Some(sample_gil()); 232 | } 233 | } 234 | } 235 | } 236 | }) 237 | }; 238 | self_.handle = Some(handle); 239 | Ok(()) 240 | } 241 | 242 | /// Is the GIL knocker thread running? 243 | #[getter] 244 | pub fn is_running(&self) -> bool { 245 | self.handle.is_some() 246 | } 247 | 248 | /// Stop polling the GIL. 249 | pub fn stop(&mut self, py: Python) -> PyResult<()> { 250 | if let Some(handle) = take(&mut self.handle) { 251 | if let Some(send) = take(&mut self.tx) { 252 | if let Err(e) = send.send(Message::Stop) { 253 | let warning = py.get_type::(); 254 | PyErr::warn(py, warning, &e.to_string(), 0)?; 255 | } 256 | 257 | let start = Instant::now(); 258 | while !handle.is_finished() { 259 | if start.elapsed() > self.timeout { 260 | let warning = py.get_type::(); 261 | PyErr::warn(py, warning, "Timed out waiting for sampling thread.", 0)?; 262 | return Ok(()); 263 | } 264 | thread::sleep(Duration::from_millis(100)); 265 | } 266 | } 267 | handle.join().ok(); // Just ignore any potential panic from sampling thread. 268 | } 269 | Ok(()) 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /benchmarks/histogram.svg: -------------------------------------------------------------------------------- 1 | 2 | Speed in Milliseconds (ms)80080088088096096010401040112011201200120012801280136013601440144015201520160016001680168017601760184018401920192020002000test_bench[a_lotta_gil-None]test_bench[a_lotta_gil-10000]test_bench[a_lotta_gil-1000]test_bench[a_lotta_gil-10]test_bench[a_lotta_gil-100]test_bench[a_lotta_gil-1]test_bench[a_little_gil-10000]test_bench[a_little_gil-1000]test_bench[a_little_gil-None]test_bench[a_little_gil-100]test_bench[a_little_gil-10]test_bench[a_little_gil-1]test_bench[some_gil-1000]test_bench[some_gil-10000]test_bench[some_gil-100]test_bench[some_gil-10]test_bench[some_gil-None]test_bench[some_gil-1]Min: 765.8688 349 | Q1-1.5IQR: 765.8688 350 | Q1: 768.6439 351 | Median: 830.1023 352 | Q3: 875.7478 353 | Q3+1.5IQR: 877.0609 354 | Max: 877.060932.15811965811966298.1497172620685Min: 767.3780 355 | Q1-1.5IQR: 767.3780 356 | Q1: 769.1420 357 | Median: 770.3322 358 | Q3: 772.0061 359 | Q3+1.5IQR: 772.9862 360 | Max: 772.986269.55128205128204310.6237519839631Min: 768.1712 361 | Q1-1.5IQR: 768.1712 362 | Q1: 771.4379 363 | Median: 772.7171 364 | Q3: 794.1582 365 | Q3+1.5IQR: 831.7624 366 | Max: 831.7624106.94444444444444306.56550304629246Min: 768.4147 367 | Q1-1.5IQR: 768.4147 368 | Q1: 768.5951 369 | Median: 772.3395 370 | Q3: 774.0088 371 | Q3+1.5IQR: 778.4607 372 | Max: 778.4607144.33760683760684310.15526672914234Min: 768.8601 373 | Q1-1.5IQR: 768.8601 374 | Q1: 771.7493 375 | Median: 773.0650 376 | Q3: 774.0789 377 | Q3+1.5IQR: 777.0762 378 | Max: 777.0762181.73076923076923310.01385783054565Min: 769.3382 379 | Q1-1.5IQR: 769.3382 380 | Q1: 769.5309 381 | Median: 771.3516 382 | Q3: 773.9489 383 | Q3+1.5IQR: 774.5498 384 | Max: 774.5498219.1239316239316310.3008457144178Min: 891.8767 385 | Q1-1.5IQR: 891.8767 386 | Q1: 898.9689 387 | Median: 902.5062 388 | Q3: 906.5567 389 | Q3+1.5IQR: 918.6800 390 | Max: 918.6800256.517094017094279.3073471703883Min: 900.8402 391 | Q1-1.5IQR: 900.8402 392 | Q1: 905.3158 393 | Median: 907.3193 394 | Q3: 911.1954 395 | Q3+1.5IQR: 916.6432 396 | Max: 916.6432293.91025641025635278.23995558626115Min: 901.2107 397 | Q1-1.5IQR: 901.2107 398 | Q1: 909.0717 399 | Median: 913.5279 400 | Q3: 918.2268 401 | Q3+1.5IQR: 919.3382 402 | Max: 919.3382331.30341880341877277.2976801454901Min: 902.2634 403 | Q1-1.5IQR: 902.2634 404 | Q1: 902.9138 405 | Median: 906.6744 406 | Q3: 923.7222 407 | Q3+1.5IQR: 930.7078 408 | Max: 930.7078368.6965811965812277.06723984187744Min: 911.4860 409 | Q1-1.5IQR: 911.4860 410 | Q1: 912.9497 411 | Median: 913.5799 412 | Q3: 917.0586 413 | Q3+1.5IQR: 921.7663 414 | Max: 921.7663406.08974358974353276.57129722701234Min: 924.3331 415 | Q1-1.5IQR: 924.3331 416 | Q1: 924.7533 417 | Median: 926.6975 418 | Q3: 932.2567 419 | Q3+1.5IQR: 934.5246 420 | Max: 934.5246443.48290598290595273.48426711325783Min: 1751.4324 421 | Q1-1.5IQR: 1751.4324 422 | Q1: 1772.1582 423 | Median: 1780.9147 424 | Q3: 1806.5847 425 | Q3+1.5IQR: 1812.8963 426 | Max: 1812.8963480.8760683760683672.38951783922403Min: 1761.5452 427 | Q1-1.5IQR: 1761.5452 428 | Q1: 1766.2688 429 | Median: 1785.8190 430 | Q3: 1792.2473 431 | Q3+1.5IQR: 1797.4639 432 | Max: 1797.4639518.269230769230773.35905411016645Min: 1793.6643 433 | Q1-1.5IQR: 1793.6643 434 | Q1: 1802.6918 435 | Median: 1806.3973 436 | Q3: 1868.1841 437 | Q3+1.5IQR: 1893.6691 438 | Max: 1893.6691555.662393162393161.08777766600792Min: 1805.6399 439 | Q1-1.5IQR: 1805.6399 440 | Q1: 1826.8622 441 | Median: 1841.1743 442 | Q3: 1852.8526 443 | Q3+1.5IQR: 1863.0552 444 | Max: 1863.0552593.055555555555559.91460464176828Min: 1887.6946 445 | Q1-1.5IQR: 1887.6946 446 | Q1: 1889.6426 447 | Median: 1904.5635 448 | Q3: 1934.7672 449 | Q3+1.5IQR: 1975.5859 450 | Max: 1975.5859630.448717948717941.001549733370496Min: 1901.6266 451 | Q1-1.5IQR: 1901.6266 452 | Q1: 1922.7651 453 | Median: 1931.3098 454 | Q3: 1980.8539 455 | Q3+1.5IQR: 2066.1156 456 | Max: 2066.1156667.841880341880331.11843050936949Speed in Milliseconds (ms)TrialDuration --------------------------------------------------------------------------------