From 3a54605d4e7ec5b4f47131e8d23255be35b7beac Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 27 Jun 2022 11:53:12 +0200 Subject: Process images in a dedicated worker --- server/helpers/image-utils.ts | 25 +++++++++++++++++-------- server/initializers/constants.ts | 4 ++++ server/lib/local-actor.ts | 5 ++--- server/lib/thumbnail.ts | 27 ++++++++++++++++++++++----- server/lib/worker/parent-process.ts | 16 +++++++++++++++- server/lib/worker/workers/image-downloader.ts | 2 +- server/lib/worker/workers/image-processor.ts | 7 +++++++ server/tests/helpers/image.ts | 14 +++++++------- 8 files changed, 75 insertions(+), 25 deletions(-) create mode 100644 server/lib/worker/workers/image-processor.ts (limited to 'server') diff --git a/server/helpers/image-utils.ts b/server/helpers/image-utils.ts index ebb102a0d..bbd4692ef 100644 --- a/server/helpers/image-utils.ts +++ b/server/helpers/image-utils.ts @@ -12,12 +12,14 @@ function generateImageFilename (extension = '.jpg') { return buildUUID() + extension } -async function processImage ( - path: string, - destination: string, - newSize: { width: number, height: number }, - keepOriginal = false -) { +async function processImage (options: { + path: string + destination: string + newSize: { width: number, height: number } + keepOriginal?: boolean // default false +}) { + const { path, destination, newSize, keepOriginal = false } = options + const extension = getLowercaseExtension(path) if (path === destination) { @@ -36,7 +38,14 @@ async function processImage ( if (keepOriginal !== true) await remove(path) } -async function generateImageFromVideoFile (fromPath: string, folder: string, imageName: string, size: { width: number, height: number }) { +async function generateImageFromVideoFile (options: { + fromPath: string + folder: string + imageName: string + size: { width: number, height: number } +}) { + const { fromPath, folder, imageName, size } = options + const pendingImageName = 'pending-' + imageName const pendingImagePath = join(folder, pendingImageName) @@ -44,7 +53,7 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima await generateThumbnailFromVideo(fromPath, folder, imageName) const destination = join(folder, imageName) - await processImage(pendingImagePath, destination, size) + await processImage({ path: pendingImagePath, destination, newSize: size }) } catch (err) { logger.error('Cannot generate image from video %s.', fromPath, { err, ...lTags() }) diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 175935835..c6989c38b 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -748,6 +748,10 @@ const WORKER_THREADS = { DOWNLOAD_IMAGE: { CONCURRENCY: 3, MAX_THREADS: 1 + }, + PROCESS_IMAGE: { + CONCURRENCY: 1, + MAX_THREADS: 5 } } diff --git a/server/lib/local-actor.ts b/server/lib/local-actor.ts index e3b04c094..1d9be76e2 100644 --- a/server/lib/local-actor.ts +++ b/server/lib/local-actor.ts @@ -6,14 +6,13 @@ import { getLowercaseExtension } from '@shared/core-utils' import { buildUUID } from '@shared/extra-utils' import { ActivityPubActorType, ActorImageType } from '@shared/models' import { retryTransactionWrapper } from '../helpers/database-utils' -import { processImage } from '../helpers/image-utils' import { CONFIG } from '../initializers/config' import { ACTOR_IMAGES_SIZE, LRU_CACHE, WEBSERVER } from '../initializers/constants' import { sequelizeTypescript } from '../initializers/database' import { MAccountDefault, MActor, MChannelDefault } from '../types/models' import { deleteActorImages, updateActorImages } from './activitypub/actors' import { sendUpdateActor } from './activitypub/send' -import { downloadImageFromWorker } from './worker/parent-process' +import { downloadImageFromWorker, processImageFromWorker } from './worker/parent-process' function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) { return new ActorModel({ @@ -42,7 +41,7 @@ async function updateLocalActorImageFiles ( const imageName = buildUUID() + extension const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName) - await processImage(imagePhysicalFile.path, destination, imageSize, true) + await processImageFromWorker({ path: imagePhysicalFile.path, destination, newSize: imageSize, keepOriginal: true }) return { imageName, diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts index f00c87623..02b867a91 100644 --- a/server/lib/thumbnail.ts +++ b/server/lib/thumbnail.ts @@ -1,6 +1,6 @@ import { join } from 'path' import { ThumbnailType } from '@shared/models' -import { generateImageFilename, generateImageFromVideoFile, processImage } from '../helpers/image-utils' +import { generateImageFilename, generateImageFromVideoFile } from '../helpers/image-utils' import { CONFIG } from '../initializers/config' import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants' import { ThumbnailModel } from '../models/video/thumbnail' @@ -9,6 +9,7 @@ import { MThumbnail } from '../types/models/video/thumbnail' import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist' import { downloadImageFromWorker } from './local-actor' import { VideoPathManager } from './video-path-manager' +import { processImageFromWorker } from './worker/parent-process' type ImageSize = { height?: number, width?: number } @@ -23,7 +24,10 @@ function updatePlaylistMiniatureFromExisting (options: { const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size) const type = ThumbnailType.MINIATURE - const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }, keepOriginal) + const thumbnailCreator = () => { + return processImageFromWorker({ path: inputPath, destination: outputPath, newSize: { width, height }, keepOriginal }) + } + return updateThumbnailFromFunction({ thumbnailCreator, filename, @@ -99,7 +103,10 @@ function updateVideoMiniatureFromExisting (options: { const { inputPath, video, type, automaticallyGenerated, size, keepOriginal = false } = options const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) - const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }, keepOriginal) + + const thumbnailCreator = () => { + return processImageFromWorker({ path: inputPath, destination: outputPath, newSize: { width, height }, keepOriginal }) + } return updateThumbnailFromFunction({ thumbnailCreator, @@ -123,8 +130,18 @@ function generateVideoMiniature (options: { const { filename, basePath, height, width, existingThumbnail, outputPath } = buildMetadataFromVideo(video, type) const thumbnailCreator = videoFile.isAudio() - ? () => processImage(ASSETS_PATH.DEFAULT_AUDIO_BACKGROUND, outputPath, { width, height }, true) - : () => generateImageFromVideoFile(input, basePath, filename, { height, width }) + ? () => processImageFromWorker({ + path: ASSETS_PATH.DEFAULT_AUDIO_BACKGROUND, + destination: outputPath, + newSize: { width, height }, + keepOriginal: true + }) + : () => generateImageFromVideoFile({ + fromPath: input, + folder: basePath, + imageName: filename, + size: { height, width } + }) return updateThumbnailFromFunction({ thumbnailCreator, diff --git a/server/lib/worker/parent-process.ts b/server/lib/worker/parent-process.ts index 18dabd97f..188001677 100644 --- a/server/lib/worker/parent-process.ts +++ b/server/lib/worker/parent-process.ts @@ -2,6 +2,7 @@ import { join } from 'path' import Piscina from 'piscina' import { WORKER_THREADS } from '@server/initializers/constants' import { downloadImage } from './workers/image-downloader' +import { processImage } from '@server/helpers/image-utils' const downloadImagerWorker = new Piscina({ filename: join(__dirname, 'workers', 'image-downloader.js'), @@ -13,6 +14,19 @@ function downloadImageFromWorker (options: Parameters[0]): return downloadImagerWorker.run(options) } +// --------------------------------------------------------------------------- + +const processImageWorker = new Piscina({ + filename: join(__dirname, 'workers', 'image-processor.js'), + concurrentTasksPerWorker: WORKER_THREADS.DOWNLOAD_IMAGE.CONCURRENCY, + maxThreads: WORKER_THREADS.DOWNLOAD_IMAGE.MAX_THREADS +}) + +function processImageFromWorker (options: Parameters[0]): Promise> { + return processImageWorker.run(options) +} + export { - downloadImageFromWorker + downloadImageFromWorker, + processImageFromWorker } diff --git a/server/lib/worker/workers/image-downloader.ts b/server/lib/worker/workers/image-downloader.ts index 8d4a6b37e..4b32f723e 100644 --- a/server/lib/worker/workers/image-downloader.ts +++ b/server/lib/worker/workers/image-downloader.ts @@ -18,7 +18,7 @@ async function downloadImage (options: { const destPath = join(destDir, destName) try { - await processImage(tmpPath, destPath, size) + await processImage({ path: tmpPath, destination: destPath, newSize: size }) } catch (err) { await remove(tmpPath) diff --git a/server/lib/worker/workers/image-processor.ts b/server/lib/worker/workers/image-processor.ts new file mode 100644 index 000000000..0ab41a5a0 --- /dev/null +++ b/server/lib/worker/workers/image-processor.ts @@ -0,0 +1,7 @@ +import { processImage } from '@server/helpers/image-utils' + +module.exports = processImage + +export { + processImage +} diff --git a/server/tests/helpers/image.ts b/server/tests/helpers/image.ts index 475ca8fb2..7c5da69b5 100644 --- a/server/tests/helpers/image.ts +++ b/server/tests/helpers/image.ts @@ -37,28 +37,28 @@ describe('Image helpers', function () { it('Should skip processing if the source image is okay', async function () { const input = buildAbsoluteFixturePath('thumbnail.jpg') - await processImage(input, imageDestJPG, thumbnailSize, true) + await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true }) await checkBuffers(input, imageDestJPG, true) }) it('Should not skip processing if the source image does not have the appropriate extension', async function () { const input = buildAbsoluteFixturePath('thumbnail.png') - await processImage(input, imageDestJPG, thumbnailSize, true) + await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true }) await checkBuffers(input, imageDestJPG, false) }) it('Should not skip processing if the source image does not have the appropriate size', async function () { const input = buildAbsoluteFixturePath('preview.jpg') - await processImage(input, imageDestJPG, thumbnailSize, true) + await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true }) await checkBuffers(input, imageDestJPG, false) }) it('Should not skip processing if the source image does not have the appropriate size', async function () { const input = buildAbsoluteFixturePath('thumbnail-big.jpg') - await processImage(input, imageDestJPG, thumbnailSize, true) + await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true }) await checkBuffers(input, imageDestJPG, false) }) @@ -67,7 +67,7 @@ describe('Image helpers', function () { const input = buildAbsoluteFixturePath('exif.jpg') expect(await hasTitleExif(input)).to.be.true - await processImage(input, imageDestJPG, { width: 100, height: 100 }, true) + await processImage({ path: input, destination: imageDestJPG, newSize: { width: 100, height: 100 }, keepOriginal: true }) await checkBuffers(input, imageDestJPG, false) expect(await hasTitleExif(imageDestJPG)).to.be.false @@ -77,7 +77,7 @@ describe('Image helpers', function () { const input = buildAbsoluteFixturePath('exif.jpg') expect(await hasTitleExif(input)).to.be.true - await processImage(input, imageDestJPG, thumbnailSize, true) + await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true }) await checkBuffers(input, imageDestJPG, false) expect(await hasTitleExif(imageDestJPG)).to.be.false @@ -87,7 +87,7 @@ describe('Image helpers', function () { const input = buildAbsoluteFixturePath('exif.png') expect(await hasTitleExif(input)).to.be.true - await processImage(input, imageDestPNG, thumbnailSize, true) + await processImage({ path: input, destination: imageDestPNG, newSize: thumbnailSize, keepOriginal: true }) expect(await hasTitleExif(imageDestPNG)).to.be.false }) -- cgit v1.2.3