Last active
November 16, 2025 12:28
-
-
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-2025.""" | |
| import ctypes, ctypes.wintypes, os, subprocess, sys, time | |
| import psutil # python -m pip install psutil | |
| MIN_BYTES_FREE = 1.1e9 # 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 1+e9 to not grow the pagefile. | |
| KILL_UNTIL_BYTES_FREE = 1.2e9 # Kill until | |
| MAX_BYTES_PROCESS = 100e6 # 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 = [ | |
| "procmon64", | |
| "chrome", | |
| "chromium", | |
| "firefox", | |
| "web content", | |
| "isolated web co", | |
| "microsoftedgecp", | |
| "msedge", | |
| "msmpeng", | |
| "node", | |
| # "python", | |
| # "python3", | |
| ] | |
| 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 < 1e9: | |
| print("WARNING: Less than 1 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.