diff --git a/frameworks/CSharp/sisk/benchmark_config.json b/frameworks/CSharp/sisk/benchmark_config.json index b2cc682ac22..1e8fdc046b0 100644 --- a/frameworks/CSharp/sisk/benchmark_config.json +++ b/frameworks/CSharp/sisk/benchmark_config.json @@ -1,21 +1,41 @@ { - "framework": "sisk", - "tests": [{ - "default": { - "plaintext_url": "/plaintext", - "json_url": "/json", - "port": 8080, - "approach": "Realistic", - "classification": "Fullstack", - "database": "None", - "framework": "Sisk", - "language": "C#", - "orm": "Raw", - "platform": ".NET", - "webserver": "Sisk", - "os": "Linux", - "database_os": "Linux", - "display_name": "Sisk Framework" - } - }] -} + "framework": "sisk", + "tests": [ + { + "default": { + "plaintext_url": "/plaintext", + "json_url": "/json", + "port": 8080, + "approach": "Realistic", + "classification": "Micro", + "database": "None", + "framework": "Sisk", + "language": "C#", + "orm": "Raw", + "platform": ".NET", + "webserver": "HttpListener", + "os": "Linux", + "database_os": "Linux", + "display_name": "Sisk Framework" + } + }, + { + "cadente": { + "plaintext_url": "/plaintext", + "json_url": "/json", + "port": 8080, + "approach": "Realistic", + "classification": "Platform", + "database": "None", + "framework": "Sisk", + "language": "C#", + "orm": "Raw", + "platform": ".NET", + "webserver": "Cadente", + "os": "Linux", + "database_os": "Linux", + "display_name": "Sisk Framework (Cadente)" + } + } + ] +} \ No newline at end of file diff --git a/frameworks/CSharp/sisk/config.toml b/frameworks/CSharp/sisk/config.toml index 25d9855ea65..af787b73130 100644 --- a/frameworks/CSharp/sisk/config.toml +++ b/frameworks/CSharp/sisk/config.toml @@ -5,11 +5,24 @@ name = "sisk" urls.plaintext = "/plaintext" urls.json = "/json" approach = "Realistic" -classification = "Fullstack" +classification = "Micro" database = "None" database_os = "Linux" os = "Linux" orm = "Raw" platform = ".NET" -webserver = "Sisk" +webserver = "HttpListener" +versus = "None" + +[cadente] +urls.plaintext = "/plaintext" +urls.json = "/json" +approach = "Realistic" +classification = "Platform" +database = "None" +database_os = "Linux" +os = "Linux" +orm = "Raw" +platform = ".NET" +webserver = "Cadente" versus = "None" diff --git a/frameworks/CSharp/sisk/sisk-cadente.dockerfile b/frameworks/CSharp/sisk/sisk-cadente.dockerfile new file mode 100644 index 00000000000..87f45780338 --- /dev/null +++ b/frameworks/CSharp/sisk/sisk-cadente.dockerfile @@ -0,0 +1,19 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /source + +# copy csproj and restore as distinct layers +COPY sisk-cadente/*.csproj . +RUN dotnet restore -r linux-musl-x64 + +# copy and publish app and libraries +COPY sisk-cadente/ . +RUN dotnet publish -c release -o /app -r linux-musl-x64 + +# final stage/image +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime +WORKDIR /app +COPY --from=build /app . + +ENTRYPOINT ["dotnet", "./sisk.dll"] + +EXPOSE 8080 \ No newline at end of file diff --git a/frameworks/CSharp/sisk/sisk-cadente/Program.cs b/frameworks/CSharp/sisk/sisk-cadente/Program.cs new file mode 100644 index 00000000000..ba19a480d16 --- /dev/null +++ b/frameworks/CSharp/sisk/sisk-cadente/Program.cs @@ -0,0 +1,36 @@ +using System.Text; +using System.Text.Json; +using Sisk.Cadente; + +var host = new HttpHost ( 8080, session => { + var request = session.Request; + + if (request.Path == "/plaintext") { + SerializePlainTextResponse ( session.Response ); + } + else if (request.Path == "/json") { + SerializeJsonResponse ( session.Response ); + } + else { + session.Response.StatusCode = 404; + } +} ); + +host.Start (); +Thread.Sleep ( Timeout.Infinite ); + +static void SerializePlainTextResponse ( HttpResponse response ) { + var contentBytes = Encoding.UTF8.GetBytes ( "Hello, world!" ); + + response.Headers.Add ( new HttpHeader ( "Content-Type", "text/plain" ) ); + response.ResponseStream = new MemoryStream ( contentBytes ); +} + +static void SerializeJsonResponse ( HttpResponse response ) { + var contentBytes = JsonSerializer.SerializeToUtf8Bytes ( new { + message = "Hello, world!" + } ); + + response.Headers.Add ( new HttpHeader ( "Content-Type", "application/json; charset=utf-8" ) ); + response.ResponseStream = new MemoryStream ( contentBytes ); +} \ No newline at end of file diff --git a/frameworks/CSharp/sisk/sisk-cadente/sisk.csproj b/frameworks/CSharp/sisk/sisk-cadente/sisk.csproj new file mode 100644 index 00000000000..4d284054c2b --- /dev/null +++ b/frameworks/CSharp/sisk/sisk-cadente/sisk.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + enable + true + + + + + + + diff --git a/frameworks/CSharp/sisk/sisk/Program.cs b/frameworks/CSharp/sisk/sisk/Program.cs index de5c69d1670..12d331767ad 100644 --- a/frameworks/CSharp/sisk/sisk/Program.cs +++ b/frameworks/CSharp/sisk/sisk/Program.cs @@ -1,26 +1,22 @@ -using Sisk.Core.Http; +using System.Net.Http.Json; +using Sisk.Core.Http; using Sisk.Core.Routing; -using System.Net.Http.Json; -var app = HttpServer.CreateBuilder(host => -{ - host.UseListeningPort("http://+:8080/"); -}); +var app = HttpServer.CreateBuilder ( host => { + host.UseListeningPort ( "http://+:8080/" ); +} ).Build (); -app.Router.SetRoute(RouteMethod.Get, "/plaintext", PlainText); -app.Router.SetRoute(RouteMethod.Get, "/json", Json); +app.Router.SetRoute ( RouteMethod.Get, "/plaintext", PlainText ); +app.Router.SetRoute ( RouteMethod.Get, "/json", Json ); -app.Start(); +app.Start (); -static HttpResponse PlainText(HttpRequest request) -{ - return new HttpResponse().WithContent("Hello, world!"); +static HttpResponse PlainText ( HttpRequest request ) { + return new HttpResponse ( "Hello, world!" ); } -static HttpResponse Json(HttpRequest request) -{ - return new HttpResponse().WithContent(JsonContent.Create(new - { +static HttpResponse Json ( HttpRequest request ) { + return new HttpResponse ( JsonContent.Create ( new { message = "Hello, world!" - })); + } ) ); } \ No newline at end of file diff --git a/frameworks/CSharp/sisk/sisk/sisk.csproj b/frameworks/CSharp/sisk/sisk/sisk.csproj index 1b95479eb01..e767d50ede0 100644 --- a/frameworks/CSharp/sisk/sisk/sisk.csproj +++ b/frameworks/CSharp/sisk/sisk/sisk.csproj @@ -1,4 +1,4 @@ - + Exe @@ -9,7 +9,7 @@ - + diff --git a/frameworks/Java/helidon/nima/pom.xml b/frameworks/Java/helidon/nima/pom.xml index c859b758cfc..0d312c8eed8 100644 --- a/frameworks/Java/helidon/nima/pom.xml +++ b/frameworks/Java/helidon/nima/pom.xml @@ -21,7 +21,7 @@ io.helidon.applications helidon-se - 4.1.2 + 4.1.5 @@ -38,6 +38,7 @@ 1.3.0 4.5.3 0.9.23 + 3.1.15 @@ -78,9 +79,9 @@ 42.6.1 - com.fizzed - rocker-runtime - ${rocker.version} + gg.jte + jte + ${jte.version} io.helidon.common.testing @@ -98,7 +99,6 @@ test - @@ -125,20 +125,21 @@ + - com.fizzed - rocker-maven-plugin - ${rocker.version} + gg.jte + jte-maven-plugin + ${jte.version} + + ${project.basedir}/src/main/resources/views + Html + - generate-rocker-templates generate-sources generate - - src/main/resources - diff --git a/frameworks/Java/helidon/nima/src/main/java/io/helidon/benchmark/nima/JsonSerializer.java b/frameworks/Java/helidon/nima/src/main/java/io/helidon/benchmark/nima/JsonSerializer.java index 322a7cf030c..0564fa8a911 100644 --- a/frameworks/Java/helidon/nima/src/main/java/io/helidon/benchmark/nima/JsonSerializer.java +++ b/frameworks/Java/helidon/nima/src/main/java/io/helidon/benchmark/nima/JsonSerializer.java @@ -2,8 +2,8 @@ import java.io.IOException; import java.util.Arrays; -import java.util.Map; import java.util.List; +import java.util.Map; import com.jsoniter.output.JsonStream; import com.jsoniter.output.JsonStreamPool; @@ -15,7 +15,7 @@ private JsonSerializer() { } /** - * Serialize an instance into a JSON object and return it as a byte array. + * Serialize an instance into a byte array. * * @param obj the instance * @return the byte array @@ -28,19 +28,31 @@ public static byte[] serialize(Object obj) { return Arrays.copyOfRange(stream.buffer().data(), 0, stream.buffer().tail()); } catch (IOException e) { throw new JsonException(e); - } finally { - JsonStreamPool.returnJsonStream(stream); } } /** - * Serialize a map of strings into a JSON object and return it as a byte array. + * Serialize an instance into a JSON stream. + * + * @param obj the instance + * @param stream the JSON stream + */ + public static void serialize(Object obj, JsonStream stream) { + try { + stream.reset(null); + stream.writeVal(obj.getClass(), obj); + } catch (IOException e) { + throw new JsonException(e); + } + } + + /** + * Serialize a map of strings into a JSON stream. * * @param map the map - * @return the byte array + * @param stream the JSON stream */ - public static byte[] serialize(Map map) { - JsonStream stream = JsonStreamPool.borrowJsonStream(); + public static void serialize(Map map, JsonStream stream) { try { stream.reset(null); stream.writeObjectStart(); @@ -53,22 +65,18 @@ public static byte[] serialize(Map map) { } }); stream.writeObjectEnd(); - return Arrays.copyOfRange(stream.buffer().data(), 0, stream.buffer().tail()); } catch (IOException e) { throw new JsonException(e); - } finally { - JsonStreamPool.returnJsonStream(stream); } } /** - * Serialize a list of objects into a JSON array and return it as a byte array. + * Serialize a list of objects into a JSON stream. * * @param objs the list of objects - * @return the byte array + * @param stream the JSON stream */ - public static byte[] serialize(List objs) { - JsonStream stream = JsonStreamPool.borrowJsonStream(); + public static void serialize(List objs, JsonStream stream) { try { stream.reset(null); stream.writeArrayStart(); @@ -82,11 +90,8 @@ public static byte[] serialize(List objs) { } stream.writeArrayEnd(); - return Arrays.copyOfRange(stream.buffer().data(), 0, stream.buffer().tail()); } catch (IOException e) { throw new JsonException(e); - } finally { - JsonStreamPool.returnJsonStream(stream); } } } diff --git a/frameworks/Java/helidon/nima/src/main/java/io/helidon/benchmark/nima/Main.java b/frameworks/Java/helidon/nima/src/main/java/io/helidon/benchmark/nima/Main.java index df669d8a7a7..800ce927537 100644 --- a/frameworks/Java/helidon/nima/src/main/java/io/helidon/benchmark/nima/Main.java +++ b/frameworks/Java/helidon/nima/src/main/java/io/helidon/benchmark/nima/Main.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2025 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,16 +19,18 @@ import java.nio.charset.StandardCharsets; import java.util.logging.Logger; +import com.jsoniter.output.JsonStream; +import com.jsoniter.output.JsonStreamPool; import io.helidon.benchmark.nima.models.DbRepository; import io.helidon.benchmark.nima.models.HikariJdbcRepository; import io.helidon.benchmark.nima.models.PgClientRepository; import io.helidon.benchmark.nima.services.DbService; import io.helidon.benchmark.nima.services.FortuneHandler; +import io.helidon.config.Config; +import io.helidon.config.ConfigException; import io.helidon.http.Header; import io.helidon.http.HeaderNames; import io.helidon.http.HeaderValues; -import io.helidon.config.Config; -import io.helidon.config.ConfigException; import io.helidon.logging.common.LogConfig; import io.helidon.webserver.WebServer; import io.helidon.webserver.http.Handler; @@ -93,7 +95,7 @@ static void routing(HttpRules rules) { static class PlaintextHandler implements Handler { static final Header CONTENT_TYPE = HeaderValues.createCached(HeaderNames.CONTENT_TYPE, - "text/plain; charset=UTF-8"); + "text/plain; charset=UTF-8"); static final Header CONTENT_LENGTH = HeaderValues.createCached(HeaderNames.CONTENT_LENGTH, "13"); private static final byte[] RESPONSE_BYTES = "Hello, World!".getBytes(StandardCharsets.UTF_8); @@ -110,14 +112,20 @@ static class JsonHandler implements Handler { private static final String MESSAGE = "Hello, World!"; private static final int JSON_LENGTH = serialize(new Message(MESSAGE)).length; static final Header CONTENT_LENGTH = HeaderValues.createCached(HeaderNames.CONTENT_LENGTH, - String.valueOf(JSON_LENGTH)); + String.valueOf(JSON_LENGTH)); @Override public void handle(ServerRequest req, ServerResponse res) { - res.header(CONTENT_LENGTH); - res.header(HeaderValues.CONTENT_TYPE_JSON); - res.header(Main.SERVER); - res.send(serialize(new Message(MESSAGE))); + JsonStream stream = JsonStreamPool.borrowJsonStream(); + try { + res.header(CONTENT_LENGTH); + res.header(HeaderValues.CONTENT_TYPE_JSON); + res.header(Main.SERVER); + serialize(new Message(MESSAGE), stream); + res.send(stream.buffer().data(), 0, stream.buffer().tail()); + } finally { + JsonStreamPool.returnJsonStream(stream); + } } } @@ -147,4 +155,4 @@ public String getMessage() { return message; } } -} +} \ No newline at end of file diff --git a/frameworks/Java/helidon/nima/src/main/java/io/helidon/benchmark/nima/models/Fortune.java b/frameworks/Java/helidon/nima/src/main/java/io/helidon/benchmark/nima/models/Fortune.java index 96a5e2070be..190c792d345 100644 --- a/frameworks/Java/helidon/nima/src/main/java/io/helidon/benchmark/nima/models/Fortune.java +++ b/frameworks/Java/helidon/nima/src/main/java/io/helidon/benchmark/nima/models/Fortune.java @@ -1,7 +1,7 @@ package io.helidon.benchmark.nima.models; -public final class Fortune { +public final class Fortune implements Comparable { public int id; public String message; @@ -17,4 +17,8 @@ public int getId() { public String getMessage() { return message; } + @Override + public int compareTo(Fortune other) { + return message.compareTo(other.message); + } } diff --git a/frameworks/Java/helidon/nima/src/main/java/io/helidon/benchmark/nima/models/PgClientConnectionPool.java b/frameworks/Java/helidon/nima/src/main/java/io/helidon/benchmark/nima/models/PgClientConnectionPool.java new file mode 100644 index 00000000000..478847702cb --- /dev/null +++ b/frameworks/Java/helidon/nima/src/main/java/io/helidon/benchmark/nima/models/PgClientConnectionPool.java @@ -0,0 +1,143 @@ + +package io.helidon.benchmark.nima.models; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.locks.ReentrantLock; + +import io.vertx.core.Vertx; +import io.vertx.pgclient.PgConnectOptions; +import io.vertx.pgclient.PgConnection; +import io.vertx.sqlclient.PreparedQuery; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowSet; + +class PgClientConnectionPool implements AutoCloseable { + + private final Vertx vertx; + private final PgConnectOptions options; + private final ReentrantLock lock = new ReentrantLock(); + private final Map connectionMap = new HashMap<>(); + + public PgClientConnectionPool(Vertx vertx, PgConnectOptions options) { + this.vertx = vertx; + this.options = options; + } + + public PgClientConnection clientConnection() { + String carrierThread = carrierThread(); + PgClientConnection connection = connectionMap.get(carrierThread); + if (connection == null) { + try { + lock.lock(); + connection = connectionMap.get(carrierThread); + if (connection == null) { + connection = newConnection(); + connectionMap.put(carrierThread, connection); + } + } finally { + lock.unlock(); + } + } + return connection; + } + + @Override + public void close() { + try { + for (PgClientConnection connection : connectionMap.values()) { + connection.close(); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private PgClientConnection newConnection() { + try { + PgConnection conn = PgConnection.connect(vertx, options) + .toCompletionStage().toCompletableFuture().get(); + PgClientConnection clientConn = new PgClientConnection(conn); + clientConn.prepare(); + return clientConn; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + static String carrierThread() { + String threadName = Thread.currentThread().toString(); + return threadName.substring(threadName.indexOf('@') + 1); + } + + public static class PgClientConnection implements AutoCloseable { + static final int UPDATE_QUERIES = 500; + private static String SELECT_WORLD = "SELECT id, randomnumber from WORLD where id=$1"; + private static String SELECT_FORTUNE = "SELECT * from FORTUNE"; + + private PreparedQuery> worldQuery; + private PreparedQuery> fortuneQuery; + private PreparedQuery>[] updateQuery; + + private final PgConnection conn; + + PgClientConnection(PgConnection conn) { + this.conn = conn; + } + + public PgConnection pgConnection() { + return conn; + } + + @Override + public void close() { + conn.close(); + } + + public PreparedQuery> worldQuery() { + return worldQuery; + } + + public PreparedQuery> fortuneQuery() { + return fortuneQuery; + } + + public PreparedQuery> updateQuery(int queryCount) { + return updateQuery[queryCount - 1]; + } + + @SuppressWarnings("unchecked") + void prepare() { + try { + worldQuery = conn.prepare(SELECT_WORLD) + .toCompletionStage().toCompletableFuture().get().query(); + fortuneQuery = conn.prepare(SELECT_FORTUNE) + .toCompletionStage().toCompletableFuture().get().query(); + updateQuery = (PreparedQuery>[]) new PreparedQuery[UPDATE_QUERIES]; + for (int i = 0; i < UPDATE_QUERIES; i++) { + updateQuery[i] = conn.prepare(singleUpdate(i + 1)) + .toCompletionStage().toCompletableFuture().get().query(); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static String singleUpdate(int count) { + StringBuilder sql = new StringBuilder(); + sql.append("UPDATE WORLD SET RANDOMNUMBER = CASE ID"); + for (int i = 0; i < count; i++) { + int k = i * 2 + 1; + sql.append(" WHEN $").append(k).append(" THEN $").append(k + 1); + } + sql.append(" ELSE RANDOMNUMBER"); + sql.append(" END WHERE ID IN ($1"); + for (int i = 1; i < count; i++) { + int k = i * 2 + 1; + sql.append(",$").append(k); + } + sql.append(")"); + return sql.toString(); + } + } +} diff --git a/frameworks/Java/helidon/nima/src/main/java/io/helidon/benchmark/nima/models/PgClientRepository.java b/frameworks/Java/helidon/nima/src/main/java/io/helidon/benchmark/nima/models/PgClientRepository.java index 291131eca17..3f3bd2624eb 100644 --- a/frameworks/Java/helidon/nima/src/main/java/io/helidon/benchmark/nima/models/PgClientRepository.java +++ b/frameworks/Java/helidon/nima/src/main/java/io/helidon/benchmark/nima/models/PgClientRepository.java @@ -2,7 +2,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ExecutionException; import java.util.logging.Logger; import io.helidon.config.Config; @@ -10,58 +9,46 @@ import io.vertx.core.Vertx; import io.vertx.core.VertxOptions; import io.vertx.pgclient.PgConnectOptions; -import io.vertx.pgclient.PgPool; -import io.vertx.sqlclient.PoolOptions; import io.vertx.sqlclient.PreparedQuery; import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowSet; -import io.vertx.sqlclient.SqlClient; import io.vertx.sqlclient.Tuple; import static io.helidon.benchmark.nima.models.DbRepository.randomWorldNumber; +import static io.helidon.benchmark.nima.models.PgClientConnectionPool.PgClientConnection.UPDATE_QUERIES; public class PgClientRepository implements DbRepository { private static final Logger LOGGER = Logger.getLogger(PgClientRepository.class.getName()); - private static final int UPDATE_QUERIES = 500; - private final SqlClient updatePool; - - private final PreparedQuery> getFortuneQuery; - private final PreparedQuery> getWorldQuery; - private final PreparedQuery>[] updateWorldSingleQuery; + private final PgClientConnectionPool connectionPool; @SuppressWarnings("unchecked") public PgClientRepository(Config config) { - Vertx vertx = Vertx.vertx(new VertxOptions().setPreferNativeTransport(true)); + VertxOptions vertxOptions = new VertxOptions() + .setPreferNativeTransport(true) + .setBlockedThreadCheckInterval(100000); + Vertx vertx = Vertx.vertx(vertxOptions); PgConnectOptions connectOptions = new PgConnectOptions() .setPort(config.get("port").asInt().orElse(5432)) - .setCachePreparedStatements(config.get("cache-prepared-statements").asBoolean().orElse(true)) .setHost(config.get("host").asString().orElse("tfb-database")) .setDatabase(config.get("db").asString().orElse("hello_world")) .setUser(config.get("username").asString().orElse("benchmarkdbuser")) .setPassword(config.get("password").asString().orElse("benchmarkdbpass")) + .setCachePreparedStatements(true) + .setPreparedStatementCacheMaxSize(UPDATE_QUERIES + 2) + .setPreparedStatementCacheSqlFilter(s -> true) // cache all + .setTcpNoDelay(true) + .setTcpQuickAck(true) + .setTcpKeepAlive(true) .setPipeliningLimit(100000); - - int sqlPoolSize = config.get("sql-pool-size").asInt().orElse(64); - PoolOptions clientOptions = new PoolOptions().setMaxSize(sqlPoolSize); - LOGGER.info("sql-pool-size is " + sqlPoolSize); - - SqlClient queryPool = PgPool.client(vertx, connectOptions, clientOptions); - updatePool = PgPool.client(vertx, connectOptions, clientOptions); - - getWorldQuery = queryPool.preparedQuery("SELECT id, randomnumber FROM world WHERE id = $1"); - getFortuneQuery = queryPool.preparedQuery("SELECT id, message FROM fortune"); - - updateWorldSingleQuery = new PreparedQuery[UPDATE_QUERIES]; - for (int i = 0; i < UPDATE_QUERIES; i++) { - updateWorldSingleQuery[i] = queryPool.preparedQuery(singleUpdate(i + 1)); - } + connectionPool = new PgClientConnectionPool(vertx, connectOptions); } @Override public World getWorld(int id) { try { - return getWorldQuery.execute(Tuple.of(id)) + PreparedQuery> worldQuery = connectionPool.clientConnection().worldQuery(); + return worldQuery.execute(Tuple.of(id)) .map(rows -> { Row r = rows.iterator().next(); return new World(r.getInteger(0), r.getInteger(1)); @@ -74,13 +61,14 @@ public World getWorld(int id) { @Override public List getWorlds(int count) { try { + PreparedQuery> worldQuery = connectionPool.clientConnection().worldQuery(); List> futures = new ArrayList<>(); for (int i = 0; i < count; i++) { - futures.add(getWorldQuery.execute(Tuple.of(randomWorldNumber())) - .map(rows -> { - Row r = rows.iterator().next(); - return new World(r.getInteger(0), r.getInteger(1)); - })); + futures.add(worldQuery.execute(Tuple.of(randomWorldNumber())) + .map(rows -> { + Row r = rows.iterator().next(); + return new World(r.getInteger(0), r.getInteger(1)); + })); } return Future.all(futures).toCompletionStage().toCompletableFuture().get().list(); } catch (Exception e) { @@ -92,7 +80,18 @@ public List getWorlds(int count) { public List updateWorlds(int count) { List worlds = getWorlds(count); try { - return updateWorlds(worlds, count, updatePool); + PreparedQuery> updateQuery = connectionPool.clientConnection().updateQuery(count); + List updateParams = new ArrayList<>(count * 2); + for (World world : worlds) { + updateParams.add(world.id); + world.randomNumber = randomWorldNumber(); + updateParams.add(world.randomNumber); + } + return updateQuery.execute(Tuple.wrap(updateParams)) + .toCompletionStage() + .thenApply(rows -> worlds) + .toCompletableFuture() + .get(); } catch (Exception e) { throw new RuntimeException(e); } @@ -101,7 +100,8 @@ public List updateWorlds(int count) { @Override public List getFortunes() { try { - return getFortuneQuery.execute() + PreparedQuery> fortuneQuery = connectionPool.clientConnection().fortuneQuery(); + return fortuneQuery.execute() .map(rows -> { List fortunes = new ArrayList<>(rows.size() + 1); for (Row r : rows) { @@ -113,37 +113,4 @@ public List getFortunes() { throw new RuntimeException(e); } } - - private List updateWorlds(List worlds, int count, SqlClient pool) - throws ExecutionException, InterruptedException { - int size = worlds.size(); - List updateParams = new ArrayList<>(size * 2); - for (World world : worlds) { - updateParams.add(world.id); - world.randomNumber = randomWorldNumber(); - updateParams.add(world.randomNumber); - } - return updateWorldSingleQuery[count - 1].execute(Tuple.wrap(updateParams)) - .toCompletionStage() - .thenApply(rows -> worlds) - .toCompletableFuture() - .get(); - } - - private static String singleUpdate(int count) { - StringBuilder sql = new StringBuilder(); - sql.append("UPDATE WORLD SET RANDOMNUMBER = CASE ID"); - for (int i = 0; i < count; i++) { - int k = i * 2 + 1; - sql.append(" WHEN $").append(k).append(" THEN $").append(k + 1); - } - sql.append(" ELSE RANDOMNUMBER"); - sql.append(" END WHERE ID IN ($1"); - for (int i = 1; i < count; i++) { - int k = i * 2 + 1; - sql.append(",$").append(k); - } - sql.append(")"); - return sql.toString(); - } } \ No newline at end of file diff --git a/frameworks/Java/helidon/nima/src/main/java/io/helidon/benchmark/nima/services/DbService.java b/frameworks/Java/helidon/nima/src/main/java/io/helidon/benchmark/nima/services/DbService.java index e3bd1fe39fc..a1e97de44b5 100644 --- a/frameworks/Java/helidon/nima/src/main/java/io/helidon/benchmark/nima/services/DbService.java +++ b/frameworks/Java/helidon/nima/src/main/java/io/helidon/benchmark/nima/services/DbService.java @@ -1,21 +1,19 @@ - package io.helidon.benchmark.nima.services; -import java.util.List; - +import com.jsoniter.output.JsonStream; +import com.jsoniter.output.JsonStreamPool; import io.helidon.benchmark.nima.models.DbRepository; -import io.helidon.benchmark.nima.models.World; +import io.helidon.common.mapper.OptionalValue; import io.helidon.common.parameters.Parameters; import io.helidon.http.HeaderValues; import io.helidon.webserver.http.HttpRules; import io.helidon.webserver.http.HttpService; import io.helidon.webserver.http.ServerRequest; import io.helidon.webserver.http.ServerResponse; -import io.helidon.common.mapper.OptionalValue; +import static io.helidon.benchmark.nima.JsonSerializer.serialize; import static io.helidon.benchmark.nima.Main.SERVER; import static io.helidon.benchmark.nima.models.DbRepository.randomWorldNumber; -import static io.helidon.benchmark.nima.JsonSerializer.serialize; public class DbService implements HttpService { @@ -33,24 +31,41 @@ public void routing(HttpRules httpRules) { } private void db(ServerRequest req, ServerResponse res) { - res.header(SERVER); - res.header(HeaderValues.CONTENT_TYPE_JSON); - res.send(serialize(repository.getWorld(randomWorldNumber()))); + JsonStream stream = JsonStreamPool.borrowJsonStream(); + try { + res.header(SERVER); + res.header(HeaderValues.CONTENT_TYPE_JSON); + serialize(repository.getWorld(randomWorldNumber()), stream); + res.send(stream.buffer().data(), 0, stream.buffer().tail()); + } finally { + JsonStreamPool.returnJsonStream(stream); + } } private void queries(ServerRequest req, ServerResponse res) { - res.header(SERVER); - res.header(HeaderValues.CONTENT_TYPE_JSON); - int count = parseQueryCount(req.query()); - res.send(serialize(repository.getWorlds(count))); + JsonStream stream = JsonStreamPool.borrowJsonStream(); + try { + res.header(SERVER); + res.header(HeaderValues.CONTENT_TYPE_JSON); + int count = parseQueryCount(req.query()); + serialize(repository.getWorlds(count), stream); + res.send(stream.buffer().data(), 0, stream.buffer().tail()); + } finally { + JsonStreamPool.returnJsonStream(stream); + } } private void updates(ServerRequest req, ServerResponse res) { - res.header(SERVER); - res.header(HeaderValues.CONTENT_TYPE_JSON); - int count = parseQueryCount(req.query()); - List worlds = repository.updateWorlds(count); - res.send(serialize(worlds)); + JsonStream stream = JsonStreamPool.borrowJsonStream(); + try { + res.header(SERVER); + res.header(HeaderValues.CONTENT_TYPE_JSON); + int count = parseQueryCount(req.query()); + serialize(repository.updateWorlds(count), stream); + res.send(stream.buffer().data(), 0, stream.buffer().tail()); + } finally { + JsonStreamPool.returnJsonStream(stream); + } } private int parseQueryCount(Parameters parameters) { @@ -66,4 +81,4 @@ private int parseQueryCount(Parameters parameters) { } return Math.min(500, Math.max(1, parsedValue)); } -} \ No newline at end of file +} diff --git a/frameworks/Java/helidon/nima/src/main/java/io/helidon/benchmark/nima/services/FortuneHandler.java b/frameworks/Java/helidon/nima/src/main/java/io/helidon/benchmark/nima/services/FortuneHandler.java index 8847b7d0bc1..b821f8f9261 100644 --- a/frameworks/Java/helidon/nima/src/main/java/io/helidon/benchmark/nima/services/FortuneHandler.java +++ b/frameworks/Java/helidon/nima/src/main/java/io/helidon/benchmark/nima/services/FortuneHandler.java @@ -1,16 +1,21 @@ package io.helidon.benchmark.nima.services; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.util.Comparator; import java.util.List; -import com.fizzed.rocker.runtime.ArrayOfByteArraysOutput; +import gg.jte.TemplateOutput; import io.helidon.benchmark.nima.models.DbRepository; import io.helidon.benchmark.nima.models.Fortune; import io.helidon.webserver.http.Handler; import io.helidon.webserver.http.ServerRequest; import io.helidon.webserver.http.ServerResponse; -import views.fortunes; + +import gg.jte.html.OwaspHtmlTemplateOutput; +import gg.jte.generated.precompiled.JtefortunesGenerated; import static io.helidon.benchmark.nima.Main.CONTENT_TYPE_HTML; import static io.helidon.benchmark.nima.Main.SERVER; @@ -33,8 +38,38 @@ public void handle(ServerRequest req, ServerResponse res) { List fortuneList = repository.getFortunes(); fortuneList.add(ADDITIONAL_FORTUNE); fortuneList.sort(Comparator.comparing(Fortune::getMessage)); - res.send(fortunes.template(fortuneList) - .render(ArrayOfByteArraysOutput.FACTORY) - .toByteArray()); + try (OutputStream os = res.outputStream()) { + JtefortunesGenerated.render(new OwaspHtmlTemplateOutput(new HelidonTemplateOutput(os)), + null, fortuneList); + } catch (IOException e) { + throw new RuntimeException(e); + } } + + static class HelidonTemplateOutput implements TemplateOutput{ + private final OutputStream os; + + HelidonTemplateOutput(OutputStream os) { + this.os = os; + } + + @Override + public void writeContent(String value) { + writeBinaryContent(value.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public void writeContent(String value, int beginIndex, int endIndex) { + writeBinaryContent(value.substring(beginIndex, endIndex).getBytes(StandardCharsets.UTF_8)); + } + + @Override + public void writeBinaryContent(byte[] value) { + try { + os.write(value); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }; } \ No newline at end of file diff --git a/frameworks/Java/helidon/nima/src/main/resources/application.yaml b/frameworks/Java/helidon/nima/src/main/resources/application.yaml index d2d8e8943b4..4a26ea0d0e0 100644 --- a/frameworks/Java/helidon/nima/src/main/resources/application.yaml +++ b/frameworks/Java/helidon/nima/src/main/resources/application.yaml @@ -36,6 +36,5 @@ host: "tfb-database" db: "hello_world" username: benchmarkdbuser password: benchmarkdbpass -sql-pool-size: 300 db-repository: "pgclient" # "pgclient" (default) or "hikari" diff --git a/frameworks/Java/helidon/nima/src/main/resources/views/fortunes.jte b/frameworks/Java/helidon/nima/src/main/resources/views/fortunes.jte new file mode 100644 index 00000000000..52dc350b25f --- /dev/null +++ b/frameworks/Java/helidon/nima/src/main/resources/views/fortunes.jte @@ -0,0 +1,2 @@ +@param java.util.List fortunes +Fortunes@for(io.helidon.benchmark.nima.models.Fortune fortune : fortunes)@endfor
idmessage
${fortune.getId()}${fortune.getMessage()}
\ No newline at end of file diff --git a/frameworks/Java/helidon/nima/src/main/resources/views/fortunes.rocker.html b/frameworks/Java/helidon/nima/src/main/resources/views/fortunes.rocker.html deleted file mode 100644 index 3ebcae44729..00000000000 --- a/frameworks/Java/helidon/nima/src/main/resources/views/fortunes.rocker.html +++ /dev/null @@ -1,24 +0,0 @@ -@import io.helidon.benchmark.nima.models.Fortune -@import java.util.List -@args (List fortunes) - - - - -Fortunes - - - - - - - - @for (f : fortunes) { - - - - - } -
idmessage
@f.getId()@f.getMessage()
- - \ No newline at end of file diff --git a/frameworks/Java/inverno/README.md b/frameworks/Java/inverno/README.md index b205ef706b6..492cd60e430 100755 --- a/frameworks/Java/inverno/README.md +++ b/frameworks/Java/inverno/README.md @@ -2,20 +2,23 @@ ### Test Type Implementation Source Code -* [JSON](src/main/java/com/techempower/inverno/benchmark/internal/Handler.java) -* [PLAINTEXT](src/main/java/com/techempower/inverno/benchmark/internal/Handler.java) -* [DB](src/main/java/com/techempower/inverno/benchmark/internal/Handler.java) -* [QUERY](src/main/java/com/techempower/inverno/benchmark/internal/Handler.java) -* [CACHED QUERY](src/main/java/com/techempower/inverno/benchmark/internal/Handler.java) -* [UPDATE](src/main/java/com/techempower/inverno/benchmark/internal/Handler.java) -* [FORTUNES](src/main/java/com/techempower/inverno/benchmark/internal/Handler.java) +* [JSON](src/main/java/com/techempower/inverno/benchmark/internal/Controller.java) +* [PLAINTEXT](src/main/java/com/techempower/inverno/benchmark/internal/Controller.java) +* [DB](src/main/java/com/techempower/inverno/benchmark/internal/Controller.java) +* [QUERY](src/main/java/com/techempower/inverno/benchmark/internal/Controller.java) +* [CACHED QUERY](src/main/java/com/techempower/inverno/benchmark/internal/Controller.java) +* [UPDATE](src/main/java/com/techempower/inverno/benchmark/internal/Controller.java) +* [FORTUNES](src/main/java/com/techempower/inverno/benchmark/internal/Controller.java) ## Important Libraries + The tests were run with: -* [Java OpenJDK 16](https://openjdk.java.net/) -* [Inverno 1.4.1](https://inverno.io) +* [Java OpenJDK 21](https://openjdk.java.net/) +* [Inverno 1.12.0](https://inverno.io) +* [DSL-JSON 2.0.2](https://github.com/ngs-doo/dsl-json) ## Test URLs + ### JSON http://localhost:8080/json diff --git a/frameworks/Java/inverno/inverno-postgres.dockerfile b/frameworks/Java/inverno/inverno-postgres.dockerfile index 737cb434013..d1d333ef5a5 100644 --- a/frameworks/Java/inverno/inverno-postgres.dockerfile +++ b/frameworks/Java/inverno/inverno-postgres.dockerfile @@ -1,8 +1,7 @@ -FROM maven:3.9.6-amazoncorretto-21 as maven +FROM maven:3.9.9-eclipse-temurin-21 as maven WORKDIR /inverno COPY src src COPY pom.xml pom.xml -RUN yum -y install binutils RUN mvn package -q -Pio.inverno.io_uring EXPOSE 8080 diff --git a/frameworks/Java/inverno/inverno.dockerfile b/frameworks/Java/inverno/inverno.dockerfile index 1dd39d53b27..5f666a70a95 100644 --- a/frameworks/Java/inverno/inverno.dockerfile +++ b/frameworks/Java/inverno/inverno.dockerfile @@ -1,8 +1,7 @@ -FROM maven:3.9.6-amazoncorretto-21 as maven +FROM maven:3.9.9-eclipse-temurin-21 as maven WORKDIR /inverno COPY src src COPY pom.xml pom.xml -RUN yum -y install binutils RUN mvn package -q -Pio.inverno.io_uring EXPOSE 8080 diff --git a/frameworks/Java/inverno/pom.xml b/frameworks/Java/inverno/pom.xml index c91b31c2d37..3b11a165f97 100644 --- a/frameworks/Java/inverno/pom.xml +++ b/frameworks/Java/inverno/pom.xml @@ -6,7 +6,7 @@ io.inverno.dist inverno-parent - 1.10.0 + 1.12.0 com.techempower inverno-benchmark @@ -53,6 +53,11 @@ unbescape 1.1.6.RELEASE + + com.dslplatform + dsl-json + 2.0.2 + io.vertx vertx-pg-client @@ -82,7 +87,52 @@ log4j-core - + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + default-compile + + + + com.dslplatform + dsl-json + 2.0.2 + + + + + + + + + + + + io.inverno.tool + inverno-maven-plugin + + + + com.dslplatform.dsl.json + + + com.dslplatform.json.Configuration + + + + + -Dlog4j2.simplelogLevel=INFO -Dlog4j2.level=INFO --add-reads com.techempower.inverno.benchmark=com.dslplatform.dsl.json --add-opens com.techempower.inverno.benchmark/com.techempower.inverno.benchmark.model=com.dslplatform.dsl.json + + + + + + io.inverno.epoll @@ -110,7 +160,7 @@ inverno-benchmark - -Xms2g -Xmx2g -server -XX:+UseNUMA -XX:+UseParallelGC -Dlog4j2.level=OFF -Dio.netty.leakDetection.level=disabled -Dio.netty.buffer.checkBounds=false -Dio.netty.buffer.checkAccessible=false -Dvertx.disableHttpHeadersValidation=true -Dvertx.disableMetrics=true -Dvertx.disableH2c=true -Dvertx.disableWebsockets=true -Dvertx.flashPolicyHandler=false -Dvertx.threadChecks=false -Dvertx.disableContextTimings=true -Dvertx.disableTCCL=true --add-modules io.netty.transport.unix.common,io.netty.transport.classes.epoll,io.netty.transport.epoll.linux.x86_64 + -Xms2g -Xmx2g -server -XX:+UseNUMA -XX:+UseParallelGC -Dlog4j2.level=OFF -Dio.netty.leakDetection.level=disabled -Dio.netty.buffer.checkBounds=false -Dio.netty.buffer.checkAccessible=false -Dvertx.disableHttpHeadersValidation=true -Dvertx.disableMetrics=true -Dvertx.disableH2c=true -Dvertx.disableWebsockets=true -Dvertx.flashPolicyHandler=false -Dvertx.threadChecks=false -Dvertx.disableContextTimings=true -Dvertx.disableTCCL=true --add-modules io.netty.transport.unix.common,io.netty.transport.classes.epoll,io.netty.transport.epoll.linux.x86_64 --add-reads com.techempower.inverno.benchmark=com.dslplatform.dsl.json --add-opens com.techempower.inverno.benchmark/com.techempower.inverno.benchmark.model=com.dslplatform.dsl.json @@ -119,9 +169,6 @@ - - --add-modules io.netty.transport.unix.common,io.netty.transport.classes.epoll,io.netty.transport.epoll.linux.x86_64 -
@@ -157,7 +204,7 @@ inverno-benchmark - -Xms2g -Xmx2g -server -XX:+UseNUMA -XX:+UseParallelGC -Dlog4j2.level=OFF -Dio.netty.leakDetection.level=disabled -Dio.netty.buffer.checkBounds=false -Dio.netty.buffer.checkAccessible=false -Dvertx.disableHttpHeadersValidation=true -Dvertx.disableMetrics=true -Dvertx.disableH2c=true -Dvertx.disableWebsockets=true -Dvertx.flashPolicyHandler=false -Dvertx.threadChecks=false -Dvertx.disableContextTimings=true -Dvertx.disableTCCL=true --add-modules io.netty.transport.unix.common,io.netty.incubator.transport.classes.io_uring,io.netty.incubator.transport.io_uring.linux.x86_64 + -Xms2g -Xmx2g -server -XX:+UseNUMA -XX:+UseParallelGC -Dlog4j2.level=OFF -Dio.netty.leakDetection.level=disabled -Dio.netty.buffer.checkBounds=false -Dio.netty.buffer.checkAccessible=false -Dvertx.disableHttpHeadersValidation=true -Dvertx.disableMetrics=true -Dvertx.disableH2c=true -Dvertx.disableWebsockets=true -Dvertx.flashPolicyHandler=false -Dvertx.threadChecks=false -Dvertx.disableContextTimings=true -Dvertx.disableTCCL=true --add-modules io.netty.transport.unix.common,io.netty.incubator.transport.classes.io_uring,io.netty.incubator.transport.io_uring.linux.x86_64 --add-reads com.techempower.inverno.benchmark=com.dslplatform.dsl.json --add-opens com.techempower.inverno.benchmark/com.techempower.inverno.benchmark.model=com.dslplatform.dsl.json @@ -166,9 +213,6 @@ - - --add-modules io.netty.transport.unix.common,io.netty.incubator.transport.classes.io_uring,io.netty.incubator.transport.io_uring.linux.x86_64 - diff --git a/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/Main.java b/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/Main.java index 74aaf730a7e..42695fc65e7 100644 --- a/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/Main.java +++ b/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/Main.java @@ -1,17 +1,16 @@ package com.techempower.inverno.benchmark; -import java.io.IOException; -import java.util.function.Supplier; - import io.inverno.core.annotation.Bean; import io.inverno.core.v1.Application; import io.inverno.mod.configuration.ConfigurationSource; import io.inverno.mod.configuration.source.BootstrapConfigurationSource; +import java.io.IOException; +import java.util.function.Supplier; public class Main { @Bean - public interface AppConfigurationSource extends Supplier> {} + public interface AppConfigurationSource extends Supplier {} public static void main(String[] args) throws IllegalStateException, IOException { Application.with(new Benchmark.Builder() diff --git a/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/internal/Controller.java b/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/internal/Controller.java index 33c94a5c7b4..c16e6d4d767 100644 --- a/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/internal/Controller.java +++ b/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/internal/Controller.java @@ -1,7 +1,5 @@ package com.techempower.inverno.benchmark.internal; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import com.techempower.inverno.benchmark.model.Fortune; import com.techempower.inverno.benchmark.model.Message; import com.techempower.inverno.benchmark.model.World; @@ -14,15 +12,14 @@ import io.inverno.mod.base.concurrent.Reactor; import io.inverno.mod.base.concurrent.ReactorScope; import io.inverno.mod.base.converter.ConverterException; +import io.inverno.mod.base.reflect.Types; import io.inverno.mod.http.base.ExchangeContext; import io.inverno.mod.http.base.HttpException; -import io.inverno.mod.http.base.InternalServerErrorException; import io.inverno.mod.http.base.Parameter; import io.inverno.mod.http.base.Status; -import io.inverno.mod.http.server.Exchange; import io.inverno.mod.http.server.ErrorExchange; +import io.inverno.mod.http.server.Exchange; import io.inverno.mod.http.server.ServerController; -import io.inverno.mod.sql.SqlClient; import io.inverno.mod.sql.UnsafeSqlOperations; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -30,13 +27,14 @@ import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.util.AsciiString; +import java.lang.reflect.Type; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.Collections; +import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -51,36 +49,32 @@ public class Controller implements ServerController> sqlClient; + private final ReactorScope jsonSerializer; + private final ReactorScope> dbRepository; private EventLoopGroup dateEventLoopGroup; private CharSequence date; public Controller(Reactor reactor, - ObjectMapper mapper, - ReactorScope> sqlClient + ReactorScope jsonSerializer, + ReactorScope> dbRepository ) { this.reactor = reactor; - this.mapper = mapper; - this.sqlClient = sqlClient; + this.jsonSerializer = jsonSerializer; + this.dbRepository = dbRepository; } @Init public void init() { this.dateEventLoopGroup = this.reactor.createIoEventLoopGroup(1); - this.dateEventLoopGroup.scheduleAtFixedRate(() -> { - this.date = new AsciiString(DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now())); - }, 0, 1000, TimeUnit.MILLISECONDS); - - + this.dateEventLoopGroup.scheduleAtFixedRate(() -> this.date = new AsciiString(DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now())), 0, 1000, TimeUnit.MILLISECONDS); } @Destroy @@ -139,15 +133,8 @@ public void handle(Exchange exchange) throws HttpException { } private static final CharSequence STATIC_PLAINTEXT_LEN_VALUE = AsciiString.cached(String.valueOf(STATIC_PLAINTEXT_LEN)); - - private static class PlaintextSupplier implements Supplier { - @Override - public ByteBuf get() { - return STATIC_PLAINTEXT_BYTEBUF.duplicate(); - } - } - - private static final Mono PLAIN_TEXT_MONO = Mono.fromSupplier(new PlaintextSupplier()); + + private static final Mono PLAIN_TEXT_MONO = Mono.fromSupplier(STATIC_PLAINTEXT_BYTEBUF::duplicate); public void handle_plaintext(Exchange exchange) throws HttpException { exchange.response() @@ -163,20 +150,15 @@ public void handle_plaintext(Exchange exchange) throws HttpExce } public void handle_json(Exchange exchange) throws HttpException { - try { - exchange.response() - .headers(h -> h - .add(HttpHeaderNames.SERVER, STATIC_SERVER) - .add(HttpHeaderNames.DATE, this.date) - .add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON) - ) - .body() - .raw() - .value(Unpooled.wrappedBuffer(this.mapper.writeValueAsBytes(new Message("Hello, World!")))); - } - catch (JsonProcessingException | IllegalStateException e) { - throw new InternalServerErrorException("Error serializing message as JSON", e); - } + exchange.response() + .headers(h -> h + .add(HttpHeaderNames.SERVER, STATIC_SERVER) + .add(HttpHeaderNames.DATE, this.date) + .add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON) + ) + .body() + .raw() + .value(this.jsonSerializer.get().serialize(new Message("Hello, World!"), Message.class)); } private static int randomWorldId() { @@ -191,20 +173,10 @@ public void handle_db(Exchange exchange) throws HttpException { .add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON) ) .body() - .raw().stream(this.sqlClient.get().flatMap(client -> - client.queryForObject( - DB_SELECT_WORLD, - row -> { - try { - return Unpooled.wrappedBuffer(this.mapper.writeValueAsBytes(new World(row.getInteger(0), row.getInteger(1)))); - } - catch (JsonProcessingException e) { - throw new InternalServerErrorException(e); - } - }, - randomWorldId() - ) - )); + .raw().stream(this.dbRepository.get() + .flatMap(repository -> repository.getWorld(randomWorldId())) + .map(world -> this.jsonSerializer.get().serialize(world, World.class)) + ); } private static final String PARAMETER_QUERIES = "queries"; @@ -227,8 +199,8 @@ public void handle_queries(Exchange exchange) throws HttpExcept .add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON) ) .body() - .raw().stream(this.sqlClient.get() - .flatMapMany(client -> ((UnsafeSqlOperations)client) + .raw().stream(this.dbRepository.get() + .flatMapMany(repository -> ((UnsafeSqlOperations)repository.getSqlClient()) .batchQueries(ops -> Flux.range(0, queries) .map(ign -> ops.queryForObject( @@ -239,58 +211,39 @@ public void handle_queries(Exchange exchange) throws HttpExcept ) ) .collectList() - .map(worlds -> { - try { - return Unpooled.wrappedBuffer(this.mapper.writeValueAsBytes(worlds)); - } - catch (JsonProcessingException e) { - throw new InternalServerErrorException(e); - } - }) + .map(worlds -> this.jsonSerializer.get().serialize(worlds, LIST_WORLD_TYPE)) ); } public void handle_updates(Exchange exchange) throws HttpException { int queries = this.extractQueriesParameter(exchange); - exchange.response() - .headers(h -> h - .add(HttpHeaderNames.SERVER, STATIC_SERVER) - .add(HttpHeaderNames.DATE, this.date) - .add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON) - ) - .body() - .raw().stream(this.sqlClient.get() - .flatMapMany(client -> Flux.from(((UnsafeSqlOperations)client) - .batchQueries(ops -> - Flux.range(0, queries) - .map(ign -> ops.queryForObject( - DB_SELECT_WORLD, - row -> new World(row.getInteger(0), randomWorldId()), - randomWorldId() - )) - )) - .collectSortedList() - .delayUntil(worlds -> client.batchUpdate( - DB_UPDATE_WORLD, - worlds.stream().map(world -> new Object[] { world.getRandomNumber(), world.getId() }) - ) + .headers(h -> h + .add(HttpHeaderNames.SERVER, STATIC_SERVER) + .add(HttpHeaderNames.DATE, this.date) + .add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON) + ) + .body() + .raw().stream(this.dbRepository.get() + .flatMapMany(repository -> Flux.from(((UnsafeSqlOperations)repository.getSqlClient()) + .batchQueries(ops -> + Flux.range(0, queries) + .map(ign -> ops.queryForObject( + DB_SELECT_WORLD, + row -> new World(row.getInteger(0), randomWorldId()), + randomWorldId() + )) + )) + .collectSortedList() + .delayUntil(repository::updateWorlds) + .map(worlds -> this.jsonSerializer.get().serialize(worlds, LIST_WORLD_TYPE)) ) - .map(worlds -> { - try { - return Unpooled.wrappedBuffer(this.mapper.writeValueAsBytes(worlds)); - } - catch (JsonProcessingException e) { - throw new InternalServerErrorException(e); - } - }) - ) - ); + ); } private static final CharSequence MEDIA_TEXT_HTML_UTF8 = AsciiString.cached("text/html; charset=utf-8"); - private static final FortunesTemplate.Renderer> FORTUNES_RENDERER = FortunesTemplate.bytebuf(() -> Unpooled.buffer()); + private static final FortunesTemplate.Renderer> FORTUNES_RENDERER = FortunesTemplate.bytebuf(Unpooled::buffer); public void handle_fortunes(Exchange exchange) throws HttpException { exchange.response() @@ -300,12 +253,7 @@ public void handle_fortunes(Exchange exchange) throws HttpExcep .add(HttpHeaderNames.CONTENT_TYPE, MEDIA_TEXT_HTML_UTF8) ) .body() - .raw().stream(this.sqlClient.get().flatMapMany(client -> - client.query( - DB_SELECT_FORTUNE, - row -> new Fortune(row.getInteger(0), row.getString(1)) - ) - ) + .raw().stream(this.dbRepository.get().flatMapMany(DbRepository::listFortunes) .collectList() .flatMap(fortunes -> { fortunes.add(new Fortune(0, "Additional fortune added at request time.")); diff --git a/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/internal/DbRepository.java b/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/internal/DbRepository.java new file mode 100644 index 00000000000..28699e3c1d0 --- /dev/null +++ b/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/internal/DbRepository.java @@ -0,0 +1,132 @@ +package com.techempower.inverno.benchmark.internal; + +import com.techempower.inverno.benchmark.AppConfiguration; +import com.techempower.inverno.benchmark.model.Fortune; +import com.techempower.inverno.benchmark.model.World; +import io.inverno.core.annotation.Bean; +import io.inverno.core.annotation.Destroy; +import io.inverno.core.annotation.Init; +import io.inverno.mod.base.concurrent.Reactor; +import io.inverno.mod.base.concurrent.VertxReactor; +import io.inverno.mod.sql.PreparedStatement; +import io.inverno.mod.sql.SqlClient; +import io.inverno.mod.sql.vertx.ConnectionSqlClient; +import io.vertx.core.Vertx; +import io.vertx.core.VertxOptions; +import io.vertx.pgclient.PgConnectOptions; +import io.vertx.pgclient.PgConnection; +import java.util.ArrayList; +import java.util.List; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class DbRepository { + + public static final String DB_SELECT_WORLD = "SELECT id, randomnumber from WORLD where id = $1"; + public static final String DB_SELECT_FORTUNE = "SELECT id, message from FORTUNE"; + + private final SqlClient sqlClient; + + private final PreparedStatement selectWorldByIdQuery; + private final PreparedStatement selectFortuneQuery; + private final PreparedStatement[] updateWorldQueries; + + public DbRepository(SqlClient sqlClient) { + this.sqlClient = sqlClient; + + this.selectWorldByIdQuery = sqlClient.preparedStatement(DB_SELECT_WORLD); + this.selectFortuneQuery = sqlClient.preparedStatement(DB_SELECT_FORTUNE); + this.updateWorldQueries = new PreparedStatement[500]; + for(int i=0;i getWorld(int id) { + return Mono.from(this.selectWorldByIdQuery.bind(id).execute(row -> new World(row.getInteger(0), row.getInteger(1)))); + } + + public Flux listFortunes() { + return Flux.from(this.selectFortuneQuery.execute(row -> new Fortune(row.getInteger(0), row.getString(1)))); + } + + public Mono updateWorlds(List worlds) { + int len = worlds.size(); + List parameters = new ArrayList<>(len * 2); + for(World world : worlds) { + parameters.add(world.getId()); + parameters.add(world.getRandomNumber()); + } + return Mono.when(this.updateWorldQueries[len - 1].bind(parameters).execute()); + } + + @Bean( name = "dbRespository", visibility = Bean.Visibility.PRIVATE ) + public static class ReactorScope extends io.inverno.mod.base.concurrent.ReactorScope> { + + private final AppConfiguration configuration; + private final Reactor reactor; + + private Vertx vertx; + private PgConnectOptions connectOptions; + + public ReactorScope(AppConfiguration configuration, Reactor reactor) { + this.configuration = configuration; + this.reactor = reactor; + } + + @Init + public void init() { + if(this.reactor instanceof VertxReactor) { + this.vertx = ((VertxReactor)this.reactor).getVertx(); + } + else { + this.vertx = Vertx.vertx(new VertxOptions().setPreferNativeTransport(this.configuration.boot().prefer_native_transport())); + } + + this.connectOptions = new PgConnectOptions() + .setHost(this.configuration.db_host()) + .setPort(this.configuration.db_port()) + .setDatabase(this.configuration.db_database()) + .setUser(this.configuration.db_username()) + .setPassword(this.configuration.db_password()) + .setCachePreparedStatements(true) + .setPreparedStatementCacheMaxSize(1024) + .setPipeliningLimit(100_100); + } + + @Destroy + public void destroy() { + if(!(this.reactor instanceof VertxReactor)) { + this.vertx.close(); + } + } + + @Override + protected Mono create() { + return Mono.fromCompletionStage(() -> PgConnection.connect(this.vertx, this.connectOptions).toCompletionStage()) + .map(pgConn -> new DbRepository(new ConnectionSqlClient(pgConn))) + .cacheInvalidateWhen(repository -> ((ConnectionSqlClient)repository.getSqlClient()).onClose()); + } + } +} diff --git a/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/internal/JsonSerializer.java b/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/internal/JsonSerializer.java new file mode 100644 index 00000000000..24ce79eedd3 --- /dev/null +++ b/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/internal/JsonSerializer.java @@ -0,0 +1,38 @@ +package com.techempower.inverno.benchmark.internal; + +import com.dslplatform.json.DslJson; +import com.dslplatform.json.JsonWriter; +import io.inverno.core.annotation.Bean; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.lang.reflect.Type; + +public class JsonSerializer { + + private static final DslJson DSL_JSON = new DslJson<>(); + + private final JsonWriter jsonWriter; + + public JsonSerializer() { + this.jsonWriter = DSL_JSON.newWriter(); + } + + public ByteBuf serialize(T value, Type type) { + try { + DSL_JSON.serialize(this.jsonWriter, type, value); + return Unpooled.wrappedBuffer(this.jsonWriter.toByteArray()); + } + finally { + this.jsonWriter.reset(); + } + } + + @Bean( name = "jsonSerializer", visibility = Bean.Visibility.PRIVATE ) + public static class ReactorScope extends io.inverno.mod.base.concurrent.ReactorScope { + + @Override + protected JsonSerializer create() { + return new JsonSerializer(); + } + } +} diff --git a/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/internal/SqlClientReactorScope.java b/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/internal/SqlClientReactorScope.java deleted file mode 100644 index d3b8a463865..00000000000 --- a/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/internal/SqlClientReactorScope.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.techempower.inverno.benchmark.internal; - -import com.techempower.inverno.benchmark.AppConfiguration; - -import io.inverno.core.annotation.Bean; -import io.inverno.core.annotation.Bean.Visibility; -import io.inverno.core.annotation.Destroy; -import io.inverno.core.annotation.Init; -import io.inverno.mod.base.concurrent.Reactor; -import io.inverno.mod.base.concurrent.ReactorScope; -import io.inverno.mod.base.concurrent.VertxReactor; -import io.inverno.mod.sql.SqlClient; -import io.inverno.mod.sql.vertx.ConnectionSqlClient; -import io.vertx.core.Vertx; -import io.vertx.core.VertxOptions; -import io.vertx.pgclient.PgConnectOptions; -import io.vertx.pgclient.PgConnection; -import reactor.core.publisher.Mono; - -@Bean( name = "SqlClient", visibility = Visibility.PRIVATE ) -public class SqlClientReactorScope extends ReactorScope> { - - private final AppConfiguration configuration; - private final Reactor reactor; - - private Vertx vertx; - private PgConnectOptions connectOptions; - - public SqlClientReactorScope(AppConfiguration configuration, Reactor reactor) { - this.configuration = configuration; - this.reactor = reactor; - } - - @Init - public void init() { - if(this.reactor instanceof VertxReactor) { - this.vertx = ((VertxReactor)this.reactor).getVertx(); - } - else { - this.vertx = Vertx.vertx(new VertxOptions().setPreferNativeTransport(this.configuration.boot().prefer_native_transport())); - } - - this.connectOptions = new PgConnectOptions() - .setHost(this.configuration.db_host()) - .setPort(this.configuration.db_port()) - .setDatabase(this.configuration.db_database()) - .setUser(this.configuration.db_username()) - .setPassword(this.configuration.db_password()) - .setCachePreparedStatements(true) - .setPipeliningLimit(100_100); - } - - @Destroy - public void destroy() { - if(!(this.reactor instanceof VertxReactor)) { - this.vertx.close(); - } - } - - @Override - protected Mono create() { - return Mono.fromCompletionStage(() -> PgConnection.connect(this.vertx, this.connectOptions).toCompletionStage()) - .map(pgConn -> (SqlClient)new ConnectionSqlClient(pgConn)) - .cacheInvalidateWhen(client -> ((ConnectionSqlClient)client).onClose()); - } -} diff --git a/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/model/Fortune.java b/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/model/Fortune.java index de4017c3c0d..b00a8a9df9f 100644 --- a/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/model/Fortune.java +++ b/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/model/Fortune.java @@ -1,5 +1,8 @@ package com.techempower.inverno.benchmark.model; +import com.dslplatform.json.CompiledJson; + +@CompiledJson public final class Fortune implements Comparable { private final int id; diff --git a/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/model/Message.java b/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/model/Message.java index c9d554903d7..306d5c348d6 100644 --- a/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/model/Message.java +++ b/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/model/Message.java @@ -1,5 +1,8 @@ package com.techempower.inverno.benchmark.model; +import com.dslplatform.json.CompiledJson; + +@CompiledJson public final class Message { private final String message; diff --git a/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/model/World.java b/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/model/World.java index f0f1825f3b9..6defeb042a3 100644 --- a/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/model/World.java +++ b/frameworks/Java/inverno/src/main/java/com/techempower/inverno/benchmark/model/World.java @@ -1,14 +1,15 @@ package com.techempower.inverno.benchmark.model; +import com.dslplatform.json.CompiledJson; + +@CompiledJson public final class World implements Comparable { - private final int id; + private int id; private int randomNumber; - public World(int id) { - this.id = id; - } - + public World() {} + public World(int id, int randomNumber) { this.id = id; this.randomNumber = randomNumber; diff --git a/frameworks/Java/inverno/src/main/java/module-info.java b/frameworks/Java/inverno/src/main/java/module-info.java index fefbf5c52a6..39d0224949b 100644 --- a/frameworks/Java/inverno/src/main/java/module-info.java +++ b/frameworks/Java/inverno/src/main/java/module-info.java @@ -10,16 +10,13 @@ requires io.netty.common; requires io.netty.codec.http; requires unbescape; - + requires static dsl.json; + requires io.vertx.client.sql.pg; requires io.vertx.client.sql; requires io.vertx.core; requires java.sql; - - //requires transitive io.netty.transport; - //requires static io.netty.transport.unix.common; - //requires static io.netty.transport.epoll; - + exports com.techempower.inverno.benchmark; exports com.techempower.inverno.benchmark.model; } diff --git a/frameworks/Java/wicket/pom.xml b/frameworks/Java/wicket/pom.xml index c0f0303051d..b7f4576382c 100644 --- a/frameworks/Java/wicket/pom.xml +++ b/frameworks/Java/wicket/pom.xml @@ -24,7 +24,7 @@ 11 2.13.0 1.7.25 - 9.18.0 + 9.19.0 diff --git a/frameworks/JavaScript/express/package.json b/frameworks/JavaScript/express/package.json index 5655ae91033..9f022f3ac69 100644 --- a/frameworks/JavaScript/express/package.json +++ b/frameworks/JavaScript/express/package.json @@ -7,7 +7,7 @@ "dateformat": "3.0.3", "escape-html": "1.0.3", "express": "4.18.2", - "mongoose": "8.8.3", + "mongoose": "8.9.5", "mysql2": "3.9.8", "pg": "8.5.0", "pg-promise": "10.7.3", diff --git a/frameworks/JavaScript/hapi/package.json b/frameworks/JavaScript/hapi/package.json index d38a23b49c9..1865dffe88e 100644 --- a/frameworks/JavaScript/hapi/package.json +++ b/frameworks/JavaScript/hapi/package.json @@ -8,7 +8,7 @@ "async": "2.1.5", "bluebird": "3.4.7", "handlebars": "4.3.0", - "mongoose": "8.8.3", + "mongoose": "8.9.5", "mysql": "2.16.0", "mysql2": "3.9.8", "pg": "8.5.1", diff --git a/frameworks/PHP/cyberphp/README.md b/frameworks/PHP/cyberphp/README.md new file mode 100755 index 00000000000..bc25c16fc20 --- /dev/null +++ b/frameworks/PHP/cyberphp/README.md @@ -0,0 +1,26 @@ +# CyberPHP Benchmarking Test + +## Test URLs +### JSON + +http://localhost:8080/json + +### PLAINTEXT + +http://localhost:8080/plaintext + +### DB + +http://localhost:8080/db + +### QUERY + +http://localhost:8080/queries/[count] + +### UPDATE + +http://localhost:8080/updates/[count] + +### FORTUNES + +http://localhost:8080/fortunes diff --git a/frameworks/PHP/cyberphp/app/config.php b/frameworks/PHP/cyberphp/app/config.php new file mode 100644 index 00000000000..ba727ea087a --- /dev/null +++ b/frameworks/PHP/cyberphp/app/config.php @@ -0,0 +1,57 @@ + 'Cyber', + // Request middleware runs after obtaining request body and before parsing route + // Mainly used for blacklist, whitelist, system maintenance, request filtering, data access, etc. + 'request_middleware' => [ + // \app\common\middleware\IpBlacklistMiddleware::class,// IP blacklist middleware + // \app\middleware\RateLimitMiddleware::class,// Rate limit middleware + // \app\middleware\SecurityMiddleware::class, // Security protection (CSRF/XSS filtering/SQL injection) middleware + ], + // Business middleware runs after parsing route and before executing controller method + // Mainly used for common business such as user authentication + 'middleware' => [ + // \app\common\middleware\Route1Middleware::class, + // \app\common\middleware\Route2Middleware::class, + ], + 'orm' => 'pdo', + 'pdo' => [ + 'dsn' => 'pgsql:host=tfb-database;dbname=hello_world', + 'username' => 'benchmarkdbuser', + 'password' => 'benchmarkdbpass', + 'options' => [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,PDO::ATTR_EMULATE_PREPARES => false] + ], + 'eloquent' => [ + 'driver' => 'mysql', + 'host' => '127.0.0.1', + 'database' => 'lavaman', + 'username' => 'root', + 'password' => 'root', + 'charset' => 'utf8mb4', + 'prefix' => '', + ], + 'thinkorm' => [ + 'default' => 'mysql', + 'connections' => [ + 'mysql' => [ + 'type' => 'mysql', // Database type + 'hostname' => '127.0.0.1',// Server address + 'database' => 'lavaman',// Database name + 'username' => 'root',// Database username + 'password' => 'root',// Database password + 'hostport' => '',// Database connection port + 'params' => [], + 'charset' => 'utf8mb4',// Database encoding default utf8 + 'prefix' => '',// Table prefix + ], + ], + ], + 'cookie' => [ + 'expires' => 0, + 'path' => '/', + 'domain' => '', + 'secure' => true, + 'httponly' => true, + 'samesite' => 'Lax' // None, Lax, Strict + ] +]; \ No newline at end of file diff --git a/frameworks/PHP/cyberphp/app/controller/Index.php b/frameworks/PHP/cyberphp/app/controller/Index.php new file mode 100644 index 00000000000..b1466094bab --- /dev/null +++ b/frameworks/PHP/cyberphp/app/controller/Index.php @@ -0,0 +1,74 @@ + 'Hello, World!']); + } + + public function plaintext() + { + return Response::text('Hello, World!'); + } + + public function db() + { + $prepare = app()->dbWorld; + $prepare->execute([mt_rand(1, 10000)]); + $data = $prepare->fetch(); + return Response::json($data); + } + public function fortunes() + { + $fortune = app()->dbFortune; + $fortune->execute(); + $arr = $fortune->fetchAll(\PDO::FETCH_KEY_PAIR); + $arr[0] = 'Additional fortune added at request time.'; + \asort($arr); + $html = ''; + foreach ($arr as $id => $message) { + $message = \htmlspecialchars($message, \ENT_QUOTES, 'UTF-8'); + $html .= "$id$message"; + } + return Response::html("Fortunes$html
idmessage
"); + } + + public function queries($q=1) + { + $statement = app()->dbWorld; + $query_count = max(min(intval($q), 500), 1); + $arr = []; + while ($query_count--) { + $statement->execute([mt_rand(1, 10000)]); + $arr[] = $statement->fetch(); + } + return Response::json($arr); + } + + public function updates($q=1) + { + static $updates = []; + + $random = app()->dbWorld; + $count = max(min(intval($q), 500), 1); + + $worlds = $keys = $values = []; + for ($i = 0; $i < $count; ++ $i) { + $values[] = $keys[] = $id = mt_rand(1, 10000); + $random->execute([$id]); + $row = $random->fetch(); + $values[] = $row['randomNumber'] = mt_rand(1, 10000); + $worlds[] = $row; + } + if (!isset($updates[$count])) { + $sql = 'UPDATE World SET randomNumber = CASE id' . str_repeat(' WHEN ?::INTEGER THEN ?::INTEGER ', $count) . 'END WHERE id IN (' . str_repeat('?::INTEGER,', $count - 1) . '?::INTEGER)'; + $updates[$count] = app()->db->prepare($sql); + } + $updates[$count]->execute([...$values, ...$keys]); + + return Response::json($worlds); + } +} diff --git a/frameworks/PHP/cyberphp/app/helpers.php b/frameworks/PHP/cyberphp/app/helpers.php new file mode 100644 index 00000000000..0b55349f7e3 --- /dev/null +++ b/frameworks/PHP/cyberphp/app/helpers.php @@ -0,0 +1,84 @@ +getConfig($key) ?? $default; + } +} + +function renderExceptionPage($e, $debug = true, $templateFile = ''): string +{ + // Determine template path + $templateFile = !empty($templateFile) ? $templateFile : __DIR__ . '/views/errors/exception.html'; + // Prepare template variables + $data = [ + 'code' => $e->getCode(), + 'message' => $debug ? $e->getMessage() : 'The current server is experiencing an error, please contact the administrator or try again later.', + 'error' => $e->getMessage(), + ]; + // Add more information in debug mode + if ($debug) { + $data['trace'] = []; + $data['file'] = $e->getFile(); + $data['line'] = $e->getLine(); + $traceFiles = $e->getTrace(); + array_unshift($traceFiles, ['file' => $data['file'], 'line' => $data['line']]); + foreach ($traceFiles as $v) { + try { + if (isset($v['file']) && isset($v['line'])) { + $startline = max(1, $v['line'] - 10); + $contents = file($v['file']); + $data['trace'][] = [ + 'file' => $v['file'], + 'line' => $v['line'], + 'source0' => $contents ? array_slice($contents, 0, 1) : '', + 'source' => [ + 'startline' => $startline, + 'content' => array_slice($contents, $startline - 1, 16) + ] + ]; + } + } catch (\Throwable $e) { + continue; + } + } + } + // Render error page + if (!file_exists($templateFile)) { + $msg = '

Error ' . $data['code'] . '

'; + $msg .= '
Sorry, the server encountered an error
'; + $msg .= '

' . htmlspecialchars($data['message']) . '

'; + $msg .= ''; + return $msg; + } + extract($data); + ob_start(); + include $templateFile; + return ob_get_clean(); +} \ No newline at end of file diff --git a/frameworks/PHP/cyberphp/app/route.php b/frameworks/PHP/cyberphp/app/route.php new file mode 100644 index 00000000000..e224ec394c3 --- /dev/null +++ b/frameworks/PHP/cyberphp/app/route.php @@ -0,0 +1,14 @@ + + + + + + + An Error Occurred + + + +
+

Error

+

Sorry, an error occurred

+
+ + + $v) { ?> +
+
# Line
+
+
  • '.htmlentities($v['source0'][0]).'
  • '; + } + if (!empty($v['source'])) { + echo '
      '; + foreach ((array) $v['source']['content'] as $key => $value) { + if (($key + $v['source']['startline']) == $v['line']) { + echo '
    1. '.htmlentities($value).'
    2. '; + } else { + echo '
    3. '.htmlentities($value).'
    4. '; + } + } + echo '
    '; + } + ?>
    +
    +
    + + + Return to Home + + + \ No newline at end of file diff --git a/frameworks/PHP/cyberphp/benchmark_config.json b/frameworks/PHP/cyberphp/benchmark_config.json new file mode 100755 index 00000000000..689f889ef03 --- /dev/null +++ b/frameworks/PHP/cyberphp/benchmark_config.json @@ -0,0 +1,30 @@ +{ + "framework": "cyberphp", + "tests": [ + { + "default": { + "json_url": "/json", + "db_url": "/db", + "query_url": "/queries/", + "fortune_url": "/fortunes", + "update_url": "/updates/", + "plaintext_url": "/plaintext", + "port": 8080, + "approach": "Realistic", + "classification": "Micro", + "database": "Postgres", + "framework": "cyberphp", + "language": "PHP", + "flavor": "PHP8", + "orm": "Raw", + "platform": "workerman", + "webserver": "None", + "os": "Linux", + "database_os": "Linux", + "display_name": "cyberphp", + "notes": "", + "versus": "workerman" + } + } + ] +} diff --git a/frameworks/PHP/cyberphp/bootstrap.php b/frameworks/PHP/cyberphp/bootstrap.php new file mode 100644 index 00000000000..f9d1a350d25 --- /dev/null +++ b/frameworks/PHP/cyberphp/bootstrap.php @@ -0,0 +1,41 @@ + __DIR__ .'/app/route.php', + + // Application configuration file location can be modified freely + + // Key represents the name of the sub-application; sub-applications not listed cannot be accessed + 'config' => [ + // Default configuration + '' => require 'app/config.php', + // If a sub-application does not mention a configuration, the content of the default configuration file will be used + // 'admin'=> (require 'app/admin/config.php') + (require 'app/config.php'), + + // Or only use the default configuration + // 'phone'=> require 'app/config.php', + + // Or do not use the default configuration, directly use your custom sub-application configuration, you can change the name freely + // 'phone'=> require 'app/config_phone.php', + + // Or this way, each configuration item is introduced separately + // 'admin'=> [ + // 'app_name' => 'admin', + // 'request_middleware' => require 'app/admin/config_request_middleware.php', + // 'middleware' => require 'app/admin/config_middleware.php', + // 'database' => require 'app/admin/config_database.php', + // 'cookie' => require 'app/admin/config_cookie.php', + // 'database' => require 'app/admin/config_database.php', + // ], + ], + + // Create route manager + 'Route' => \DI\create(\Cyber\Route::class), + 'Middleware' => \DI\create(\Cyber\Middleware::class), + + // Create request object for handling HTTP requests + 'Request' => \DI\create(\Cyber\Request::class), + // Create response object for generating HTTP responses + 'Response' => \DI\create(\Cyber\Response::class), +]; \ No newline at end of file diff --git a/frameworks/PHP/cyberphp/composer.json b/frameworks/PHP/cyberphp/composer.json new file mode 100644 index 00000000000..68678af9697 --- /dev/null +++ b/frameworks/PHP/cyberphp/composer.json @@ -0,0 +1,29 @@ +{ + "name": "eoioer/cyberphp", + "authors": [ + { + "name": "eoioer", + "email": "eoioer@qq.com" + } + ], + "type": "project", + "description": "The Fastest and Smallest PHP Framework", + "license": "MIT", + "require": { + "php": ">=8.3", + "php-di/php-di": "^7.0", + "nikic/fast-route": "^1.3", + "workerman/workerman": "^4.1" + }, + "autoload": { + "psr-4": { + "app\\": "app/", + "Cyber\\": "src/" + }, + "files": [ + "app/helpers.php" + ] + }, + "minimum-stability": "stable", + "prefer-stable": true +} diff --git a/frameworks/PHP/cyberphp/cyberphp.dockerfile b/frameworks/PHP/cyberphp/cyberphp.dockerfile new file mode 100644 index 00000000000..e1c91791a05 --- /dev/null +++ b/frameworks/PHP/cyberphp/cyberphp.dockerfile @@ -0,0 +1,26 @@ +FROM ubuntu:24.04 + +ENV TEST_TYPE default + +ARG DEBIAN_FRONTEND=noninteractive + +RUN apt-get update -yqq && apt-get install -yqq software-properties-common > /dev/null +RUN LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php > /dev/null && \ + apt-get update -yqq > /dev/null && apt-get upgrade -yqq > /dev/null + +RUN apt-get install -yqq php8.4-cli php8.4-pgsql php8.4-xml > /dev/null + +COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer + +RUN apt-get update -yqq && apt-get install -y php-pear php8.4-dev libevent-dev git > /dev/null +RUN pecl install event-3.1.4 > /dev/null && echo "extension=event.so" > /etc/php/8.4/cli/conf.d/30-event.ini + +WORKDIR /cyberphp +COPY --link . . + +RUN composer install --optimize-autoloader --classmap-authoritative --no-dev --quiet +COPY php.ini /etc/php/8.4/cli/conf.d/10-opcache.ini + +EXPOSE 8080 + +CMD php /cyberphp/server.php start \ No newline at end of file diff --git a/frameworks/PHP/cyberphp/php.ini b/frameworks/PHP/cyberphp/php.ini new file mode 100644 index 00000000000..f4817cc9e3a --- /dev/null +++ b/frameworks/PHP/cyberphp/php.ini @@ -0,0 +1,11 @@ +zend_extension=opcache.so +opcache.enable=1 +opcache.enable_cli=1 +opcache.validate_timestamps=0 +opcache.save_comments=0 +opcache.enable_file_override=1 +opcache.huge_code_pages=1 +mysqlnd.collect_statistics = Off +memory_limit = 512M +opcache.jit_buffer_size=128M +opcache.jit=tracing \ No newline at end of file diff --git a/frameworks/PHP/cyberphp/public/favicon.ico b/frameworks/PHP/cyberphp/public/favicon.ico new file mode 100644 index 00000000000..ada4d61b845 Binary files /dev/null and b/frameworks/PHP/cyberphp/public/favicon.ico differ diff --git a/frameworks/PHP/cyberphp/public/index.php b/frameworks/PHP/cyberphp/public/index.php new file mode 100644 index 00000000000..cbfd55373c0 --- /dev/null +++ b/frameworks/PHP/cyberphp/public/index.php @@ -0,0 +1,23 @@ +run(); + if (!$response instanceof Response) { + $response = Response::html($response ?? ''); + } + echo $response->send(); +} catch (Exception $e) { + echo renderExceptionPage($e); +} catch (Throwable $e) { + echo renderExceptionPage($e); +} diff --git a/frameworks/PHP/cyberphp/server.php b/frameworks/PHP/cyberphp/server.php new file mode 100644 index 00000000000..e9a354b2ce9 --- /dev/null +++ b/frameworks/PHP/cyberphp/server.php @@ -0,0 +1,85 @@ +count = 4; + +// Initialize ThinkPHP application +$app = \Cyber\App::bootstrap(__DIR__.'/bootstrap.php'); + +/** + * Callback function to handle HTTP requests + * @param TcpConnection $connection Client connection object + * @param WorkermanRequest $request HTTP request object + */ +$http_worker->onMessage = function(TcpConnection $connection, WorkermanRequest $request) use ($app) { + // Initialize request object + $_GET = $request->get(); // Get GET parameters + $_POST = $request->post(); // Get POST parameters + $_FILES = $request->file(); // Get file uploads + $_COOKIE = $request->cookie(); // Get COOKIE + + // Merge server variables + $_SERVER = array_merge($_SERVER, [ + 'RAW_BODY' => $request->rawBody(), // Raw request body + 'REQUEST_METHOD' => $request->method(), // Request method + 'REQUEST_URI' => $request->uri(), // Request URI + 'QUERY_STRING' => $request->queryString(), // Query string + 'REMOTE_ADDR' => $connection->getRemoteIp(), // Client IP + 'REMOTE_PORT' => $connection->getRemotePort(), // Client port + 'SERVER_PROTOCOL' => 'HTTP/'.$request->protocolVersion(), // Protocol version + ]); + + // Handle request headers + foreach ($request->header() as $key => $value) { + $_SERVER['HTTP_' . strtoupper(str_replace('-', '_', $key))] = $value; + } + + try { + ob_start(); // Start output buffering + $response = $app->run(); // Run ThinkPHP application + + // Handle response + if(!$response instanceof Response){ + // If not a Response object, directly output content + echo $response; + $content = ob_get_clean(); + $connection->send($content); + }else{ + // If it is a Response object, send HTTP response + echo $response->send(); + $content = ob_get_clean(); + $connection->send(new Workerman\Protocols\Http\Response( + $response->getStatusCode(), // Status code + $response->getHeaders(), // Response headers + $content // Response content + )); + } + } catch (Exception $e) { + // Catch exceptions and render error page + $connection->send(renderExceptionPage($e)); + } catch (Throwable $e) { + // Catch all errors + $connection->send(renderExceptionPage($e)); + } +}; + +/** + * Run all Worker instances + * This method will block the current process until all Workers stop + */ +Worker::runAll(); diff --git a/frameworks/PHP/cyberphp/src/App.php b/frameworks/PHP/cyberphp/src/App.php new file mode 100644 index 00000000000..288e969e0a9 --- /dev/null +++ b/frameworks/PHP/cyberphp/src/App.php @@ -0,0 +1,154 @@ +start_time = time(); + + /* Build container instance */ + $this->container = new Container($containerConfig); + + /* Load route configuration */ + $routes = require $this->container->get('route_path'); + /* Create route manager */ + $this->route = $this->container->get('Route'); + /* Call route dispatcher */ + $this->route->dispatcher($routes); + + /* Configuration */ + $this->config = $this->container->get('config'); + /* Request object */ + $this->request = $this->container->get('Request'); + + /* Database */ + $pdo = new PDO(...$this->getConfig('pdo')); + $this->db = $pdo; + $this->dbWorld = $pdo->prepare('SELECT id,randomNumber FROM World WHERE id=?'); + $this->dbFortune = $pdo->prepare('SELECT id,message FROM Fortune'); + + } + /** + * Run application + */ + public function run() + { + $this->timestamps = time(); + /* cli mode maintains database connection */ + if (php_sapi_name() === 'cli' and time() - $this->start_time > 1) { + $this->start_time = time(); + $pdo = new PDO(...$this->getConfig('pdo')); + $this->db = $pdo; + $this->dbWorld = $pdo->prepare('SELECT id,randomNumber FROM World WHERE id=?'); + $this->dbFortune = $pdo->prepare('SELECT id,message FROM Fortune'); + } + + /* Return response */ + return $this->route->handleRoute(); + } + + /** + * Get the current application configuration + * $app->getConfig(); // Returns the entire configuration content of the current application + * $app->getConfig('app_name'); // Get the value of ['app_name'] in the current application configuration + * $app->getConfig('cookie.expires'); // Get the value of ['cookie']['expires'] + * $app->getConfig(null, 'admin'); // Returns the entire configuration content of the admin application + * $app->getConfig('app_name', 'admin'); // Get the value of ['app_name'] in the admin application configuration + * $app->getConfig('cookie.expires','admin'); // Get the value of ['cookie']['expires'] in the admin application configuration + */ + public function getConfig($key = null, $appName = null): mixed + { + $appName = $appName ?? $this->appName ?? ''; + $config = $this->config[$appName] ?? null; + // Get the entire application configuration + if ($key === null) { + return $config; + } + // Split the key into an array + $keys = explode('.', $key); + // Traverse the key array and get the configuration layer by layer + foreach ($keys as $k) { + if (is_array($config) && array_key_exists($k, $config)) { + $config = $config[$k]; + } else { + return null; // If a layer does not exist, return null + } + } + return $config; // Return the final configuration value + } + /** + * Initialize the application + * @param string $bootstrap Configuration file + * @return self + */ + public static function bootstrap($bootstrap = null): self + { + if (!$bootstrap) { + throw new \Exception('App::bootstrap parameter does not exist'); + } + if (self::$instance === null) { + /* Load container configuration file */ + if (!file_exists($bootstrap) || !is_readable($bootstrap)) { + throw new \Exception("App::bootstrap parameter {$bootstrap} path error"); + } + $containerConfig = require_once $bootstrap; + self::$instance = new self($containerConfig); + return self::$instance; + }else{ + throw new \Exception('Application has started'); + } + } + /** + * Get the application singleton instance + * @return self + */ + public static function getInstance(): self + { + if (self::$instance === null) { + throw new \Exception('Application has not started'); + } + return self::$instance; + } +} diff --git a/frameworks/PHP/cyberphp/src/Middleware.php b/frameworks/PHP/cyberphp/src/Middleware.php new file mode 100644 index 00000000000..c482ebe1347 --- /dev/null +++ b/frameworks/PHP/cyberphp/src/Middleware.php @@ -0,0 +1,48 @@ +request; + foreach ($requestMiddlewares as $middleware) { + if (!class_exists($middleware)) { + throw new \Exception("The parameter class {$middleware} for processing the request middleware does not exist"); + } + $instance = app()->container->get($middleware); + if (!method_exists($instance, 'handle')) { + throw new \Exception("The parameter class {$middleware} for processing the request middleware does not have a handle method"); + } + /* Call the handle method of the request data middleware */ + $request = $instance->handle($request); + } + return $request; + } + + public function handle(array $Middlewares, callable $finalHandler) + { + $request = app()->request; + $container = app()->container; + // Start wrapping the handler from the last middleware layer by layer + $response = array_reduce( + array_reverse($Middlewares), + function($next, $middleware) use ($request,$container) { + if (!class_exists($middleware)) { + throw new \Exception("The middleware parameter class {$middleware} does not exist"); + } + $instance = $container->get($middleware); + if (!method_exists($instance, 'handle')) { + throw new \Exception("The middleware parameter class {$middleware} does not have a handle method"); + } + return function() use ($instance, $request, $next) { + return $instance->handle($request, $next); + }; + }, + $finalHandler + ); + // Execute the middleware chain + return $response(); + } +} \ No newline at end of file diff --git a/frameworks/PHP/cyberphp/src/Request.php b/frameworks/PHP/cyberphp/src/Request.php new file mode 100644 index 00000000000..ec54d88b94c --- /dev/null +++ b/frameworks/PHP/cyberphp/src/Request.php @@ -0,0 +1,232 @@ + $value) { + if (strpos($key, 'HTTP_') === 0) { + $key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($key, 5))))); + $headers[$key] = $value; + } + } + ksort($headers); + return $headers; + } + return $_SERVER['HTTP_'.strtoupper(str_replace('-', '_', $key))] ?? $default; + } + /** + * Set request header + * @param string $key Header information name + * @param string $value Header information value + * @return void + */ + public function setHeader(string $key, string $value): void + { + if(is_array($key)){ + foreach ($key as $k => $v) { + $_SERVER['HTTP_' . strtoupper(str_replace('-', '_', $k))] = $v; + } + }else{ + $_SERVER['HTTP_' . strtoupper(str_replace('-', '_', $key))] = $value; + } + } +} diff --git a/frameworks/PHP/cyberphp/src/Response.php b/frameworks/PHP/cyberphp/src/Response.php new file mode 100644 index 00000000000..7e6698231e1 --- /dev/null +++ b/frameworks/PHP/cyberphp/src/Response.php @@ -0,0 +1,218 @@ + 'OK', + 201 => 'Created', + 204 => 'No Content', + + // 3xx Redirection + 301 => 'Moved Permanently', + 302 => 'Found', + 304 => 'Not Modified', + + // 4xx Client Errors + 400 => 'Bad Request', + 401 => 'Unauthorized', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 408 => 'Request Timeout', + 422 => 'Unprocessable Entity', + 429 => 'Too Many Requests', + + // 5xx Server Errors + 500 => 'Internal Server Error', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout' + ]; + + protected string $content = ''; + protected int $statusCode = 200; + protected array $headers = []; + protected bool $sent = false; + + public function __construct(string $content = '', int $statusCode = 200, array $headers = []) + { + $this->content = $content; + $this->statusCode = $statusCode; + $this->headers = $headers; + } + + /** + * Get status code description + */ + public function getStatusText(): string + { + return self::HTTP_STATUS[$this->statusCode]; + } + /** + * Get response body + */ + public function getStatusCode(): int + { + return $this->statusCode; + } + /** + * Get response body + */ + public function getContent(): string + { + return $this->content; + } + + /** + * Get all response headers + */ + public function getHeaders(): array + { + return $this->headers; + } + + /** + * Add response header + * + * @throws Exception + */ + public function withHeader(string $name, string|array $value): static + { + // Validate header name legality + if (!preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/', $name)) { + throw new Exception('Header name can only contain letters, numbers, and special characters'); + } + if (empty($name)) { + throw new Exception('Header name cannot be empty'); + } + + $clone = clone $this; + $clone->headers[$name] = is_array($value) ? $value : [$value]; + return $clone; + } + + /** + * Add multiple response headers + */ + public function withHeaders(array $headers): static + { + $clone = clone $this; + foreach ($headers as $name => $value) { + $clone = $clone->withHeader($name, $value); + } + return $clone; + } + + /** + * Create JSON response + * + * @throws Exception + */ + public static function json(mixed $data, int $status = 200, array $headers = []): static + { + try { + $json = json_encode($data, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE); + } catch (\JsonException $e) { + throw new Exception('Unable to encode data to JSON', 0, $e); + } + + $headers['Content-Type'] = 'application/json; charset=utf-8'; + $headers['Date'] = gmdate('D, d M Y H:i:s').' GMT'; + return new static($json, $status, $headers); + } + + /** + * Create HTML response + */ + public static function html(string $html, int $status = 200, array $headers = []): static + { + $headers['Content-Type'] = 'text/html; charset=utf-8'; + $headers['Date'] = gmdate('D, d M Y H:i:s').' GMT'; + return new static($html, $status, $headers); + } + + /** + * Create text response + */ + public static function text(string $text, int $status = 200, array $headers = []): static + { + $headers['Content-Type'] = 'text/plain; charset=utf-8'; + $headers['Date'] = gmdate('D, d M Y H:i:s').' GMT'; + return new static($text, $status, $headers); + } + /** + * Create file response + */ + public static function file(string $file, string $filename, int $status = 200, array $headers = []): static + { + $headers['Content-Type'] = 'application/octet-stream'; + $headers['Date'] = gmdate('D, d M Y H:i:s').' GMT'; + $headers['Content-Disposition'] = 'attachment; filename="' . $filename . '"'; + return new static(file_get_contents($file), $status, $headers); + } + + /** + * Create redirect response + * + * @throws Exception + */ + public static function redirect(string $url, int $status = 302, array $headers = []): static + { + if (!filter_var($url, FILTER_VALIDATE_URL) && !str_starts_with($url, '/')) { + throw new Exception('Invalid URL format'); + } + return new static('', $status, array_merge($headers, ['Location' => $url])); + } + + /** + * Send response + * + * @throws Exception + */ + public function send(): void + { + if ($this->isSent()) { + throw new Exception('Response already sent'); + } + if (!headers_sent()) { + // // 发送状态码 + http_response_code($this->statusCode); + + // // 确保有 Content-Type 头 + if (!isset($this->headers['Content-Type'])) { + $this->headers['Content-Type'] = ['text/html; charset=utf-8']; + } + + // // 发送响应头 + foreach ($this->headers as $name => $values) { + $values = (array) $values; + foreach ($values as $value) { + header($name . ': ' . $value, false); + } + } + } + + // // 发送响应内容 + echo $this->content; + + // $this->sent = true; + } + + /** + * Check if the response has been sent + */ + public function isSent(): bool + { + return $this->sent; + } +} diff --git a/frameworks/PHP/cyberphp/src/Route.php b/frameworks/PHP/cyberphp/src/Route.php new file mode 100644 index 00000000000..b1b70f4f873 --- /dev/null +++ b/frameworks/PHP/cyberphp/src/Route.php @@ -0,0 +1,61 @@ +dispatcher = FastRoute\simpleDispatcher(function (RouteCollector $r) use ($routes) { + foreach ($routes as $route) { + // Check the number of array members. Three members indicate a single route configuration. + if (count($route) == 3) { + $r->addRoute(preg_split('/\s*,\s*/', $route[1]), $route[0], $route[2]); + // Two members indicate a group route. + } elseif (count($route) == 2) { + $r->addGroup($route[0], function (RouteCollector $r) use ($route) { + foreach ($route[1] as $childRoute) { + $r->addRoute(preg_split('/\s*,\s*/', trim($childRoute[1])), $childRoute[0], $childRoute[2]); + } + }); + } + } + }); + } + public function handleRoute() + { + $request = app()->request; + $container = app()->container; + // Parse the current route + $routeInfo = $this->dispatcher->dispatch($request->getMethod(), $request->getPathInfo()); + if ($routeInfo[0] == 0) { + throw new \Exception('Page not found', 404); + } elseif ($routeInfo[0] == 2) { + throw new \Exception('Request method error', 405); + } elseif ($routeInfo[0] == 1) { + $handler = $routeInfo[1]; + $vars = $routeInfo[2]; + $parameters = [...array_values($vars)]; + + // Create a closure to pass to your middleware to execute the final handler + $finalHandler = function() use ($handler, $parameters, $container) { + // If handler is a string (controller@method) + if (is_string($handler)) { + list($controller, $method) = explode('@', $handler); + $ctrl = $container->get($controller); + return $ctrl->$method(...$parameters); + } elseif (is_callable($handler)) { + return $handler(...$parameters); + } else { + throw new \Exception('Route handler configuration error'); + } + }; + return $finalHandler(); + } + } +} \ No newline at end of file diff --git a/frameworks/PHP/cyberphp/src/Utility.php b/frameworks/PHP/cyberphp/src/Utility.php new file mode 100644 index 00000000000..17ab3948176 --- /dev/null +++ b/frameworks/PHP/cyberphp/src/Utility.php @@ -0,0 +1,47 @@ + 'Handling JSON data', + 'mbstring' => 'Handling multibyte strings', + 'pdo' => 'Database connection', + 'pdo_mysql' => 'MySQL database support', + 'openssl' => 'Encryption and HTTPS support' + ]; + // Check required extensions + $missingExtensions = []; + foreach ($requiredExtensions as $extension => $purpose) { + if (!extension_loaded($extension)) { + $missingExtensions[] = sprintf( + "- %s (%s)", + $extension, + $purpose + ); + } + } + // If there are missing required extensions, throw an exception + if (!empty($missingExtensions)) { + throw new \Exception(sprintf( + "Missing required PHP extensions:\n%s\nPlease install these extensions before running the program.", + implode("\n", $missingExtensions) + )); + } + return true; + } +} diff --git a/frameworks/PHP/laravel/deploy/franken/Caddyfile b/frameworks/PHP/laravel/deploy/franken/Caddyfile new file mode 100644 index 00000000000..6c4cc54627c --- /dev/null +++ b/frameworks/PHP/laravel/deploy/franken/Caddyfile @@ -0,0 +1,24 @@ +{ + {$CADDY_GLOBAL_OPTIONS} + + admin {$CADDY_SERVER_ADMIN_HOST}:{$CADDY_SERVER_ADMIN_PORT} + + frankenphp { + worker "{$APP_PUBLIC_PATH}/frankenphp-worker.php" {$CADDY_SERVER_WORKER_COUNT} + } +} + +{$CADDY_SERVER_SERVER_NAME} { + route { + # Mercure configuration is injected here... + {$CADDY_SERVER_EXTRA_DIRECTIVES} + + # FrankenPHP! + # disable static files for this benchmark + # by using php instead of php_server + rewrite frankenphp-worker.php + php { + root "{$APP_PUBLIC_PATH}" + } + } +} diff --git a/frameworks/PHP/laravel/laravel-octane-frankenphp.dockerfile b/frameworks/PHP/laravel/laravel-octane-frankenphp.dockerfile index 2ad30a612e3..c21eeeacf8a 100644 --- a/frameworks/PHP/laravel/laravel-octane-frankenphp.dockerfile +++ b/frameworks/PHP/laravel/laravel-octane-frankenphp.dockerfile @@ -1,7 +1,6 @@ FROM dunglas/frankenphp RUN install-php-extensions \ - intl \ opcache \ pcntl \ pdo_mysql \ @@ -26,4 +25,4 @@ RUN frankenphp -v EXPOSE 8080 -ENTRYPOINT ["php", "artisan", "octane:frankenphp", "--port=8080"] +ENTRYPOINT ["php", "artisan", "octane:frankenphp", "--port=8080", "--caddyfile=/app/deploy/franken/Caddyfile"] diff --git a/frameworks/PHP/laravel/laravel-ripple.dockerfile b/frameworks/PHP/laravel/laravel-ripple.dockerfile index 4f1bb8bc3a9..2fc4a480716 100644 --- a/frameworks/PHP/laravel/laravel-ripple.dockerfile +++ b/frameworks/PHP/laravel/laravel-ripple.dockerfile @@ -33,14 +33,13 @@ RUN mkdir -p bootstrap/cache \ storage/framework/views \ storage/framework/cache -RUN echo "PRP_HTTP_LISTEN=http://0.0.0.0:8080" >> .env -RUN echo "PRP_HTTP_WORKERS=64" >> .env -RUN echo "PRP_HTTP_RELOAD=0" >> .env -RUN echo "PRP_HTTP_SANDBOX=1" >> .env +RUN echo "RIP_HTTP_LISTEN=http://0.0.0.0:8080" >> .env +RUN echo "RIP_HTTP_WORKERS=64" >> .env +RUN echo "RIP_HTTP_RELOAD=0" >> .env # Configure RUN composer install --quiet -RUN composer require cloudtay/ripple-driver --quiet +RUN composer require cloudtay/laravel-ripple --quiet RUN php artisan vendor:publish --tag=ripple-config RUN php artisan optimize diff --git a/frameworks/PHP/php/deploy/franken/Caddyfile b/frameworks/PHP/php/deploy/franken/Caddyfile index b6aa805f871..72400a7e489 100644 --- a/frameworks/PHP/php/deploy/franken/Caddyfile +++ b/frameworks/PHP/php/deploy/franken/Caddyfile @@ -11,11 +11,14 @@ :8080 route { - root * /php # FrankenPHP! + # disable static files for this benchmark + # by using php instead of php_server @phpFiles path *.php - php @phpFiles + php @phpFiles { + root /php + } respond 404 } \ No newline at end of file diff --git a/frameworks/PHP/reactphp/app.php b/frameworks/PHP/reactphp/app.php index 4f9dd816652..bea0c65038d 100644 --- a/frameworks/PHP/reactphp/app.php +++ b/frameworks/PHP/reactphp/app.php @@ -10,6 +10,7 @@ use React\Promise\PromiseInterface; use function React\Promise\all; +use function React\Promise\resolve; /** @return Closure(Request):ResponseInterface */ function requestHandler(): Closure @@ -29,7 +30,7 @@ function requestHandler(): Closure }; return static function (Request $request) use ($world, $fortune, $update): ResponseInterface | PromiseInterface { - return match($request->getUri()->getPath()) { + return resolve((match($request->getUri()->getPath()) { '/plaintext' => Response::plaintext('Hello, World!'), '/json' => Response::json(['message' => 'Hello, World!']), '/db' => db($world), @@ -38,7 +39,9 @@ function requestHandler(): Closure '/update' => updateraw(queryCount($request), $world, $update), // '/info' => info(), default => new Response(404, [], 'Error 404'), - }; + }))->catch( + static fn (Throwable $error): PromiseInterface => resolve(Response::plaintext($error->getMessage())->withStatus(500)), + ); }; } diff --git a/frameworks/PHP/reactphp/benchmark_config.json b/frameworks/PHP/reactphp/benchmark_config.json index fb29ad3c207..e53c029d748 100644 --- a/frameworks/PHP/reactphp/benchmark_config.json +++ b/frameworks/PHP/reactphp/benchmark_config.json @@ -1,5 +1,6 @@ { "framework": "reactphp", + "maintainers": ["WyriHaximus"], "tests": [{ "default": { "json_url": "/json", @@ -16,9 +17,28 @@ "webserver": "None", "os": "Linux", "database_os": "Linux", - "display_name": "reactphp", + "display_name": "reactphp [libevent]", "notes": "", "versus": "php" + }, + "libuv": { + "json_url": "/json", + "plaintext_url": "/plaintext", + "port": 8080, + "approach": "Realistic", + "classification": "Platform", + "framework": "reactphp", + "language": "PHP", + "flavor": "PHP8", + "database": "MySQL", + "orm": "Raw", + "platform": "reactphp", + "webserver": "None", + "os": "Linux", + "database_os": "Linux", + "display_name": "reactphp [libuv]", + "notes": "", + "versus": "reactphp" } }] } diff --git a/frameworks/PHP/reactphp/reactphp-libuv.dockerfile b/frameworks/PHP/reactphp/reactphp-libuv.dockerfile new file mode 100644 index 00000000000..48126b5c59c --- /dev/null +++ b/frameworks/PHP/reactphp/reactphp-libuv.dockerfile @@ -0,0 +1,35 @@ +FROM ubuntu:24.04 + +ARG DEBIAN_FRONTEND=noninteractive + +RUN apt-get update -yqq && apt-get install -yqq software-properties-common > /dev/null +RUN LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php > /dev/null +RUN apt-get update -yqq > /dev/null && \ + apt-get install -yqq git unzip wget curl build-essential \ + php8.4-cli php8.4-mbstring php8.4-dev php8.4-xml > /dev/null + +# An extension is required! +# We deal with concurrencies over 1k, which stream_select doesn't support. +# libuv +RUN apt-get install -yqq libuv1-dev > /dev/null \ + && pecl install uv-beta > /dev/null \ + && echo "extension=uv.so" > /etc/php/8.4/cli/conf.d/uv.ini + +# libevent +# RUN apt-get install -y libevent-dev > /dev/null \ +# && pecl install event-3.1.4 > /dev/null \ +# && echo "extension=event.so" > /etc/php/8.4/cli/conf.d/event.ini + +COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer + +COPY --link deploy/conf/* /etc/php/8.4/cli/conf.d/ + +WORKDIR /reactphp +COPY --link . . + +RUN composer install --prefer-dist --optimize-autoloader --no-dev --quiet + +EXPOSE 8080 + +ENTRYPOINT ["/usr/bin/php"] +CMD ["server.php"] diff --git a/frameworks/PHP/reactphp/reactphp.dockerfile b/frameworks/PHP/reactphp/reactphp.dockerfile index a46c613938f..0ecb546a1c3 100644 --- a/frameworks/PHP/reactphp/reactphp.dockerfile +++ b/frameworks/PHP/reactphp/reactphp.dockerfile @@ -27,7 +27,7 @@ COPY --link deploy/conf/* /etc/php/8.4/cli/conf.d/ WORKDIR /reactphp COPY --link . . -RUN composer install --prefer-dist --optimize-autoloader --no-dev +RUN composer install --prefer-dist --optimize-autoloader --no-dev --quiet EXPOSE 8080 diff --git a/frameworks/PHP/reactphp/server.php b/frameworks/PHP/reactphp/server.php index ddfb5a07145..278208b6291 100644 --- a/frameworks/PHP/reactphp/server.php +++ b/frameworks/PHP/reactphp/server.php @@ -8,6 +8,9 @@ require_once __DIR__.'/app.php'; $server = new HttpServer(requestHandler()); +$server->on('error', function (\Throwable $error) { + echo $error->getMessage(), PHP_EOL; +}); $socket = new SocketServer('0.0.0.0:8080'); $server->listen($socket); diff --git a/frameworks/PHP/symfony/deploy/Caddyfile b/frameworks/PHP/symfony/deploy/Caddyfile index f794fd3d647..6faa66909af 100644 --- a/frameworks/PHP/symfony/deploy/Caddyfile +++ b/frameworks/PHP/symfony/deploy/Caddyfile @@ -11,18 +11,13 @@ :8080 route { - root * /symfony/public - # If the requested file does not exist, try index files - @indexFiles file { - try_files {path} {path}/runtime.php runtime.php - split_path .php + # FrankenPHP! + # disable static files for this benchmark + # by using php instead of php_server + rewrite runtime.php + php { + root /symfony/public } - rewrite @indexFiles {http.matchers.file.relative} - - # FrankenPHP! - @phpFiles path *.php - php @phpFiles - respond 404 } diff --git a/frameworks/PHP/symfony/symfony-franken.dockerfile b/frameworks/PHP/symfony/symfony-franken.dockerfile index 967b9370514..05b8267409b 100644 --- a/frameworks/PHP/symfony/symfony-franken.dockerfile +++ b/frameworks/PHP/symfony/symfony-franken.dockerfile @@ -2,7 +2,6 @@ FROM dunglas/frankenphp # add additional extensions here: RUN install-php-extensions \ - intl \ opcache \ pdo_pgsql \ zip > /dev/null diff --git a/frameworks/Perl/feersum/cpanfile b/frameworks/Perl/feersum/cpanfile index 9b868f48ffe..12b329a825a 100644 --- a/frameworks/Perl/feersum/cpanfile +++ b/frameworks/Perl/feersum/cpanfile @@ -1,4 +1,4 @@ -requires 'Feersum', '== 1.503'; +requires 'Feersum', '== 1.504'; requires 'JSON::XS', '== 4.03'; requires 'DBD::MariaDB', '== 1.23'; requires 'DBD::Pg', '== 3.18.0'; diff --git a/frameworks/Python/clace/.dockerignore b/frameworks/Python/clace/.dockerignore new file mode 100644 index 00000000000..9fce104ae8c --- /dev/null +++ b/frameworks/Python/clace/.dockerignore @@ -0,0 +1,2 @@ +.venv +README.md diff --git a/frameworks/Python/clace/README.md b/frameworks/Python/clace/README.md new file mode 100755 index 00000000000..f510c5c8f54 --- /dev/null +++ b/frameworks/Python/clace/README.md @@ -0,0 +1,11 @@ +# Introduction + +[Clace](https://github.com/claceio/clace) is a platform for developing and deploying internal tools. + +Clace is implemented in Go. Clace apps are written in [Starlark](https://starlark-lang.org/). Starlark is a thread-safe language with Python syntax, designed for embedding. Clace uses the [Starlark Go](https://github.com/google/starlark-go) implementation. Since apps are developed using a python like syntax, the benchmark is added under the Python category. + +# Benchmarking + +The JSON and plaintext tests are implemented. Clace supports SQLite database only currently, so the database tests are not implemented. + +The Dockerfile starts the Clace server and creates a single app which implements the benchmark apis (app.star). diff --git a/frameworks/Python/clace/app.star b/frameworks/Python/clace/app.star new file mode 100644 index 00000000000..86c0dd68346 --- /dev/null +++ b/frameworks/Python/clace/app.star @@ -0,0 +1,6 @@ +app = ace.app("testapp", + routes = [ + ace.api("/json", lambda req: {'message': 'Hello, world!'}, type=ace.JSON), + ace.api("/plaintext", lambda req: 'Hello, world!', type=ace.TEXT) + ] +) diff --git a/frameworks/Python/clace/benchmark_config.json b/frameworks/Python/clace/benchmark_config.json new file mode 100755 index 00000000000..c836299e939 --- /dev/null +++ b/frameworks/Python/clace/benchmark_config.json @@ -0,0 +1,23 @@ +{ + "framework": "clace", + "tests": [ + { + "default": { + "json_url": "/json", + "plaintext_url": "/plaintext", + "port": 8080, + "approach": "Realistic", + "classification": "Micro", + "framework": "Clace", + "language": "python", + "flavor": "Starlark", + "platform": "None", + "webserver": "Clace", + "os": "Linux", + "display_name": "Clace", + "notes": "", + "versus": "None" + } + } + ] +} diff --git a/frameworks/Python/clace/clace.dockerfile b/frameworks/Python/clace/clace.dockerfile new file mode 100644 index 00000000000..40c8c69608b --- /dev/null +++ b/frameworks/Python/clace/clace.dockerfile @@ -0,0 +1,11 @@ +FROM python:3.11 +WORKDIR /clace/ + +RUN curl -L https://clace.io/install.sh | bash +ENV CL_HOME="/root/clhome" +ENV PATH="/root/clhome/bin:$PATH" + +COPY . . + +EXPOSE 8080 +CMD /clace/run.sh diff --git a/frameworks/Python/clace/run.sh b/frameworks/Python/clace/run.sh new file mode 100755 index 00000000000..d5dc87d345e --- /dev/null +++ b/frameworks/Python/clace/run.sh @@ -0,0 +1,19 @@ +#!/bin/sh +cd /root + +cat < /root/clhome/clace.toml +[logging] +console = false +file = false +access_logging = false + +[http] +host = "0.0.0.0" +port = 8080 +EOF + + +clace server start & +sleep 2 +clace app create --auth=none --approve /clace tfb-server:/ +tail -f /dev/null diff --git a/frameworks/Ruby/rack-sequel/benchmark_config.json b/frameworks/Ruby/rack-sequel/benchmark_config.json index 30ba62a6223..64663f27632 100644 --- a/frameworks/Ruby/rack-sequel/benchmark_config.json +++ b/frameworks/Ruby/rack-sequel/benchmark_config.json @@ -8,7 +8,7 @@ "fortune_url": "/fortunes", "update_url": "/updates?queries=", "port": 8080, - "approach": "Stripped", + "approach": "Realistic", "classification": "Micro", "database": "MySQL", "framework": "rack", @@ -28,7 +28,7 @@ "fortune_url": "/fortunes", "update_url": "/updates?queries=", "port": 8080, - "approach": "Stripped", + "approach": "Realistic", "classification": "Micro", "database": "Postgres", "framework": "rack", @@ -48,7 +48,7 @@ "update_url": "/updates?queries=", "plaintext_url": "/plaintext", "port": 8080, - "approach": "Stripped", + "approach": "Realistic", "classification": "Micro", "database": "MySQL", "framework": "rack", @@ -68,7 +68,7 @@ "fortune_url": "/fortunes", "update_url": "/updates?queries=", "port": 8080, - "approach": "Stripped", + "approach": "Realistic", "classification": "Micro", "database": "Postgres", "framework": "rack", @@ -88,7 +88,7 @@ "fortune_url": "/fortunes", "update_url": "/updates?queries=", "port": 8080, - "approach": "Stripped", + "approach": "Realistic", "classification": "Micro", "database": "MySQL", "framework": "rack", @@ -108,7 +108,7 @@ "fortune_url": "/fortunes", "update_url": "/updates?queries=", "port": 8080, - "approach": "Stripped", + "approach": "Realistic", "classification": "Micro", "database": "Postgres", "framework": "rack", diff --git a/frameworks/Ruby/rack/benchmark_config.json b/frameworks/Ruby/rack/benchmark_config.json index 97227d81e36..914d2093dbb 100644 --- a/frameworks/Ruby/rack/benchmark_config.json +++ b/frameworks/Ruby/rack/benchmark_config.json @@ -10,7 +10,7 @@ "fortune_url": "/fortunes", "update_url": "/updates?queries=", "port": 8080, - "approach": "Stripped", + "approach": "Realistic", "classification": "Micro", "orm": "raw", "database": "Postgres", @@ -31,7 +31,7 @@ "fortune_url": "/fortunes", "update_url": "/updates?queries=", "port": 8080, - "approach": "Stripped", + "approach": "Realistic", "classification": "Micro", "orm": "raw", "database": "Postgres", @@ -52,7 +52,7 @@ "fortune_url": "/fortunes", "update_url": "/updates?queries=", "port": 8080, - "approach": "Stripped", + "approach": "Realistic", "classification": "Micro", "orm": "raw", "database": "Postgres", @@ -73,7 +73,7 @@ "fortune_url": "/fortunes", "update_url": "/updates?queries=", "port": 8080, - "approach": "Stripped", + "approach": "Realistic", "classification": "Micro", "orm": "raw", "database": "Postgres", @@ -94,7 +94,7 @@ "fortune_url": "/fortunes", "update_url": "/updates?queries=", "port": 8080, - "approach": "Stripped", + "approach": "Realistic", "classification": "Micro", "orm": "raw", "database": "Postgres", diff --git a/frameworks/Ruby/rack/rack-iodine.dockerfile b/frameworks/Ruby/rack/rack-iodine.dockerfile index 999eb9fc6e7..9b4908fb177 100644 --- a/frameworks/Ruby/rack/rack-iodine.dockerfile +++ b/frameworks/Ruby/rack/rack-iodine.dockerfile @@ -19,4 +19,4 @@ COPY . . EXPOSE 8080 -CMD bundle exec iodine -p 8080 +CMD bundle exec iodine -p 8080 -w $(ruby config/auto_tune.rb | grep -Eo '[0-9]+' | head -n 1) diff --git a/frameworks/Ruby/rage-sequel/Gemfile b/frameworks/Ruby/rage-sequel/Gemfile new file mode 100644 index 00000000000..4ee2e620f6e --- /dev/null +++ b/frameworks/Ruby/rage-sequel/Gemfile @@ -0,0 +1,7 @@ +source "https://rubygems.org" + +gem "rage-rb", "~> 1.10" + +gem "pg", "~> 1.0" +gem 'sequel', '~> 5.0' +gem 'sequel_pg', '~> 1.6', platforms: :ruby, require: false diff --git a/frameworks/Ruby/rage-sequel/Gemfile.lock b/frameworks/Ruby/rage-sequel/Gemfile.lock new file mode 100644 index 00000000000..d1ad00bb988 --- /dev/null +++ b/frameworks/Ruby/rage-sequel/Gemfile.lock @@ -0,0 +1,37 @@ +GEM + remote: https://rubygems.org/ + specs: + bigdecimal (3.1.9) + pg (1.5.9) + rack (2.2.10) + rack-test (2.2.0) + rack (>= 1.3) + rage-iodine (4.0.0) + rage-rb (1.11.0) + rack (~> 2.0) + rack-test (~> 2.1) + rage-iodine (~> 4.0) + rake (>= 12.0) + thor (~> 1.0) + zeitwerk (~> 2.6) + rake (13.2.1) + sequel (5.88.0) + bigdecimal + sequel_pg (1.17.1) + pg (>= 0.18.0, != 1.2.0) + sequel (>= 4.38.0) + thor (1.3.2) + zeitwerk (2.7.1) + +PLATFORMS + ruby + x86_64-darwin-20 + +DEPENDENCIES + pg (~> 1.0) + rage-rb (~> 1.10) + sequel (~> 5.0) + sequel_pg (~> 1.6) + +BUNDLED WITH + 2.5.6 diff --git a/frameworks/Ruby/rage-sequel/README.md b/frameworks/Ruby/rage-sequel/README.md new file mode 100755 index 00000000000..d299d0834ba --- /dev/null +++ b/frameworks/Ruby/rage-sequel/README.md @@ -0,0 +1,47 @@ +# Rage Benchmarking Test + +Rage is a fast web framework compatible with Rails. It uses an event-driven architecture and implements a lightweight, cooperative concurrency model based on Ruby Fibers. + +https://github.com/rage-rb/rage + +### Test Type Implementation Source Code + +* [JSON](app/controllers/benchmarks_controller.rb) +* [PLAINTEXT](app/controllers/benchmarks_controller.rb) +* [DB](app/controllers/benchmarks_controller.rb) +* [QUERY](app/controllers/benchmarks_controller.rb) +* [UPDATE](app/controllers/benchmarks_controller.rb) +* [FORTUNES](app/controllers/benchmarks_controller.rb) + +## Important Libraries + +The tests were run with: + +* [Sequel](https://rubygems.org/gems/sequel) +* [PG](https://rubygems.org/gems/pg) + +## Test URLs + +### JSON + +http://localhost:8080/json + +### PLAINTEXT + +http://localhost:8080/plaintext + +### DB + +http://localhost:8080/db + +### QUERY + +http://localhost:8080/queries?queries= + +### UPDATE + +http://localhost:8080/updates?queries= + +### FORTUNES + +http://localhost:8080/fortunes diff --git a/frameworks/Ruby/rage-sequel/Rakefile b/frameworks/Ruby/rage-sequel/Rakefile new file mode 100644 index 00000000000..046f1fcbd8d --- /dev/null +++ b/frameworks/Ruby/rage-sequel/Rakefile @@ -0,0 +1 @@ +require_relative "config/application" diff --git a/frameworks/Ruby/rage-sequel/app/controllers/application_controller.rb b/frameworks/Ruby/rage-sequel/app/controllers/application_controller.rb new file mode 100644 index 00000000000..c3238c52392 --- /dev/null +++ b/frameworks/Ruby/rage-sequel/app/controllers/application_controller.rb @@ -0,0 +1,2 @@ +class ApplicationController < RageController::API +end diff --git a/frameworks/Ruby/rage-sequel/app/controllers/benchmarks_controller.rb b/frameworks/Ruby/rage-sequel/app/controllers/benchmarks_controller.rb new file mode 100644 index 00000000000..082905ccda8 --- /dev/null +++ b/frameworks/Ruby/rage-sequel/app/controllers/benchmarks_controller.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +class BenchmarksController < ApplicationController + ALL_DB_IDS = (1..10_000).to_a + FORTUNES_TEMPLATE = ERB.new(Rage.root.join("app/views/fortunes.html.erb").read) + + before_action do + headers["server"] = "rage" + end + + def db + render json: World.with_pk(random_id).values + end + + def queries + worlds = DB.synchronize do + requested_ids.map do |id| + World.with_pk(id) + end + end + + render json: worlds.map!(&:values) + end + + def fortunes + records = Fortune.all + + records << Fortune.new(id: 0, message: "Additional fortune added at request time.") + records.sort_by!(&:message) + + render plain: FORTUNES_TEMPLATE.result(binding) + headers["content-type"] = "text/html; charset=utf-8" + end + + def updates + worlds = nil + + DB.synchronize do + worlds = requested_ids.map do |id| + world = World.with_pk(id) + new_value = random_id + new_value = random_id while new_value == world.randomnumber + world.randomnumber = new_value + + world + end + + World.batch_update(worlds) + end + + render json: worlds.map!(&:values) + end + + private + + def requested_ids + num = params[:queries].to_i + + if num > 500 + num = 500 + elsif num < 1 + num = 1 + end + + ALL_DB_IDS.sample(num) + end + + def random_id + Random.rand(9_999) + 1 + end +end diff --git a/frameworks/Ruby/rage-sequel/app/views/fortunes.html.erb b/frameworks/Ruby/rage-sequel/app/views/fortunes.html.erb new file mode 100644 index 00000000000..2614ca7a7b2 --- /dev/null +++ b/frameworks/Ruby/rage-sequel/app/views/fortunes.html.erb @@ -0,0 +1,12 @@ + + + Fortunes + + + + <% records.each do |record| %> + + <% end %> +
    idmessage
    <%= record.id %><%= CGI.escape_html(record.message) %>
    + + diff --git a/frameworks/Ruby/rage-sequel/benchmark_config.json b/frameworks/Ruby/rage-sequel/benchmark_config.json new file mode 100755 index 00000000000..4cbe6d248f4 --- /dev/null +++ b/frameworks/Ruby/rage-sequel/benchmark_config.json @@ -0,0 +1,28 @@ +{ + "framework": "rage-sequel", + "tests": [ + { + "default": { + "db_url": "/db", + "query_url": "/queries?queries=", + "fortune_url": "/fortunes", + "update_url": "/updates?queries=", + "port": 8080, + "approach": "Realistic", + "classification": "Micro", + "database": "postgres", + "framework": "Rage", + "language": "Ruby", + "flavor": "None", + "orm": "Full", + "platform": "Rack", + "webserver": "Rage-Iodine", + "os": "Linux", + "database_os": "Linux", + "display_name": "Rage-Sequel", + "notes": "", + "versus": "None" + } + } + ] +} diff --git a/frameworks/Ruby/rage-sequel/config.ru b/frameworks/Ruby/rage-sequel/config.ru new file mode 100644 index 00000000000..049a1ad509d --- /dev/null +++ b/frameworks/Ruby/rage-sequel/config.ru @@ -0,0 +1,3 @@ +require_relative "config/application" + +run Rage.application diff --git a/frameworks/Ruby/rage-sequel/config/application.rb b/frameworks/Ruby/rage-sequel/config/application.rb new file mode 100644 index 00000000000..9af142340b3 --- /dev/null +++ b/frameworks/Ruby/rage-sequel/config/application.rb @@ -0,0 +1,14 @@ +require "bundler/setup" +require "rage" +Bundler.require(*Rage.groups) + +require "rage/all" + +Rage.configure do + # use this to add settings that are constant across all environments +end + +require "erb" +require "cgi" + +require "rage/setup" diff --git a/frameworks/Ruby/rage-sequel/config/environments/development.rb b/frameworks/Ruby/rage-sequel/config/environments/development.rb new file mode 100644 index 00000000000..35d9e7ae7d6 --- /dev/null +++ b/frameworks/Ruby/rage-sequel/config/environments/development.rb @@ -0,0 +1,4 @@ +Rage.configure do + config.server.workers_count = -1 + config.logger = Rage::Logger.new(STDOUT) +end diff --git a/frameworks/Ruby/rage-sequel/config/environments/production.rb b/frameworks/Ruby/rage-sequel/config/environments/production.rb new file mode 100644 index 00000000000..0189c7742fa --- /dev/null +++ b/frameworks/Ruby/rage-sequel/config/environments/production.rb @@ -0,0 +1,3 @@ +Rage.configure do + config.logger = nil +end diff --git a/frameworks/Ruby/rage-sequel/config/initializers/sequel.rb b/frameworks/Ruby/rage-sequel/config/initializers/sequel.rb new file mode 100644 index 00000000000..273694b7b15 --- /dev/null +++ b/frameworks/Ruby/rage-sequel/config/initializers/sequel.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +SEQUEL_NO_ASSOCIATIONS = true +Sequel.extension :fiber_concurrency + +# Determine thread pool size and timeout +opts = { + max_connections: 512, + pool_timeout: 10 +} + +DB = Sequel.connect \ + '%{adapter}://%{host}/%{database}?user=%{user}&password=%{password}' % { + :adapter=>'postgres', + :host=>'tfb-database', + :database=>'hello_world', + :user=>'benchmarkdbuser', + :password=>'benchmarkdbpass' + }, opts + +# Define ORM models +class World < Sequel::Model(:World) + def self.batch_update(worlds) + ids = [] + sql = String.new("UPDATE world SET randomnumber = CASE id ") + worlds.each do |world| + sql << "when #{world.id} then #{world.randomnumber} " + ids << world.id + end + sql << "ELSE randomnumber END WHERE id IN ( #{ids.join(',')})" + DB.run(sql) + end +end + +class Fortune < Sequel::Model(:Fortune) + # Allow setting id to zero (0) per benchmark requirements + unrestrict_primary_key +end + +[World, Fortune].each(&:freeze) +DB.freeze diff --git a/frameworks/Ruby/rage-sequel/config/routes.rb b/frameworks/Ruby/rage-sequel/config/routes.rb new file mode 100644 index 00000000000..c03b04a7f52 --- /dev/null +++ b/frameworks/Ruby/rage-sequel/config/routes.rb @@ -0,0 +1,8 @@ +Rage.routes.draw do + root to: ->(env) { [200, {}, "It works!"] } + + get "db", to: "benchmarks#db" + get "queries", to: "benchmarks#queries" + get "fortunes", to: "benchmarks#fortunes" + get "updates", to: "benchmarks#updates" +end diff --git a/frameworks/Ruby/rage-sequel/lib/.keep b/frameworks/Ruby/rage-sequel/lib/.keep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/frameworks/Ruby/rage-sequel/rage-sequel.dockerfile b/frameworks/Ruby/rage-sequel/rage-sequel.dockerfile new file mode 100644 index 00000000000..096b256fe18 --- /dev/null +++ b/frameworks/Ruby/rage-sequel/rage-sequel.dockerfile @@ -0,0 +1,13 @@ +FROM ruby:3.4 + +EXPOSE 8080 +WORKDIR /rage-sequel + +COPY Gemfile* /rage-sequel/ +RUN bundle install --jobs=8 +COPY . /rage-sequel + +ENV RUBY_YJIT_ENABLE=1 +ENV BUNDLE_FORCE_RUBY_PLATFORM=true + +CMD bundle exec rage s -b 0.0.0.0 -p 8080 -e production diff --git a/frameworks/Ruby/rails/rails-iodine.dockerfile b/frameworks/Ruby/rails/rails-iodine.dockerfile index ffa3b078dd0..b91de6b668d 100644 --- a/frameworks/Ruby/rails/rails-iodine.dockerfile +++ b/frameworks/Ruby/rails/rails-iodine.dockerfile @@ -24,4 +24,4 @@ ENV RAILS_ENV=production_postgresql ENV PORT=8080 ENV REDIS_URL=redis://localhost:6379/0 CMD service redis-server start && \ - bundle exec iodine + bundle exec iodine -w $(ruby config/auto_tune.rb | grep -Eo '[0-9]+' | head -n 1) diff --git a/frameworks/Ruby/roda-sequel/roda-sequel-postgres-iodine-mri.dockerfile b/frameworks/Ruby/roda-sequel/roda-sequel-postgres-iodine-mri.dockerfile index 05851b9f9f1..c7f9fe856d1 100644 --- a/frameworks/Ruby/roda-sequel/roda-sequel-postgres-iodine-mri.dockerfile +++ b/frameworks/Ruby/roda-sequel/roda-sequel-postgres-iodine-mri.dockerfile @@ -18,4 +18,4 @@ ENV DBTYPE=postgresql EXPOSE 8080 -CMD bundle exec iodine -p 8080 +CMD bundle exec iodine -p 8080 -w $(ruby config/auto_tune.rb | grep -Eo '[0-9]+' | head -n 1) diff --git a/frameworks/Ruby/sinatra-sequel/sinatra-sequel-postgres-iodine-mri.dockerfile b/frameworks/Ruby/sinatra-sequel/sinatra-sequel-postgres-iodine-mri.dockerfile index 3f1bfb24b79..bf29d5a1e9e 100644 --- a/frameworks/Ruby/sinatra-sequel/sinatra-sequel-postgres-iodine-mri.dockerfile +++ b/frameworks/Ruby/sinatra-sequel/sinatra-sequel-postgres-iodine-mri.dockerfile @@ -17,4 +17,4 @@ ENV DBTYPE=postgresql EXPOSE 8080 -CMD bundle exec iodine -p 8080 +CMD bundle exec iodine -p 8080 -w $(ruby config/auto_tune.rb | grep -Eo '[0-9]+' | head -n 1)