Skip to content

Commit 7556422

Browse files
authored
dotnet performance boost (#472)
1 parent 63db0ef commit 7556422

22 files changed

+368
-431
lines changed
Lines changed: 18 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,30 @@
1+
using Amazon;
2+
using Amazon.Runtime;
13
using Amazon.S3;
24
using Amazon.S3.Model;
3-
using Amazon.Runtime;
45

5-
internal sealed class AmazonS3Uploader : IDisposable
6+
namespace cs_app_aot;
7+
8+
public sealed class AmazonS3Uploader
69
{
7-
private readonly AmazonS3Client client;
10+
private readonly AmazonS3Client _client;
811

9-
public AmazonS3Uploader(string? accessKey, string? secretKey, string? endpoint)
12+
public AmazonS3Uploader(string? accessKey, string? secretKey, string? endpoint, string? region)
1013
{
11-
var credentials = new BasicAWSCredentials(accessKey, secretKey);
12-
var clientConfig = new AmazonS3Config()
14+
var creds = new BasicAWSCredentials(accessKey, secretKey);
15+
var cfg = new AmazonS3Config
1316
{
14-
ServiceURL = endpoint,
15-
ForcePathStyle = true,
17+
ForcePathStyle = true
1618
};
1719

18-
client = new AmazonS3Client(credentials, clientConfig);
19-
}
20+
if (!string.IsNullOrWhiteSpace(endpoint))
21+
cfg.ServiceURL = endpoint;
22+
if (!string.IsNullOrWhiteSpace(region))
23+
cfg.RegionEndpoint = RegionEndpoint.GetBySystemName(region);
2024

21-
public void Dispose()
22-
{
23-
client.Dispose();
25+
_client = new AmazonS3Client(creds, cfg);
2426
}
2527

26-
public async Task Upload(string? bucket, string key, string? path)
27-
{
28-
try
29-
{
30-
PutObjectRequest putRequest = new PutObjectRequest
31-
{
32-
BucketName = bucket,
33-
Key = key,
34-
FilePath = path
35-
};
36-
37-
PutObjectResponse response = await client.PutObjectAsync(putRequest);
38-
}
39-
catch (AmazonS3Exception amazonS3Exception)
40-
{
41-
if (amazonS3Exception.ErrorCode != null &&
42-
(amazonS3Exception.ErrorCode.Equals("InvalidAccessKeyId")
43-
||
44-
amazonS3Exception.ErrorCode.Equals("InvalidSecurity")))
45-
{
46-
throw new Exception("Check the provided AWS Credentials.");
47-
}
48-
else
49-
{
50-
throw new Exception("Error occurred: " + amazonS3Exception.Message);
51-
}
52-
}
53-
}
54-
}
28+
public Task Upload(string? bucket, string key, string? path, CancellationToken ct = default)
29+
=> _client.PutObjectAsync(new PutObjectRequest { BucketName = bucket, Key = key, FilePath = path }, ct);
30+
}

lessons/202/cs-app-aot/DbOptions.cs

Lines changed: 0 additions & 11 deletions
This file was deleted.

lessons/202/cs-app-aot/Device.cs

Lines changed: 0 additions & 13 deletions
This file was deleted.

lessons/202/cs-app-aot/Dockerfile

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
1-
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-jammy AS build
1+
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0-noble AS build
22
ARG TARGETARCH
3-
WORKDIR /source
4-
5-
# copy csproj and restore as distinct layers
6-
COPY *.csproj .
3+
WORKDIR /src
4+
COPY *.csproj ./
75
RUN dotnet restore -a $TARGETARCH
8-
9-
# copy everything else and build app
106
COPY . .
11-
RUN dotnet publish -a $TARGETARCH --no-restore -o /app /p:AOT=true /p:UseAppHost=false
12-
RUN rm /app/*.dbg /app/*.Development.json
13-
7+
# ensure assembly name (adjust if your project file name differs)
8+
RUN dotnet publish -c Release -a $TARGETARCH -r linux-x64 -o /app \
9+
/p:PublishAot=true /p:StripSymbols=true /p:IlcGenerateStackTraceData=false \
10+
/p:AssemblyName=cs-app-aot
1411

15-
# final stage/image
16-
FROM mcr.microsoft.com/dotnet/aspnet:8.0-jammy-chiseled
17-
EXPOSE 8080
12+
FROM mcr.microsoft.com/dotnet/runtime-deps:9.0-noble-chiseled
13+
ENV ASPNETCORE_URLS=http://+:8080 \
14+
DOTNET_EnableDiagnostics=0
1815
WORKDIR /app
19-
COPY --from=build /app .
20-
ENTRYPOINT ["./cs-app"]
16+
COPY --from=build /app ./
17+
EXPOSE 8080
18+
ENTRYPOINT ["./cs-app-aot"]

lessons/202/cs-app-aot/Dockerfile.alpine

Lines changed: 0 additions & 24 deletions
This file was deleted.

lessons/202/cs-app-aot/Image.cs

Lines changed: 0 additions & 17 deletions
This file was deleted.

lessons/202/cs-app-aot/Program.cs

Lines changed: 105 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,131 @@
1-
using Prometheus;
21
using System.Diagnostics;
3-
using Npgsql;
4-
using cs_app;
52
using System.Text.Json.Serialization;
3+
using cs_app_aot;
4+
using Npgsql;
5+
using NpgsqlTypes;
6+
using Prometheus;
7+
using Metrics = Prometheus.Metrics;
68

7-
// Initialize the Web App
89
var builder = WebApplication.CreateSlimBuilder(args);
910

10-
// Configure JSON source generatino for AOT support
11-
builder.Services.ConfigureHttpJsonOptions(options =>
11+
// hard-off logging for benchmarks (no sinks)
12+
builder.Logging.ClearProviders();
13+
builder.Logging.SetMinimumLevel(LogLevel.None);
14+
15+
// JSON source-gen for AOT; harmless on JIT
16+
builder.Services.ConfigureHttpJsonOptions(o =>
1217
{
13-
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
18+
o.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
1419
});
1520

16-
// Load configuration
17-
var dbOptions = new DbOptions();
18-
builder.Configuration.GetSection(DbOptions.PATH).Bind(dbOptions);
19-
20-
var s3Options = new S3Options();
21-
builder.Configuration.GetSection(S3Options.PATH).Bind(s3Options);
22-
23-
// Establish S3 session.
24-
using var amazonS3 = new AmazonS3Uploader(s3Options.User, s3Options.Secret, s3Options.Endpoint);
25-
26-
// Create Postgre connection string
27-
var connString = $"Host={dbOptions.Host};Username={dbOptions.User};Password={dbOptions.Password};Database={dbOptions.Database}";
21+
// config
22+
var cfg = new AppConfig(builder.Configuration);
23+
builder.Services.AddSingleton(cfg);
2824

29-
Console.WriteLine(connString);
25+
// AWS S3
26+
builder.Services.AddSingleton(new AmazonS3Uploader(cfg.User, cfg.Secret, cfg.S3Endpoint, cfg.Region));
3027

31-
// Establish Postgres connection
32-
await using var dataSource = new NpgsqlSlimDataSourceBuilder(connString).Build();
33-
34-
// Counter variable is used to increment image id
35-
var counter = 0;
28+
// Npgsql (AOT/trimming friendly)
29+
var csb = new NpgsqlConnectionStringBuilder
30+
{
31+
Host = cfg.DbHost,
32+
Username = cfg.DbUser,
33+
Password = cfg.DbPassword,
34+
Database = cfg.DbDatabase,
35+
Pooling = true,
36+
MaxPoolSize = 256,
37+
MinPoolSize = 16,
38+
NoResetOnClose = true,
39+
AutoPrepareMinUsages = 2,
40+
MaxAutoPrepare = 32,
41+
Multiplexing = true
42+
};
43+
builder.Services.AddSingleton(_ => new NpgsqlSlimDataSourceBuilder(csb.ConnectionString).Build());
3644

3745
var app = builder.Build();
3846

39-
// Create Summary Prometheus metric to measure latency of the requests.
40-
var summary = Metrics.CreateSummary("myapp_request_duration_seconds", "Duration of the request.", new SummaryConfiguration
41-
{
42-
LabelNames = ["op"],
43-
Objectives = [new QuantileEpsilonPair(0.9, 0.01), new QuantileEpsilonPair(0.99, 0.001)]
44-
});
47+
// Prometheus Summary (prebind labels to skip tiny allocs)
48+
var summary = Metrics.CreateSummary("myapp_request_duration_seconds", "Duration of the request.",
49+
new SummaryConfiguration
50+
{
51+
LabelNames = ["op"],
52+
Objectives = [new QuantileEpsilonPair(0.9, 0.01), new QuantileEpsilonPair(0.99, 0.001)]
53+
});
54+
var s3Dur = summary.WithLabels("s3");
55+
var dbDur = summary.WithLabels("db");
4556

46-
// Enable the /metrics page to export Prometheus metrics.
57+
// endpoints
4758
app.MapMetrics();
59+
app.MapGet("/healthz", () => Results.Ok());
60+
app.MapGet("/api/devices", () => Results.Ok(StaticData.Devices));
4861

49-
// Create endpoint that returns the status of the application.
50-
// Placeholder for the health check
51-
app.MapGet("/healthz", () => Results.Ok("OK"));
62+
app.MapGet("/api/images",
63+
async (HttpContext http,
64+
AmazonS3Uploader s3,
65+
NpgsqlDataSource dataSource) =>
66+
{
67+
var id = Interlocked.Increment(ref StaticData.Counter) - 1;
68+
var image = new Image($"cs-thumbnail-{id}.png");
69+
70+
// S3
71+
var t0 = Stopwatch.GetTimestamp();
72+
await s3.Upload(cfg.S3Bucket, image.ObjKey, cfg.S3ImgPath, http.RequestAborted);
73+
s3Dur.Observe(Stopwatch.GetElapsedTime(t0).TotalSeconds);
74+
75+
// DB
76+
var t1 = Stopwatch.GetTimestamp();
77+
await using (var cmd = dataSource.CreateCommand(StaticData.ImageInsertSql))
78+
{
79+
cmd.Parameters.Add(new NpgsqlParameter<Guid> { NpgsqlDbType = NpgsqlDbType.Uuid, Value = image.ImageUuid });
80+
cmd.Parameters.Add(new NpgsqlParameter<string> { NpgsqlDbType = NpgsqlDbType.Text, Value = image.ObjKey });
81+
cmd.Parameters.Add(new NpgsqlParameter<DateTime>
82+
{ NpgsqlDbType = NpgsqlDbType.TimestampTz, Value = image.CreatedAt });
83+
await cmd.ExecuteNonQueryAsync(http.RequestAborted);
84+
}
85+
86+
dbDur.Observe(Stopwatch.GetElapsedTime(t1).TotalSeconds);
87+
return Results.Ok();
88+
});
5289

53-
// Create endpoint that returns a list of connected devices.
54-
app.MapGet("/api/devices", () =>
55-
{
56-
Device[] devices = [
57-
new("b0e42fe7-31a5-4894-a441-007e5256afea", "5F-33-CC-1F-43-82", "2.1.6"),
58-
new("0c3242f5-ae1f-4e0c-a31b-5ec93825b3e7", "EF-2B-C4-F5-D6-34", "2.1.5"),
59-
new("b16d0b53-14f1-4c11-8e29-b9fcef167c26", "62-46-13-B7-B3-A1", "3.0.0"),
60-
new("51bb1937-e005-4327-a3bd-9f32dcf00db8", "96-A8-DE-5B-77-14", "1.0.1"),
61-
new("e0a1d085-dce5-48db-a794-35640113fa67", "7E-3B-62-A6-09-12", "3.5.6")
62-
];
63-
64-
return Results.Ok(devices);
65-
});
90+
app.Run();
6691

67-
// Create endpoint that uoloades image to S3 and writes metadate to Postgres
68-
app.MapGet("/api/images", async () =>
69-
{
70-
// Generate a new image.
71-
var image = new Image($"cs-thumbnail-{counter}.png");
92+
// ---- app types ----
93+
[JsonSerializable(typeof(Device[]))]
94+
internal partial class AppJsonSerializerContext : JsonSerializerContext;
7295

73-
// Get the current time to record the duration of the S3 request.
74-
var s3StartTime = Stopwatch.GetTimestamp();
96+
public sealed class AppConfig(IConfiguration config)
97+
{
98+
public string? DbDatabase = config.GetValue<string>("Db:database");
99+
public string? DbHost = config.GetValue<string>("Db:host");
100+
public string? DbPassword = config.GetValue<string>("Db:password");
101+
public string? DbUser = config.GetValue<string>("Db:user");
75102

76-
// Upload the image to S3.
77-
await amazonS3.Upload(s3Options.Bucket, image.ObjKey, s3Options.ImgPath);
103+
public string? Region = config.GetValue<string>("S3:region");
104+
public string? S3Bucket = config.GetValue<string>("S3:bucket");
105+
public string? S3Endpoint = config.GetValue<string>("S3:endpoint");
106+
public string? S3ImgPath = config.GetValue<string>("S3:imgPath");
107+
public string? Secret = config.GetValue<string>("S3:secret");
108+
public string? User = config.GetValue<string>("S3:user");
109+
}
78110

79-
// Record the duration of the request to S3.
80-
summary.WithLabels(["s3"]).Observe(Stopwatch.GetElapsedTime(s3StartTime).TotalSeconds);
81111

82-
// Get the current time to record the duration of the Database request.
83-
var dbStartTime = Stopwatch.GetTimestamp();
112+
public sealed class Device
113+
{
114+
public required string Uuid { get; init; }
115+
public required string Mac { get; init; }
116+
public required string Firmware { get; init; }
117+
}
84118

85-
// Prepare the database query to insert a record.
86-
const string sqlQuery = "INSERT INTO cs_image VALUES ($1, $2, $3)";
119+
public readonly struct Image
120+
{
121+
public string ObjKey { get; }
122+
public Guid ImageUuid { get; }
123+
public DateTime CreatedAt { get; }
87124

88-
// Execute the query to create a new image record.
89-
await using (var cmd = dataSource.CreateCommand(sqlQuery))
125+
public Image(string key)
90126
{
91-
cmd.Parameters.AddWithValue(image.ImageUuid);
92-
cmd.Parameters.AddWithValue(image.ObjKey);
93-
cmd.Parameters.AddWithValue(image.CreatedAt);
94-
await cmd.ExecuteNonQueryAsync();
127+
ObjKey = key;
128+
ImageUuid = Guid.NewGuid();
129+
CreatedAt = DateTime.UtcNow;
95130
}
96-
97-
// Record the duration of the insert query.
98-
summary.WithLabels(["db"]).Observe(Stopwatch.GetElapsedTime(dbStartTime).TotalSeconds);
99-
100-
// Increment the counter.
101-
counter++;
102-
103-
return Results.Ok("Saved!");
104-
});
105-
106-
app.Run();
107-
108-
[JsonSerializable(typeof(Device[]))]
109-
internal partial class AppJsonSerializerContext : JsonSerializerContext;
131+
}

0 commit comments

Comments
 (0)