Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions frameworks/aspnet-minimal-iouring/AppData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using System.Collections.Concurrent;
using System.Text.Json;
using Microsoft.Data.Sqlite;
using Npgsql;

static class AppData
{
public static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

public static List<DatasetItem>? DatasetItems;
public static byte[]? LargeJsonResponse;

public static SqlitePool? DbPool;
public static NpgsqlDataSource? PgDataSource;

public static void Load()
{
LoadDataset();
LoadLargeDataset();
OpenDatabase();
OpenPgPool();
}

static void LoadDataset()
{
var path = Environment.GetEnvironmentVariable("DATASET_PATH") ?? "/data/dataset.json";
if (!File.Exists(path)) return;
DatasetItems = JsonSerializer.Deserialize<List<DatasetItem>>(File.ReadAllText(path), JsonOptions);
}

static void LoadLargeDataset()
{
var path = "/data/dataset-large.json";
if (!File.Exists(path)) return;
var items = JsonSerializer.Deserialize<List<DatasetItem>>(File.ReadAllText(path), JsonOptions);
if (items == null) return;

var processed = new List<ProcessedItem>(items.Count);
foreach (var item in items)
{
processed.Add(new ProcessedItem
{
Id = item.Id, Name = item.Name, Category = item.Category,
Price = item.Price, Quantity = item.Quantity, Active = item.Active,
Tags = item.Tags, Rating = item.Rating,
Total = Math.Round(item.Price * item.Quantity, 2)
});
}
LargeJsonResponse = JsonSerializer.SerializeToUtf8Bytes(
new { items = processed, count = processed.Count }, JsonOptions);
}

static void OpenPgPool()
{
var dbUrl = Environment.GetEnvironmentVariable("DATABASE_URL");
if (string.IsNullOrEmpty(dbUrl)) return;
try
{
// Parse postgres:// URI into Npgsql connection string
var uri = new Uri(dbUrl);
var userInfo = uri.UserInfo.Split(':');
var connStr = $"Host={uri.Host};Port={uri.Port};Username={userInfo[0]};Password={userInfo[1]};Database={uri.AbsolutePath.TrimStart('/')};Maximum Pool Size=256;Minimum Pool Size=64;Multiplexing=true;No Reset On Close=true;Max Auto Prepare=4;Auto Prepare Min Usages=1";
var builder = new NpgsqlDataSourceBuilder(connStr);
PgDataSource = builder.Build();
}
catch { }
}

static void OpenDatabase()
{
var path = "/data/benchmark.db";
if (!File.Exists(path)) return;
DbPool = new SqlitePool($"Data Source={path};Mode=ReadOnly", Environment.ProcessorCount);
}
}

sealed class SqlitePool
{
private readonly ConcurrentBag<SqliteConnection> _connections = new();

public SqlitePool(string connectionString, int size)
{
for (int i = 0; i < size; i++)
{
var conn = new SqliteConnection(connectionString);
conn.Open();
using var pragma = conn.CreateCommand();
pragma.CommandText = "PRAGMA mmap_size=268435456";
pragma.ExecuteNonQuery();
_connections.Add(conn);
}
}

public SqliteConnection Rent()
{
SqliteConnection? conn;
var spin = new SpinWait();
while (!_connections.TryTake(out conn))
spin.SpinOnce();
return conn;
}

public void Return(SqliteConnection conn) => _connections.Add(conn);
}
39 changes: 39 additions & 0 deletions frameworks/aspnet-minimal-iouring/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Stage 1: Build the custom .NET runtime with io_uring socket engine
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS runtime-build
RUN apt-get update && apt-get install -y --no-install-recommends \
git cmake clang libicu-dev libssl-dev libkrb5-dev zlib1g-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /runtime
RUN git clone --depth 1 --branch io_uring https://github.com/benaadams/runtime.git . \
&& ./build.sh -subset clr+libs -c Release -bl

# Stage 2: Build the app using the official SDK
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS app-build
WORKDIR /app
COPY *.csproj ./
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o out

# Stage 3: Final image — patch the runtime with io_uring binaries
FROM mcr.microsoft.com/dotnet/aspnet:10.0

# Install libmsquic for HTTP/3
ADD https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb /packages-microsoft-prod.deb
RUN dpkg -i packages-microsoft-prod.deb && rm packages-microsoft-prod.deb \
&& apt-get update \
&& apt-get install -y --no-install-recommends libmsquic \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

# Overlay custom runtime libraries (io_uring socket engine)
COPY --from=runtime-build /runtime/artifacts/bin/runtime/net10.0-linux-Release-x64/ /usr/share/dotnet/shared/Microsoft.NETCore.App/10.0.0-preview.*/

WORKDIR /app
COPY --from=app-build /app/out .
EXPOSE 8080 8443/tcp 8443/udp

# Enable io_uring socket engine
ENV DOTNET_SYSTEM_NET_SOCKETS_IO_URING=1

ENTRYPOINT ["dotnet", "aspnet-minimal-iouring.dll"]
171 changes: 171 additions & 0 deletions frameworks/aspnet-minimal-iouring/Handlers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
using System.Text.Json;
using System.Buffers;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Http.HttpResults;


[JsonSerializable(typeof(ResponseDto<ProcessedItem>))]
[JsonSerializable(typeof(ResponseDto<DbResponseItemDto>))]
[JsonSerializable(typeof(DbResponseItemDto))]
[JsonSerializable(typeof(ProcessedItem))]
[JsonSerializable(typeof(RatingInfo))]
[JsonSerializable(typeof(List<string>))]
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
partial class AppJsonContext : JsonSerializerContext { }

static class Handlers
{
public static int Sum(int a, int b) => a + b;

public static async ValueTask<int> SumBody(int a, int b, HttpRequest req)
{
using var reader = new StreamReader(req.Body);
return a + b + int.Parse(await reader.ReadToEndAsync());
}

public static string Text() => "ok";

public static async ValueTask<string> Upload(HttpRequest req)
{
long size = 0;
var buffer = ArrayPool<byte>.Shared.Rent(65536);
try
{
int read;
while ((read = await req.Body.ReadAsync(buffer.AsMemory(0, buffer.Length))) > 0)
{
size += read;
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}

return size.ToString();
}

public static Results<JsonHttpResult<ResponseDto<ProcessedItem>>, ProblemHttpResult> Json()
{
var source = AppData.DatasetItems;
if (source == null)
return TypedResults.Problem("Dataset not loaded");

int count = source.Count;

var items = new ProcessedItem[count];

for (int i = 0; i < count; i++)
{
var item = source[i];
items[i] = new ProcessedItem
{
Id = item.Id,
Name = item.Name,
Category = item.Category,
Price = item.Price,
Quantity = item.Quantity,
Active = item.Active,
Tags = item.Tags,
Rating = item.Rating,
Total = Math.Round(item.Price * item.Quantity, 2)
};
}

return TypedResults.Json(new ResponseDto<ProcessedItem>(items, count), AppJsonContext.Default.ResponseDtoProcessedItem);
}

public static Results<FileContentHttpResult, ProblemHttpResult> Compression()
{
if (AppData.LargeJsonResponse == null)
return TypedResults.Problem("Dataset not loaded");

return TypedResults.Bytes(AppData.LargeJsonResponse, "application/json");
}

public static Results<JsonHttpResult<ResponseDto<DbResponseItemDto>>, ProblemHttpResult> Database(HttpRequest req)
{
if (AppData.DbPool == null)
return TypedResults.Problem("DB not available");

double min = 10, max = 50;
// Optimize query lookups
var query = req.Query;
if (query.TryGetValue("min", out var minStr) && double.TryParse(minStr, out var pmin)) min = pmin;
if (query.TryGetValue("max", out var maxStr) && double.TryParse(maxStr, out var pmax)) max = pmax;

var conn = AppData.DbPool.Rent();
try
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN @min AND @max LIMIT 50";

cmd.Parameters.AddWithValue("@min", min);
cmd.Parameters.AddWithValue("@max", max);

using var reader = cmd.ExecuteReader();

var items = new List<DbResponseItemDto>(50);

while (reader.Read())
{
items.Add(new DbResponseItemDto
{
Id = reader.GetInt32(0),
Name = reader.GetString(1),
Category = reader.GetString(2),
Price = reader.GetDouble(3),
Quantity = reader.GetInt32(4),
Active = reader.GetInt32(5) == 1,
Tags = JsonSerializer.Deserialize(reader.GetString(6), AppJsonContext.Default.ListString)!,
Rating = new RatingInfo { Score = reader.GetDouble(7), Count = reader.GetInt32(8) },
});
}

return TypedResults.Json(new ResponseDto<DbResponseItemDto>(items, items.Count), AppJsonContext.Default.ResponseDtoDbResponseItemDto);
}
finally
{
AppData.DbPool.Return(conn);
}
}

public static async Task<Results<JsonHttpResult<ResponseDto<DbResponseItemDto>>, ProblemHttpResult>> AsyncDatabase(HttpRequest req)
{
if (AppData.PgDataSource == null)
return TypedResults.Problem("DB not available");

// Query Parsing
double min = 10, max = 50;
var query = req.Query;
if (query.TryGetValue("min", out var minVal) && double.TryParse(minVal, out var pmin)) min = pmin;
if (query.TryGetValue("max", out var maxVal) && double.TryParse(maxVal, out var pmax)) max = pmax;

await using var cmd = AppData.PgDataSource.CreateCommand(
"SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN $1 AND $2 LIMIT 50");

cmd.Parameters.AddWithValue(min);
cmd.Parameters.AddWithValue(max);

await using var reader = await cmd.ExecuteReaderAsync();

var items = new List<DbResponseItemDto>(50);

while (await reader.ReadAsync())
{
items.Add(new DbResponseItemDto
{
Id = reader.GetInt32(0),
Name = reader.GetString(1),
Category = reader.GetString(2),
Price = reader.GetDouble(3),
Quantity = reader.GetInt32(4),
Active = reader.GetBoolean(5),
Tags = JsonSerializer.Deserialize(reader.GetString(6), AppJsonContext.Default.ListString)!,
Rating = new RatingInfo { Score = reader.GetDouble(7), Count = reader.GetInt32(8) }
});
}

return TypedResults.Json(new ResponseDto<DbResponseItemDto>(items, items.Count), AppJsonContext.Default.ResponseDtoDbResponseItemDto);
}
}
45 changes: 45 additions & 0 deletions frameworks/aspnet-minimal-iouring/Models.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
sealed record ResponseDto<T>(IReadOnlyList<T> Items, int Count);


sealed class DbResponseItemDto
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string Category { get; set; } = "";
public double Price { get; set; }
public int Quantity { get; set; }
public bool Active { get; set; }
public List<string> Tags { get; set; } = [];
public RatingInfo Rating { get; set; } = new();
}

sealed class DatasetItem
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string Category { get; set; } = "";
public double Price { get; set; }
public int Quantity { get; set; }
public bool Active { get; set; }
public List<string> Tags { get; set; } = [];
public RatingInfo Rating { get; set; } = new();
}

sealed class ProcessedItem
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string Category { get; set; } = "";
public double Price { get; set; }
public int Quantity { get; set; }
public bool Active { get; set; }
public List<string> Tags { get; set; } = [];
public RatingInfo Rating { get; set; } = new();
public double Total { get; set; }
}

sealed class RatingInfo
{
public double Score { get; set; }
public int Count { get; set; }
}
Loading
Loading