Last active
March 21, 2026 09:19
-
-
Save CTimmerman/6d1fc20c3fb61ef0ba3e2d6de2c582ce to your computer and use it in GitHub Desktop.
Out Of Memory (OOM) Killer to save your SSD and other processes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| pylint: | |
| disable: | |
| - line-too-long | |
| - multiple-imports | |
| - pointless-string-statement | |
| - too-many-nested-blocks |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """Out Of Memory (OOM) Killer by Cees Timmerman, 2012-2026.""" | |
| import ctypes, ctypes.wintypes, os, subprocess, sys, time | |
| import psutil # python -m pip install psutil | |
| # Windows 11 becomes unresponsive while resizing its swap and/or page files by 4 GB to commit (promise) more/less RAM to Chrome for background YouTube tabs. Terminating those instead should save the SSD. 3e9 instead of 2e9 here appears to save 8 GB swap. | |
| MIN_BYTES_FREE = 4e9 # Windows 10 and/or Chrome appear to fill the disk rather than put free RAM below 20 MB of 12 GB. 128 MB is also hard to hit. 250e6 works. 404e6 for Mint Cinnamon. 550e6 for Windows 10 when MsMpEng is hogging all resources. 1e9 for temporary blackouts with 100% pagefile use by memcompression instead of nonresponsive Chrome with 16 - 1.1 GB RAM. | |
| # Windows 11 needs 2+e9 to not grow the pagefile, but on 2026-01-10 it appears that Memory Compression took RAM use from 57+ GB down to 49+ GB, slowing down user apps. Both my pagefile.sys and hyberfil.sys are unchanged at 26+ GB. Chrome crashed my focused YouTube tab again, possibly due to MC telling it to because i had GBs to spare. 2026-01-13 20:13 pagefil.sys is now 60 GB. | |
| # 1.5 (and maybe <5GB SSD) results in unresponsive/suspended/crashed Brave & Chrome, possibly memcompression-related as the system is only using 39.9/64 GB RAM. | |
| KILL_UNTIL_BYTES_FREE = 4.1e9 # Kill until | |
| MAX_BYTES_PROCESS = 128e6 # Kills tabs bigger than this. TODO: Don't kill focused tab. | |
| # Lowercase list of process names that don't need confirmation to be killed. | |
| HIT_LIST = [ | |
| "brave", | |
| "chrome", | |
| "chromium", | |
| "firefox", | |
| "isolated web co", | |
| "procmon64", | |
| "microsoftedgecp", | |
| "msedge", | |
| "msmpeng", | |
| "node", | |
| # "python", | |
| # "python3", | |
| "web content", | |
| ] | |
| def in_visible_windows(pid) -> bool: | |
| "https://stackoverflow.com/a/71844662/819417" | |
| if sys.platform == "win32": | |
| import win32gui # pip install pywin32 | |
| class TITLEBARINFO(ctypes.Structure): | |
| _fields_ = [ | |
| ("cbSize", ctypes.wintypes.DWORD), | |
| ("rcTitleBar", ctypes.wintypes.RECT), | |
| ("rgstate", ctypes.wintypes.DWORD * 6), | |
| ] | |
| # visible_windows = [] | |
| pid_visible = {} | |
| def callback(hwnd, _): | |
| nonlocal pid_visible | |
| title_info = TITLEBARINFO() | |
| title_info.cbSize = ctypes.sizeof(title_info) | |
| ctypes.windll.user32.GetTitleBarInfo(hwnd, ctypes.byref(title_info)) | |
| cloaked = ctypes.c_int(0) | |
| ctypes.WinDLL("dwmapi").DwmGetWindowAttribute( | |
| hwnd, 14, ctypes.byref(cloaked), ctypes.sizeof(cloaked) | |
| ) | |
| title = win32gui.GetWindowText(hwnd) | |
| if ( | |
| not win32gui.IsIconic(hwnd) | |
| and win32gui.IsWindowVisible(hwnd) | |
| and title != "" | |
| and cloaked.value == 0 | |
| and not (title_info.rgstate[0] & 0x00008000) # STATE_SYSTEM_INVISIBLE | |
| ): | |
| cpid = ctypes.wintypes.DWORD() | |
| ctypes.windll.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(cpid)) | |
| # visible_windows.append({ | |
| # "title": title, | |
| # "pid": cpid, | |
| # "hwnd": hwnd | |
| # }) | |
| print(cpid.value, title) | |
| pid_visible[pid] = pid == cpid.value | |
| print("enum") | |
| win32gui.EnumWindows(callback, None) | |
| # TODO | |
| """ | |
| import ctypes | |
| EnumWindows = ctypes.windll.user32.EnumWindows | |
| GetWindowThreadProcessId = ctypes.windll.user32.GetWindowThreadProcessId | |
| EnumWindowsProc = ctypes.WINFUNCTYPE(ctypes.c_bool, types.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_int)) | |
| GetWindowText = ctypes.windll.user32.GetWindowTextW | |
| GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW | |
| IsWindowVisible = ctypes.windll.user32.IsWindowVisible | |
| IsWindowEnabled = ctypes.windll.user32.IsWindowEnabled | |
| """ | |
| print(f"visible pid {pid}?", pid_visible) | |
| return pid_visible[pid] | |
| elif sys.platform == "linux": | |
| pass | |
| return False | |
| def kill(pid): | |
| if os.name == "nt": | |
| print( | |
| subprocess.check_output(["TASKKILL", "/PID", str(pid), "/T", "/F"]) # nosec | |
| ) # /Tree /Force. /IM image didn't work though all child processes were named the same. | |
| else: | |
| # 15 is SIGTERM according to https://en.wikipedia.org/wiki/Signal_(IPC) which is friendlier than SIGKILL (9). On Windows this is just the exit code for the process. | |
| os.kill(pid, 15) | |
| def main(verbose=True): | |
| lines = __doc__.splitlines() | |
| print(lines[0], lines[-1]) | |
| print( | |
| f"If free RAM < {MIN_BYTES_FREE/1e9:,.2f} GB, kill {MAX_BYTES_PROCESS/1e9:,.2f} GB {HIT_LIST} until {KILL_UNTIL_BYTES_FREE/1e9:,.2f} GB free." | |
| ) | |
| proc_data = [] | |
| while True: | |
| time.sleep(4) | |
| try: | |
| ram_free = psutil.virtual_memory().available | |
| if verbose or ram_free < MIN_BYTES_FREE: | |
| print(f"\n{time.ctime()} {ram_free / 1e9:,.2f} GB free:") | |
| if psutil.disk_usage("/").free < KILL_UNTIL_BYTES_FREE: | |
| print(f"\7WARNING: Less than {KILL_UNTIL_BYTES_FREE} GB HDD free. Check downloads.") | |
| try: | |
| proc_data = [ | |
| ( | |
| p.memory_info().rss, | |
| p.pid, | |
| p.name().split(".")[0].lower(), | |
| p.parent() | |
| and p.parent().name().split(".")[0].lower() | |
| or None, | |
| p.parent() and p.parent().pid or None, | |
| ) | |
| for p in psutil.process_iter() | |
| ] | |
| except Exception as ex: # OOM. [WinError 1455] The paging file is too small for this operation to complete | |
| print(ex, ". Using last known process data.") | |
| proc_data.sort(reverse=True) | |
| print(" ".join(f"{p[2]} {p[0] / 1e6:,.2f} MB" for p in proc_data[:5])) | |
| if ram_free >= MIN_BYTES_FREE: | |
| continue | |
| names = " ".join(p[2] for p in proc_data) | |
| # if "memcompression" in names: | |
| # print("Allowing memcompression.") | |
| # continue | |
| if "pcdrmemory" in names: | |
| print("Allowing pcdrmemory test.") | |
| continue | |
| skipped = [] | |
| for rss, pid, name, parent, parent_id in proc_data: | |
| if ( | |
| rss > MAX_BYTES_PROCESS | |
| and ram_free < KILL_UNTIL_BYTES_FREE | |
| and name in HIT_LIST | |
| and True # not in_visible_windows(parent_id) | |
| ): | |
| if ( | |
| parent != name and name not in skipped | |
| ): # and name in ("chrome", "firefox", "msedge"): | |
| print("Skipping main", name) | |
| skipped.append(name) | |
| continue | |
| print( | |
| f"\7Killing {rss/1e6:,.2f} MB {name} {pid} of {parent} {parent_id} on {time.ctime()}" | |
| ) | |
| kill(pid) | |
| time.sleep(4) | |
| ram_free = psutil.virtual_memory().available | |
| print(f"Free RAM: {ram_free / 1e9:,.2f} GB") | |
| except Exception as ex: | |
| print(type(ex).__name__, ex, f"on line {sys.exc_info()[2].tb_lineno}") | |
| import traceback | |
| traceback.print_stack() | |
| if __name__ == "__main__": | |
| try: | |
| main("verbose" in sys.argv) | |
| except KeyboardInterrupt: | |
| pass # Normal user exit by Ctrl+Break or Ctrl+C. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| [tool.pylint.messages_control] | |
| disable = [ | |
| "bad-continuation", | |
| "broad-except", | |
| "invalid-name", | |
| "line-too-long", | |
| "missing-function-docstring", | |
| "mixed-indentation", | |
| "multiple-imports", | |
| "multiple-statements", | |
| "pointless-string-statement", | |
| "too-many-nested-blocks", | |
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| [bandit] | |
| skips = B101,B311,B404,B603,B607 | |
| [flake8] | |
| ignore = E266,E401,E402,E501,E701,W503,W191 | |
| [mypy] | |
| ignore_missing_imports = True | |
| ignore_missing_imports_per_module = True | |
| [pycodestyle] | |
| ignore = E265,E266,E401,E402,E501,E701,W191,W503,W504 | |
| [pydocstyle] | |
| ignore = D100,D103,D105,D107,D203,D213,D400,D415 | |
| [pylama] | |
| ignore = C901 |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
On Windows 10, you can open
C:\Users\[username]\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startupby enteringshell:startupafter pressing Win+R. Put a .bat file there containingpython.exe -u "C:\Users\[username]\Documents\code\OOM_killer\OOM_killer.py"to run it on startup.