From 8e0b2e383d3d00ce12521a4845cb1c83f0d77d2e Mon Sep 17 00:00:00 2001 From: Bowen Ding Date: Thu, 28 Jan 2021 21:55:15 +0800 Subject: [PATCH] Limit-number-of-concurrent-preview-generations Signed-off-by: Bowen Ding --- lib/private/Preview/Generator.php | 100 ++++++++++++++++++++++++++++++ lib/private/PreviewManager.php | 9 ++- 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/lib/private/Preview/Generator.php b/lib/private/Preview/Generator.php index 2be08d1b2c..075aeb9e19 100644 --- a/lib/private/Preview/Generator.php +++ b/lib/private/Preview/Generator.php @@ -44,6 +44,8 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\GenericEvent; class Generator { + public const SEMAPHORE_ID_ALL = 0x0a11; + public const SEMAPHORE_ID_NEW = 0x07ea; /** @var IPreview */ private $previewManager; @@ -204,6 +206,95 @@ class Generator { return $preview; } + /** + * @param int $semId + * @param int $concurrency + * @return bool|resource the semaphore on success or false on failure + */ + public static function guardWithSemaphore(int $semId, int $concurrency) { + if (extension_loaded('sysvsem')) { + $sem = sem_get($semId, $concurrency); + if ($sem === false) { + return false; + } + if (!sem_acquire($sem)) { + return false; + } + return $sem; + } + return false; + } + + /** + * @param resource $sem + * @return bool + */ + public static function unguardWithSemaphore($sem) { + if (!is_resource($sem)) { + return false; + } + if (extension_loaded('sysvsem')) { + return sem_release($sem); + } + return false; + } + + /** + * Get the number of concurrent threads supported by the host + * + * @return int number of concurrent threads, or 0 if it cannot be determined + */ + public static function getHardwareConcurrency() { + static $width; + if (!isset($width)) { + if (is_file("/proc/cpuinfo")) { + $width = substr_count(file_get_contents("/proc/cpuinfo"), "processor"); + } else { + $width = 0; + } + } + return $width; + } + + /** + * Get number of concurrent preview generations from system config + * + * Two config entries, `preview_concurrency_new` and `preview_concurrency_all`, + * are available. If not set, the default values are determined with the hardware concurrency + * of the host. In case the hardware concurrency cannot be determined, or the user sets an + * invalid value, fallback values are: + * For new images whose previews do not exist and need to be generated, 4; + * For all preview generation requests, 8. + * Value of `preview_concurrency_all` should be greater than or equal to that of + * `preview_concurrency_new`, otherwise, the latter is returned. + * + * @param string $type either `preview_concurrency_new` or `preview_concurrency_all` + * @return int number of concurrent preview generations, or -1 if $type is invalid + */ + public function getNumConcurrentPreviews($type) { + static $cached = array(); + if (array_key_exists($type, $cached)) { + return $cached[$type]; + } + + $hardwareConcurrency = self::getHardwareConcurrency(); + switch ($type) { + case "preview_concurrency_all": + $fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency * 2 : 8; + $concurrency_all = $this->config->getSystemValueInt($type, $fallback); + $concurrency_new = $this->getNumConcurrentPreviews("preview_concurrency_new"); + $cached[$type] = max($concurrency_all, $concurrency_new); + break; + case "preview_concurrency_new": + $fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency : 4; + $cached[$type] = $this->config->getSystemValueInt($type, $fallback); + break; + default: + return -1; + } + return $cached[$type]; + } + /** * @param ISimpleFolder $previewFolder * @param File $file @@ -241,8 +332,13 @@ class Generator { $maxWidth = (int)$this->config->getSystemValue('preview_max_x', 4096); $maxHeight = (int)$this->config->getSystemValue('preview_max_y', 4096); + $previewConcurrency = $this->getNumConcurrentPreviews('preview_concurrency_new'); + $sem = self::guardWithSemaphore(self::SEMAPHORE_ID_NEW, $previewConcurrency); + $preview = $this->helper->getThumbnail($provider, $file, $maxWidth, $maxHeight); + self::unguardWithSemaphore($sem); + if (!($preview instanceof IImage)) { continue; } @@ -407,6 +503,9 @@ class Generator { throw new \InvalidArgumentException('Failed to generate preview, failed to load image'); } + $previewConcurrency = $this->getNumConcurrentPreviews('preview_concurrency_new'); + $sem = self::guardWithSemaphore(self::SEMAPHORE_ID_NEW, $previewConcurrency); + if ($crop) { if ($height !== $preview->height() && $width !== $preview->width()) { //Resize @@ -429,6 +528,7 @@ class Generator { $preview = $maxPreview->resizeCopy(max($width, $height)); } + self::unguardWithSemaphore($sem); $path = $this->generatePath($width, $height, $crop, $preview->dataMimeType(), $prefix); try { diff --git a/lib/private/PreviewManager.php b/lib/private/PreviewManager.php index d6dc700e4a..0307b91763 100644 --- a/lib/private/PreviewManager.php +++ b/lib/private/PreviewManager.php @@ -187,7 +187,14 @@ class PreviewManager implements IPreview { * @since 11.0.0 - \InvalidArgumentException was added in 12.0.0 */ public function getPreview(File $file, $width = -1, $height = -1, $crop = false, $mode = IPreview::MODE_FILL, $mimeType = null) { - return $this->getGenerator()->getPreview($file, $width, $height, $crop, $mode, $mimeType); + $previewConcurrency = $this->getGenerator()->getNumConcurrentPreviews('preview_concurrency_all'); + $sem = Generator::guardWithSemaphore(Generator::SEMAPHORE_ID_ALL, $previewConcurrency); + + $preview = $this->getGenerator()->getPreview($file, $width, $height, $crop, $mode, $mimeType); + + Generator::unguardWithSemaphore($sem); + + return $preview; } /**