add RAM usage tracking to thumbnailer;
prevents server OOM from high RAM usage by FFmpeg when generating spectrograms and waveforms: https://trac.ffmpeg.org/ticket/10797
This commit is contained in:
		
							parent
							
								
									a55e0d6eb8
								
							
						
					
					
						commit
						95a599961e
					
				| @ -20,7 +20,18 @@ import time | |||||||
| import traceback | import traceback | ||||||
| import uuid | import uuid | ||||||
| 
 | 
 | ||||||
| from .__init__ import ANYWIN, CORES, EXE, MACOS, PY2, VT100, WINDOWS, E, EnvParams, unicode | from .__init__ import ( | ||||||
|  |     ANYWIN, | ||||||
|  |     CORES, | ||||||
|  |     EXE, | ||||||
|  |     MACOS, | ||||||
|  |     PY2, | ||||||
|  |     VT100, | ||||||
|  |     WINDOWS, | ||||||
|  |     E, | ||||||
|  |     EnvParams, | ||||||
|  |     unicode, | ||||||
|  | ) | ||||||
| from .__version__ import CODENAME, S_BUILD_DT, S_VERSION | from .__version__ import CODENAME, S_BUILD_DT, S_VERSION | ||||||
| from .authsrv import expand_config_file, split_cfg_ln, upgrade_cfg_fmt | from .authsrv import expand_config_file, split_cfg_ln, upgrade_cfg_fmt | ||||||
| from .cfg import flagcats, onedash | from .cfg import flagcats, onedash | ||||||
| @ -1139,6 +1150,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, help="conversion timeout in seconds (volflag=convt)") |     ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60, help="conversion timeout in seconds (volflag=convt)") | ||||||
|  |     ap2.add_argument("--th-ram-max", metavar="GB", type=float, default=6, help="max memory usage (GiB) permitted by thumbnailer; not very accurate") | ||||||
|     ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image by default (client can override in UI) (volflag=nocrop)") |     ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image by default (client can override in UI) (volflag=nocrop)") | ||||||
|     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") | ||||||
|     ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output") |     ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output") | ||||||
|  | |||||||
| @ -129,6 +129,8 @@ class ThumbSrv(object): | |||||||
| 
 | 
 | ||||||
|         self.mutex = threading.Lock() |         self.mutex = threading.Lock() | ||||||
|         self.busy: dict[str, list[threading.Condition]] = {} |         self.busy: dict[str, list[threading.Condition]] = {} | ||||||
|  |         self.ram: dict[str, float] = {} | ||||||
|  |         self.memcond = threading.Condition(self.mutex) | ||||||
|         self.stopping = False |         self.stopping = False | ||||||
|         self.nthr = max(1, self.args.th_mt) |         self.nthr = max(1, self.args.th_mt) | ||||||
| 
 | 
 | ||||||
| @ -214,7 +216,7 @@ class ThumbSrv(object): | |||||||
|         with self.mutex: |         with self.mutex: | ||||||
|             try: |             try: | ||||||
|                 self.busy[tpath].append(cond) |                 self.busy[tpath].append(cond) | ||||||
|                 self.log("wait {}".format(tpath)) |                 self.log("joined waiting room for %s" % (tpath,)) | ||||||
|             except: |             except: | ||||||
|                 thdir = os.path.dirname(tpath) |                 thdir = os.path.dirname(tpath) | ||||||
|                 bos.makedirs(os.path.join(thdir, "w")) |                 bos.makedirs(os.path.join(thdir, "w")) | ||||||
| @ -265,6 +267,23 @@ class ThumbSrv(object): | |||||||
|             "ffa": self.fmt_ffa, |             "ffa": self.fmt_ffa, | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |     def wait4ram(self, need: float, ttpath: str) -> None: | ||||||
|  |         ram = self.args.th_ram_max | ||||||
|  |         if need > ram * 0.99: | ||||||
|  |             t = "file too big; need %.2f GiB RAM, but --th-ram-max is only %.1f" | ||||||
|  |             raise Exception(t % (need, ram)) | ||||||
|  | 
 | ||||||
|  |         while True: | ||||||
|  |             with self.mutex: | ||||||
|  |                 used = sum([v for k, v in self.ram.items() if k != ttpath]) + need | ||||||
|  |                 if used < ram: | ||||||
|  |                     # self.log("XXX self.ram: %s" % (self.ram,), 5) | ||||||
|  |                     self.ram[ttpath] = need | ||||||
|  |                     return | ||||||
|  |             with self.memcond: | ||||||
|  |                 # self.log("at RAM limit; used %.2f GiB, need %.2f more" % (used-need, need), 1) | ||||||
|  |                 self.memcond.wait(3) | ||||||
|  | 
 | ||||||
|     def worker(self) -> None: |     def worker(self) -> None: | ||||||
|         while not self.stopping: |         while not self.stopping: | ||||||
|             task = self.q.get() |             task = self.q.get() | ||||||
| @ -330,11 +349,15 @@ class ThumbSrv(object): | |||||||
|             with self.mutex: |             with self.mutex: | ||||||
|                 subs = self.busy[tpath] |                 subs = self.busy[tpath] | ||||||
|                 del self.busy[tpath] |                 del self.busy[tpath] | ||||||
|  |                 self.ram.pop(ttpath, None) | ||||||
| 
 | 
 | ||||||
|             for x in subs: |             for x in subs: | ||||||
|                 with x: |                 with x: | ||||||
|                     x.notify_all() |                     x.notify_all() | ||||||
| 
 | 
 | ||||||
|  |             with self.memcond: | ||||||
|  |                 self.memcond.notify_all() | ||||||
|  | 
 | ||||||
|         with self.mutex: |         with self.mutex: | ||||||
|             self.nthr -= 1 |             self.nthr -= 1 | ||||||
| 
 | 
 | ||||||
| @ -366,6 +389,7 @@ class ThumbSrv(object): | |||||||
|         return im |         return im | ||||||
| 
 | 
 | ||||||
|     def conv_pil(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: |     def conv_pil(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: | ||||||
|  |         self.wait4ram(0.2, tpath) | ||||||
|         with Image.open(fsenc(abspath)) as im: |         with Image.open(fsenc(abspath)) as im: | ||||||
|             try: |             try: | ||||||
|                 im = self.fancy_pillow(im, fmt, vn) |                 im = self.fancy_pillow(im, fmt, vn) | ||||||
| @ -395,6 +419,7 @@ class ThumbSrv(object): | |||||||
|             im.save(tpath, **args) |             im.save(tpath, **args) | ||||||
| 
 | 
 | ||||||
|     def conv_vips(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: |     def conv_vips(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: | ||||||
|  |         self.wait4ram(0.2, tpath) | ||||||
|         crops = ["centre", "none"] |         crops = ["centre", "none"] | ||||||
|         if fmt.endswith("f"): |         if fmt.endswith("f"): | ||||||
|             crops = ["none"] |             crops = ["none"] | ||||||
| @ -415,6 +440,7 @@ class ThumbSrv(object): | |||||||
|         img.write_to_file(tpath, Q=40) |         img.write_to_file(tpath, Q=40) | ||||||
| 
 | 
 | ||||||
|     def conv_ffmpeg(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: |     def conv_ffmpeg(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: | ||||||
|  |         self.wait4ram(0.2, tpath) | ||||||
|         ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) |         ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) | ||||||
|         if not ret: |         if not ret: | ||||||
|             return |             return | ||||||
| @ -517,8 +543,21 @@ class ThumbSrv(object): | |||||||
|         if "ac" not in ret: |         if "ac" not in ret: | ||||||
|             raise Exception("not audio") |             raise Exception("not audio") | ||||||
| 
 | 
 | ||||||
|         flt = ( |         # jt_versi.xm: 405M/839s | ||||||
|             b"[0:a:0]" |         dur = ret[".dur"][1] if ".dur" in ret else 300 | ||||||
|  |         need = 0.2 + dur / 3000 | ||||||
|  |         speedup = b"" | ||||||
|  |         if need > self.args.th_ram_max * 0.7: | ||||||
|  |             self.log("waves too big (need %.2f GiB); trying to optimize" % (need,)) | ||||||
|  |             need = 0.2 + dur / 4200  # only helps about this much... | ||||||
|  |             speedup = b"aresample=8000," | ||||||
|  |         if need > self.args.th_ram_max * 0.96: | ||||||
|  |             raise Exception("file too big; cannot waves") | ||||||
|  | 
 | ||||||
|  |         self.wait4ram(need, tpath) | ||||||
|  | 
 | ||||||
|  |         flt = b"[0:a:0]" + speedup | ||||||
|  |         flt += ( | ||||||
|             b"compand=.3|.3:1|1:-90/-60|-60/-40|-40/-30|-20/-20:6:0:-90:0.2" |             b"compand=.3|.3:1|1:-90/-60|-60/-40|-40/-30|-20/-20:6:0:-90:0.2" | ||||||
|             b",volume=2" |             b",volume=2" | ||||||
|             b",showwavespic=s=2048x64:colors=white" |             b",showwavespic=s=2048x64:colors=white" | ||||||
| @ -545,6 +584,15 @@ class ThumbSrv(object): | |||||||
|         if "ac" not in ret: |         if "ac" not in ret: | ||||||
|             raise Exception("not audio") |             raise Exception("not audio") | ||||||
| 
 | 
 | ||||||
|  |         # https://trac.ffmpeg.org/ticket/10797 | ||||||
|  |         # expect 1 GiB every 600 seconds when duration is tricky; | ||||||
|  |         # simple filetypes are generally safer so let's special-case those | ||||||
|  |         safe = ("flac", "wav", "aif", "aiff", "opus") | ||||||
|  |         coeff = 1800 if abspath.split(".")[-1].lower() in safe else 600 | ||||||
|  |         dur = ret[".dur"][1] if ".dur" in ret else 300 | ||||||
|  |         need = 0.2 + dur / coeff | ||||||
|  |         self.wait4ram(need, tpath) | ||||||
|  | 
 | ||||||
|         fc = "[0:a:0]aresample=48000{},showspectrumpic=s=640x512,crop=780:544:70:50[o]" |         fc = "[0:a:0]aresample=48000{},showspectrumpic=s=640x512,crop=780:544:70:50[o]" | ||||||
| 
 | 
 | ||||||
|         if self.args.th_ff_swr: |         if self.args.th_ff_swr: | ||||||
| @ -587,6 +635,7 @@ class ThumbSrv(object): | |||||||
|         if self.args.no_acode: |         if self.args.no_acode: | ||||||
|             raise Exception("disabled in server config") |             raise Exception("disabled in server config") | ||||||
| 
 | 
 | ||||||
|  |         self.wait4ram(0.2, tpath) | ||||||
|         ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) |         ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) | ||||||
|         if "ac" not in ret: |         if "ac" not in ret: | ||||||
|             raise Exception("not audio") |             raise Exception("not audio") | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 ed
						ed