* perf(images): read PNG text without decoding pixels (#523) Pillow's PngImageFile.text loads every pixel to reach text chunks written after IDAT. The scan's metadata batch and the image monitor now read PNG text through utils/png_text.py, which runs Pillow's own chunk handlers after the first IDAT and seeks over the pixel data. The legacy .text read is the floor: a differential test compares both readers on every fixture image, a curated corpus and a seeded fuzz. The only difference is a file whose pixel data is undecodable or cut short: it now keeps the text read before its pixels instead of reading nothing. Animated PNGs still use .text. * test(images): cover the text budget, XMP and layout guard of the PNG text walk (#523) * perf(images): run image work on two own pools and trim after jobs (#523) Image work no longer spreads over the default executor's threads, where glibc kept an image-sized malloc arena per thread. - Interactive image work (view previews behind their gate, workspace card facts, both inspector routes) runs on the API's fixed pm-image pool of 3 threads; the on-demand preview pool is merged into it. - map_parallel (scan metadata, thumbnail and convert jobs, the legacy gallery generation, capture previews) shares one long-lived pm-worker pool of Worker threads, reused between jobs. The package teardown stops it within its bound (part "workers"). - After a scan, thumbnail or convert job, glibc's malloc_trim(0) returns free pages once; a no-op on Windows, macOS and musl. - tools/perf/memory_drive.py measures RSS, heap, threads and arenas under the #523 load on the disposable browser test server (manual only). * fix(images): start no image thread after the teardown; join retired job pools (#523)
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
"""Give glibc's free heap pages back to the OS after a long image job (#523).
|
||||
|
||||
Image work (decode, resize, encode) leaves image-sized free blocks in the
|
||||
malloc arenas of the threads that did it; glibc keeps those pages resident.
|
||||
``release_free_memory`` calls ``malloc_trim(0)`` once, which returns the free
|
||||
pages of every arena and changes no allocator setting (no ``mallopt``, no
|
||||
``MALLOC_ARENA_MAX``: ComfyUI owns this process's allocator).
|
||||
|
||||
glibc only: on Windows, macOS and musl (Alpine) it does nothing. It never
|
||||
raises; a missing or failing ``malloc_trim`` is a quiet ``False``.
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
|
||||
_lock = threading.Lock()
|
||||
_trim = None
|
||||
_looked_up = False
|
||||
|
||||
|
||||
def _glibc() -> bool:
|
||||
"""Whether this process runs on glibc (musl has no CS_GNU_LIBC_VERSION)."""
|
||||
if not sys.platform.startswith("linux"):
|
||||
return False
|
||||
try:
|
||||
version = os.confstr("CS_GNU_LIBC_VERSION")
|
||||
except (AttributeError, ValueError, OSError):
|
||||
return False
|
||||
return bool(version) and version.startswith("glibc")
|
||||
|
||||
|
||||
def _malloc_trim():
|
||||
"""glibc's ``malloc_trim``, looked up once; None where there is none."""
|
||||
global _trim, _looked_up
|
||||
with _lock:
|
||||
if not _looked_up:
|
||||
_looked_up = True
|
||||
_trim = _lookup()
|
||||
return _trim
|
||||
|
||||
|
||||
def _lookup():
|
||||
if not _glibc():
|
||||
return None
|
||||
try:
|
||||
trim = ctypes.CDLL(None).malloc_trim
|
||||
except (OSError, AttributeError):
|
||||
return None
|
||||
trim.argtypes = [ctypes.c_size_t]
|
||||
trim.restype = ctypes.c_int
|
||||
return trim
|
||||
|
||||
|
||||
def release_free_memory() -> bool:
|
||||
"""Return free heap pages to the OS once (glibc); True when it ran."""
|
||||
trim = _malloc_trim()
|
||||
if trim is None:
|
||||
return False
|
||||
try:
|
||||
trim(0)
|
||||
except Exception: # a foreign libc symbol: never fail the caller's job
|
||||
return False
|
||||
return True
|
||||
Reference in New Issue
Block a user