|
@@ -48,6 +48,8 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
|
use Symfony\Component\EventDispatcher\GenericEvent;
|
|
use Symfony\Component\EventDispatcher\GenericEvent;
|
|
|
|
|
|
class Generator {
|
|
class Generator {
|
|
|
|
+ public const SEMAPHORE_ID_ALL = 0x0a11;
|
|
|
|
+ public const SEMAPHORE_ID_NEW = 0x07ea;
|
|
|
|
|
|
/** @var IPreview */
|
|
/** @var IPreview */
|
|
private $previewManager;
|
|
private $previewManager;
|
|
@@ -302,6 +304,98 @@ class Generator {
|
|
throw new NotFoundException('No provider successfully handled the preview generation');
|
|
throw new NotFoundException('No provider successfully handled the preview generation');
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
+ * Acquire a semaphore of the specified id and concurrency, blocking if necessary.
|
|
|
|
+ * Return an identifier of the semaphore on success, which can be used to release it via
|
|
|
|
+ * {@see Generator::unguardWithSemaphore()}.
|
|
|
|
+ *
|
|
|
|
+ * @param int $semId
|
|
|
|
+ * @param int $concurrency
|
|
|
|
+ * @return false|resource the semaphore on success or false on failure
|
|
|
|
+ */
|
|
|
|
+ public static function guardWithSemaphore(int $semId, int $concurrency) {
|
|
|
|
+ if (!extension_loaded('sysvsem')) {
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+ $sem = sem_get($semId, $concurrency);
|
|
|
|
+ if ($sem === false) {
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+ if (!sem_acquire($sem)) {
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+ return $sem;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Releases the semaphore acquired from {@see Generator::guardWithSemaphore()}.
|
|
|
|
+ *
|
|
|
|
+ * @param resource|bool $semId the semaphore identifier returned by guardWithSemaphore
|
|
|
|
+ * @return bool
|
|
|
|
+ */
|
|
|
|
+ public static function unguardWithSemaphore($semId): bool {
|
|
|
|
+ if (!is_resource($semId) || !extension_loaded('sysvsem')) {
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+ return sem_release($semId);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 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(): int {
|
|
|
|
+ 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(string $type): int {
|
|
|
|
+ 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 ISimpleFolder $previewFolder
|
|
* @param File $file
|
|
* @param File $file
|
|
@@ -340,7 +434,13 @@ class Generator {
|
|
$maxWidth = $this->config->getSystemValueInt('preview_max_x', 4096);
|
|
$maxWidth = $this->config->getSystemValueInt('preview_max_x', 4096);
|
|
$maxHeight = $this->config->getSystemValueInt('preview_max_y', 4096);
|
|
$maxHeight = $this->config->getSystemValueInt('preview_max_y', 4096);
|
|
|
|
|
|
- $preview = $this->helper->getThumbnail($provider, $file, $maxWidth, $maxHeight);
|
|
|
|
|
|
+ $previewConcurrency = $this->getNumConcurrentPreviews('preview_concurrency_new');
|
|
|
|
+ $sem = self::guardWithSemaphore(self::SEMAPHORE_ID_NEW, $previewConcurrency);
|
|
|
|
+ try {
|
|
|
|
+ $preview = $this->helper->getThumbnail($provider, $file, $maxWidth, $maxHeight);
|
|
|
|
+ } finally {
|
|
|
|
+ self::unguardWithSemaphore($sem);
|
|
|
|
+ }
|
|
|
|
|
|
if (!($preview instanceof IImage)) {
|
|
if (!($preview instanceof IImage)) {
|
|
continue;
|
|
continue;
|
|
@@ -510,29 +610,34 @@ class Generator {
|
|
throw new \InvalidArgumentException('Failed to generate preview, failed to load image');
|
|
throw new \InvalidArgumentException('Failed to generate preview, failed to load image');
|
|
}
|
|
}
|
|
|
|
|
|
- if ($crop) {
|
|
|
|
- if ($height !== $preview->height() && $width !== $preview->width()) {
|
|
|
|
- //Resize
|
|
|
|
- $widthR = $preview->width() / $width;
|
|
|
|
- $heightR = $preview->height() / $height;
|
|
|
|
-
|
|
|
|
- if ($widthR > $heightR) {
|
|
|
|
- $scaleH = $height;
|
|
|
|
- $scaleW = $maxWidth / $heightR;
|
|
|
|
- } else {
|
|
|
|
- $scaleH = $maxHeight / $widthR;
|
|
|
|
- $scaleW = $width;
|
|
|
|
|
|
+ $previewConcurrency = $this->getNumConcurrentPreviews('preview_concurrency_new');
|
|
|
|
+ $sem = self::guardWithSemaphore(self::SEMAPHORE_ID_NEW, $previewConcurrency);
|
|
|
|
+ try {
|
|
|
|
+ if ($crop) {
|
|
|
|
+ if ($height !== $preview->height() && $width !== $preview->width()) {
|
|
|
|
+ //Resize
|
|
|
|
+ $widthR = $preview->width() / $width;
|
|
|
|
+ $heightR = $preview->height() / $height;
|
|
|
|
+
|
|
|
|
+ if ($widthR > $heightR) {
|
|
|
|
+ $scaleH = $height;
|
|
|
|
+ $scaleW = $maxWidth / $heightR;
|
|
|
|
+ } else {
|
|
|
|
+ $scaleH = $maxHeight / $widthR;
|
|
|
|
+ $scaleW = $width;
|
|
|
|
+ }
|
|
|
|
+ $preview = $preview->preciseResizeCopy((int)round($scaleW), (int)round($scaleH));
|
|
}
|
|
}
|
|
- $preview = $preview->preciseResizeCopy((int)round($scaleW), (int)round($scaleH));
|
|
|
|
|
|
+ $cropX = (int)floor(abs($width - $preview->width()) * 0.5);
|
|
|
|
+ $cropY = (int)floor(abs($height - $preview->height()) * 0.5);
|
|
|
|
+ $preview = $preview->cropCopy($cropX, $cropY, $width, $height);
|
|
|
|
+ } else {
|
|
|
|
+ $preview = $maxPreview->resizeCopy(max($width, $height));
|
|
}
|
|
}
|
|
- $cropX = (int)floor(abs($width - $preview->width()) * 0.5);
|
|
|
|
- $cropY = (int)floor(abs($height - $preview->height()) * 0.5);
|
|
|
|
- $preview = $preview->cropCopy($cropX, $cropY, $width, $height);
|
|
|
|
- } else {
|
|
|
|
- $preview = $maxPreview->resizeCopy(max($width, $height));
|
|
|
|
|
|
+ } finally {
|
|
|
|
+ self::unguardWithSemaphore($sem);
|
|
}
|
|
}
|
|
|
|
|
|
-
|
|
|
|
$path = $this->generatePath($width, $height, $crop, $preview->dataMimeType(), $prefix);
|
|
$path = $this->generatePath($width, $height, $crop, $preview->dataMimeType(), $prefix);
|
|
try {
|
|
try {
|
|
$file = $previewFolder->newFile($path);
|
|
$file = $previewFolder->newFile($path);
|