From eeba66e5988c7cb0059795da8e8ff1881900bee0 Mon Sep 17 00:00:00 2001 From: notsafeforgit Date: Mon, 23 Mar 2026 00:48:21 -0700 Subject: [PATCH] perf(ui): use slim image data in duplicate checker This fixes a severe performance bottleneck where the image duplicate checker would hang indefinitely or crash the server when finding many duplicates. Previously, the GraphQL query requested the full 'ImageData' fragment for every duplicate found, forcing the backend to resolve and serialize all related entities (galleries, studios, tags, performers) for thousands of images at once. By switching to the 'SlimImageData' fragment (mirroring how the Scene duplicate checker operates), the payload size and resolution time are drastically reduced, allowing the tool to scale correctly. --- ui/v2.5/graphql/queries/image.graphql | 2 +- .../ImageDuplicateChecker.tsx | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ui/v2.5/graphql/queries/image.graphql b/ui/v2.5/graphql/queries/image.graphql index 9ba08c1ee..41df8b67e 100644 --- a/ui/v2.5/graphql/queries/image.graphql +++ b/ui/v2.5/graphql/queries/image.graphql @@ -38,6 +38,6 @@ query FindImage($id: ID!, $checksum: String) { query FindDuplicateImages($distance: Int!) { findDuplicateImages(distance: $distance) { - ...ImageData + ...SlimImageData } } diff --git a/ui/v2.5/src/components/ImageDuplicateChecker/ImageDuplicateChecker.tsx b/ui/v2.5/src/components/ImageDuplicateChecker/ImageDuplicateChecker.tsx index a6f9dc0d0..0a3b90ed6 100644 --- a/ui/v2.5/src/components/ImageDuplicateChecker/ImageDuplicateChecker.tsx +++ b/ui/v2.5/src/components/ImageDuplicateChecker/ImageDuplicateChecker.tsx @@ -54,7 +54,7 @@ const ImageDuplicateChecker: React.FC = () => { {} ); const [selectedImages, setSelectedImages] = - useState(); + useState(); const [deletingImages, setDeletingImages] = useState(false); const [editingImages, setEditingImages] = useState(false); @@ -89,7 +89,7 @@ const ImageDuplicateChecker: React.FC = () => { fetchPolicy: "network-only", }); - const getGroupTotalSize = (group: GQL.ImageDataFragment[]) => { + const getGroupTotalSize = (group: GQL.SlimImageDataFragment[]) => { return group.reduce((groupTotal, img) => { const imgTotal = img.visual_files.reduce( (fileTotal, file) => fileTotal + (file.size ?? 0), @@ -188,8 +188,8 @@ const ImageDuplicateChecker: React.FC = () => { setCheckedImages(updatedImages); }; - const findLargestImage = (group: GQL.ImageDataFragment[]) => { - const totalSize = (image: GQL.ImageDataFragment) => { + const findLargestImage = (group: GQL.SlimImageDataFragment[]) => { + const totalSize = (image: GQL.SlimImageDataFragment) => { return image.visual_files.reduce( (prev: number, f) => Math.max(prev, f.size ?? 0), 0 @@ -202,8 +202,8 @@ const ImageDuplicateChecker: React.FC = () => { }); }; - const findLargestResolutionImage = (group: GQL.ImageDataFragment[]) => { - const imgResolution = (image: GQL.ImageDataFragment) => { + const findLargestResolutionImage = (group: GQL.SlimImageDataFragment[]) => { + const imgResolution = (image: GQL.SlimImageDataFragment) => { return image.visual_files.reduce( (prev: number, f) => Math.max(prev, (f.height ?? 0) * (f.width ?? 0)), 0 @@ -218,7 +218,7 @@ const ImageDuplicateChecker: React.FC = () => { const findFirstFileByAge = ( oldest: boolean, - compareImages: GQL.ImageDataFragment[] + compareImages: GQL.SlimImageDataFragment[] ) => { let selectedFile: GQL.ImageFileDataFragment | GQL.VideoFileDataFragment; let oldestTimestamp: Date | undefined = undefined; @@ -243,7 +243,7 @@ const ImageDuplicateChecker: React.FC = () => { ); }; - function checkSameResolution(dataGroup: GQL.ImageDataFragment[]) { + function checkSameResolution(dataGroup: GQL.SlimImageDataFragment[]) { const resolutions = dataGroup.map( (s) => (s.visual_files[0]?.width ?? 0) * (s.visual_files[0]?.height ?? 0) ); @@ -300,7 +300,7 @@ const ImageDuplicateChecker: React.FC = () => { setCheckedImages(checkedArray); }; - const handleDeleteImage = (image: GQL.ImageDataFragment) => { + const handleDeleteImage = (image: GQL.SlimImageDataFragment) => { setSelectedImages([image]); setDeletingImages(true); }; @@ -372,7 +372,7 @@ const ImageDuplicateChecker: React.FC = () => { ); } - function maybeRenderPopoverButtonGroup(image: GQL.ImageDataFragment) { + function maybeRenderPopoverButtonGroup(image: GQL.SlimImageDataFragment) { if ( image.tags.length > 0 || image.performers.length > 0 ||