From 9944042978ee85f7230404f14af906504705579e Mon Sep 17 00:00:00 2001 From: shimoncohen Date: Tue, 4 Aug 2026 10:20:15 +0300 Subject: [PATCH] perf(image): drop per-pixel allocs in upscale and merged-image copy (MAPCO-11321) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Image hot path allocated on every pixel and copied the whole merged image. - TileScaler.Upscale: read the source pixel buffer once via GetValues() and index it by offset instead of calling GetValue(i,j) — which allocates a byte[] per source pixel — inside the nested scaling loop. - TileMerger.MergeTiles: Flatten() already returns an image independent of the collection, so keep it directly instead of cloning it into a new MagickImage just to survive the using scope. Output is byte-identical: the TileScaler/TileMerger golden-image tests pass unchanged. Note: the deeper encode->decode->encode round-trip is coupled to GetTileFormat's opaque->jpeg behavior (MAPCO-4731) and is intentionally left untouched here to keep format-selection semantics out of this change. Co-Authored-By: Claude Opus 4.8 (1M context) --- MergerLogic/ImageProcessing/TileMerger.cs | 18 +++++++++--------- MergerLogic/ImageProcessing/TileScaler.cs | 8 ++++++-- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/MergerLogic/ImageProcessing/TileMerger.cs b/MergerLogic/ImageProcessing/TileMerger.cs index 4a9a5a24..e57c2308 100644 --- a/MergerLogic/ImageProcessing/TileMerger.cs +++ b/MergerLogic/ImageProcessing/TileMerger.cs @@ -57,15 +57,15 @@ public TileMerger(ITileScaler tileScaler, ILogger logger) } this._logger.LogDebug($"[{MethodBase.GetCurrentMethod()?.Name}] {imageCollection.Count} where found for merge, start 'imageMagic' merging"); - using (var mergedImage = imageCollection.Flatten(MagickColor.FromRgba(0, 0, 0, 0))) - { - ImageFormatter.RemoveImageDateAttributes(mergedImage); - - mergedImage.ColorSpace = ColorSpace.sRGB; - mergedImage.ColorType = mergedImage.HasAlpha ? ColorType.TrueColorAlpha : ColorType.TrueColor; - image = new MagickImage(mergedImage); - this._logger.LogDebug($"[{MethodBase.GetCurrentMethod()?.Name}] 'imageMagic' merging finished"); - } + // Flatten returns an image independent of the collection, so it outlives the + // collection's disposal without an extra full-image copy. + IMagickImage mergedImage = imageCollection.Flatten(MagickColor.FromRgba(0, 0, 0, 0))!; + ImageFormatter.RemoveImageDateAttributes(mergedImage); + + mergedImage.ColorSpace = ColorSpace.sRGB; + mergedImage.ColorType = mergedImage.HasAlpha ? ColorType.TrueColorAlpha : ColorType.TrueColor; + image = mergedImage; + this._logger.LogDebug($"[{MethodBase.GetCurrentMethod()?.Name}] 'imageMagic' merging finished"); } break; } diff --git a/MergerLogic/ImageProcessing/TileScaler.cs b/MergerLogic/ImageProcessing/TileScaler.cs index 592c18ce..745411c6 100644 --- a/MergerLogic/ImageProcessing/TileScaler.cs +++ b/MergerLogic/ImageProcessing/TileScaler.cs @@ -85,12 +85,16 @@ public TileScaler(IMetricsProvider metricsProvider, ILogger logger) int maxRowOffset = TILE_SIZE * scaledChannels; int pixelRowBytes = TILE_SIZE * channels; + // Read the whole source pixel buffer once instead of allocating a byte[] per source pixel. + var srcValues = srcPixels.GetValues()!; + int srcStride = baseImage.Width * channels; + //loop relevant source pixels for (int i = pixelX; i < maxSrcX; i++) { for (int j = pixelY; j < maxSrcY; j++) { - var srcPixel = srcPixels.GetValue(i, j); + int srcPixelIdx = (j * srcStride) + (i * channels); var targetXStart = (i - pixelX) * scale; var targetYStart = (j - pixelY) * scale; var targetPixelIdxStart = (targetXStart + (TILE_SIZE * targetYStart)) * channels; @@ -99,7 +103,7 @@ public TileScaler(IMetricsProvider metricsProvider, ILogger logger) for (int pixelRowOffset = 0; pixelRowOffset < maxRowOffset; pixelRowOffset += pixelRowBytes) { int pixelIdx = targetPixelIdxStart + pixelColOffset + pixelRowOffset; - srcPixel!.CopyTo(pixels, pixelIdx); //copy all channels + Array.Copy(srcValues, srcPixelIdx, pixels, pixelIdx, channels); //copy all channels } } }