optimize: use ReadOnlySpan instead of string

This commit is contained in:
Renjaya Raga Zenta 2025-07-31 00:31:51 +07:00
parent 7c637eec3e
commit 1456503a3f
2 changed files with 136 additions and 36 deletions

View file

@ -6,13 +6,21 @@
<ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
<PublishAot>true</PublishAot>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NetVips" Version="3.1.0" />
<PackageReference Include="NetVips.Native.linux-x64" Version="8.17.1" />
<PackageReference Include="Validation" Version="2.6.68" />
<PackageReference Include="ZLogger" Version="2.5.10" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\vendor\NetVips\NetVips.csproj" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Oh.My.Stitcher.Benchmark" />
</ItemGroup>
</Project>

View file

@ -1,8 +1,11 @@
// ReSharper disable ReplaceSliceWithRangeIndexer
using System.Buffers.Text;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Extensions.Caching.Memory;
using NetVips;
using Oh.My.Stitcher.NetVips;
using ZLogger;
namespace Oh.My.Stitcher;
@ -73,6 +76,71 @@ public static class Tile
return true;
}
public static bool TryCreateFast(in Stitch request, string tilesDirectory, List<Image> images, ILogger logger,
IMemoryCache cache, out Image? image, out string? cacheKey, out string? cacheFile)
{
if( !TryParseRect(request.CanvasRect, out int minRow, out int maxRow, out int minCol, out int maxCol) )
throw new ArgumentException($"Invalid canvas_rect: '{request.CanvasRect}'");
if( maxRow > byte.MaxValue || maxCol > byte.MaxValue )
throw new NotSupportedException($"Unsupported rect row: {maxRow}, col: {maxCol}");
logger.ZLogDebug(
$"rect: {request.CanvasRect}, minRow: {minRow}, maxRow: {maxRow}, minCol: {minCol}, maxCol: {maxCol}");
int width = maxCol - minCol + 1;
int height = maxRow - minRow + 1;
float cropOffsetX = request.CropOffset.X;
float cropOffsetY = request.CropOffset.Y;
float cropSizeW = request.CropSize.Width;
float cropSizeH = request.CropSize.Height;
float outputScale = request.OutputScale;
image = null;
cacheKey = cacheFile = null;
// predicted size
int predictedW = (int)( width * TILE_SIZE * cropSizeW * outputScale );
int predictedH = (int)( height * TILE_SIZE * cropSizeH * outputScale );
bool canBeCached = ( predictedW * predictedH ) <= MAX_CACHED_TILE_SIZE;
if( canBeCached )
{
Span<byte> buffer = stackalloc byte[24];
byte minRowByte = (byte)minRow;
byte maxRowByte = (byte)maxRow;
byte minColByte = (byte)minCol;
byte maxColByte = (byte)maxCol;
MemoryMarshal.Write(buffer, in minRowByte);
MemoryMarshal.Write(buffer[1..], in maxRowByte);
MemoryMarshal.Write(buffer[2..], in minColByte);
MemoryMarshal.Write(buffer[3..], in maxColByte);
MemoryMarshal.Write(buffer[4..], in cropOffsetX);
MemoryMarshal.Write(buffer[8..], in cropOffsetY);
MemoryMarshal.Write(buffer[12..], in cropSizeW);
MemoryMarshal.Write(buffer[16..], in cropSizeH);
MemoryMarshal.Write(buffer[20..], in outputScale);
cacheKey = Convert.ToHexString(buffer).ToLowerInvariant();
if( cache.TryGetValue(cacheKey, out cacheFile) && File.Exists(cacheFile) )
return false;
}
Span<byte> pathBuffer = stackalloc byte[512];
for( int row = minRow; row <= maxRow; row++ )
for( int col = minCol; col <= maxCol; col++ )
{
int length = FullPathFast(tilesDirectory, row, col, pathBuffer);
images.Add(( OperationHacks.Call("pngload", pathBuffer.Slice(0, length + 1)) as Image )!);
}
using var canvasImage = Image.Arrayjoin(images.ToArray(), width);
int cropLeft = (int)( canvasImage.Width * cropOffsetX );
int cropTop = (int)( canvasImage.Height * cropOffsetY );
int cropWidth = (int)( canvasImage.Width * cropSizeW );
int cropHeight = (int)( canvasImage.Height * cropSizeH );
image = canvasImage.Crop(cropLeft, cropTop, cropWidth, cropHeight).Resize(outputScale);
return true;
}
public static Image CreateError(Exception e)
{
const int padding = 20;
@ -80,7 +148,7 @@ public static class Tile
return text.Embed(padding, padding, text.Width + ( 2 * padding ), text.Height + ( 2 * padding ));
}
private static string FullPath(string directory, int row, int col)
internal static string FullPath(string directory, int row, int col)
{
string letterPart = "";
while( row > 0 )
@ -89,19 +157,47 @@ public static class Tile
letterPart = (char)( 'A' + remainder ) + letterPart;
row = ( row - 1 ) / 26;
}
string fileName = $"{letterPart}{col}.png";
return Path.Combine(directory, fileName);
}
private static bool TryParseRect(string rect, out int minRow, out int maxRow, out int minCol, out int maxCol)
internal static int FullPathFast(string directory, int row, int col, Span<byte> buffer)
{
int p = 0;
p += Encoding.UTF8.GetBytes(directory, buffer);
buffer[p++] = (byte)Path.DirectorySeparatorChar;
int letterStart = p;
if( row > 0 )
{
while( row > 0 )
{
buffer[p++] = (byte)( 'A' + ( row - 1 ) % 26 );
row = ( row - 1 ) / 26;
}
buffer.Slice(letterStart, p - letterStart).Reverse();
}
Utf8Formatter.TryFormat(col, buffer.Slice(p), out int numBytesWritten);
p += numBytesWritten;
".png"u8.CopyTo(buffer.Slice(p));
p += 4;
buffer[p] = 0; // null terminator
return p;
}
internal static bool TryParseRect(ReadOnlySpan<char> rect, out int minRow, out int maxRow, out int minCol,
out int maxCol)
{
minRow = maxRow = minCol = maxCol = 0;
string[] corners = rect.Split(':');
switch( corners.Length )
int colonIndex = rect.IndexOf(':');
if( colonIndex == -1 )
{
case 1:
{
if( !TryParseName(corners[0], out int r, out int c) )
if( !TryParseName(rect, out int r, out int c) )
return false;
minRow = maxRow = r;
@ -109,9 +205,10 @@ public static class Tile
return true;
}
case 2:
{
if( !TryParseName(corners[0], out int r1, out int c1) || !TryParseName(corners[1], out int r2, out int c2) )
ReadOnlySpan<char> left = rect.Slice(0, colonIndex);
ReadOnlySpan<char> right = rect.Slice(colonIndex + 1);
if( !TryParseName(left, out int r1, out int c1) || !TryParseName(right, out int r2, out int c2) )
return false;
minRow = Math.Min(r1, r2);
@ -121,15 +218,10 @@ public static class Tile
return true;
}
default:
return false;
}
}
private static bool TryParseName(string name, out int row, out int col)
private static bool TryParseName(ReadOnlySpan<char> name, out int row, out int col)
{
row = col = 0;
ReadOnlySpan<char> span = name.AsSpan().Trim();
ReadOnlySpan<char> span = name.Trim();
int splitIndex = span.IndexOfAnyInRange('0', '9');
if( splitIndex <= 0 )
return false;