From f7ee8de7bc52b805bb9202caabc27120ea899f6e Mon Sep 17 00:00:00 2001 From: Jonas Claeson Date: Fri, 9 Jan 2026 13:12:02 +0100 Subject: [PATCH 1/4] Add JSON output and configurable percentiles for CI integration Features: - Add -o/--output option for JSON output (file or stdout with "-") - Add -p/--percentiles option for custom latency percentiles - JSON includes: requests, errors, bytes, duration, rps, latency stats Build improvements: - Add USE_SYSTEM_LUAJIT option to use system LuaJIT instead of bundled - Support pkg-config for LuaJIT and OpenSSL, with manual path fallback - Add flake.nix for nix build support (works on macOS ARM64) - Fix ARM64 compatibility (conditional x86intrin.h include) - Fix newer LuaJIT API compatibility (luaL_reg -> luaL_Reg) Example usage: wrk2 -t4 -c100 -d30s -R1000 -p 50,95,99 -o result.json http://localhost/ --- .gitignore | 4 + Makefile | 40 ++++++++-- flake.lock | 61 +++++++++++++++ flake.nix | 86 +++++++++++++++++++++ src/hdr_histogram.c | 2 + src/script.c | 5 ++ src/wrk.c | 177 ++++++++++++++++++++++++++++++++++++++++---- src/wrk.h | 2 +- 8 files changed, 357 insertions(+), 20 deletions(-) create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.gitignore b/.gitignore index e20e7c7b..95633c9e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,7 @@ deps/luajit/src/luajit .idea CMakeLists.txt + +# Nix +result +result-* diff --git a/Makefile b/Makefile index a537a686..35efc03d 100644 --- a/Makefile +++ b/Makefile @@ -11,8 +11,11 @@ else ifeq ($(TARGET), darwin) # is not set then it's forced to 10.4, which breaks compile on Mojave. export MACOSX_DEPLOYMENT_TARGET = $(shell sw_vers -productVersion) LDFLAGS += -pagezero_size 10000 -image_base 100000000 - LIBS += -L/usr/local/opt/openssl/lib - CFLAGS += -I/usr/local/include -I/usr/local/opt/openssl/include + # OpenSSL: try pkg-config, fall back to user-specified OPENSSL_INC/OPENSSL_LIB + OPENSSL_CFLAGS := $(or $(shell pkg-config --cflags openssl 2>/dev/null), -I$(OPENSSL_INC)) + OPENSSL_LIBS := $(or $(shell pkg-config --libs openssl 2>/dev/null), -L$(OPENSSL_LIB) -lcrypto -lssl) + CFLAGS += $(OPENSSL_CFLAGS) + LIBS += $(OPENSSL_LIBS) else ifeq ($(TARGET), linux) CFLAGS += -D_POSIX_C_SOURCE=200809L -D_BSD_SOURCE LIBS += -ldl @@ -29,37 +32,62 @@ BIN := wrk ODIR := obj OBJ := $(patsubst %.c,$(ODIR)/%.o,$(SRC)) $(ODIR)/bytecode.o -LDIR = deps/luajit/src -LIBS := -lluajit $(LIBS) -CFLAGS += -I$(LDIR) -LDFLAGS += -L$(LDIR) +# LuaJIT configuration +# Use system LuaJIT instead of bundled: make USE_SYSTEM_LUAJIT=1 +# With pkg-config (auto-detects paths): make USE_SYSTEM_LUAJIT=1 +# With manual paths: make USE_SYSTEM_LUAJIT=1 LUAJIT_INC=/path/to/include LUAJIT_LIB=/path/to/lib +ifdef USE_SYSTEM_LUAJIT + # Try pkg-config first, fall back to user-specified paths + LUAJIT_CFLAGS := $(or $(shell pkg-config --cflags luajit 2>/dev/null), -I$(LUAJIT_INC)) + LUAJIT_LIBS := $(or $(shell pkg-config --libs luajit 2>/dev/null), -L$(LUAJIT_LIB) -lluajit-5.1) + LUAJIT_BIN ?= luajit + CFLAGS += $(LUAJIT_CFLAGS) + LIBS := $(LUAJIT_LIBS) $(LIBS) +else + LDIR = deps/luajit/src + LIBS := -lluajit $(LIBS) + CFLAGS += -I$(LDIR) + LDFLAGS += -L$(LDIR) +endif all: $(BIN) clean: $(RM) $(BIN) obj/* +ifndef USE_SYSTEM_LUAJIT @$(MAKE) -C deps/luajit clean +endif $(BIN): $(OBJ) @echo LINK $(BIN) @$(CC) $(LDFLAGS) -o $@ $^ $(LIBS) +ifdef USE_SYSTEM_LUAJIT +$(OBJ): config.h Makefile | $(ODIR) +else $(OBJ): config.h Makefile $(LDIR)/libluajit.a | $(ODIR) +endif $(ODIR): @mkdir -p $@ $(ODIR)/bytecode.o: src/wrk.lua @echo LUAJIT $< +ifdef USE_SYSTEM_LUAJIT + @$(LUAJIT_BIN) -b $< $@ +else @$(SHELL) -c 'cd $(LDIR) && ./luajit -b $(CURDIR)/$< $(CURDIR)/$@' +endif $(ODIR)/%.o : %.c @echo CC $< @$(CC) $(CFLAGS) -c -o $@ $< +ifndef USE_SYSTEM_LUAJIT $(LDIR)/libluajit.a: @echo Building LuaJIT... @$(MAKE) -C $(LDIR) BUILDMODE=static +endif .PHONY: all clean .SUFFIXES: diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..e4a6ef93 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1767799921, + "narHash": "sha256-r4GVX+FToWVE2My8VVZH4V0pTIpnu2ZE8/Z4uxGEMBE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d351d0653aeb7877273920cd3e823994e7579b0b", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..58f244a4 --- /dev/null +++ b/flake.nix @@ -0,0 +1,86 @@ +{ + description = "wrk2 - a HTTP benchmarking tool based mostly on wrk"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + + # Extract version from src/wrk.h + version = builtins.head (builtins.match ''.*#define VERSION[[:space:]]+"([^"]+)".*'' (builtins.readFile ./src/wrk.h)); + + sources = [ + "wrk" "net" "ssl" "aprintf" "stats" "script" + "units" "ae" "zmalloc" "http_parser" "tinymt64" "hdr_histogram" + ]; + + compileSource = name: '' + echo "CC ${name}.c" + $CC -std=c99 -Wall -O2 -D_REENTRANT \ + -I ${pkgs.luajit}/include/luajit-2.1 \ + -I ${pkgs.openssl.dev}/include \ + -I ${pkgs.zlib.dev}/include \ + -I src \ + -c -o obj/${name}.o src/${name}.c + ''; + in + { + packages.default = pkgs.stdenv.mkDerivation { + pname = "wrk2"; + inherit version; + + src = ./.; + + nativeBuildInputs = [ pkgs.luajit ]; + + buildInputs = [ + pkgs.openssl + pkgs.luajit + pkgs.zlib + ]; + + buildPhase = '' + mkdir -p obj + + echo "LUAJIT src/wrk.lua" + ${pkgs.luajit}/bin/luajit -b src/wrk.lua obj/bytecode.o + + ${pkgs.lib.concatMapStringsSep "\n" compileSource sources} + + echo "LINK wrk" + $CC -o wrk obj/*.o \ + -L ${pkgs.luajit}/lib \ + -L ${pkgs.openssl.out}/lib \ + -L ${pkgs.zlib}/lib \ + -lluajit-5.1 -lpthread -lm -lcrypto -lssl -lz + ''; + + installPhase = '' + mkdir -p $out/bin + cp wrk $out/bin/wrk2 + ''; + + meta = with pkgs.lib; { + description = "wrk2 - a HTTP benchmarking tool based mostly on wrk"; + homepage = "https://github.com/giltene/wrk2"; + license = licenses.bsd3; + platforms = platforms.unix; + mainProgram = "wrk2"; + }; + }; + + devShells.default = pkgs.mkShell { + buildInputs = [ + pkgs.luajit + pkgs.openssl + pkgs.gnumake + ]; + }; + } + ); +} diff --git a/src/hdr_histogram.c b/src/hdr_histogram.c index 57c63242..cdaf05b2 100644 --- a/src/hdr_histogram.c +++ b/src/hdr_histogram.c @@ -7,7 +7,9 @@ #include #include #include +#if defined(__x86_64__) || defined(__i386__) #include +#endif #include #include #include diff --git a/src/script.c b/src/script.c index 79cd42cc..a75fe058 100644 --- a/src/script.c +++ b/src/script.c @@ -7,6 +7,11 @@ #include "stats.h" #include "zmalloc.h" +// Compatibility with newer LuaJIT that uses luaL_Reg instead of luaL_reg +#ifndef luaL_reg +#define luaL_reg luaL_Reg +#endif + typedef struct { char *name; int type; diff --git a/src/wrk.c b/src/wrk.c index 1049f0b2..1eb86da3 100644 --- a/src/wrk.c +++ b/src/wrk.c @@ -9,6 +9,8 @@ // Max recordable latency of 1 day #define MAX_LATENCY 24L * 60 * 60 * 1000000 +#define MAX_PERCENTILES 32 + static struct config { uint64_t threads; uint64_t connections; @@ -24,6 +26,9 @@ static struct config { char *host; char *script; SSL_CTX *ctx; + char *output_file; + double percentiles[MAX_PERCENTILES]; + int num_percentiles; } cfg; static struct { @@ -45,6 +50,14 @@ static struct http_parser_settings parser_settings = { static volatile sig_atomic_t stop = 0; +// Forward declarations for output functions +static void print_text_results(uint64_t, errors *, uint64_t, uint64_t, + struct hdr_histogram *, struct hdr_histogram *, + lua_State *); +static void print_json_latency(FILE *, const char *, struct hdr_histogram *, int); +static void print_json_results(const char *, uint64_t, errors *, uint64_t, uint64_t, + struct hdr_histogram *, struct hdr_histogram *); + static void handler(int sig) { stop = 1; } @@ -68,7 +81,12 @@ static void usage() { " -R, --rate work rate (throughput) \n" " in requests/sec (total) \n" " [Required Parameter] \n" - " \n" + " -p, --percentiles

Latency percentiles to \n" + " compute (comma-separated, \n" + " default: 50,75,90,99, \n" + " 99.9,99.99,99.999,100) \n" + " -o, --output Output results as JSON \n" + " (use - for stdout) \n" " \n" " Numeric arguments may include a SI unit (1k, 1M, 1G)\n" " Time arguments may include a time unit (2s, 2m, 2h)\n"); @@ -195,6 +213,23 @@ int main(int argc, char **argv) { hdr_add(u_latency_histogram, t->u_latency_histogram); } + if (cfg.output_file) { + print_json_results(cfg.output_file, complete, &errors, runtime_us, bytes, + latency_histogram, + cfg.u_latency ? u_latency_histogram : NULL); + } else { + print_text_results(complete, &errors, runtime_us, bytes, + latency_histogram, u_latency_histogram, L); + } + + return 0; +} + +static void print_text_results(uint64_t complete, errors *errors, + uint64_t runtime_us, uint64_t bytes, + struct hdr_histogram *latency_histogram, + struct hdr_histogram *u_latency_histogram, + lua_State *L) { long double runtime_s = runtime_us / 1000000.0; long double req_per_s = complete / runtime_s; long double bytes_per_s = bytes / runtime_s; @@ -226,13 +261,13 @@ int main(int argc, char **argv) { printf(" %"PRIu64" requests in %s, %sB read\n", complete, runtime_msg, format_binary(bytes)); - if (errors.connect || errors.read || errors.write || errors.timeout) { + if (errors->connect || errors->read || errors->write || errors->timeout) { printf(" Socket errors: connect %d, read %d, write %d, timeout %d\n", - errors.connect, errors.read, errors.write, errors.timeout); + errors->connect, errors->read, errors->write, errors->timeout); } - if (errors.status) { - printf(" Non-2xx or 3xx responses: %d\n", errors.status); + if (errors->status) { + printf(" Non-2xx or 3xx responses: %d\n", errors->status); } printf("Requests/sec: %9.2Lf\n", req_per_s); @@ -240,11 +275,9 @@ int main(int argc, char **argv) { if (script_has_done(L)) { script_summary(L, runtime_us, complete, bytes); - script_errors(L, &errors); + script_errors(L, errors); script_done(L, latency_stats, statistics.requests); } - - return 0; } void *thread_main(void *arg) { @@ -691,6 +724,33 @@ static char *copy_url_part(char *url, struct http_parser_url *parts, enum http_p return part; } +static int parse_percentiles(char *spec, double *percentiles, int *count) { + char *str = strdup(spec); + char *token; + int n = 0; + + token = strtok(str, ","); + while (token != NULL && n < MAX_PERCENTILES) { + char *endptr; + double val = strtod(token, &endptr); + if (*endptr != '\0' || val <= 0 || val > 100) { + fprintf(stderr, "invalid percentile: %s\n", token); + free(str); + return -1; + } + percentiles[n++] = val; + token = strtok(NULL, ","); + } + + free(str); + if (n == 0) { + fprintf(stderr, "no valid percentiles specified\n"); + return -1; + } + *count = n; + return 0; +} + static struct option longopts[] = { { "connections", required_argument, NULL, 'c' }, { "duration", required_argument, NULL, 'd' }, @@ -704,6 +764,8 @@ static struct option longopts[] = { { "help", no_argument, NULL, 'h' }, { "version", no_argument, NULL, 'v' }, { "rate", required_argument, NULL, 'R' }, + { "percentiles", required_argument, NULL, 'p' }, + { "output", required_argument, NULL, 'o' }, { NULL, 0, NULL, 0 } }; @@ -718,7 +780,7 @@ static int parse_args(struct config *cfg, char **url, struct http_parser_url *pa cfg->rate = 0; cfg->record_all_responses = true; - while ((c = getopt_long(argc, argv, "t:c:d:s:H:T:R:LUBrv?", longopts, NULL)) != -1) { + while ((c = getopt_long(argc, argv, "t:c:d:s:H:T:R:p:o:LUBrv?", longopts, NULL)) != -1) { switch (c) { case 't': if (scan_metric(optarg, &cfg->threads)) return -1; @@ -752,6 +814,12 @@ static int parse_args(struct config *cfg, char **url, struct http_parser_url *pa case 'R': if (scan_metric(optarg, &cfg->rate)) return -1; break; + case 'p': + if (parse_percentiles(optarg, cfg->percentiles, &cfg->num_percentiles)) return -1; + break; + case 'o': + cfg->output_file = optarg; + break; case 'v': printf("wrk %s [%s] ", VERSION, aeGetApiName()); printf("Copyright (C) 2012 Will Glozer\n"); @@ -785,6 +853,19 @@ static int parse_args(struct config *cfg, char **url, struct http_parser_url *pa *url = argv[optind]; *header = NULL; + // Set default percentiles if none specified + if (cfg->num_percentiles == 0) { + cfg->percentiles[0] = 50.0; + cfg->percentiles[1] = 75.0; + cfg->percentiles[2] = 90.0; + cfg->percentiles[3] = 99.0; + cfg->percentiles[4] = 99.9; + cfg->percentiles[5] = 99.99; + cfg->percentiles[6] = 99.999; + cfg->percentiles[7] = 100.0; + cfg->num_percentiles = 8; + } + return 0; } @@ -818,12 +899,11 @@ static void print_stats(char *name, stats *stats, char *(*fmt)(long double)) { } static void print_hdr_latency(struct hdr_histogram* histogram, const char* description) { - long double percentiles[] = { 50.0, 75.0, 90.0, 99.0, 99.9, 99.99, 99.999, 100.0}; printf(" Latency Distribution (HdrHistogram - %s)\n", description); - for (size_t i = 0; i < sizeof(percentiles) / sizeof(long double); i++) { - long double p = percentiles[i]; + for (int i = 0; i < cfg.num_percentiles; i++) { + double p = cfg.percentiles[i]; int64_t n = hdr_value_at_percentile(histogram, p); - printf("%7.3Lf%%", p); + printf("%7.3f%%", p); print_units(n, format_time_us, 10); printf("\n"); } @@ -842,3 +922,74 @@ static void print_stats_latency(stats *stats) { printf("\n"); } } + +static void print_json_results(const char *filename, uint64_t complete, errors *err, + uint64_t runtime_us, uint64_t bytes, + struct hdr_histogram *latency_histogram, + struct hdr_histogram *u_latency_histogram) { + FILE *out; + if (strcmp(filename, "-") == 0) { + out = stdout; + } else { + out = fopen(filename, "w"); + if (!out) { + fprintf(stderr, "unable to open output file: %s\n", filename); + return; + } + } + + long double runtime_s = runtime_us / 1000000.0; + long double req_per_s = complete / runtime_s; + uint32_t total_errors = err->connect + err->read + err->write + err->status + err->timeout; + + fprintf(out, "{\n"); + fprintf(out, " \"requests\": %"PRIu64",\n", complete); + fprintf(out, " \"errors\": {\n"); + fprintf(out, " \"connect\": %"PRIu32",\n", err->connect); + fprintf(out, " \"read\": %"PRIu32",\n", err->read); + fprintf(out, " \"write\": %"PRIu32",\n", err->write); + fprintf(out, " \"status\": %"PRIu32",\n", err->status); + fprintf(out, " \"timeout\": %"PRIu32",\n", err->timeout); + fprintf(out, " \"total\": %"PRIu32"\n", total_errors); + fprintf(out, " },\n"); + fprintf(out, " \"bytes\": %"PRIu64",\n", bytes); + fprintf(out, " \"duration_seconds\": %.3Lf,\n", runtime_s); + fprintf(out, " \"rps\": %.2Lf,\n", req_per_s); + + print_json_latency(out, "latency_us", latency_histogram, 2); + + if (u_latency_histogram) { + fprintf(out, ",\n"); + print_json_latency(out, "uncorrected_latency_us", u_latency_histogram, 2); + } + + fprintf(out, "\n}\n"); + + if (out != stdout) fclose(out); +} + +static void print_json_latency(FILE *out, const char *name, struct hdr_histogram *h, int indent) { + fprintf(out, "%*s\"%s\": {\n", indent, "", name); + fprintf(out, "%*s\"min\": %"PRId64",\n", indent + 2, "", hdr_min(h)); + fprintf(out, "%*s\"max\": %"PRId64",\n", indent + 2, "", hdr_max(h)); + fprintf(out, "%*s\"mean\": %.2f,\n", indent + 2, "", hdr_mean(h)); + fprintf(out, "%*s\"stdev\": %.2f", indent + 2, "", hdr_stddev(h)); + + for (int i = 0; i < cfg.num_percentiles; i++) { + double p = cfg.percentiles[i]; + int64_t val = hdr_value_at_percentile(h, p); + fprintf(out, ",\n"); + // Format percentile key: p50, p75, p99, p99_9, p99_99, etc. + if (p == (int)p) { + fprintf(out, "%*s\"p%d\": %"PRId64, indent + 2, "", (int)p, val); + } else if (p * 10 == (int)(p * 10)) { + fprintf(out, "%*s\"p%.1f\": %"PRId64, indent + 2, "", p, val); + } else if (p * 100 == (int)(p * 100)) { + fprintf(out, "%*s\"p%.2f\": %"PRId64, indent + 2, "", p, val); + } else { + fprintf(out, "%*s\"p%.3f\": %"PRId64, indent + 2, "", p, val); + } + } + fprintf(out, "\n%*s}", indent, ""); +} + diff --git a/src/wrk.h b/src/wrk.h index 202a4c34..e6421d4e 100644 --- a/src/wrk.h +++ b/src/wrk.h @@ -17,7 +17,7 @@ #include "http_parser.h" #include "hdr_histogram.h" -#define VERSION "4.0.0" +#define VERSION "4.1.0" #define RECVBUF 8192 #define SAMPLES 100000000 From 669c57cc32212f35d7ccafe5cabdbb7367d3e5c2 Mon Sep 17 00:00:00 2001 From: Jonas Claeson Date: Fri, 9 Jan 2026 15:26:25 +0100 Subject: [PATCH 2/4] Remove unnecessary x86intrin.h include The only function used from this header was __builtin_clzll, which is a compiler builtin available on all architectures (ARM, x86, etc.), not an x86 intrinsic. The include was added when hdr_histogram.c was first vendored in 2014 but was never actually needed. --- src/hdr_histogram.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/hdr_histogram.c b/src/hdr_histogram.c index cdaf05b2..df6a6526 100644 --- a/src/hdr_histogram.c +++ b/src/hdr_histogram.c @@ -7,9 +7,6 @@ #include #include #include -#if defined(__x86_64__) || defined(__i386__) -#include -#endif #include #include #include From 14af8594d0d99799511301089c6f80cbb7a841d9 Mon Sep 17 00:00:00 2001 From: Jonas Claeson Date: Fri, 9 Jan 2026 16:03:58 +0100 Subject: [PATCH 3/4] Send status messages to stderr when JSON output goes to stdout When using -o - to output JSON to stdout, redirect the "Running test" and "Thread calibration" messages to stderr so that stdout contains only valid JSON. This allows piping directly to jq and other tools. --- src/wrk.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/wrk.c b/src/wrk.c index 1eb86da3..e0eda952 100644 --- a/src/wrk.c +++ b/src/wrk.c @@ -177,8 +177,9 @@ int main(int argc, char **argv) { sigaction(SIGINT, &sa, NULL); char *time = format_time_s(cfg.duration); - printf("Running %s test @ %s\n", time, url); - printf(" %"PRIu64" threads and %"PRIu64" connections\n", + FILE *log = (cfg.output_file && strcmp(cfg.output_file, "-") == 0) ? stderr : stdout; + fprintf(log, "Running %s test @ %s\n", time, url); + fprintf(log, " %"PRIu64" threads and %"PRIu64" connections\n", cfg.threads, cfg.connections); uint64_t start = time_us(); @@ -392,7 +393,8 @@ static int calibrate(aeEventLoop *loop, long long id, void *data) { thread->interval = interval; thread->requests = 0; - printf(" Thread calibration: mean lat.: %.3fms, rate sampling interval: %dms\n", + FILE *log = (cfg.output_file && strcmp(cfg.output_file, "-") == 0) ? stderr : stdout; + fprintf(log, " Thread calibration: mean lat.: %.3fms, rate sampling interval: %dms\n", (thread->mean)/1000.0, thread->interval); From 9fb6201fd9e0db6c8a98a2a30a4fc70839a048f4 Mon Sep 17 00:00:00 2001 From: Jonas Claeson Date: Fri, 9 Jan 2026 16:12:51 +0100 Subject: [PATCH 4/4] Enhance JSON output with config, URL, and full precision - Add url field to identify tested endpoint - Add config section with threads, connections, duration, rate - Add bytes_per_second for transfer rate - Use full precision (%g) for float values instead of truncated %.2f - Return error code 1 when output file cannot be opened --- src/wrk.c | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/src/wrk.c b/src/wrk.c index e0eda952..4606e3b9 100644 --- a/src/wrk.c +++ b/src/wrk.c @@ -55,8 +55,9 @@ static void print_text_results(uint64_t, errors *, uint64_t, uint64_t, struct hdr_histogram *, struct hdr_histogram *, lua_State *); static void print_json_latency(FILE *, const char *, struct hdr_histogram *, int); -static void print_json_results(const char *, uint64_t, errors *, uint64_t, uint64_t, - struct hdr_histogram *, struct hdr_histogram *); +static int print_json_results(const char *, const char *, uint64_t, errors *, + uint64_t, uint64_t, struct hdr_histogram *, + struct hdr_histogram *); static void handler(int sig) { stop = 1; @@ -215,9 +216,10 @@ int main(int argc, char **argv) { } if (cfg.output_file) { - print_json_results(cfg.output_file, complete, &errors, runtime_us, bytes, - latency_histogram, - cfg.u_latency ? u_latency_histogram : NULL); + int err = print_json_results(cfg.output_file, url, complete, &errors, + runtime_us, bytes, latency_histogram, + cfg.u_latency ? u_latency_histogram : NULL); + if (err) return err; } else { print_text_results(complete, &errors, runtime_us, bytes, latency_histogram, u_latency_histogram, L); @@ -925,10 +927,11 @@ static void print_stats_latency(stats *stats) { } } -static void print_json_results(const char *filename, uint64_t complete, errors *err, - uint64_t runtime_us, uint64_t bytes, - struct hdr_histogram *latency_histogram, - struct hdr_histogram *u_latency_histogram) { +static int print_json_results(const char *filename, const char *url, + uint64_t complete, errors *err, + uint64_t runtime_us, uint64_t bytes, + struct hdr_histogram *latency_histogram, + struct hdr_histogram *u_latency_histogram) { FILE *out; if (strcmp(filename, "-") == 0) { out = stdout; @@ -936,15 +939,23 @@ static void print_json_results(const char *filename, uint64_t complete, errors * out = fopen(filename, "w"); if (!out) { fprintf(stderr, "unable to open output file: %s\n", filename); - return; + return 1; } } long double runtime_s = runtime_us / 1000000.0; long double req_per_s = complete / runtime_s; + long double bytes_per_s = bytes / runtime_s; uint32_t total_errors = err->connect + err->read + err->write + err->status + err->timeout; fprintf(out, "{\n"); + fprintf(out, " \"url\": \"%s\",\n", url); + fprintf(out, " \"config\": {\n"); + fprintf(out, " \"threads\": %"PRIu64",\n", cfg.threads); + fprintf(out, " \"connections\": %"PRIu64",\n", cfg.connections); + fprintf(out, " \"duration_s\": %"PRIu64",\n", cfg.duration); + fprintf(out, " \"rate\": %"PRIu64"\n", cfg.rate); + fprintf(out, " },\n"); fprintf(out, " \"requests\": %"PRIu64",\n", complete); fprintf(out, " \"errors\": {\n"); fprintf(out, " \"connect\": %"PRIu32",\n", err->connect); @@ -955,8 +966,9 @@ static void print_json_results(const char *filename, uint64_t complete, errors * fprintf(out, " \"total\": %"PRIu32"\n", total_errors); fprintf(out, " },\n"); fprintf(out, " \"bytes\": %"PRIu64",\n", bytes); - fprintf(out, " \"duration_seconds\": %.3Lf,\n", runtime_s); - fprintf(out, " \"rps\": %.2Lf,\n", req_per_s); + fprintf(out, " \"duration_seconds\": %.6Lf,\n", runtime_s); + fprintf(out, " \"rps\": %.6Lf,\n", req_per_s); + fprintf(out, " \"bytes_per_second\": %.6Lf,\n", bytes_per_s); print_json_latency(out, "latency_us", latency_histogram, 2); @@ -968,14 +980,15 @@ static void print_json_results(const char *filename, uint64_t complete, errors * fprintf(out, "\n}\n"); if (out != stdout) fclose(out); + return 0; } static void print_json_latency(FILE *out, const char *name, struct hdr_histogram *h, int indent) { fprintf(out, "%*s\"%s\": {\n", indent, "", name); fprintf(out, "%*s\"min\": %"PRId64",\n", indent + 2, "", hdr_min(h)); fprintf(out, "%*s\"max\": %"PRId64",\n", indent + 2, "", hdr_max(h)); - fprintf(out, "%*s\"mean\": %.2f,\n", indent + 2, "", hdr_mean(h)); - fprintf(out, "%*s\"stdev\": %.2f", indent + 2, "", hdr_stddev(h)); + fprintf(out, "%*s\"mean\": %g,\n", indent + 2, "", hdr_mean(h)); + fprintf(out, "%*s\"stdev\": %g", indent + 2, "", hdr_stddev(h)); for (int i = 0; i < cfg.num_percentiles; i++) { double p = cfg.percentiles[i];