Bladeren bron

[D] Add Juptune Framework (#9991)

Bradley Chatha 2 maanden geleden
bovenliggende
commit
59172604d6

+ 19 - 0
frameworks/D/juptune/README.md

@@ -0,0 +1,19 @@
+# Juptune Benchmarking Test
+
+Benchmarks for the WIP Juptune library - an upcoming async I/O library based around io_uring.
+
+### Test Type Implementation Source Code
+
+* [PLAINTEXT](./src/tests/plaintext.d)
+
+## Important Libraries
+
+The tests were run with:
+
+* [Juptune](https://github.com/Juptune/juptune)
+
+## Test URLs
+
+### PLAINTEXT
+
+http://localhost:8080/plaintext

+ 25 - 0
frameworks/D/juptune/benchmark_config.json

@@ -0,0 +1,25 @@
+{
+  "framework": "juptune",
+  "tests": [
+    {
+      "default": {
+        "plaintext_url": "/plaintext",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Platform",
+        "database": "None",
+        "framework": "juptune",
+        "language": "D",
+        "flavor": "ldc2",
+        "orm": "None",
+        "platform": "None",
+        "webserver": "None",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "Juptune",
+        "notes": "",
+        "versus": "None"
+      }
+    }
+  ]
+}

+ 30 - 0
frameworks/D/juptune/juptune.dockerfile

@@ -0,0 +1,30 @@
+FROM debian:bookworm-slim
+
+ARG LDC_VERSION=1.41.0
+ARG JUPTUNE_REF=3a27a36ce2f5f2ff7df7151311e18d9c3660ea8c
+ARG TFB_TEST_NAME
+
+ENV TEST_NAME=${TFB_TEST_NAME}
+
+# Install system deps & LDC
+RUN apt update \
+    && apt install -y curl xz-utils gnupg libsodium-dev meson unzip pkg-config \
+    && curl -fsS https://dlang.org/install.sh | bash -s ldc-${LDC_VERSION}
+
+# Install Juptune
+WORKDIR /juptune
+RUN curl -fsSL https://github.com/Juptune/juptune/archive/${JUPTUNE_REF}.zip -o code.zip \
+    && unzip code.zip \
+    && cd juptune* \
+    && . ~/dlang/ldc-${LDC_VERSION}/activate \
+    && meson setup build --buildtype debugoptimized -Ddefault_library=static \
+    && meson install -C build
+
+# Compile everything
+WORKDIR /app
+COPY ./src/ .
+RUN . ~/dlang/ldc-${LDC_VERSION}/activate \
+    && meson setup build --buildtype debugoptimized -Ddefault_library=static \
+    && meson compile -C build
+
+ENTRYPOINT [ "/app/build/juptune-tfb" ]

+ 274 - 0
frameworks/D/juptune/src/main.d

@@ -0,0 +1,274 @@
+import core.time : msecs;
+import std.parallelism : totalCPUs;
+import std.process : environment;
+
+import  juptune.core.util, 
+        juptune.core.ds, 
+        juptune.event, 
+        juptune.event.fiber, 
+        juptune.http;
+
+import tests.common : log;
+import tests.plaintext;
+
+/++++ Constant config ++++/
+
+enum SOCKET_BACKLOG_PER_THREAD  = 1000;
+enum FIBER_CALL_STACK_BYTES     = 1024 * 100;
+enum HTTP_READ_BUFFER_BYTES     = 1024;
+enum HTTP_WRITE_BUFFER_BYTES    = 1024;
+
+enum HTTP_CONFIG = Http1Config()
+                    .withReadTimeout(1000.msecs)
+                    .withWriteTimeout(1000.msecs);
+
+static assert(
+    HTTP_READ_BUFFER_BYTES + HTTP_WRITE_BUFFER_BYTES < FIBER_CALL_STACK_BYTES / 4,
+    "To be safe, please ensure the buffer bytes are only a quarter the size of a fiber call stack."
+);
+
+/++++ Globals ++++/
+
+__gshared TcpSocket server; // Currently there's no mechanism to directly pass data to new threads, so global state has to be used.
+
+/++++ Functions ++++/
+
+void main()
+{
+    auto loop = EventLoop(
+        EventLoopConfig()
+        .withFiberAllocatorConfig(
+            FiberAllocatorConfig()
+            .withBlockStackSize(FIBER_CALL_STACK_BYTES)
+        )
+    );
+
+    // open() and listen() can't be ran outside of an event loop thread, so currently this is the janky way to setup the server.
+    loop.addNoGCThread(() @nogc nothrow {
+        server.open().resultAssert;
+        server.listen("0.0.0.0:8080", SOCKET_BACKLOG_PER_THREAD*totalCPUs).resultAssert;
+        juptuneEventLoopCancelThread();
+    });
+    loop.join();
+
+    // Then we can setup the proper loop threads.
+    foreach(i; 0..totalCPUs)
+        loop.addGCThread(&router);
+    loop.join();
+}
+
+// Juptune currently does not provide higher-level server features out of the box, so we have
+// to hand-make a custom router.
+//
+// This is realistic in the sense that building a custom router is a completely valid, supported pattern
+// for people who want/need something very specialised.
+//
+// This is unrealistic in the sense that once Juptune has a native router, the native router would
+// almost certainly be used in a case like this (but since that's a TODO, this will have to do for now).
+void router() nothrow
+{
+    try
+    {
+        enum Route
+        {
+            FAILSAFE,
+            plaintext,
+        }
+
+        enum Method
+        {
+            FAILSAFE,
+            get
+        }
+
+        union RouteInput
+        {
+            PlainTextHeaderInput plaintext;
+        }
+
+        while(!juptuneEventLoopIsThreadCanceled())
+        {
+            TcpSocket client;
+
+            auto result = server.accept(client);
+            if(result.isError)
+            {
+                log("error accepting socket: ", result);
+                continue;
+            }
+
+            result = async(function () nothrow {
+                auto client = juptuneEventLoopGetContext!TcpSocket;
+                scope(exit) if(client.isOpen)
+                    auto _ = client.close();
+
+                Http1MessageSummary readSummary, writeSummary;
+                do
+                {
+                    if(!client.isOpen)
+                        return;
+
+                    // Read & Write primitives
+                    ubyte[HTTP_READ_BUFFER_BYTES] readBuffer;
+                    ubyte[HTTP_WRITE_BUFFER_BYTES] writeBuffer;
+                    auto reader = Http1Reader(client, readBuffer, HTTP_CONFIG);
+                    auto writer = Http1Writer(client, writeBuffer, HTTP_CONFIG);
+
+                    // Routing state
+                    Route route;
+                    Method method;
+                    RouteInput input;
+
+                    // Error handling
+                    uint errorCode;
+                    string errorMsg;
+                    void setError(uint code, string msg)
+                    {
+                        if(errorMsg !is null)
+                            return;
+                        errorCode = code;
+                        errorMsg = msg;
+                    }
+
+                    // Parse request line
+                    {
+                        Http1RequestLine requestLine;
+                        auto result = reader.readRequestLine(requestLine);
+                        if(result.isError)
+                        {
+                            log("readRequestLine() failed: ", result.error, ": ", result.context.slice);
+                            return;
+                        }
+
+                        requestLine.access((scope methodString, scope uri){
+                            switch(methodString)
+                            {
+                                case "GET":
+                                    method = Method.get;
+                                    break;
+
+                                default:
+                                    setError(405, "Unexpected method");
+                                    break;
+                            }
+
+                            switch(uri.path)
+                            {
+                                case "/plaintext":
+                                    route = Route.plaintext;
+                                    break;
+
+                                default:
+                                    setError(404, "Not found");
+                                    break;
+                            }
+                        });
+                    }
+
+                    // Read headers
+                    bool foundEndOfHeaders;
+                    while(!foundEndOfHeaders)
+                    {
+                        auto result = reader.checkEndOfHeaders(foundEndOfHeaders);
+                        if(result.isError)
+                        {
+                            log("checkEndOfHeaders() failed: ", result);
+                            return;
+                        }
+                        else if(foundEndOfHeaders)
+                            break;
+
+                        Http1Header header;
+                        result = reader.readHeader(header);
+                        if(result.isError)
+                        {
+                            log("readHeader() failed: ", result);
+                            return;
+                        }
+
+                        // Since we're using a custom router, we have the luxury of handling/ignoring headers during routing rather
+                        // than stuffing them all into a hashmap, and doing the processing post-routing.
+                        header.access((scope name, scope value){
+                            final switch(route) with(Route)
+                            {
+                                case FAILSAFE: break;
+                                
+                                case plaintext:
+                                    break;
+                            }
+                        });
+                    }
+
+                    // Read body
+                    Http1BodyChunk chunk;
+                    do {
+                        chunk = Http1BodyChunk();
+                        auto result = reader.readBody(chunk);
+                        if(result.isError)
+                        {
+                            log("readBody() failed: ", result);
+                            return;
+                        }
+
+                        // Likewise, we only need to deal with body data in certain routes, so we can ignore them in others.
+                        chunk.access((scope data){
+                            final switch(route) with(Route)
+                            {
+                                case FAILSAFE: break;
+                                
+                                case plaintext:
+                                    break;
+                            }
+                        });
+                    } while(chunk.hasDataLeft);
+
+                    // Finish reading the message, and either dispatch it to a handler, or report an error back.
+                    auto result = reader.finishMessage(readSummary);
+                    if(result.isError)
+                    {
+                        log("finishMessage() failed: ", result);
+                        return;
+                    }
+
+                    if(errorMsg !is null)
+                    {
+                        import tests.common : putServerAndDate;
+                        result = writer.putResponseLine(Http1Version.http11, errorCode, errorMsg).then!(
+                            () => writer.putServerAndDate(),
+                            () => writer.finishHeaders(),
+                            () => writer.finishBody(),
+                            () => writer.finishTrailers(),
+                            () => writer.finishMessage(writeSummary),
+                        );
+                        if(result.isError)
+                        {
+                            log("finishing a message [error variant] failed: ", result);
+                            return;
+                        }
+                        continue;
+                    }
+
+                    final switch(route) with(Route)
+                    {
+                        case FAILSAFE: break;
+                        
+                        case plaintext:
+                            handlePlainText(input.plaintext, writer, writeSummary);
+                            break;
+                    }
+                } while(!readSummary.connectionClosed && !writeSummary.connectionClosed);
+            }, client, &asyncMoveSetter!TcpSocket);
+            if(result.isError)
+            {
+                log("error calling async(): ", result);
+                continue;
+            }
+        }
+    }
+    catch(Throwable ex) // @suppress(dscanner.suspicious.catch_em_all)
+    {
+        import std.exception : assumeWontThrow;
+        log("uncaught exception: ", ex.msg).assumeWontThrow;
+        debug log(ex.info).assumeWontThrow;
+    }
+}

+ 25 - 0
frameworks/D/juptune/src/meson.build

@@ -0,0 +1,25 @@
+#### Project configuration ####
+
+project('juptune-tfb', 'd')
+
+#### Sources ####
+
+srcs = files(
+    './main.d',
+    './tests/common.d',
+    './tests/plaintext.d',
+)
+
+#### Dependencies ####
+
+juptune_dep = dependency('juptune')
+
+dep = declare_dependency(
+    include_directories: include_directories('.'),
+    sources: srcs,
+    dependencies: [juptune_dep],
+)
+
+#### Executables ####
+
+main_exe = executable('juptune-tfb', dependencies: [dep])

+ 81 - 0
frameworks/D/juptune/src/tests/common.d

@@ -0,0 +1,81 @@
+module tests.common;
+
+import juptune.core.util : Result, then;
+import juptune.http : Http1Writer;
+
+enum ENABLE_LOGGING = false;
+
+void log(Args...)(scope auto ref Args args)
+{
+    import std.exception : assumeWontThrow;
+    import std.stdio : writeln;
+    static if(ENABLE_LOGGING)
+        writeln(args).assumeWontThrow;
+}
+
+Result putServerAndDate(scope ref Http1Writer writer) nothrow
+{
+    import std.datetime : Clock, DayOfWeek, Month;
+    import std.exception : assumeWontThrow;
+
+    char[256] dateBuffer;
+    size_t dateCursor;
+
+    void put(const(char)[] value)
+    {
+        dateBuffer[dateCursor..dateCursor+value.length] = value[0..$];
+        dateCursor += value.length;
+    }
+
+    void putInt(int value)
+    {
+        import juptune.core.util.conv : toBase10, IntToCharBuffer;
+        IntToCharBuffer buffer;
+        put(toBase10(value, buffer));
+    }
+
+    const currTime = Clock.currTime.assumeWontThrow.toUTC();
+    final switch(currTime.dayOfWeek) with(DayOfWeek)
+    {
+        case mon: put("Mon, "); break;
+        case tue: put("Tue, "); break;
+        case wed: put("Wed, "); break;
+        case thu: put("Thu, "); break;
+        case fri: put("Fri, "); break;
+        case sat: put("Sat, "); break;
+        case sun: put("Sun, "); break;
+    }
+
+    putInt(currTime.day);
+    put(" ");
+
+    final switch(currTime.month) with(Month)
+    {
+        case jan: put("Jan "); break;
+        case feb: put("Feb "); break;
+        case mar: put("Mar "); break;
+        case apr: put("Apr "); break;
+        case may: put("May "); break;
+        case jun: put("Jun "); break;
+        case jul: put("Jul "); break;
+        case aug: put("Aug "); break;
+        case sep: put("Sep "); break;
+        case oct: put("Oct "); break;
+        case nov: put("Nov "); break;
+        case dec: put("Dec "); break;
+    }
+
+    putInt(currTime.year);
+    put(" ");
+
+    putInt(currTime.hour);
+    put(":");
+    putInt(currTime.minute);
+    put(":");
+    putInt(currTime.second);
+    put(" GMT");
+
+    return writer.putHeader("Server", "Juptune (Linux)").then!(
+        () => writer.putHeader("Date", dateBuffer[0..dateCursor])
+    );
+}

+ 41 - 0
frameworks/D/juptune/src/tests/plaintext.d

@@ -0,0 +1,41 @@
+module tests.plaintext;
+
+import juptune.http : Http1Writer, Http1Version, Http1MessageSummary;
+
+import tests.common : putServerAndDate, log;
+
+struct PlainTextHeaderInput
+{
+    // No header input, it's here just so the overarching logic exists.
+}
+
+void handlePlainText(
+    scope ref PlainTextHeaderInput input, 
+    scope ref Http1Writer writer,
+    scope ref Http1MessageSummary summary,
+) nothrow
+{
+    import juptune.core.util : then;
+    import juptune.core.util.conv : IntToCharBuffer, toBase10;
+
+    immutable RESPONSE = "Hello, World!";
+
+    IntToCharBuffer contentLengthBuffer;
+    const contentLength = toBase10(RESPONSE.length, contentLengthBuffer);
+
+    auto result = writer.putResponseLine(Http1Version.http11, 200, "OK").then!(
+        () => writer.putServerAndDate(),
+        () => writer.putHeader("Content-Length", contentLength),
+        () => writer.putHeader("Content-Type", "text/plain"),
+        () => writer.finishHeaders(),
+        () => writer.putBody("Hello, World!"),
+        () => writer.finishBody(),
+        () => writer.finishTrailers(),
+        () => writer.finishMessage(summary)
+    );
+    if(result.isError)
+    {
+        log("writing response [plaintext] failed: ", result);
+        return;
+    }
+}