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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Tag **@BennyFranciscus** on your PR for help with implementation or benchmark qu
| Category | Profiles | Description |
|----------|----------|-------------|
| Connection | Baseline (512-32K), Pipelined, Limited | Performance scaling with connection count |
| Workload | JSON, Compression, Upload, Database | Serialization, gzip, I/O, queries |
| Workload | JSON, Compression, Upload, Database, Async DB | Serialization, gzip, I/O, SQLite queries, async Postgres |
| Resilience | Noisy, Mixed | Malformed requests, concurrent endpoints |
| Protocol | HTTP/2, HTTP/3, gRPC, WebSocket | Multi-protocol support |

Expand Down
100,017 changes: 100,017 additions & 0 deletions data/pgdb-seed.sql

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions frameworks/actix/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
num_cpus = "1"
rusqlite = { version = "0.31", features = ["bundled"] }
tokio-postgres = { version = "0.7", features = ["with-serde_json-1"] }
deadpool-postgres = { version = "0.14", features = ["rt_tokio_1"] }

[profile.release]
opt-level = 3
Expand Down
1 change: 1 addition & 0 deletions frameworks/actix/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"upload",
"compression",
"mixed",
"async-db",
"baseline-h2",
"static-h2"
]
Expand Down
86 changes: 83 additions & 3 deletions frameworks/actix/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use actix_web::http::header::{ContentType, HeaderValue, SERVER};
use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer};
use deadpool_postgres::{Manager, ManagerConfig, Pool, RecyclingMethod};
use rusqlite::Connection;
use rustls::ServerConfig;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -226,7 +227,16 @@ async fn compression(state: web::Data<Arc<AppState>>) -> HttpResponse {
.body(state.json_large_cache.clone())
}

async fn db_endpoint(req: HttpRequest, db: web::Data<WorkerDb>) -> HttpResponse {
async fn db_endpoint(req: HttpRequest, db: web::Data<Option<WorkerDb>>) -> HttpResponse {
let db = match db.as_ref() {
Some(d) => d,
None => {
return HttpResponse::Ok()
.insert_header((SERVER, SERVER_HDR.clone()))
.content_type(ContentType::json())
.body(r#"{"items":[],"count":0}"#);
}
};
let min: f64 = req.uri().query().and_then(|q| {
q.split('&').find_map(|p| p.strip_prefix("min=").and_then(|v| v.parse().ok()))
}).unwrap_or(10.0);
Expand Down Expand Up @@ -263,6 +273,65 @@ async fn db_endpoint(req: HttpRequest, db: web::Data<WorkerDb>) -> HttpResponse
.body(result.to_string())
}

async fn pgdb_endpoint(req: HttpRequest, pool: web::Data<Option<Pool>>) -> HttpResponse {
let pool = match pool.as_ref() {
Some(p) => p,
None => {
return HttpResponse::Ok()
.insert_header((SERVER, SERVER_HDR.clone()))
.content_type(ContentType::json())
.body(r#"{"items":[],"count":0}"#);
}
};
let min: f64 = req.uri().query().and_then(|q| {
q.split('&').find_map(|p| p.strip_prefix("min=").and_then(|v| v.parse().ok()))
}).unwrap_or(10.0);
let max: f64 = req.uri().query().and_then(|q| {
q.split('&').find_map(|p| p.strip_prefix("max=").and_then(|v| v.parse().ok()))
}).unwrap_or(50.0);
let client = match pool.get().await {
Ok(c) => c,
Err(_) => {
return HttpResponse::Ok()
.insert_header((SERVER, SERVER_HDR.clone()))
.content_type(ContentType::json())
.body(r#"{"items":[],"count":0}"#);
}
};
let stmt = client.prepare_cached(
"SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN $1 AND $2 LIMIT 50"
).await.unwrap();
let rows = match client.query(&stmt, &[&min, &max]).await {
Ok(r) => r,
Err(_) => {
return HttpResponse::Ok()
.insert_header((SERVER, SERVER_HDR.clone()))
.content_type(ContentType::json())
.body(r#"{"items":[],"count":0}"#);
}
};
let items: Vec<serde_json::Value> = rows.iter().map(|row| {
serde_json::json!({
"id": row.get::<_, i32>(0) as i64,
"name": row.get::<_, &str>(1),
"category": row.get::<_, &str>(2),
"price": row.get::<_, f64>(3),
"quantity": row.get::<_, i32>(4) as i64,
"active": row.get::<_, bool>(5),
"tags": row.get::<_, serde_json::Value>(6),
"rating": {
"score": row.get::<_, f64>(7),
"count": row.get::<_, i32>(8) as i64,
}
})
}).collect();
let result = serde_json::json!({"items": items, "count": items.len()});
HttpResponse::Ok()
.insert_header((SERVER, SERVER_HDR.clone()))
.content_type(ContentType::json())
.body(result.to_string())
}

async fn static_file(
state: web::Data<Arc<AppState>>,
path: web::Path<String>,
Expand Down Expand Up @@ -311,26 +380,36 @@ async fn main() -> io::Result<()> {
static_files: load_static_files(),
});

let pg_pool: Option<Pool> = std::env::var("DATABASE_URL").ok().and_then(|url| {
let pg_config: tokio_postgres::Config = url.parse().ok()?;
let mgr = Manager::from_config(pg_config, deadpool_postgres::tokio_postgres::NoTls,
ManagerConfig { recycling_method: RecyclingMethod::Fast });
let pool_size = (num_cpus::get() * 4).max(64);
Pool::builder(mgr).max_size(pool_size).build().ok()
});

let tls_config = load_tls_config();
let workers = num_cpus::get();

let mut server = HttpServer::new({
let state = state.clone();
let pg_pool = pg_pool.clone();
move || {
let worker_db = Connection::open_with_flags(
"/data/benchmark.db",
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
)
.ok()
.map(|conn| {
conn.execute_batch("PRAGMA mmap_size=268435456").ok();
WorkerDb(Mutex::new(conn))
})
.expect("Failed to open database");
});
App::new()
.wrap(actix_web::middleware::Compress::default())
.app_data(web::Data::new(state.clone()))
.app_data(web::Data::new(worker_db))
.app_data(web::PayloadConfig::new(25 * 1024 * 1024))
.app_data(web::Data::new(pg_pool.clone()))
.route("/pipeline", web::get().to(pipeline))
.route("/baseline11", web::get().to(baseline11_get))
.route("/baseline11", web::post().to(baseline11_post))
Expand All @@ -339,6 +418,7 @@ async fn main() -> io::Result<()> {
.route("/compression", web::get().to(compression))
.route("/db", web::get().to(db_endpoint))
.route("/upload", web::post().to(upload))
.route("/async-db", web::get().to(pgdb_endpoint))
.route("/static/{filename}", web::get().to(static_file))
}
})
Expand Down
19 changes: 19 additions & 0 deletions frameworks/aspnet-minimal/AppData.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Text.Json;
using Microsoft.Data.Sqlite;
using Npgsql;

static class AppData
{
Expand All @@ -13,13 +14,15 @@ static class AppData
public static byte[]? LargeJsonResponse;
public static Dictionary<string, (byte[] Data, string ContentType)> StaticFiles = new();
public static SqliteConnection? DbConnection;
public static NpgsqlDataSource? PgDataSource;

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

static void LoadDataset()
Expand Down Expand Up @@ -70,6 +73,22 @@ static void LoadStaticFiles()
}
}

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";
Expand Down
35 changes: 35 additions & 0 deletions frameworks/aspnet-minimal/Handlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,41 @@ public static IResult Database(HttpRequest req)
return Results.Json(new { items, count = items.Count });
}

public static async Task<IResult> AsyncDatabase(HttpRequest req)
{
if (AppData.PgDataSource == null)
return Results.Json(new { items = Array.Empty<object>(), count = 0 });

double min = 10, max = 50;
if (req.Query.ContainsKey("min") && double.TryParse(req.Query["min"], out double pmin))
min = pmin;
if (req.Query.ContainsKey("max") && double.TryParse(req.Query["max"], out double 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<object>();
while (await reader.ReadAsync())
{
items.Add(new
{
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<List<string>>(reader.GetString(6)),
rating = new { score = reader.GetDouble(7), count = reader.GetInt32(8) },
});
}
return Results.Json(new { items, count = items.Count });
}

static int SumQuery(HttpRequest req)
{
int sum = 0;
Expand Down
1 change: 1 addition & 0 deletions frameworks/aspnet-minimal/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
app.MapGet("/json", Handlers.Json);
app.MapGet("/compression", Handlers.Compression);
app.MapGet("/db", Handlers.Database);
app.MapGet("/async-db", Handlers.AsyncDatabase);
app.MapGet("/static/{filename}", Handlers.StaticFile);

app.Run();
1 change: 1 addition & 0 deletions frameworks/aspnet-minimal/aspnet-minimal.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.3" />
<PackageReference Include="Npgsql" Version="9.0.3" />
</ItemGroup>
</Project>
1 change: 1 addition & 0 deletions frameworks/aspnet-minimal/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"compression",
"noisy",
"mixed",
"async-db",
"baseline-h2",
"static-h2",
"baseline-h3",
Expand Down
1 change: 1 addition & 0 deletions frameworks/express/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"compression",
"mixed",
"noisy",
"async-db",
"baseline-h2",
"static-h2"
]
Expand Down
3 changes: 2 additions & 1 deletion frameworks/express/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"dependencies": {
"express": "^5.1.0",
"better-sqlite3": "^11.0.0"
"better-sqlite3": "^11.0.0",
"pg": "^8.13.0"
}
}
41 changes: 41 additions & 0 deletions frameworks/express/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const SERVER_NAME = 'express';
let datasetItems;
let largeJsonBuf;
let dbStmt;
let pgPool;
const staticFiles = {};
const MIME_TYPES = {
'.css': 'text/css', '.js': 'application/javascript', '.html': 'text/html',
Expand Down Expand Up @@ -57,6 +58,15 @@ function loadDatabase() {
} catch (e) {}
}

function loadPgPool() {
const dbUrl = process.env.DATABASE_URL;
if (!dbUrl) return;
try {
const { Pool } = require('pg');
pgPool = new Pool({ connectionString: dbUrl, max: 4 });
} catch (e) {}
}

function sumQuery(query) {
let sum = 0;
if (query) {
Expand All @@ -73,6 +83,7 @@ function startWorker() {
loadLargeDataset();
loadStaticFiles();
loadDatabase();
loadPgPool();

const express = require('express');
const app = express();
Expand Down Expand Up @@ -163,6 +174,36 @@ function startWorker() {
.send(body);
});

// --- /async-db ---
app.get('/async-db', async (req, res) => {
if (!pgPool) {
return res.set('server', SERVER_NAME).type('application/json').send('{"items":[],"count":0}');
}
let min = 10, max = 50;
if (req.query.min) min = parseFloat(req.query.min) || 10;
if (req.query.max) max = parseFloat(req.query.max) || 50;
try {
const result = await pgPool.query(
'SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN $1 AND $2 LIMIT 50',
[min, max]
);
const items = result.rows.map(r => ({
id: r.id, name: r.name, category: r.category,
price: r.price, quantity: r.quantity, active: r.active,
tags: r.tags,
rating: { score: r.rating_score, count: r.rating_count }
}));
const body = JSON.stringify({ items, count: items.length });
res
.set('server', SERVER_NAME)
.set('content-type', 'application/json')
.set('content-length', Buffer.byteLength(body))
.send(body);
} catch (e) {
res.set('server', SERVER_NAME).type('application/json').send('{"items":[],"count":0}');
}
});

// --- /upload ---
app.post('/upload', (req, res) => {
const body = Buffer.isBuffer(req.body) ? req.body : Buffer.from(req.body || '');
Expand Down
Loading
Loading