Browse Source

Process images in a dedicated worker

Chocobozzz 1 year ago
parent
commit
3a54605d4e

+ 1 - 1
scripts/migrations/peertube-4.2.ts

@@ -110,7 +110,7 @@ async function generateSmallerAvatar (actor: MActorDefault) {
   const source = join(CONFIG.STORAGE.ACTOR_IMAGES, sourceFilename)
   const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, newImageName)
 
-  await processImage(source, destination, imageSize, true)
+  await processImage({ path: source, destination, newSize: imageSize, keepOriginal: true })
 
   const actorImageInfo = {
     name: newImageName,

+ 1 - 1
scripts/regenerate-thumbnails.ts

@@ -52,7 +52,7 @@ async function processVideo (id: number) {
   thumbnail.height = size.height
 
   const thumbnailPath = thumbnail.getPath()
-  await processImage(previewPath, thumbnailPath, size, true)
+  await processImage({ path: previewPath, destination: thumbnailPath, newSize: size, keepOriginal: true })
 
   // Save new attributes
   await thumbnail.save()

+ 17 - 8
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() })
 

+ 4 - 0
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
   }
 }
 

+ 2 - 3
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,

+ 22 - 5
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,

+ 15 - 1
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<typeof downloadImage>[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<typeof processImage>[0]): Promise<ReturnType<typeof processImage>> {
+  return processImageWorker.run(options)
+}
+
 export {
-  downloadImageFromWorker
+  downloadImageFromWorker,
+  processImageFromWorker
 }

+ 1 - 1
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)
 

+ 7 - 0
server/lib/worker/workers/image-processor.ts

@@ -0,0 +1,7 @@
+import { processImage } from '@server/helpers/image-utils'
+
+module.exports = processImage
+
+export {
+  processImage
+}

+ 7 - 7
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
   })