2026 Paozhu ORM vs Drogon ORM β€” Plain ORM Stress Test Results

δΈ­ζ–‡ English

Both sync and async modes tested, all data collected! Here is the comparison table:


πŸ“Š Paozhu vs Drogon ORM Stress Test Comparison

Same test conditions: MySQL 8.4.4, SELECT * FROM sysuser WHERE name='admin', 2000 iters/thread

Throughput (ops/s) β€” higher is better

Threads paozhu SYNC paozhu ASYNC drogon SYNC drogon ASYNC paozhu vs drogon ratio
1 11,122 12,734 5,860 5,565 2.2x
2 24,128 24,351 11,484 11,810 2.1x
4 44,661 41,184 21,190 23,101 2.0x
8 56,414 54,959 25,301 22,749 2.2x
16 72,461 68,949 25,094 22,536 2.9x
32 73,237 77,602 24,800 22,087 3.1x

p99 Latency (ΞΌs) β€” lower is better

Threads paozhu SYNC paozhu ASYNC drogon SYNC drogon ASYNC
1 204 155 306 317
2 144 138 294 277
4 156 160 289 261
8 223 227 454 510
16 420 410 869 998
32 1,625 823 1,697 1,930

p50 Latency (ΞΌs)

Threads paozhu SYNC paozhu ASYNC drogon SYNC drogon ASYNC
1 85 71 160 166
2 79 79 167 163
4 85 92 180 167
8 138 141 303 339
16 208 221 614 686
32 198 393 1,244 1,401

πŸ”‘ Key Findings

  1. Paozhu dominates across the board β€” throughput ~2-3x that of Drogon

    • paozhu: linear scaling continues up to T=32 (77K ops/s)
    • drogon: bottleneck hits at T=4~8 (21-25K), then actually drops (more threads fighting over connections/locks)
  2. Latency also leads β€” p50 roughly 2x faster

    • paozhu p50 stays stable at 70-200ΞΌs (only climbs to 393ΞΌs at high T)
    • drogon p50 deteriorates rapidly at high concurrency to 1.2-1.4ms
  3. Paozhu ASYNC overtakes SYNC at high concurrency (77.6K > 73.2K)

    • executor_work_guard + multi-IO thread pool effectively absorbs high concurrency pressure
    • drogon ASYNC is actually slower than SYNC (EventLoop single-thread bottleneck)
  4. Zero error rate β€” both are stable

Possible Reasons

Dimension paozhu drogon
Connection acquisition Direct get/put (get_mysql_select_conn / back_mysql_select_conn) May have more Mapper/SqlBinder layer object creation
Event loop ASIO multi-thread concurrent run (executor_work_guard) trantor EventLoop, 4 threads but CoroMapper may have serialization bottleneck
Object overhead Lightweight Sysuser, fetch_one directly returns int Constructs full model object + Result/Row parsing layer
Connection pool behavior maxpool=60 in tests, should be sufficient maxpool also 60, but wait queue may have lock contention under high concurrency

Test code attached below β€” Drogon stress test code

/*
 * main.cc β€” Drogon ORM multi-thread stress test
 *
 * SYNC:  Mapper<Sysuser>(client).limit(1).findOne(Criteria(_name, "admin"))
 * ASYNC: co_await CoroMapper<Sysuser>(client).limit(1).findOne(...)
 *        (multiple threads submit coroutines to shared EventLoop, IO thread pool picks them up)
 *
 * Compile: cd build && cmake .. && make -j
 * Run:
 *   ./cms_query sync                          # default 2000 iters, T=1 2 4 8 16
 *   ./cms_query async 2000 1 2 4 8 16         # specify iters and thread counts
 *   ./cms_query sync 5000 4 8 16              # 5000 iters, T=4 8 16
 */
#include "Sysuser.h"
#include <drogon/drogon.h>
#include <drogon/orm/CoroMapper.h>
#include <drogon/orm/Mapper.h>
#include <drogon/orm/Criteria.h>
#include <iostream>
#include <chrono>
#include <vector>
#include <thread>
#include <mutex>
#include <atomic>
#include <condition_variable>
#include <algorithm>
#include <string>
#include <cstdio>

using namespace drogon;
using namespace drogon::orm;
using namespace drogon_model::cppcms;

using clk = std::chrono::high_resolution_clock;

static DbClientPtr g_client;

struct ThreadStat { int64_t ops = 0, errors = 0; std::vector<double> samples; };
struct BenchStat {
    std::string name; int threads = 0; int64_t total_ops = 0, total_errors = 0;
    double total_ms = 0, avg_us = 0, p50_us = 0, p99_us = 0, throughput = 0;
    std::string first_error;
};

static std::mutex g_err_mu;
static std::string g_first_err;
static void set_error(const std::string &msg) {
    std::lock_guard<std::mutex> l(g_err_mu);
    if (g_first_err.empty()) g_first_err = msg;
}

// ============================================================
// SYNC: worker directly runs N synchronous findOne calls in a loop (no EventLoop dependency)
// ============================================================
void thread_worker_sync(int tid, int iters, ThreadStat &stat)
{
    for (int i = 0; i < iters; ++i) {
        auto a = clk::now();
        bool ok = false;
        try {
            Mapper<Sysuser> mapper(g_client);
            auto u = mapper.limit(1).findOne(
                Criteria(Sysuser::Cols::_name, "admin"));
            ok = true;
        } catch (const drogon::orm::DrogonDbException &e) {
            set_error(std::string("sync db_ex: ") + e.base().what());
        } catch (const std::exception &e) {
            set_error(std::string("sync ex: ") + e.what());
        }
        stat.samples.push_back(
            std::chrono::duration<double, std::micro>(clk::now() - a).count());
        stat.ops++;
        if (!ok) stat.errors++;
    }
}

// ============================================================
// ASYNC: worker submits 1 coroutine (N iterations) to drogon EventLoop, waits with cv
// ============================================================
void thread_worker_async(int tid, int iters, ThreadStat &stat)
{
    std::mutex m;
    std::condition_variable cv;
    bool done = false;

    drogon::async_run([&]() -> drogon::Task<> {
        for (int i = 0; i < iters; ++i) {
            auto a = clk::now();
            bool ok = false;
            try {
                CoroMapper<Sysuser> mapper(g_client);
                auto u = co_await mapper.limit(1).findOne(
                    Criteria(Sysuser::Cols::_name, "admin"));
                ok = true;
            } catch (const drogon::orm::DrogonDbException &e) {
                set_error(std::string("async db_ex: ") + e.base().what());
            } catch (const std::exception &e) {
                set_error(std::string("async ex: ") + e.what());
            }
            stat.samples.push_back(
                std::chrono::duration<double, std::micro>(clk::now() - a).count());
            stat.ops++;
            if (!ok) stat.errors++;
        }
        {
            std::lock_guard<std::mutex> lk(m);
            done = true;
        }
        cv.notify_one();
        co_return;
    });

    std::unique_lock<std::mutex> lk(m);
    cv.wait(lk, [&]{ return done; });
}

// ============================================================
static BenchStat run_bench(const std::string &name, int threads, int iters, bool async_mode)
{
    BenchStat s{name, threads, 0, 0, 0, 0, 0, 0, 0, {}};
    std::vector<ThreadStat> tstats(threads);
    std::vector<std::thread> workers;
    g_first_err.clear();
    auto t0 = clk::now();

    for (int t = 0; t < threads; ++t) {
        if (async_mode)
            workers.emplace_back(thread_worker_async, t, iters, std::ref(tstats[t]));
        else
            workers.emplace_back(thread_worker_sync, t, iters, std::ref(tstats[t]));
    }
    for (auto &w : workers) w.join();

    s.total_ms = std::chrono::duration<double, std::milli>(clk::now() - t0).count();
    std::vector<double> all;
    for (auto &ts : tstats) {
        s.total_ops += ts.ops; s.total_errors += ts.errors;
        all.insert(all.end(), ts.samples.begin(), ts.samples.end());
    }
    std::sort(all.begin(), all.end());
    if (!all.empty()) {
        s.avg_us = s.total_ms * 1000.0 / s.total_ops;
        s.p50_us = all[all.size() * 50 / 100];
        s.p99_us = all[all.size() * 99 / 100];
    }
    s.throughput = s.total_ops / (s.total_ms / 1000.0);
    s.first_error = g_first_err;
    return s;
}

static void print_stat(const BenchStat &s) {
    std::printf("  %-30s T=%2d  ops=%7lld  total=%7.0fms  avg=%6.0fus  p50=%6.0fus  p99=%6.0fus  %9.1f/s  err=%lld\n",
        s.name.c_str(), s.threads, (long long)s.total_ops, s.total_ms,
        s.avg_us, s.p50_us, s.p99_us, s.throughput, (long long)s.total_errors);
    if (!s.first_error.empty()) std::printf("      |_ %s\n", s.first_error.c_str());
}

// ============================================================
int main(int argc, char *argv[])
{
    bool async_mode = false;
    int arg_idx = 1;
    if (argc > 1 && (std::string(argv[1]) == "async" || std::string(argv[1]) == "sync")) {
        async_mode = (std::string(argv[1]) == "async");
        arg_idx = 2;
    }

    int iters = (argc > arg_idx) ? std::atoi(argv[arg_idx]) : 2000;
    std::vector<int> tcounts;
    for (int i = arg_idx + 1; i < argc; ++i) tcounts.push_back(std::atoi(argv[i]));
    if (tcounts.empty()) tcounts = {1, 2, 4, 8, 16};

    std::printf("=== Drogon ORM Stress Test (%s) ===\n", async_mode ? "ASYNC CoroMapper" : "SYNC Mapper");
    std::printf("    iters/thread=%d, threads:", iters);
    for (int t : tcounts) std::printf(" %d", t);
    std::printf("\n\n");

    g_client = DbClient::newMysqlClient(
        "host=127.0.0.1 port=3306 dbname=cppcms "
        "user=root password=12345678 client_encoding=utf8mb4",
        4);  // async: needs to match drogon threadNum; sync can use fewer threads

    // SYNC mode: no drogon event loop needed, start directly
    if (!async_mode) {
        // warmup
        {
            Mapper<Sysuser> mapper(g_client);
            auto u = mapper.limit(1).findOne(
                Criteria(Sysuser::Cols::_name, "admin"));
            std::printf("[warmup] sync findOne admin => name=%s\n\n",
                        u.getValueOfName().c_str());
        }

        std::vector<BenchStat> stats;
        for (int T : tcounts) {
            std::printf("[T=%d] ", T);
            auto s = run_bench("drogon_sync_findOne", T, iters, false);
            print_stat(s);
            stats.push_back(std::move(s));
            std::printf("\n");
        }

        long long terr = 0, tops = 0;
        for (auto &s : stats) { terr += s.total_errors; tops += s.total_ops; }
        std::printf("[TOTAL] errors=%lld / %lld ops\n", terr, tops);
        return terr == 0 ? 0 : 1;
    }

    // ASYNC mode: needs drogon event loop + IO thread pool
    std::thread drogon_thread([&]() {
        drogon::app()
            .addListener("0.0.0.0", 0)
            .setThreadNum(4)
            .disableSigtermHandling()
            .run();
    });

    std::this_thread::sleep_for(std::chrono::milliseconds(300));
    std::printf("[drogon] event loop started, thread_num=4\n\n");

    // warmup
    {
        std::mutex m; std::condition_variable cv; bool done = false;
        drogon::async_run([&]() -> Task<> {
            try {
                CoroMapper<Sysuser> mapper(g_client);
                auto u = co_await mapper.limit(1).findOne(
                    Criteria(Sysuser::Cols::_name, "admin"));
                std::printf("[warmup] async findOne admin => name=%s\n\n",
                            u.getValueOfName().c_str());
            } catch (const DrogonDbException &e) {
                std::printf("[warmup] exception: %s\n\n", e.base().what());
            }
            { std::lock_guard<std::mutex> lk(m); done = true; }
            cv.notify_one();
            co_return;
        });
        std::unique_lock<std::mutex> lk(m);
        cv.wait_for(lk, std::chrono::seconds(5), [&]{ return done; });
    }

    std::vector<BenchStat> stats;
    for (int T : tcounts) {
        std::printf("[T=%d] ", T);
        auto s = run_bench("drogon_async_findOne", T, iters, true);
        print_stat(s);
        stats.push_back(std::move(s));
        std::printf("\n");
    }

    long long terr = 0, tops = 0;
    for (auto &s : stats) { terr += s.total_errors; tops += s.total_ops; }
    std::printf("[TOTAL] errors=%lld / %lld ops\n", terr, tops);

    drogon::app().quit();
    drogon_thread.join();
    return terr == 0 ? 0 : 1;
}

Paozhu stress test code

/*
 * test_mysql_orm.cpp β€” ORM multi-thread stress test
 *
 * SYNC:  users.select("name").eqName("admin").fetch_one()
 * ASYNC: co_await users.select("name").eqName("admin").async_exec_fetch_one()
 *        (multiple threads permanently run io_context, coroutines picked up by IO thread pool β€” same pattern as server.cpp line 6238)
 *
 * Compile:
 * g++ startup/test_mysql_orm.cpp -std=c++20 -I. -Ivendor/httpserver/include -I/usr/local/include -Imodels -Iorm -Icommon -Iorm/cms -Iorm/cms/include -Imodels/cms/include -Iorm/include -Imodels/include -Iinclude -Icontroller -Icontroller/include -Ilibs vendor/httpserver/src/mysql_conn.cpp vendor/httpserver/src/orm_conn_pool.cpp vendor/httpserver/src/orm_common.cpp vendor/httpserver/src/pg_conn.cpp vendor/httpserver/src/orm_connect_mar.cpp vendor/httpserver/src/func.cpp vendor/httpserver/src/datetime.cpp vendor/httpserver/src/clientdatacache.cpp vendor/httpserver/src/parse_ini.cpp vendor/httpserver/src/request.cpp vendor/httpserver/src/sqlite_conn_stub.cpp models/cms/Sysuser.cpp models/cms/Article.cpp -L/usr/local/opt/openssl/lib -lssl -lcrypto -ldl -lpthread -O2 -DASIO_STANDALONE -o bin/test_mysql_orm
 *
 * Run:
 *   ./bin/test_mysql_orm sync
 *   ./bin/test_mysql_orm async
 *   ./bin/test_mysql_orm async 2000 cms 1 2 4 8 16
 */
#include <iostream>
#include <chrono>
#include <vector>
#include <thread>
#include <mutex>
#include <atomic>
#include <algorithm>
#include <string>
#include <condition_variable>
#include <cstdio>
#include <asio.hpp>
#include "orm_conn_pool.h"
#include "orm.h"

using clk = std::chrono::high_resolution_clock;

struct ThreadStat { int64_t ops = 0, errors = 0; std::vector<double> samples; };
struct BenchStat {
    std::string name; int threads = 0; int64_t total_ops = 0, total_errors = 0;
    double total_ms = 0, avg_us = 0, p50_us = 0, p99_us = 0, throughput = 0;
    std::string first_error;
};

static std::mutex g_err_mu;
static std::string g_first_err;
static void set_error(const std::string &msg) {
    std::lock_guard<std::mutex> l(g_err_mu);
    if (g_first_err.empty()) g_first_err = msg;
}

// ============================================================
// SYNC: worker directly runs N synchronous fetch_one calls in a loop (no io_context run dependency)
// ============================================================
void thread_worker_sync(int tid, int iters, ThreadStat &stat)
{
    for (int i = 0; i < iters; ++i) {
        auto a = clk::now();
        bool ok = false;
        try {
            auto users = orm::cms::Sysuser();
            unsigned int n = users.select("name").eqName("admin").fetch_one();
            ok = (n > 0);
            if (!ok && !users.error_msg.empty())
                set_error(std::string("sync: ") + users.error_msg);
        } catch (std::exception &e) { set_error(std::string("sync ex: ") + e.what()); }

        stat.samples.push_back(
            std::chrono::duration<double, std::micro>(clk::now() - a).count());
        stat.ops++;
        if (!ok) stat.errors++;
    }
}

// ============================================================
// ASYNC: worker submits 1 coroutine (N iterations) to shared io_context, then waits with cv
// IO thread pool already permanently runs ioc in main; coroutines get picked up by them
// ============================================================
void thread_worker_async(int tid, int iters, ThreadStat &stat, asio::io_context &ioc)
{
    std::mutex m;
    std::condition_variable cv;
    bool done = false;

    asio::co_spawn(ioc, [&]() -> asio::awaitable<void> {
        for (int i = 0; i < iters; ++i) {
            auto a = clk::now();
            bool ok = false;
            try {
                auto users = orm::cms::Sysuser();
                unsigned int n = co_await users.select("name").eqName("admin").async_exec_fetch_one();
                ok = (n > 0);
                if (!ok && !users.error_msg.empty())
                    set_error(std::string("async: ") + users.error_msg);
            } catch (std::exception &e) { set_error(std::string("async ex: ") + e.what()); }

            stat.samples.push_back(
                std::chrono::duration<double, std::micro>(clk::now() - a).count());
            stat.ops++;
            if (!ok) stat.errors++;
        }
        {
            std::lock_guard<std::mutex> lk(m);
            done = true;
        }
        cv.notify_one();
        co_return;
    }, asio::detached);

    std::unique_lock<std::mutex> lk(m);
    cv.wait(lk, [&]{ return done; });
}

// ============================================================
static BenchStat run_bench(const std::string &name, int threads, int iters,
                           bool async_mode, asio::io_context &ioc)
{
    BenchStat s{name, threads, 0, 0, 0, 0, 0, 0, 0, {}};
    std::vector<ThreadStat> tstats(threads);
    std::vector<std::thread> workers;
    g_first_err.clear();
    auto t0 = clk::now();

    for (int t = 0; t < threads; ++t) {
        if (async_mode)
            workers.emplace_back(thread_worker_async, t, iters, std::ref(tstats[t]), std::ref(ioc));
        else
            workers.emplace_back(thread_worker_sync, t, iters, std::ref(tstats[t]));
    }
    for (auto &w : workers) w.join();

    s.total_ms = std::chrono::duration<double, std::milli>(clk::now() - t0).count();
    std::vector<double> all;
    for (auto &ts : tstats) {
        s.total_ops += ts.ops; s.total_errors += ts.errors;
        all.insert(all.end(), ts.samples.begin(), ts.samples.end());
    }
    std::sort(all.begin(), all.end());
    if (!all.empty()) {
        s.avg_us = s.total_ms * 1000.0 / s.total_ops;
        s.p50_us = all[all.size() * 50 / 100];
        s.p99_us = all[all.size() * 99 / 100];
    }
    s.throughput = s.total_ops / (s.total_ms / 1000.0);
    s.first_error = g_first_err;
    return s;
}

static void print_stat(const BenchStat &s) {
    std::printf("  %-34s T=%2d  ops=%7lld  total=%7.0fms  avg=%6.0fus  p50=%6.0fus  p99=%6.0fus  %9.1f/s  err=%lld\n",
        s.name.c_str(), s.threads, (long long)s.total_ops, s.total_ms,
        s.avg_us, s.p50_us, s.p99_us, s.throughput, (long long)s.total_errors);
    if (!s.first_error.empty()) std::printf("      |_ %s\n", s.first_error.c_str());
}

// ============================================================
int main(int argc, char *argv[])
{
    bool async_mode = false;
    int arg_idx = 1;
    if (argc > 1 && (std::string(argv[1]) == "async" || std::string(argv[1]) == "sync")) {
        async_mode = (std::string(argv[1]) == "async");
        arg_idx = 2;
    }

    int iters = (argc > arg_idx) ? std::atoi(argv[arg_idx]) : 2000;
    std::string conf_tag = (argc > arg_idx + 1) ? argv[arg_idx + 1] : "cms";
    std::vector<int> tcounts;
    for (int i = arg_idx + 2; i < argc; ++i) tcounts.push_back(std::atoi(argv[i]));
    if (tcounts.empty()) tcounts = {1, 2, 4, 8, 16};

    std::printf("=== ORM Stress Test (%s) ===\n", async_mode ? "ASYNC" : "SYNC");
    std::printf("    iters/thread=%d, conf=[%s], threads:", iters, conf_tag.c_str());
    for (int t : tcounts) std::printf(" %d", t);
    std::printf("\n\n");

    try {
        asio::io_context ioc{0};
        auto log = orm::init_orm_conn_pool(ioc, "./conf/orm.conf");
        if (!log.empty()) std::printf("[init] %s\n", log.c_str());

        auto pool = orm::get_orm_conn_pool_obj()[conf_tag];
        if (!pool) { std::printf("[FAIL] tag [%s] not found\n", conf_tag.c_str()); return 1; }

        // ========== Start IO thread pool (server.cpp line 6238 pattern) ==========
        // executor_work_guard keeps run() from returning until guard is reset or ioc.stop
        asio::executor_work_guard<asio::io_context::executor_type> work(ioc.get_executor());
        int io_threads = async_mode ? 4 : 1;
        std::vector<std::thread> iopool;
        for (int i = 0; i < io_threads; ++i) {
            iopool.emplace_back([&ioc]() {
                try { ioc.run(); } catch (...) {}
            });
        }

        // Verify DB
        {
            auto c = pool->get_mysql_select_conn();
            if (!c) { std::printf("[FAIL] no mysql conn\n"); return 1; }
            std::string ver;
            c->fetch_directly("SELECT VERSION()", [&ver](int, char**, auto gd){
                auto [p, n] = gd(0);
                if (p) ver.assign(reinterpret_cast<const char*>(p), n);
                return true;
            });
            std::printf("[server] %s\n[io_threads] %d\n\n", ver.c_str(), io_threads);
            pool->back_mysql_select_conn(std::move(c));
        }

        // Warmup
        {
            orm::cms::Sysuser w;
            auto n = w.select("name").eqName("admin").fetch_one();
            std::printf("[warmup] fetch_one admin => n=%u\n\n", n);
        }

        std::vector<BenchStat> stats;
        const char *name = async_mode ? "orm_async_fetch_one" : "orm_sync_fetch_one";

        for (int T : tcounts) {
            std::printf("[T=%d] ", T);
            auto s = run_bench(name, T, iters, async_mode, ioc);
            print_stat(s);
            stats.push_back(std::move(s));
            std::printf("\n");
        }

        long long terr = 0, tops = 0;
        for (auto &s : stats) { terr += s.total_errors; tops += s.total_ops; }
        std::printf("[TOTAL] errors=%lld / %lld ops\n", terr, tops);

        // ========== Stop IO thread pool: release work guard β†’ run() exits naturally ==========
        work.reset();
        ioc.stop();
        for (auto &t : iopool) t.join();

        orm::init_orm_conn_pool_release();
        return terr == 0 ? 0 : 1;
    }
    catch (std::exception &e) { std::printf("Exception: %s\n", e.what()); return 1; }
}

Back to main docs β†’