detect free RAM on startup for sane defaults

* if free ram on startup is less than 2 GiB,
   use smaller chunks for parallel file hashing

* if --th-max-ram is lower than 0.25 (256 MiB),
   print a warning that thumbnails will not work

* make thumbnail cleaner immediately do a sweep on startup,
   forgetting any failed conversions so they can be retried
   in case the memory limit was increased since last run
This commit is contained in:
ed 2024-11-10 15:43:19 +00:00
parent 8aba5aed4f
commit 2bf9055cae
4 changed files with 44 additions and 4 deletions

View file

@ -50,6 +50,8 @@ from .util import (
PARTFTPY_VER, PARTFTPY_VER,
PY_DESC, PY_DESC,
PYFTPD_VER, PYFTPD_VER,
RAM_AVAIL,
RAM_TOTAL,
SQLITE_VER, SQLITE_VER,
UNPLICATIONS, UNPLICATIONS,
Daemon, Daemon,
@ -1325,6 +1327,8 @@ def add_admin(ap):
def add_thumbnail(ap): def add_thumbnail(ap):
th_ram = (RAM_AVAIL or RAM_TOTAL or 9) * 0.6
th_ram = int(max(min(th_ram, 6), 1) * 10) / 10
ap2 = ap.add_argument_group('thumbnail options') ap2 = ap.add_argument_group('thumbnail options')
ap2.add_argument("--no-thumb", action="store_true", help="disable all thumbnails (volflag=dthumb)") ap2.add_argument("--no-thumb", action="store_true", help="disable all thumbnails (volflag=dthumb)")
ap2.add_argument("--no-vthumb", action="store_true", help="disable video thumbnails (volflag=dvthumb)") ap2.add_argument("--no-vthumb", action="store_true", help="disable video thumbnails (volflag=dvthumb)")
@ -1332,7 +1336,7 @@ def add_thumbnail(ap):
ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res (volflag=thsize)") ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res (volflag=thsize)")
ap2.add_argument("--th-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for generating thumbnails") ap2.add_argument("--th-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for generating thumbnails")
ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60.0, help="conversion timeout in seconds (volflag=convt)") ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60.0, help="conversion timeout in seconds (volflag=convt)")
ap2.add_argument("--th-ram-max", metavar="GB", type=float, default=6.0, help="max memory usage (GiB) permitted by thumbnailer; not very accurate") ap2.add_argument("--th-ram-max", metavar="GB", type=float, default=th_ram, help="max memory usage (GiB) permitted by thumbnailer; not very accurate")
ap2.add_argument("--th-crop", metavar="TXT", type=u, default="y", help="crop thumbnails to 4:3 or keep dynamic height; client can override in UI unless force. [\033[32my\033[0m]=crop, [\033[32mn\033[0m]=nocrop, [\033[32mfy\033[0m]=force-y, [\033[32mfn\033[0m]=force-n (volflag=crop)") ap2.add_argument("--th-crop", metavar="TXT", type=u, default="y", help="crop thumbnails to 4:3 or keep dynamic height; client can override in UI unless force. [\033[32my\033[0m]=crop, [\033[32mn\033[0m]=nocrop, [\033[32mfy\033[0m]=force-y, [\033[32mfn\033[0m]=force-n (volflag=crop)")
ap2.add_argument("--th-x3", metavar="TXT", type=u, default="n", help="show thumbs at 3x resolution; client can override in UI unless force. [\033[32my\033[0m]=yes, [\033[32mn\033[0m]=no, [\033[32mfy\033[0m]=force-yes, [\033[32mfn\033[0m]=force-no (volflag=th3x)") ap2.add_argument("--th-x3", metavar="TXT", type=u, default="n", help="show thumbs at 3x resolution; client can override in UI unless force. [\033[32my\033[0m]=yes, [\033[32mn\033[0m]=no, [\033[32mfy\033[0m]=force-yes, [\033[32mfn\033[0m]=force-no (volflag=th3x)")
ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,ff", help="image decoders, in order of preference") ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,ff", help="image decoders, in order of preference")

View file

@ -211,6 +211,15 @@ class SvcHub(object):
t = "WARNING: --s-rd-sz (%d) is larger than --iobuf (%d); this may lead to reduced performance" t = "WARNING: --s-rd-sz (%d) is larger than --iobuf (%d); this may lead to reduced performance"
self.log("root", t % (args.s_rd_sz, args.iobuf), 3) self.log("root", t % (args.s_rd_sz, args.iobuf), 3)
zs = ""
if args.th_ram_max < 0.22:
zs = "generate thumbnails"
elif args.th_ram_max < 1:
zs = "generate audio waveforms or spectrograms"
if zs:
t = "WARNING: --th-ram-max is very small (%.2f GiB); will not be able to %s"
self.log("root", t % (args.th_ram_max, zs), 3)
if args.chpw and args.idp_h_usr: if args.chpw and args.idp_h_usr:
t = "ERROR: user-changeable passwords is incompatible with IdP/identity-providers; you must disable either --chpw or --idp-h-usr" t = "ERROR: user-changeable passwords is incompatible with IdP/identity-providers; you must disable either --chpw or --idp-h-usr"
self.log("root", t, 1) self.log("root", t, 1)

View file

@ -20,7 +20,6 @@ from .util import (
FFMPEG_URL, FFMPEG_URL,
Cooldown, Cooldown,
Daemon, Daemon,
Pebkac,
afsenc, afsenc,
fsenc, fsenc,
min_ex, min_ex,
@ -164,6 +163,7 @@ class ThumbSrv(object):
self.ram: dict[str, float] = {} self.ram: dict[str, float] = {}
self.memcond = threading.Condition(self.mutex) self.memcond = threading.Condition(self.mutex)
self.stopping = False self.stopping = False
self.rm_nullthumbs = True # forget failed conversions on startup
self.nthr = max(1, self.args.th_mt) self.nthr = max(1, self.args.th_mt)
self.q: Queue[Optional[tuple[str, str, str, VFS]]] = Queue(self.nthr * 4) self.q: Queue[Optional[tuple[str, str, str, VFS]]] = Queue(self.nthr * 4)
@ -862,7 +862,6 @@ class ThumbSrv(object):
def cleaner(self) -> None: def cleaner(self) -> None:
interval = self.args.th_clean interval = self.args.th_clean
while True: while True:
time.sleep(interval)
ndirs = 0 ndirs = 0
for vol, histpath in self.asrv.vfs.histtab.items(): for vol, histpath in self.asrv.vfs.histtab.items():
if histpath.startswith(vol): if histpath.startswith(vol):
@ -876,6 +875,8 @@ class ThumbSrv(object):
self.log("\033[Jcln err in %s: %r" % (histpath, ex), 3) self.log("\033[Jcln err in %s: %r" % (histpath, ex), 3)
self.log("\033[Jcln ok; rm {} dirs".format(ndirs)) self.log("\033[Jcln ok; rm {} dirs".format(ndirs))
self.rm_nullthumbs = False
time.sleep(interval)
def clean(self, histpath: str) -> int: def clean(self, histpath: str) -> int:
ret = 0 ret = 0
@ -939,6 +940,10 @@ class ThumbSrv(object):
continue continue
if self.rm_nullthumbs and not inf.st_size:
bos.unlink(fp)
continue
if b64 == prev_b64: if b64 == prev_b64:
self.log("rm replaced [{}]".format(fp)) self.log("rm replaced [{}]".format(fp))
bos.unlink(prev_fp) bos.unlink(prev_fp)

View file

@ -436,6 +436,27 @@ UNHUMANIZE_UNITS = {
VF_CAREFUL = {"mv_re_t": 5, "rm_re_t": 5, "mv_re_r": 0.1, "rm_re_r": 0.1} VF_CAREFUL = {"mv_re_t": 5, "rm_re_t": 5, "mv_re_r": 0.1, "rm_re_r": 0.1}
def read_ram() -> tuple[float, float]:
a = b = 0
try:
with open("/proc/meminfo", "rb", 0x10000) as f:
zsl = f.read(0x10000).decode("ascii", "replace").split("\n")
p = re.compile("^MemTotal:.* kB")
zs = next((x for x in zsl if p.match(x)))
a = int((int(zs.split()[1]) / 0x100000) * 100) / 100
p = re.compile("^MemAvailable:.* kB")
zs = next((x for x in zsl if p.match(x)))
b = int((int(zs.split()[1]) / 0x100000) * 100) / 100
except:
pass
return a, b
RAM_TOTAL, RAM_AVAIL = read_ram()
pybin = sys.executable or "" pybin = sys.executable or ""
if EXE: if EXE:
pybin = "" pybin = ""
@ -1030,6 +1051,7 @@ class MTHash(object):
self.sz = 0 self.sz = 0
self.csz = 0 self.csz = 0
self.stop = False self.stop = False
self.readsz = 1024 * 1024 * (2 if (RAM_AVAIL or 2) < 1 else 12)
self.omutex = threading.Lock() self.omutex = threading.Lock()
self.imutex = threading.Lock() self.imutex = threading.Lock()
self.work_q: Queue[int] = Queue() self.work_q: Queue[int] = Queue()
@ -1105,7 +1127,7 @@ class MTHash(object):
while chunk_rem > 0: while chunk_rem > 0:
with self.imutex: with self.imutex:
f.seek(ofs) f.seek(ofs)
buf = f.read(min(chunk_rem, 1024 * 1024 * 12)) buf = f.read(min(chunk_rem, self.readsz))
if not buf: if not buf:
raise Exception("EOF at " + str(ofs)) raise Exception("EOF at " + str(ofs))