caching
This commit is contained in:
parent
8b10c3b27e
commit
6333a95b25
2 changed files with 89 additions and 21 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.IO.Pipelines;
|
using System.IO.Pipelines;
|
||||||
using Microsoft.AspNetCore.Http.Json;
|
using Microsoft.AspNetCore.Http.Json;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using NetVips;
|
using NetVips;
|
||||||
using Oh.My.Stitcher;
|
using Oh.My.Stitcher;
|
||||||
using Validation;
|
using Validation;
|
||||||
|
|
@ -8,6 +9,7 @@ using ZLogger;
|
||||||
|
|
||||||
WebApplicationBuilder builder = WebApplication.CreateSlimBuilder(args);
|
WebApplicationBuilder builder = WebApplication.CreateSlimBuilder(args);
|
||||||
builder.Logging.ClearProviders().AddZLoggerConsole();
|
builder.Logging.ClearProviders().AddZLoggerConsole();
|
||||||
|
builder.Services.AddMemoryCache();
|
||||||
builder.Services.Configure<JsonOptions>(options =>
|
builder.Services.Configure<JsonOptions>(options =>
|
||||||
{
|
{
|
||||||
options.SerializerOptions.TypeInfoResolver = StitchSerializerContext.Default;
|
options.SerializerOptions.TypeInfoResolver = StitchSerializerContext.Default;
|
||||||
|
|
@ -18,45 +20,71 @@ ILoggerFactory loggerFactory = app.Services.GetRequiredService<ILoggerFactory>()
|
||||||
ILogger logger = loggerFactory.CreateLogger<Program>();
|
ILogger logger = loggerFactory.CreateLogger<Program>();
|
||||||
|
|
||||||
string? tilesDirectory = Environment.GetEnvironmentVariable("ASSET_PATH_RO");
|
string? tilesDirectory = Environment.GetEnvironmentVariable("ASSET_PATH_RO");
|
||||||
|
string cacheDirectory = Path.Combine(Path.GetTempPath(), "oh-my-stitch");
|
||||||
|
Directory.CreateDirectory(cacheDirectory);
|
||||||
|
|
||||||
// sanity check
|
// sanity check
|
||||||
Assumes.NotNullOrEmpty(tilesDirectory);
|
Assumes.NotNullOrEmpty(tilesDirectory);
|
||||||
Assumes.True(File.Exists(Path.Combine(tilesDirectory, "A1.png")));
|
Assumes.True(File.Exists(Path.Combine(tilesDirectory, "A1.png")));
|
||||||
Assumes.True(File.Exists(Path.Combine(tilesDirectory, "AE55.png")));
|
Assumes.True(File.Exists(Path.Combine(tilesDirectory, "AE55.png")));
|
||||||
|
|
||||||
|
|
||||||
app.UseDefaultFiles();
|
app.UseDefaultFiles();
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
app.MapPost("/api/image/generate", (Stitch request) =>
|
app.MapPost("/api/image/generate", (Stitch request, IMemoryCache cache) =>
|
||||||
{
|
{
|
||||||
Pipe pipe = new();
|
Pipe pipe = new();
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
|
Image? image = null;
|
||||||
List<Image> images = [];
|
List<Image> images = [];
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using Image image = Tile.Create(in request, tilesDirectory, images, logger);
|
bool created = Tile.TryCreate(in request, tilesDirectory, images, logger, cache, out image,
|
||||||
|
out string? cacheKey, out string? cacheFile);
|
||||||
|
if( !created && cacheFile != null )
|
||||||
|
{
|
||||||
|
logger.ZLogDebug($"cache hit key: {cacheKey}, file: {cacheFile}");
|
||||||
|
await using FileStream cacheStream = new(cacheFile, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
await cacheStream.CopyToAsync(pipe.Writer.AsStream());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( cacheKey == null )
|
||||||
|
{
|
||||||
|
image?.WriteToStream(pipe.Writer.AsStream(), ".png");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Pipe innerPipe = new();
|
Pipe innerPipe = new();
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
|
string newCacheFile = Path.Combine(cacheDirectory, $"{cacheKey}.png");
|
||||||
|
MemoryCacheEntryOptions cacheEntryOptions = new MemoryCacheEntryOptions()
|
||||||
|
.SetSlidingExpiration(TimeSpan.FromMinutes(10))
|
||||||
|
.RegisterPostEvictionCallback((_, value, _, _) =>
|
||||||
|
{
|
||||||
|
if( value is string path )
|
||||||
|
File.Delete(path);
|
||||||
|
});
|
||||||
|
logger.ZLogDebug($"save cache key: {cacheKey}, file: {newCacheFile}");
|
||||||
|
cache.Set(cacheKey!, newCacheFile, cacheEntryOptions);
|
||||||
|
|
||||||
|
await using FileStream cacheStream = new(newCacheFile, FileMode.Create, FileAccess.Write, FileShare.Read);
|
||||||
while( true )
|
while( true )
|
||||||
{
|
{
|
||||||
ReadResult result = await innerPipe.Reader.ReadAsync();
|
ReadResult result = await innerPipe.Reader.ReadAsync();
|
||||||
ReadOnlySequence<byte> buffer = result.Buffer;
|
ReadOnlySequence<byte> buffer = result.Buffer;
|
||||||
if(!buffer.IsEmpty )
|
|
||||||
{
|
|
||||||
foreach( ReadOnlyMemory<byte> segment in buffer )
|
foreach( ReadOnlyMemory<byte> segment in buffer )
|
||||||
{
|
await Task.WhenAll(pipe.Writer.WriteAsync(segment).AsTask(), cacheStream.WriteAsync(segment).AsTask());
|
||||||
await pipe.Writer.WriteAsync(segment);
|
|
||||||
}
|
|
||||||
innerPipe.Reader.AdvanceTo(buffer.End);
|
innerPipe.Reader.AdvanceTo(buffer.End);
|
||||||
if (result.IsCompleted)
|
if( result.IsCompleted )
|
||||||
{
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
image.WriteToStream(innerPipe.Writer.AsStream(), ".png");
|
image?.WriteToStream(innerPipe.Writer.AsStream(), ".png");
|
||||||
}
|
}
|
||||||
catch( Exception e )
|
catch( Exception e )
|
||||||
{
|
{
|
||||||
|
|
@ -66,6 +94,7 @@ app.MapPost("/api/image/generate", (Stitch request) =>
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
image?.Dispose();
|
||||||
foreach( Image img in images )
|
foreach( Image img in images )
|
||||||
img.Dispose();
|
img.Dispose();
|
||||||
await pipe.Writer.CompleteAsync();
|
await pipe.Writer.CompleteAsync();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
// ReSharper disable ReplaceSliceWithRangeIndexer
|
// ReSharper disable ReplaceSliceWithRangeIndexer
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using NetVips;
|
using NetVips;
|
||||||
using ZLogger;
|
using ZLogger;
|
||||||
|
|
||||||
|
|
@ -7,7 +9,11 @@ namespace Oh.My.Stitcher;
|
||||||
|
|
||||||
public static class Tile
|
public static class Tile
|
||||||
{
|
{
|
||||||
public static Image Create(in Stitch request, string tilesDirectory, List<Image> images, ILogger logger)
|
private const int TILE_SIZE = 720;
|
||||||
|
private const int MAX_CACHED_TILE_SIZE = TILE_SIZE * TILE_SIZE * 128;
|
||||||
|
|
||||||
|
public static bool TryCreate(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) )
|
if( !TryParseRect(request.CanvasRect, out int minRow, out int maxRow, out int minCol, out int maxCol) )
|
||||||
throw new ArgumentException($"Invalid canvas_rect: '{request.CanvasRect}'");
|
throw new ArgumentException($"Invalid canvas_rect: '{request.CanvasRect}'");
|
||||||
|
|
@ -15,17 +21,50 @@ public static class Tile
|
||||||
logger.ZLogDebug(
|
logger.ZLogDebug(
|
||||||
$"rect: {request.CanvasRect}, minRow: {minRow}, maxRow: {maxRow}, minCol: {minCol}, maxCol: {maxCol}");
|
$"rect: {request.CanvasRect}, minRow: {minRow}, maxRow: {maxRow}, minCol: {minCol}, maxCol: {maxCol}");
|
||||||
int width = maxCol - minCol + 1;
|
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[36];
|
||||||
|
MemoryMarshal.Write(buffer, in minRow);
|
||||||
|
MemoryMarshal.Write(buffer[4..], in maxRow);
|
||||||
|
MemoryMarshal.Write(buffer[8..], in minCol);
|
||||||
|
MemoryMarshal.Write(buffer[12..], in maxCol);
|
||||||
|
MemoryMarshal.Write(buffer[16..], in cropOffsetX);
|
||||||
|
MemoryMarshal.Write(buffer[20..], in cropOffsetY);
|
||||||
|
MemoryMarshal.Write(buffer[24..], in cropSizeW);
|
||||||
|
MemoryMarshal.Write(buffer[28..], in cropSizeH);
|
||||||
|
MemoryMarshal.Write(buffer[32..], in outputScale);
|
||||||
|
cacheKey = Convert.ToHexString(buffer).ToLowerInvariant();
|
||||||
|
if( cache.TryGetValue(cacheKey, out cacheFile) && File.Exists(cacheFile) )
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
for( int row = minRow; row <= maxRow; row++ )
|
for( int row = minRow; row <= maxRow; row++ )
|
||||||
for( int col = minCol; col <= maxCol; col++ )
|
for( int col = minCol; col <= maxCol; col++ )
|
||||||
images.Add(Image.NewFromFile(FullPath(tilesDirectory, row, col)));
|
images.Add(Image.NewFromFile(FullPath(tilesDirectory, row, col)));
|
||||||
|
|
||||||
using var canvasImage = Image.Arrayjoin(images.ToArray(), width);
|
using var canvasImage = Image.Arrayjoin(images.ToArray(), width);
|
||||||
int cropLeft = (int)( canvasImage.Width * request.CropOffset.X );
|
int cropLeft = (int)( canvasImage.Width * cropOffsetX );
|
||||||
int cropTop = (int)( canvasImage.Height * request.CropOffset.Y );
|
int cropTop = (int)( canvasImage.Height * cropOffsetY );
|
||||||
int cropWidth = (int)( canvasImage.Width * request.CropSize.Width );
|
int cropWidth = (int)( canvasImage.Width * cropSizeW );
|
||||||
int cropHeight = (int)( canvasImage.Height * request.CropSize.Height );
|
int cropHeight = (int)( canvasImage.Height * cropSizeH );
|
||||||
|
|
||||||
return canvasImage.Crop(cropLeft, cropTop, cropWidth, cropHeight).Resize(request.OutputScale);
|
image = canvasImage.Crop(cropLeft, cropTop, cropWidth, cropHeight).Resize(outputScale);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Image CreateError(Exception e)
|
public static Image CreateError(Exception e)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue