diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml new file mode 100644 index 00000000..384b7fc1 --- /dev/null +++ b/.github/workflows/c-cpp.yml @@ -0,0 +1,17 @@ +name: C/C++ CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: make + run: make -j4 diff --git a/.gitignore b/.gitignore index e25de364..b0a9b64a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ obj/ wrk +*.sw[pon] diff --git a/Makefile b/Makefile index 395b98a7..debbcab2 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ else ifeq ($(TARGET), freebsd) endif SRC := wrk.c net.c ssl.c aprintf.c stats.c script.c units.c \ - ae.c zmalloc.c http_parser.c + ae.c zmalloc.c http_parser.c lua_cjson.c strbuf.c BIN := wrk VER ?= $(shell git describe --tags --always --dirty) @@ -30,11 +30,13 @@ DEPS := CFLAGS += -I$(ODIR)/include LDFLAGS += -L$(ODIR)/lib +LUAJIT_VER ?= luajit-2.1 + ifneq ($(WITH_LUAJIT),) - CFLAGS += -I$(WITH_LUAJIT)/include + CFLAGS += -I$(WITH_LUAJIT)/include/$(LUAJIT_VER) LDFLAGS += -L$(WITH_LUAJIT)/lib else - CFLAGS += -I$(ODIR)/include/luajit-2.1 + CFLAGS += -I$(ODIR)/include/$(LUAJIT_VER) DEPS += $(ODIR)/lib/libluajit-5.1.a endif @@ -87,7 +89,7 @@ $(ODIR)/$(OPENSSL): deps/$(OPENSSL).tar.gz | $(ODIR) $(ODIR)/lib/libluajit-5.1.a: $(ODIR)/$(LUAJIT) @echo Building LuaJIT... @$(MAKE) -C $< PREFIX=$(abspath $(ODIR)) BUILDMODE=static install - @cd $(ODIR)/bin && ln -s luajit-2.1.0-beta3 luajit + -@cd $(ODIR)/bin && ln -s luajit-2.1.0-beta3 luajit $(ODIR)/lib/libssl.a: $(ODIR)/$(OPENSSL) @echo Building OpenSSL... diff --git a/README.md b/README.md index ac61e0d9..7333020b 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ --latency: print detailed latency statistics + -j --json-format: print output as JSON format + --timeout: record a timeout if a response is not received within this amount of time. diff --git a/scripts/post_json.lua b/scripts/post_json.lua new file mode 100644 index 00000000..2c48f486 --- /dev/null +++ b/scripts/post_json.lua @@ -0,0 +1,9 @@ +-- example HTTP POST script which demonstrates setting the +-- HTTP method, body, and adding a header + +local cjson = require "cjson" +local json_encode = cjson.encode +local json_decode = cjson.decode +wrk.method = "POST" +wrk.body = json_encode({["key"] = "value"}) +wrk.headers["Content-Type"] = "application/x-www-form-urlencoded" diff --git a/src/ae.c b/src/ae.c index e66808a8..d6af6c00 100644 --- a/src/ae.c +++ b/src/ae.c @@ -75,6 +75,8 @@ aeEventLoop *aeCreateEventLoop(int setsize) { eventLoop->stop = 0; eventLoop->maxfd = -1; eventLoop->beforesleep = NULL; + eventLoop->checkThreadStop = NULL; + eventLoop->checkThreadStopData = NULL; if (aeApiCreate(eventLoop) == -1) goto err; /* Events with mask == AE_NONE are not set. So let's initialize the * vector with it. */ @@ -416,6 +418,13 @@ int aeProcessEvents(aeEventLoop *eventLoop, int flags) fe->wfileProc(eventLoop,fd,fe->clientData,mask); } processed++; + + if (eventLoop->checkThreadStop != NULL) { + if (eventLoop->checkThreadStop(eventLoop) == 1) { + eventLoop->stop = 1; + break; + } + } } } /* Check time events */ @@ -463,3 +472,9 @@ char *aeGetApiName(void) { void aeSetBeforeSleepProc(aeEventLoop *eventLoop, aeBeforeSleepProc *beforesleep) { eventLoop->beforesleep = beforesleep; } + +void aeSetCheckThreadStopProc(aeEventLoop *eventLoop, + aeCheckThreadStopProc *checkThreadStop, void *checkData) { + eventLoop->checkThreadStop = checkThreadStop; + eventLoop->checkThreadStopData = checkData; +} diff --git a/src/ae.h b/src/ae.h index 827c4c9e..f45882d5 100644 --- a/src/ae.h +++ b/src/ae.h @@ -44,29 +44,30 @@ #define AE_FILE_EVENTS 1 #define AE_TIME_EVENTS 2 -#define AE_ALL_EVENTS (AE_FILE_EVENTS|AE_TIME_EVENTS) +#define AE_ALL_EVENTS (AE_FILE_EVENTS | AE_TIME_EVENTS) #define AE_DONT_WAIT 4 #define AE_NOMORE -1 #define AE_DELETED_EVENT_ID -1 /* Macros */ -#define AE_NOTUSED(V) ((void) V) +#define AE_NOTUSED(V) ((void)V) struct aeEventLoop; /* Types and data structures */ -typedef void aeFileProc(struct aeEventLoop *eventLoop, int fd, void *clientData, int mask); -typedef int aeTimeProc(struct aeEventLoop *eventLoop, long long id, void *clientData); -typedef void aeEventFinalizerProc(struct aeEventLoop *eventLoop, void *clientData); -typedef void aeBeforeSleepProc(struct aeEventLoop *eventLoop); +typedef void aeFileProc(struct aeEventLoop* eventLoop, int fd, void* clientData, int mask); +typedef int aeTimeProc(struct aeEventLoop* eventLoop, long long id, void* clientData); +typedef void aeEventFinalizerProc(struct aeEventLoop* eventLoop, void* clientData); +typedef void aeBeforeSleepProc(struct aeEventLoop* eventLoop); +typedef int aeCheckThreadStopProc(struct aeEventLoop* eventLoop); /* File event structure */ typedef struct aeFileEvent { int mask; /* one of AE_(READABLE|WRITABLE) */ - aeFileProc *rfileProc; - aeFileProc *wfileProc; - void *clientData; + aeFileProc* rfileProc; + aeFileProc* wfileProc; + void* clientData; } aeFileEvent; /* Time event structure */ @@ -74,10 +75,10 @@ typedef struct aeTimeEvent { long long id; /* time event identifier. */ long when_sec; /* seconds */ long when_ms; /* milliseconds */ - aeTimeProc *timeProc; - aeEventFinalizerProc *finalizerProc; - void *clientData; - struct aeTimeEvent *next; + aeTimeProc* timeProc; + aeEventFinalizerProc* finalizerProc; + void* clientData; + struct aeTimeEvent* next; } aeTimeEvent; /* A fired event */ @@ -88,36 +89,38 @@ typedef struct aeFiredEvent { /* State of an event based program */ typedef struct aeEventLoop { - int maxfd; /* highest file descriptor currently registered */ + int maxfd; /* highest file descriptor currently registered */ int setsize; /* max number of file descriptors tracked */ long long timeEventNextId; - time_t lastTime; /* Used to detect system clock skew */ - aeFileEvent *events; /* Registered events */ - aeFiredEvent *fired; /* Fired events */ - aeTimeEvent *timeEventHead; + time_t lastTime; /* Used to detect system clock skew */ + aeFileEvent* events; /* Registered events */ + aeFiredEvent* fired; /* Fired events */ + aeTimeEvent* timeEventHead; int stop; - void *apidata; /* This is used for polling API specific data */ - aeBeforeSleepProc *beforesleep; + void* apidata; /* This is used for polling API specific data */ + aeBeforeSleepProc* beforesleep; + aeCheckThreadStopProc* checkThreadStop; + void* checkThreadStopData; } aeEventLoop; /* Prototypes */ -aeEventLoop *aeCreateEventLoop(int setsize); -void aeDeleteEventLoop(aeEventLoop *eventLoop); -void aeStop(aeEventLoop *eventLoop); -int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask, - aeFileProc *proc, void *clientData); -void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask); -int aeGetFileEvents(aeEventLoop *eventLoop, int fd); -long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds, - aeTimeProc *proc, void *clientData, - aeEventFinalizerProc *finalizerProc); -int aeDeleteTimeEvent(aeEventLoop *eventLoop, long long id); -int aeProcessEvents(aeEventLoop *eventLoop, int flags); +aeEventLoop* aeCreateEventLoop(int setsize); +void aeDeleteEventLoop(aeEventLoop* eventLoop); +void aeStop(aeEventLoop* eventLoop); +int aeCreateFileEvent(aeEventLoop* eventLoop, int fd, int mask, + aeFileProc* proc, void* clientData); +void aeDeleteFileEvent(aeEventLoop* eventLoop, int fd, int mask); +int aeGetFileEvents(aeEventLoop* eventLoop, int fd); +long long aeCreateTimeEvent(aeEventLoop* eventLoop, long long milliseconds, + aeTimeProc* proc, void* clientData, + aeEventFinalizerProc* finalizerProc); +int aeDeleteTimeEvent(aeEventLoop* eventLoop, long long id); +int aeProcessEvents(aeEventLoop* eventLoop, int flags); int aeWait(int fd, int mask, long long milliseconds); -void aeMain(aeEventLoop *eventLoop); -char *aeGetApiName(void); -void aeSetBeforeSleepProc(aeEventLoop *eventLoop, aeBeforeSleepProc *beforesleep); -int aeGetSetSize(aeEventLoop *eventLoop); -int aeResizeSetSize(aeEventLoop *eventLoop, int setsize); +void aeMain(aeEventLoop* eventLoop); +char* aeGetApiName(void); +void aeSetBeforeSleepProc(aeEventLoop* eventLoop, aeBeforeSleepProc* beforesleep); +void aeSetCheckThreadStopProc(aeEventLoop* eventLoop, + aeCheckThreadStopProc* checkThreadStop, void* checkData); #endif diff --git a/src/lua_cjson.c b/src/lua_cjson.c new file mode 100644 index 00000000..2e272b00 --- /dev/null +++ b/src/lua_cjson.c @@ -0,0 +1,1299 @@ +#define VERSION "1.0.3" + +/* CJSON - JSON support for Lua + * + * Copyright (c) 2010-2011 Mark Pulford + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/* Caveats: + * - JSON "null" values are represented as lightuserdata since Lua + * tables cannot contain "nil". Compare with cjson.null. + * - Invalid UTF-8 characters are not detected and will be passed + * untouched. If required, UTF-8 error checking should be done + * outside this library. + * - Javascript comments are not part of the JSON spec, and are not + * currently supported. + * + * Note: Decoding is slower than encoding. Lua spends significant + * time (30%) managing tables when parsing JSON since it is + * difficult to know object/array sizes ahead of time. + */ + +#include +#include +#include +#include "lua.h" +#include "lauxlib.h" + +#include "strbuf.h" + +#ifdef MISSING_ISINF +#define isinf(x) (!isnan(x) && isnan((x) - (x))) +#endif + +#define DEFAULT_SPARSE_CONVERT 0 +#define DEFAULT_SPARSE_RATIO 2 +#define DEFAULT_SPARSE_SAFE 10 +#define DEFAULT_MAX_DEPTH 20 +#define DEFAULT_ENCODE_REFUSE_BADNUM 1 +#define DEFAULT_DECODE_REFUSE_BADNUM 0 +#define DEFAULT_ENCODE_KEEP_BUFFER 1 + +typedef enum { + T_OBJ_BEGIN, + T_OBJ_END, + T_ARR_BEGIN, + T_ARR_END, + T_STRING, + T_NUMBER, + T_BOOLEAN, + T_NULL, + T_COLON, + T_COMMA, + T_END, + T_WHITESPACE, + T_ERROR, + T_UNKNOWN +} json_token_type_t; + +static const char *json_token_type_name[] = { + "T_OBJ_BEGIN", + "T_OBJ_END", + "T_ARR_BEGIN", + "T_ARR_END", + "T_STRING", + "T_NUMBER", + "T_BOOLEAN", + "T_NULL", + "T_COLON", + "T_COMMA", + "T_END", + "T_WHITESPACE", + "T_ERROR", + "T_UNKNOWN", + NULL +}; + +typedef struct { + json_token_type_t ch2token[256]; + char escape2char[256]; /* Decoding */ +#if 0 + char escapes[35][8]; /* Pre-generated escape string buffer */ + char *char2escape[256]; /* Encoding */ +#endif + strbuf_t encode_buf; + char number_fmt[8]; /* "%.XXg\0" */ + int current_depth; + + int encode_sparse_convert; + int encode_sparse_ratio; + int encode_sparse_safe; + int encode_max_depth; + int encode_refuse_badnum; + int decode_refuse_badnum; + int encode_keep_buffer; + int encode_number_precision; +} json_config_t; + +typedef struct { + const char *data; + int index; + strbuf_t *tmp; /* Temporary storage for strings */ + json_config_t *cfg; +} json_parse_t; + +typedef struct { + json_token_type_t type; + int index; + union { + const char *string; + double number; + int boolean; + } value; + int string_len; +} json_token_t; + +static const char *char2escape[256] = { + "\\u0000", "\\u0001", "\\u0002", "\\u0003", + "\\u0004", "\\u0005", "\\u0006", "\\u0007", + "\\b", "\\t", "\\n", "\\u000b", + "\\f", "\\r", "\\u000e", "\\u000f", + "\\u0010", "\\u0011", "\\u0012", "\\u0013", + "\\u0014", "\\u0015", "\\u0016", "\\u0017", + "\\u0018", "\\u0019", "\\u001a", "\\u001b", + "\\u001c", "\\u001d", "\\u001e", "\\u001f", + NULL, NULL, "\\\"", NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, "\\/", + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, "\\\\", NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, "\\u007f", + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, +}; + +static int json_config_key; + +/* ===== CONFIGURATION ===== */ + +static json_config_t *json_fetch_config(lua_State *l) +{ + json_config_t *cfg; + + lua_pushlightuserdata(l, &json_config_key); + lua_gettable(l, LUA_REGISTRYINDEX); + cfg = lua_touserdata(l, -1); + if (!cfg) + luaL_error(l, "BUG: Unable to fetch CJSON configuration"); + + lua_pop(l, 1); + + return cfg; +} + +static void json_verify_arg_count(lua_State *l, int args) +{ + luaL_argcheck(l, lua_gettop(l) <= args, args + 1, + "found too many arguments"); +} + +/* Configures handling of extremely sparse arrays: + * convert: Convert extremely sparse arrays into objects? Otherwise error. + * ratio: 0: always allow sparse; 1: never allow sparse; >1: use ratio + * safe: Always use an array when the max index <= safe */ +static int json_cfg_encode_sparse_array(lua_State *l) +{ + json_config_t *cfg; + int val; + + json_verify_arg_count(l, 3); + cfg = json_fetch_config(l); + + switch (lua_gettop(l)) { + case 3: + val = luaL_checkinteger(l, 3); + luaL_argcheck(l, val >= 0, 3, "expected integer >= 0"); + cfg->encode_sparse_safe = val; + case 2: + val = luaL_checkinteger(l, 2); + luaL_argcheck(l, val >= 0, 2, "expected integer >= 0"); + cfg->encode_sparse_ratio = val; + case 1: + luaL_argcheck(l, lua_isboolean(l, 1), 1, "expected boolean"); + cfg->encode_sparse_convert = lua_toboolean(l, 1); + } + + lua_pushboolean(l, cfg->encode_sparse_convert); + lua_pushinteger(l, cfg->encode_sparse_ratio); + lua_pushinteger(l, cfg->encode_sparse_safe); + + return 3; +} + +/* Configures the maximum number of nested arrays/objects allowed when + * encoding */ +static int json_cfg_encode_max_depth(lua_State *l) +{ + json_config_t *cfg; + int depth; + + json_verify_arg_count(l, 1); + cfg = json_fetch_config(l); + + if (lua_gettop(l)) { + depth = luaL_checkinteger(l, 1); + luaL_argcheck(l, depth > 0, 1, "expected positive integer"); + cfg->encode_max_depth = depth; + } + + lua_pushinteger(l, cfg->encode_max_depth); + + return 1; +} + +static void json_set_number_precision(json_config_t *cfg, int prec) +{ + cfg->encode_number_precision = prec; + sprintf(cfg->number_fmt, "%%.%dg", prec); +} + +/* Configures number precision when converting doubles to text */ +static int json_cfg_encode_number_precision(lua_State *l) +{ + json_config_t *cfg; + int precision; + + json_verify_arg_count(l, 1); + cfg = json_fetch_config(l); + + if (lua_gettop(l)) { + precision = luaL_checkinteger(l, 1); + luaL_argcheck(l, 1 <= precision && precision <= 14, 1, + "expected integer between 1 and 14"); + json_set_number_precision(cfg, precision); + } + + lua_pushinteger(l, cfg->encode_number_precision); + + return 1; +} + +/* Configures JSON encoding buffer persistence */ +static int json_cfg_encode_keep_buffer(lua_State *l) +{ + json_config_t *cfg; + + json_verify_arg_count(l, 1); + cfg = json_fetch_config(l); + + if (lua_gettop(l)) { + luaL_checktype(l, 1, LUA_TBOOLEAN); + cfg->encode_keep_buffer = lua_toboolean(l, 1); + } + + lua_pushboolean(l, cfg->encode_keep_buffer); + + return 1; +} + +/* On argument: decode enum and set config variables + * **options must point to a NULL terminated array of 4 enums + * Returns: current enum value */ +static void json_enum_option(lua_State *l, const char **options, + int *opt1, int *opt2) +{ + int setting; + + if (lua_gettop(l)) { + if (lua_isboolean(l, 1)) + setting = lua_toboolean(l, 1) * 3; + else + setting = luaL_checkoption(l, 1, NULL, options); + + *opt1 = setting & 1 ? 1 : 0; + *opt2 = setting & 2 ? 1 : 0; + } else { + setting = *opt1 | (*opt2 << 1); + } + + if (setting) + lua_pushstring(l, options[setting]); + else + lua_pushboolean(l, 0); +} + + +/* When enabled, rejects: NaN, Infinity, hexidecimal numbers */ +static int json_cfg_refuse_invalid_numbers(lua_State *l) +{ + static const char *options_enc_dec[] = { "none", "encode", "decode", + "both", NULL }; + json_config_t *cfg; + + json_verify_arg_count(l, 1); + cfg = json_fetch_config(l); + + json_enum_option(l, options_enc_dec, + &cfg->encode_refuse_badnum, + &cfg->decode_refuse_badnum); + + return 1; +} + +static int json_destroy_config(lua_State *l) +{ + json_config_t *cfg; + + cfg = lua_touserdata(l, 1); + if (cfg) + strbuf_free(&cfg->encode_buf); + cfg = NULL; + + return 0; +} + +static void json_create_config(lua_State *l) +{ + json_config_t *cfg; + int i; + + cfg = lua_newuserdata(l, sizeof(*cfg)); + + /* Create GC method to clean up strbuf */ + lua_newtable(l); + lua_pushcfunction(l, json_destroy_config); + lua_setfield(l, -2, "__gc"); + lua_setmetatable(l, -2); + + strbuf_init(&cfg->encode_buf, 0); + + cfg->encode_sparse_convert = DEFAULT_SPARSE_CONVERT; + cfg->encode_sparse_ratio = DEFAULT_SPARSE_RATIO; + cfg->encode_sparse_safe = DEFAULT_SPARSE_SAFE; + cfg->encode_max_depth = DEFAULT_MAX_DEPTH; + cfg->encode_refuse_badnum = DEFAULT_ENCODE_REFUSE_BADNUM; + cfg->decode_refuse_badnum = DEFAULT_DECODE_REFUSE_BADNUM; + cfg->encode_keep_buffer = DEFAULT_ENCODE_KEEP_BUFFER; + json_set_number_precision(cfg, 14); + + /* Decoding init */ + + /* Tag all characters as an error */ + for (i = 0; i < 256; i++) + cfg->ch2token[i] = T_ERROR; + + /* Set tokens that require no further processing */ + cfg->ch2token['{'] = T_OBJ_BEGIN; + cfg->ch2token['}'] = T_OBJ_END; + cfg->ch2token['['] = T_ARR_BEGIN; + cfg->ch2token[']'] = T_ARR_END; + cfg->ch2token[','] = T_COMMA; + cfg->ch2token[':'] = T_COLON; + cfg->ch2token['\0'] = T_END; + cfg->ch2token[' '] = T_WHITESPACE; + cfg->ch2token['\t'] = T_WHITESPACE; + cfg->ch2token['\n'] = T_WHITESPACE; + cfg->ch2token['\r'] = T_WHITESPACE; + + /* Update characters that require further processing */ + cfg->ch2token['f'] = T_UNKNOWN; /* false? */ + cfg->ch2token['i'] = T_UNKNOWN; /* inf, ininity? */ + cfg->ch2token['I'] = T_UNKNOWN; + cfg->ch2token['n'] = T_UNKNOWN; /* null, nan? */ + cfg->ch2token['N'] = T_UNKNOWN; + cfg->ch2token['t'] = T_UNKNOWN; /* true? */ + cfg->ch2token['"'] = T_UNKNOWN; /* string? */ + cfg->ch2token['+'] = T_UNKNOWN; /* number? */ + cfg->ch2token['-'] = T_UNKNOWN; + for (i = 0; i < 10; i++) + cfg->ch2token['0' + i] = T_UNKNOWN; + + /* Lookup table for parsing escape characters */ + for (i = 0; i < 256; i++) + cfg->escape2char[i] = 0; /* String error */ + cfg->escape2char['"'] = '"'; + cfg->escape2char['\\'] = '\\'; + cfg->escape2char['/'] = '/'; + cfg->escape2char['b'] = '\b'; + cfg->escape2char['t'] = '\t'; + cfg->escape2char['n'] = '\n'; + cfg->escape2char['f'] = '\f'; + cfg->escape2char['r'] = '\r'; + cfg->escape2char['u'] = 'u'; /* Unicode parsing required */ + + +#if 0 + /* Initialise separate storage for pre-generated escape codes. + * Escapes 0-31 map directly, 34, 92, 127 follow afterwards to + * save memory. */ + for (i = 0 ; i < 32; i++) + sprintf(cfg->escapes[i], "\\u%04x", i); + strcpy(cfg->escapes[8], "\b"); /* Override simpler escapes */ + strcpy(cfg->escapes[9], "\t"); + strcpy(cfg->escapes[10], "\n"); + strcpy(cfg->escapes[12], "\f"); + strcpy(cfg->escapes[13], "\r"); + strcpy(cfg->escapes[32], "\\\""); /* chr(34) */ + strcpy(cfg->escapes[33], "\\\\"); /* chr(92) */ + sprintf(cfg->escapes[34], "\\u%04x", 127); /* char(127) */ + + /* Initialise encoding escape lookup table */ + for (i = 0; i < 32; i++) + cfg->char2escape[i] = cfg->escapes[i]; + for (i = 32; i < 256; i++) + cfg->char2escape[i] = NULL; + cfg->char2escape[34] = cfg->escapes[32]; + cfg->char2escape[92] = cfg->escapes[33]; + cfg->char2escape[127] = cfg->escapes[34]; +#endif +} + +/* ===== ENCODING ===== */ + +static void json_encode_exception(lua_State *l, json_config_t *cfg, int lindex, + const char *reason) +{ + if (!cfg->encode_keep_buffer) + strbuf_free(&cfg->encode_buf); + luaL_error(l, "Cannot serialise %s: %s", + lua_typename(l, lua_type(l, lindex)), reason); +} + +/* json_append_string args: + * - lua_State + * - JSON strbuf + * - String (Lua stack index) + * + * Returns nothing. Doesn't remove string from Lua stack */ +static void json_append_string(lua_State *l, strbuf_t *json, int lindex) +{ + const char *escstr; + int i; + const char *str; + size_t len; + + str = lua_tolstring(l, lindex, &len); + + /* Worst case is len * 6 (all unicode escapes). + * This buffer is reused constantly for small strings + * If there are any excess pages, they won't be hit anyway. + * This gains ~5% speedup. */ + strbuf_ensure_empty_length(json, len * 6 + 2); + + strbuf_append_char_unsafe(json, '\"'); + for (i = 0; i < len; i++) { + escstr = char2escape[(unsigned char)str[i]]; + if (escstr) + strbuf_append_string(json, escstr); + else + strbuf_append_char_unsafe(json, str[i]); + } + strbuf_append_char_unsafe(json, '\"'); +} + +/* Find the size of the array on the top of the Lua stack + * -1 object (not a pure array) + * >=0 elements in array + */ +static int lua_array_length(lua_State *l, json_config_t *cfg) +{ + double k; + int max; + int items; + + max = 0; + items = 0; + + lua_pushnil(l); + /* table, startkey */ + while (lua_next(l, -2) != 0) { + /* table, key, value */ + if (lua_type(l, -2) == LUA_TNUMBER && + (k = lua_tonumber(l, -2))) { + /* Integer >= 1 ? */ + if (floor(k) == k && k >= 1) { + if (k > max) + max = k; + items++; + lua_pop(l, 1); + continue; + } + } + + /* Must not be an array (non integer key) */ + lua_pop(l, 2); + return -1; + } + + /* Encode excessively sparse arrays as objects (if enabled) */ + if (cfg->encode_sparse_ratio > 0 && + max > items * cfg->encode_sparse_ratio && + max > cfg->encode_sparse_safe) { + if (!cfg->encode_sparse_convert) + json_encode_exception(l, cfg, -1, "excessively sparse array"); + + return -1; + } + + return max; +} + +static void json_encode_descend(lua_State *l, json_config_t *cfg) +{ + cfg->current_depth++; + + if (cfg->current_depth > cfg->encode_max_depth) { + if (!cfg->encode_keep_buffer) + strbuf_free(&cfg->encode_buf); + luaL_error(l, "Cannot serialise, excessive nesting (%d)", + cfg->current_depth); + } +} + +static void json_append_data(lua_State *l, json_config_t *cfg, strbuf_t *json); + +/* json_append_array args: + * - lua_State + * - JSON strbuf + * - Size of passwd Lua array (top of stack) */ +static void json_append_array(lua_State *l, json_config_t *cfg, strbuf_t *json, + int array_length) +{ + int comma, i; + + json_encode_descend(l, cfg); + + strbuf_append_char(json, '['); + + comma = 0; + for (i = 1; i <= array_length; i++) { + if (comma) + strbuf_append_char(json, ','); + else + comma = 1; + + lua_rawgeti(l, -1, i); + json_append_data(l, cfg, json); + lua_pop(l, 1); + } + + strbuf_append_char(json, ']'); + + cfg->current_depth--; +} + +static void json_append_number(lua_State *l, strbuf_t *json, int index, + json_config_t *cfg) +{ + double num = lua_tonumber(l, index); + + if (cfg->encode_refuse_badnum && (isinf(num) || isnan(num))) + json_encode_exception(l, cfg, index, "must not be NaN or Inf"); + + /* Lowest double printed with %.14g is 21 characters long: + * -1.7976931348623e+308 + * + * Use 32 to include the \0, and a few extra just in case.. + */ + strbuf_append_fmt(json, 32, cfg->number_fmt, num); +} + +static void json_append_object(lua_State *l, json_config_t *cfg, + strbuf_t *json) +{ + int comma, keytype; + + json_encode_descend(l, cfg); + + /* Object */ + strbuf_append_char(json, '{'); + + lua_pushnil(l); + /* table, startkey */ + comma = 0; + while (lua_next(l, -2) != 0) { + if (comma) + strbuf_append_char(json, ','); + else + comma = 1; + + /* table, key, value */ + keytype = lua_type(l, -2); + if (keytype == LUA_TNUMBER) { + strbuf_append_char(json, '"'); + json_append_number(l, json, -2, cfg); + strbuf_append_mem(json, "\":", 2); + } else if (keytype == LUA_TSTRING) { + json_append_string(l, json, -2); + strbuf_append_char(json, ':'); + } else { + json_encode_exception(l, cfg, -2, + "table key must be a number or string"); + /* never returns */ + } + + /* table, key, value */ + json_append_data(l, cfg, json); + lua_pop(l, 1); + /* table, key */ + } + + strbuf_append_char(json, '}'); + + cfg->current_depth--; +} + +/* Serialise Lua data into JSON string. */ +static void json_append_data(lua_State *l, json_config_t *cfg, strbuf_t *json) +{ + int len; + + switch (lua_type(l, -1)) { + case LUA_TSTRING: + json_append_string(l, json, -1); + break; + case LUA_TNUMBER: + json_append_number(l, json, -1, cfg); + break; + case LUA_TBOOLEAN: + if (lua_toboolean(l, -1)) + strbuf_append_mem(json, "true", 4); + else + strbuf_append_mem(json, "false", 5); + break; + case LUA_TTABLE: + len = lua_array_length(l, cfg); + if (len > 0) + json_append_array(l, cfg, json, len); + else + json_append_object(l, cfg, json); + break; + case LUA_TNIL: + strbuf_append_mem(json, "null", 4); + break; + case LUA_TLIGHTUSERDATA: + if (lua_touserdata(l, -1) == NULL) { + strbuf_append_mem(json, "null", 4); + break; + } + default: + /* Remaining types (LUA_TFUNCTION, LUA_TUSERDATA, LUA_TTHREAD, + * and LUA_TLIGHTUSERDATA) cannot be serialised */ + json_encode_exception(l, cfg, -1, "type not supported"); + /* never returns */ + } +} + +static int json_encode(lua_State *l) +{ + json_config_t *cfg; + char *json; + int len; + + /* Can't use json_verify_arg_count() since we need to ensure + * there is only 1 argument */ + luaL_argcheck(l, lua_gettop(l) == 1, 1, "expected 1 argument"); + + cfg = json_fetch_config(l); + cfg->current_depth = 0; + + /* Reset the persistent buffer if it exists. + * Otherwise allocate a new buffer. */ + if (strbuf_allocated(&cfg->encode_buf)) + strbuf_reset(&cfg->encode_buf); + else + strbuf_init(&cfg->encode_buf, 0); + + json_append_data(l, cfg, &cfg->encode_buf); + json = strbuf_string(&cfg->encode_buf, &len); + + lua_pushlstring(l, json, len); + + if (!cfg->encode_keep_buffer) + strbuf_free(&cfg->encode_buf); + + return 1; +} + +/* ===== DECODING ===== */ + +static void json_process_value(lua_State *l, json_parse_t *json, + json_token_t *token); + +static int hexdigit2int(char hex) +{ + if ('0' <= hex && hex <= '9') + return hex - '0'; + + /* Force lowercase */ + hex |= 0x20; + if ('a' <= hex && hex <= 'f') + return 10 + hex - 'a'; + + return -1; +} + +static int decode_hex4(const char *hex) +{ + int digit[4]; + int i; + + /* Convert ASCII hex digit to numeric digit + * Note: this returns an error for invalid hex digits, including + * NULL */ + for (i = 0; i < 4; i++) { + digit[i] = hexdigit2int(hex[i]); + if (digit[i] < 0) { + return -1; + } + } + + return (digit[0] << 12) + + (digit[1] << 8) + + (digit[2] << 4) + + digit[3]; +} + +/* Converts a Unicode codepoint to UTF-8. + * Returns UTF-8 string length, and up to 4 bytes in *utf8 */ +static int codepoint_to_utf8(char *utf8, int codepoint) +{ + /* 0xxxxxxx */ + if (codepoint <= 0x7F) { + utf8[0] = codepoint; + return 1; + } + + /* 110xxxxx 10xxxxxx */ + if (codepoint <= 0x7FF) { + utf8[0] = (codepoint >> 6) | 0xC0; + utf8[1] = (codepoint & 0x3F) | 0x80; + return 2; + } + + /* 1110xxxx 10xxxxxx 10xxxxxx */ + if (codepoint <= 0xFFFF) { + utf8[0] = (codepoint >> 12) | 0xE0; + utf8[1] = ((codepoint >> 6) & 0x3F) | 0x80; + utf8[2] = (codepoint & 0x3F) | 0x80; + return 3; + } + + /* 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx */ + if (codepoint <= 0x1FFFFF) { + utf8[0] = (codepoint >> 18) | 0xF0; + utf8[1] = ((codepoint >> 12) & 0x3F) | 0x80; + utf8[2] = ((codepoint >> 6) & 0x3F) | 0x80; + utf8[3] = (codepoint & 0x3F) | 0x80; + return 4; + } + + return 0; +} + + +/* Called when index pointing to beginning of UTF-16 code escape: \uXXXX + * \u is guaranteed to exist, but the remaining hex characters may be + * missing. + * Translate to UTF-8 and append to temporary token string. + * Must advance index to the next character to be processed. + * Returns: 0 success + * -1 error + */ +static int json_append_unicode_escape(json_parse_t *json) +{ + char utf8[4]; /* Surrogate pairs require 4 UTF-8 bytes */ + int codepoint; + int surrogate_low; + int len; + int escape_len = 6; + + /* Fetch UTF-16 code unit */ + codepoint = decode_hex4(&json->data[json->index + 2]); + if (codepoint < 0) + return -1; + + /* UTF-16 surrogate pairs take the following 2 byte form: + * 11011 x yyyyyyyyyy + * When x = 0: y is the high 10 bits of the codepoint + * x = 1: y is the low 10 bits of the codepoint + * + * Check for a surrogate pair (high or low) */ + if ((codepoint & 0xF800) == 0xD800) { + /* Error if the 1st surrogate is not high */ + if (codepoint & 0x400) + return -1; + + /* Ensure the next code is a unicode escape */ + if (json->data[json->index + escape_len] != '\\' || + json->data[json->index + escape_len + 1] != 'u') { + return -1; + } + + /* Fetch the next codepoint */ + surrogate_low = decode_hex4(&json->data[json->index + 2 + escape_len]); + if (surrogate_low < 0) + return -1; + + /* Error if the 2nd code is not a low surrogate */ + if ((surrogate_low & 0xFC00) != 0xDC00) + return -1; + + /* Calculate Unicode codepoint */ + codepoint = (codepoint & 0x3FF) << 10; + surrogate_low &= 0x3FF; + codepoint = (codepoint | surrogate_low) + 0x10000; + escape_len = 12; + } + + /* Convert codepoint to UTF-8 */ + len = codepoint_to_utf8(utf8, codepoint); + if (!len) + return -1; + + /* Append bytes and advance parse index */ + strbuf_append_mem_unsafe(json->tmp, utf8, len); + json->index += escape_len; + + return 0; +} + +static void json_set_token_error(json_token_t *token, json_parse_t *json, + const char *errtype) +{ + token->type = T_ERROR; + token->index = json->index; + token->value.string = errtype; +} + +static void json_next_string_token(json_parse_t *json, json_token_t *token) +{ + char *escape2char = json->cfg->escape2char; + char ch; + + /* Caller must ensure a string is next */ + assert(json->data[json->index] == '"'); + + /* Skip " */ + json->index++; + + /* json->tmp is the temporary strbuf used to accumulate the + * decoded string value. */ + strbuf_reset(json->tmp); + while ((ch = json->data[json->index]) != '"') { + if (!ch) { + /* Premature end of the string */ + json_set_token_error(token, json, "unexpected end of string"); + return; + } + + /* Handle escapes */ + if (ch == '\\') { + /* Fetch escape character */ + ch = json->data[json->index + 1]; + + /* Translate escape code and append to tmp string */ + ch = escape2char[(unsigned char)ch]; + if (ch == 'u') { + if (json_append_unicode_escape(json) == 0) + continue; + + json_set_token_error(token, json, + "invalid unicode escape code"); + return; + } + if (!ch) { + json_set_token_error(token, json, "invalid escape code"); + return; + } + + /* Skip '\' */ + json->index++; + } + /* Append normal character or translated single character + * Unicode escapes are handled above */ + strbuf_append_char_unsafe(json->tmp, ch); + json->index++; + } + json->index++; /* Eat final quote (") */ + + strbuf_ensure_null(json->tmp); + + token->type = T_STRING; + token->value.string = strbuf_string(json->tmp, &token->string_len); +} + +/* JSON numbers should take the following form: + * -?(0|[1-9]|[1-9][0-9]+)(.[0-9]+)?([eE][-+]?[0-9]+)? + * + * json_next_number_token() uses strtod() which allows other forms: + * - numbers starting with '+' + * - NaN, -NaN, infinity, -infinity + * - hexidecimal numbers + * - numbers with leading zeros + * + * json_is_invalid_number() detects "numbers" which may pass strtod()'s + * error checking, but should not be allowed with strict JSON. + * + * json_is_invalid_number() may pass numbers which cause strtod() + * to generate an error. + */ +static int json_is_invalid_number(json_parse_t *json) +{ + int i = json->index; + + /* Reject numbers starting with + */ + if (json->data[i] == '+') + return 1; + + /* Skip minus sign if it exists */ + if (json->data[i] == '-') + i++; + + /* Reject numbers starting with 0x, or leading zeros */ + if (json->data[i] == '0') { + int ch2 = json->data[i + 1]; + + if ((ch2 | 0x20) == 'x' || /* Hex */ + ('0' <= ch2 && ch2 <= '9')) /* Leading zero */ + return 1; + + return 0; + } else if (json->data[i] <= '9') { + return 0; /* Ordinary number */ + } + + + /* Reject inf/nan */ + if (!strncasecmp(&json->data[i], "inf", 3)) + return 1; + if (!strncasecmp(&json->data[i], "nan", 3)) + return 1; + + /* Pass all other numbers which may still be invalid, but + * strtod() will catch them. */ + return 0; +} + +static void json_next_number_token(json_parse_t *json, json_token_t *token) +{ + const char *startptr; + char *endptr; + + token->type = T_NUMBER; + startptr = &json->data[json->index]; + token->value.number = strtod(&json->data[json->index], &endptr); + if (startptr == endptr) + json_set_token_error(token, json, "invalid number"); + else + json->index += endptr - startptr; /* Skip the processed number */ + + return; +} + +/* Fills in the token struct. + * T_STRING will return a pointer to the json_parse_t temporary string + * T_ERROR will leave the json->index pointer at the error. + */ +static void json_next_token(json_parse_t *json, json_token_t *token) +{ + json_token_type_t *ch2token = json->cfg->ch2token; + int ch; + + /* Eat whitespace. FIXME: UGLY */ + token->type = ch2token[(unsigned char)json->data[json->index]]; + while (token->type == T_WHITESPACE) + token->type = ch2token[(unsigned char)json->data[++json->index]]; + + token->index = json->index; + + /* Don't advance the pointer for an error or the end */ + if (token->type == T_ERROR) { + json_set_token_error(token, json, "invalid token"); + return; + } + + if (token->type == T_END) { + return; + } + + /* Found a known single character token, advance index and return */ + if (token->type != T_UNKNOWN) { + json->index++; + return; + } + + /* Process characters which triggered T_UNKNOWN */ + ch = json->data[json->index]; + + /* Must use strncmp() to match the front of the JSON string. + * JSON identifier must be lowercase. + * When strict_numbers if disabled, either case is allowed for + * Infinity/NaN (since we are no longer following the spec..) */ + if (ch == '"') { + json_next_string_token(json, token); + return; + } else if (ch == '-' || ('0' <= ch && ch <= '9')) { + if (json->cfg->decode_refuse_badnum && json_is_invalid_number(json)) { + json_set_token_error(token, json, "invalid number"); + return; + } + json_next_number_token(json, token); + return; + } else if (!strncmp(&json->data[json->index], "true", 4)) { + token->type = T_BOOLEAN; + token->value.boolean = 1; + json->index += 4; + return; + } else if (!strncmp(&json->data[json->index], "false", 5)) { + token->type = T_BOOLEAN; + token->value.boolean = 0; + json->index += 5; + return; + } else if (!strncmp(&json->data[json->index], "null", 4)) { + token->type = T_NULL; + json->index += 4; + return; + } else if (!json->cfg->decode_refuse_badnum && + json_is_invalid_number(json)) { + /* When refuse_badnum is disabled, only attempt to process + * numbers we know are invalid JSON (Inf, NaN, hex) + * This is required to generate an appropriate token error, + * otherwise all bad tokens will register as "invalid number" + */ + json_next_number_token(json, token); + return; + } + + /* Token starts with t/f/n but isn't recognised above. */ + json_set_token_error(token, json, "invalid token"); +} + +/* This function does not return. + * DO NOT CALL WITH DYNAMIC MEMORY ALLOCATED. + * The only supported exception is the temporary parser string + * json->tmp struct. + * json and token should exist on the stack somewhere. + * luaL_error() will long_jmp and release the stack */ +static void json_throw_parse_error(lua_State *l, json_parse_t *json, + const char *exp, json_token_t *token) +{ + const char *found; + + strbuf_free(json->tmp); + + if (token->type == T_ERROR) + found = token->value.string; + else + found = json_token_type_name[token->type]; + + /* Note: token->index is 0 based, display starting from 1 */ + luaL_error(l, "Expected %s but found %s at character %d", + exp, found, token->index + 1); +} + +static void json_decode_checkstack(lua_State *l, json_parse_t *json, int n) +{ + if (lua_checkstack(l, n)) + return; + + strbuf_free(json->tmp); + luaL_error(l, "Too many nested data structures"); +} + +static void json_parse_object_context(lua_State *l, json_parse_t *json) +{ + json_token_t token; + + /* 3 slots required: + * .., table, key, value */ + json_decode_checkstack(l, json, 3); + + lua_newtable(l); + + json_next_token(json, &token); + + /* Handle empty objects */ + if (token.type == T_OBJ_END) { + return; + } + + while (1) { + if (token.type != T_STRING) + json_throw_parse_error(l, json, "object key string", &token); + + /* Push key */ + lua_pushlstring(l, token.value.string, token.string_len); + + json_next_token(json, &token); + if (token.type != T_COLON) + json_throw_parse_error(l, json, "colon", &token); + + /* Fetch value */ + json_next_token(json, &token); + json_process_value(l, json, &token); + + /* Set key = value */ + lua_rawset(l, -3); + + json_next_token(json, &token); + + if (token.type == T_OBJ_END) + return; + + if (token.type != T_COMMA) + json_throw_parse_error(l, json, "comma or object end", &token); + + json_next_token(json, &token); + } +} + +/* Handle the array context */ +static void json_parse_array_context(lua_State *l, json_parse_t *json) +{ + json_token_t token; + int i; + + /* 2 slots required: + * .., table, value */ + json_decode_checkstack(l, json, 2); + + lua_newtable(l); + + json_next_token(json, &token); + + /* Handle empty arrays */ + if (token.type == T_ARR_END) + return; + + for (i = 1; ; i++) { + json_process_value(l, json, &token); + lua_rawseti(l, -2, i); /* arr[i] = value */ + + json_next_token(json, &token); + + if (token.type == T_ARR_END) + return; + + if (token.type != T_COMMA) + json_throw_parse_error(l, json, "comma or array end", &token); + + json_next_token(json, &token); + } +} + +/* Handle the "value" context */ +static void json_process_value(lua_State *l, json_parse_t *json, + json_token_t *token) +{ + switch (token->type) { + case T_STRING: + lua_pushlstring(l, token->value.string, token->string_len); + break;; + case T_NUMBER: + lua_pushnumber(l, token->value.number); + break;; + case T_BOOLEAN: + lua_pushboolean(l, token->value.boolean); + break;; + case T_OBJ_BEGIN: + json_parse_object_context(l, json); + break;; + case T_ARR_BEGIN: + json_parse_array_context(l, json); + break;; + case T_NULL: + /* In Lua, setting "t[k] = nil" will delete k from the table. + * Hence a NULL pointer lightuserdata object is used instead */ + lua_pushlightuserdata(l, NULL); + break;; + default: + json_throw_parse_error(l, json, "value", token); + } +} + +/* json_text must be null terminated string */ +static void lua_json_decode(lua_State *l, const char *json_text, int json_len) +{ + json_parse_t json; + json_token_t token; + + json.cfg = json_fetch_config(l); + json.data = json_text; + json.index = 0; + + /* Ensure the temporary buffer can hold the entire string. + * This means we no longer need to do length checks since the decoded + * string must be smaller than the entire json string */ + json.tmp = strbuf_new(json_len); + + json_next_token(&json, &token); + json_process_value(l, &json, &token); + + /* Ensure there is no more input left */ + json_next_token(&json, &token); + + if (token.type != T_END) + json_throw_parse_error(l, &json, "the end", &token); + + strbuf_free(json.tmp); +} + +static int json_decode(lua_State *l) +{ + const char *json; + size_t len; + + json_verify_arg_count(l, 1); + + json = luaL_checklstring(l, 1, &len); + + /* Detect Unicode other than UTF-8 (see RFC 4627, Sec 3) + * + * CJSON can support any simple data type, hence only the first + * character is guaranteed to be ASCII (at worst: '"'). This is + * still enough to detect whether the wrong encoding is in use. */ + if (len >= 2 && (!json[0] || !json[1])) + luaL_error(l, "JSON parser does not support UTF-16 or UTF-32"); + + lua_json_decode(l, json, len); + + return 1; +} + +/* ===== INITIALISATION ===== */ + +int luaopen_cjson(lua_State *l) +{ + luaL_Reg reg[] = { + { "encode", json_encode }, + { "decode", json_decode }, + { "encode_sparse_array", json_cfg_encode_sparse_array }, + { "encode_max_depth", json_cfg_encode_max_depth }, + { "encode_number_precision", json_cfg_encode_number_precision }, + { "encode_keep_buffer", json_cfg_encode_keep_buffer }, + { "refuse_invalid_numbers", json_cfg_refuse_invalid_numbers }, + { NULL, NULL } + }; + + /* Use json_fetch_config as a pointer. + * It's faster than using a config string, and more unique */ + lua_pushlightuserdata(l, &json_config_key); + json_create_config(l); + lua_settable(l, LUA_REGISTRYINDEX); + + luaL_register(l, "cjson", reg); + + /* Set cjson.null */ + lua_pushlightuserdata(l, NULL); + lua_setfield(l, -2, "null"); + + /* Set cjson.version */ + lua_pushliteral(l, VERSION); + lua_setfield(l, -2, "version"); + + /* Return cjson table */ + return 1; +} + +/* vi:ai et sw=4 ts=4: + */ diff --git a/src/script.c b/src/script.c index 68a5d081..c03ffb37 100644 --- a/src/script.c +++ b/src/script.c @@ -45,9 +45,25 @@ static const struct luaL_Reg threadlib[] = { { NULL, NULL } }; +/* + * load lua lib and call it + */ +void lua_load_lib(lua_State* L, const char* libname, lua_CFunction luafunc) +{ + lua_pushcfunction(L, luafunc); + lua_pushstring(L, libname); + lua_call(L, 1, 0); +} + +int(luaopen_cjson)(lua_State* L); + lua_State *script_create(char *file, char *url, char **headers) { lua_State *L = luaL_newstate(); luaL_openlibs(L); + + // cjson lib + lua_load_lib(L, "cjson", luaopen_cjson); + (void) luaL_dostring(L, "wrk = require \"wrk\""); luaL_newmetatable(L, "wrk.addr"); @@ -90,7 +106,7 @@ lua_State *script_create(char *file, char *url, char **headers) { } lua_pop(L, 5); - if (file && luaL_dofile(L, file)) { + if (file && luaL_dofile(L, strcmp("-", file) ? file : NULL)) { const char *cause = lua_tostring(L, -1); fprintf(stderr, "%s: %s\n", file, cause); } @@ -485,8 +501,11 @@ void script_copy_value(lua_State *src, lua_State *dst, int index) { case LUA_TNUMBER: lua_pushnumber(dst, lua_tonumber(src, index)); break; - case LUA_TSTRING: - lua_pushstring(dst, lua_tostring(src, index)); + case LUA_TSTRING: { + size_t len = 0; + const char *str = lua_tolstring(src, index, &len); + lua_pushlstring(dst, str, len); + } break; case LUA_TTABLE: lua_newtable(dst); diff --git a/src/strbuf.c b/src/strbuf.c new file mode 100644 index 00000000..976925a8 --- /dev/null +++ b/src/strbuf.c @@ -0,0 +1,251 @@ +/* strbuf - string buffer routines + * + * Copyright (c) 2010-2011 Mark Pulford + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include + +#include "strbuf.h" + +void die(const char *fmt, ...) +{ + va_list arg; + + va_start(arg, fmt); + vfprintf(stderr, fmt, arg); + va_end(arg); + fprintf(stderr, "\n"); + + exit(-1); +} + +void strbuf_init(strbuf_t *s, int len) +{ + int size; + + if (len <= 0) + size = STRBUF_DEFAULT_SIZE; + else + size = len + 1; /* \0 terminator */ + + s->buf = NULL; + s->size = size; + s->length = 0; + s->increment = STRBUF_DEFAULT_INCREMENT; + s->dynamic = 0; + s->reallocs = 0; + s->debug = 0; + + s->buf = malloc(size); + if (!s->buf) + die("Out of memory"); + + strbuf_ensure_null(s); +} + +strbuf_t *strbuf_new(int len) +{ + strbuf_t *s; + + s = malloc(sizeof(strbuf_t)); + if (!s) + die("Out of memory"); + + strbuf_init(s, len); + + /* Dynamic strbuf allocation / deallocation */ + s->dynamic = 1; + + return s; +} + +void strbuf_set_increment(strbuf_t *s, int increment) +{ + /* Increment > 0: Linear buffer growth rate + * Increment < -1: Exponential buffer growth rate */ + if (increment == 0 || increment == -1) + die("BUG: Invalid string increment"); + + s->increment = increment; +} + +static inline void debug_stats(strbuf_t *s) +{ + if (s->debug) { + fprintf(stderr, "strbuf(%lx) reallocs: %d, length: %d, size: %d\n", + (long)s, s->reallocs, s->length, s->size); + } +} + +/* If strbuf_t has not been dynamically allocated, strbuf_free() can + * be called any number of times strbuf_init() */ +void strbuf_free(strbuf_t *s) +{ + debug_stats(s); + + if (s->buf) { + free(s->buf); + s->buf = NULL; + } + if (s->dynamic) + free(s); +} + +char *strbuf_free_to_string(strbuf_t *s, int *len) +{ + char *buf; + + debug_stats(s); + + strbuf_ensure_null(s); + + buf = s->buf; + if (len) + *len = s->length; + + if (s->dynamic) + free(s); + + return buf; +} + +static int calculate_new_size(strbuf_t *s, int len) +{ + int reqsize, newsize; + + if (len <= 0) + die("BUG: Invalid strbuf length requested"); + + /* Ensure there is room for optional NULL termination */ + reqsize = len + 1; + + /* If the user has requested to shrink the buffer, do it exactly */ + if (s->size > reqsize) + return reqsize; + + newsize = s->size; + if (s->increment < 0) { + /* Exponential sizing */ + while (newsize < reqsize) + newsize *= -s->increment; + } else { + /* Linear sizing */ + newsize = ((newsize + s->increment - 1) / s->increment) * s->increment; + } + + return newsize; +} + + +/* Ensure strbuf can handle a string length bytes long (ignoring NULL + * optional termination). */ +void strbuf_resize(strbuf_t *s, int len) +{ + int newsize; + + newsize = calculate_new_size(s, len); + + if (s->debug > 1) { + fprintf(stderr, "strbuf(%lx) resize: %d => %d\n", + (long)s, s->size, newsize); + } + + s->size = newsize; + s->buf = realloc(s->buf, s->size); + if (!s->buf) + die("Out of memory"); + s->reallocs++; +} + +void strbuf_append_string(strbuf_t *s, const char *str) +{ + int space, i; + + space = strbuf_empty_length(s); + + for (i = 0; str[i]; i++) { + if (space < 1) { + strbuf_resize(s, s->length + 1); + space = strbuf_empty_length(s); + } + + s->buf[s->length] = str[i]; + s->length++; + space--; + } +} + +/* strbuf_append_fmt() should only be used when an upper bound + * is known for the output string. */ +void strbuf_append_fmt(strbuf_t *s, int len, const char *fmt, ...) +{ + va_list arg; + int fmt_len; + + strbuf_ensure_empty_length(s, len); + + va_start(arg, fmt); + fmt_len = vsnprintf(s->buf + s->length, len, fmt, arg); + va_end(arg); + + if (fmt_len < 0) + die("BUG: Unable to convert number"); /* This should never happen.. */ + + s->length += fmt_len; +} + +/* strbuf_append_fmt_retry() can be used when the there is no known + * upper bound for the output string. */ +void strbuf_append_fmt_retry(strbuf_t *s, const char *fmt, ...) +{ + va_list arg; + int fmt_len, try; + int empty_len; + + /* If the first attempt to append fails, resize the buffer appropriately + * and try again */ + for (try = 0; ; try++) { + va_start(arg, fmt); + /* Append the new formatted string */ + /* fmt_len is the length of the string required, excluding the + * trailing NULL */ + empty_len = strbuf_empty_length(s); + /* Add 1 since there is also space to store the terminating NULL. */ + fmt_len = vsnprintf(s->buf + s->length, empty_len + 1, fmt, arg); + va_end(arg); + + if (fmt_len <= empty_len) + break; /* SUCCESS */ + if (try > 0) + die("BUG: length of formatted string changed"); + + strbuf_resize(s, s->length + fmt_len); + } + + s->length += fmt_len; +} + +/* vi:ai et sw=4 ts=4: + */ diff --git a/src/strbuf.h b/src/strbuf.h new file mode 100644 index 00000000..f856543a --- /dev/null +++ b/src/strbuf.h @@ -0,0 +1,142 @@ +/* strbuf - String buffer routines + * + * Copyright (c) 2010-2011 Mark Pulford + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include +#include + +/* Size: Total bytes allocated to *buf + * Length: String length, excluding optional NULL terminator. + * Increment: Allocation increments when resizing the string buffer. + * Dynamic: True if created via strbuf_new() + */ + +typedef struct { + char *buf; + int size; + int length; + int increment; + int dynamic; + int reallocs; + int debug; +} strbuf_t; + +#ifndef STRBUF_DEFAULT_SIZE +#define STRBUF_DEFAULT_SIZE 1023 +#endif +#ifndef STRBUF_DEFAULT_INCREMENT +#define STRBUF_DEFAULT_INCREMENT -2 +#endif + +/* Initialise */ +extern strbuf_t *strbuf_new(int len); +extern void strbuf_init(strbuf_t *s, int len); +extern void strbuf_set_increment(strbuf_t *s, int increment); + +/* Release */ +extern void strbuf_free(strbuf_t *s); +extern char *strbuf_free_to_string(strbuf_t *s, int *len); + +/* Management */ +extern void strbuf_resize(strbuf_t *s, int len); +static int strbuf_empty_length(strbuf_t *s); +static int strbuf_length(strbuf_t *s); +static char *strbuf_string(strbuf_t *s, int *len); +static void strbuf_ensure_empty_length(strbuf_t *s, int len); + +/* Update */ +extern void strbuf_append_fmt(strbuf_t *s, int len, const char *fmt, ...); +extern void strbuf_append_fmt_retry(strbuf_t *s, const char *format, ...); +static void strbuf_append_mem(strbuf_t *s, const char *c, int len); +extern void strbuf_append_string(strbuf_t *s, const char *str); +static void strbuf_append_char(strbuf_t *s, const char c); +static void strbuf_ensure_null(strbuf_t *s); + +/* Reset string for before use */ +static inline void strbuf_reset(strbuf_t *s) +{ + s->length = 0; +} + +static inline int strbuf_allocated(strbuf_t *s) +{ + return s->buf != NULL; +} + +/* Return bytes remaining in the string buffer + * Ensure there is space for a NULL terminator. */ +static inline int strbuf_empty_length(strbuf_t *s) +{ + return s->size - s->length - 1; +} + +static inline void strbuf_ensure_empty_length(strbuf_t *s, int len) +{ + if (len > strbuf_empty_length(s)) + strbuf_resize(s, s->length + len); +} + +static inline int strbuf_length(strbuf_t *s) +{ + return s->length; +} + +static inline void strbuf_append_char(strbuf_t *s, const char c) +{ + strbuf_ensure_empty_length(s, 1); + s->buf[s->length++] = c; +} + +static inline void strbuf_append_char_unsafe(strbuf_t *s, const char c) +{ + s->buf[s->length++] = c; +} + +static inline void strbuf_append_mem(strbuf_t *s, const char *c, int len) +{ + strbuf_ensure_empty_length(s, len); + memcpy(s->buf + s->length, c, len); + s->length += len; +} + +static inline void strbuf_append_mem_unsafe(strbuf_t *s, const char *c, int len) +{ + memcpy(s->buf + s->length, c, len); + s->length += len; +} + +static inline void strbuf_ensure_null(strbuf_t *s) +{ + s->buf[s->length] = 0; +} + +static inline char *strbuf_string(strbuf_t *s, int *len) +{ + if (len) + *len = s->length; + + return s->buf; +} + +/* vi:ai et sw=4 ts=4: + */ diff --git a/src/wrk.c b/src/wrk.c index 51f46f72..fdf98c0b 100644 --- a/src/wrk.c +++ b/src/wrk.c @@ -1,33 +1,35 @@ // Copyright (C) 2012 - Will Glozer. All rights reserved. #include "wrk.h" -#include "script.h" #include "main.h" +#include "script.h" static struct config { uint64_t connections; uint64_t duration; uint64_t threads; + uint64_t requests; uint64_t timeout; uint64_t pipeline; - bool delay; - bool dynamic; - bool latency; - char *host; - char *script; - SSL_CTX *ctx; + bool delay; + bool dynamic; + bool latency; + bool json_format; + char* host; + char* script; + SSL_CTX* ctx; } cfg; static struct { - stats *latency; - stats *requests; + stats* latency; + stats* requests; } statistics; static struct sock sock = { - .connect = sock_connect, - .close = sock_close, - .read = sock_read, - .write = sock_write, + .connect = sock_connect, + .close = sock_close, + .read = sock_read, + .write = sock_write, .readable = sock_readable }; @@ -37,20 +39,32 @@ static struct http_parser_settings parser_settings = { static volatile sig_atomic_t stop = 0; -static void handler(int sig) { +static void handler(int sig) +{ stop = 1; } -static void usage() { +static int aeCheckThreadStop(struct aeEventLoop* eventLoop) +{ + thread* t = (thread*)eventLoop->checkThreadStopData; + if (t->complete == t->complete_stop) + return 1; + return 0; +} + +static void usage() +{ printf("Usage: wrk \n" " Options: \n" " -c, --connections Connections to keep open \n" " -d, --duration Duration of test \n" " -t, --threads Number of threads to use \n" + " -r, --requests Number of requests to limit\n" " \n" " -s, --script Load Lua script file \n" " -H, --header Add header to request \n" " --latency Print latency statistics \n" + " -j --json-format Print output as JSON \n" " --timeout Socket/request timeout \n" " -v, --version Print version details \n" " \n" @@ -58,8 +72,9 @@ static void usage() { " Time arguments may include a time unit (2s, 2m, 2h)\n"); } -int main(int argc, char **argv) { - char *url, **headers = zmalloc(argc * sizeof(char *)); +int main(int argc, char** argv) +{ + char *url, **headers = zmalloc(argc * sizeof(char*)); struct http_parser_url parts = {}; if (parse_args(&cfg, &url, &parts, headers, argc, argv)) { @@ -67,10 +82,10 @@ int main(int argc, char **argv) { exit(1); } - char *schema = copy_url_part(url, &parts, UF_SCHEMA); - char *host = copy_url_part(url, &parts, UF_HOST); - char *port = copy_url_part(url, &parts, UF_PORT); - char *service = port ? port : schema; + char* schema = copy_url_part(url, &parts, UF_SCHEMA); + char* host = copy_url_part(url, &parts, UF_HOST); + char* port = copy_url_part(url, &parts, UF_PORT); + char* service = port ? port : schema; if (!strncmp("https", schema, 5)) { if ((cfg.ctx = ssl_init()) == NULL) { @@ -78,23 +93,23 @@ int main(int argc, char **argv) { ERR_print_errors_fp(stderr); exit(1); } - sock.connect = ssl_connect; - sock.close = ssl_close; - sock.read = ssl_read; - sock.write = ssl_write; + sock.connect = ssl_connect; + sock.close = ssl_close; + sock.read = ssl_read; + sock.write = ssl_write; sock.readable = ssl_readable; } signal(SIGPIPE, SIG_IGN); - signal(SIGINT, SIG_IGN); + signal(SIGINT, SIG_IGN); - statistics.latency = stats_alloc(cfg.timeout * 1000); + statistics.latency = stats_alloc(cfg.timeout * 1000); statistics.requests = stats_alloc(MAX_THREAD_RATE_S); - thread *threads = zcalloc(cfg.threads * sizeof(thread)); + thread* threads = zcalloc(cfg.threads * sizeof(thread)); - lua_State *L = script_create(cfg.script, url, headers); + lua_State* L = script_create(cfg.script, url, headers); if (!script_resolve(L, host, service)) { - char *msg = strerror(errno); + char* msg = strerror(errno); fprintf(stderr, "unable to connect to %s:%s %s\n", host, service, msg); exit(1); } @@ -102,107 +117,176 @@ int main(int argc, char **argv) { cfg.host = host; for (uint64_t i = 0; i < cfg.threads; i++) { - thread *t = &threads[i]; - t->loop = aeCreateEventLoop(10 + cfg.connections * 3); - t->connections = cfg.connections / cfg.threads; + thread* t = &threads[i]; + t->loop = aeCreateEventLoop(10 + cfg.connections * 3); + if (t->loop == NULL) { + char* msg = strerror(errno); + fprintf(stderr, "unable to create ae eventloop: %s\n", msg); + exit(2); + } + t->connections = cfg.connections / cfg.threads; t->L = script_create(cfg.script, url, headers); script_init(L, t, argc - optind, &argv[optind]); + if (cfg.requests > 0) { + t->complete_stop = cfg.requests / cfg.threads; + if (i == (cfg.threads - 1)) + t->complete_stop += (cfg.requests % cfg.threads); + aeSetCheckThreadStopProc(t->loop, aeCheckThreadStop, (void*)t); + } + if (i == 0) { cfg.pipeline = script_verify_request(t->L); - cfg.dynamic = !script_is_static(t->L); - cfg.delay = script_has_delay(t->L); + cfg.dynamic = !script_is_static(t->L); + cfg.delay = script_has_delay(t->L); if (script_want_response(t->L)) { parser_settings.on_header_field = header_field; parser_settings.on_header_value = header_value; - parser_settings.on_body = response_body; + parser_settings.on_body = response_body; } } - if (!t->loop || pthread_create(&t->thread, NULL, &thread_main, t)) { - char *msg = strerror(errno); - fprintf(stderr, "unable to create thread %"PRIu64": %s\n", i, msg); + if (pthread_create(&t->thread, NULL, &thread_main, t)) { + char* msg = strerror(errno); + fprintf(stderr, "unable to create thread %" PRIu64 ": %s\n", i, msg); exit(2); } } struct sigaction sa = { .sa_handler = handler, - .sa_flags = 0, + .sa_flags = 0, }; sigfillset(&sa.sa_mask); 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", cfg.threads, cfg.connections); + char* time = format_time_s(cfg.duration); + if (!cfg.json_format) { + printf("Running %s test @ %s\n", time, url); + printf(" %" PRIu64 " threads and %" PRIu64 " connections\n", cfg.threads, cfg.connections); + } - uint64_t start = time_us(); + uint64_t start = time_us(); uint64_t complete = 0; - uint64_t bytes = 0; - errors errors = { 0 }; + uint64_t bytes = 0; + errors errors = { 0 }; - sleep(cfg.duration); - stop = 1; + if (cfg.duration > 0) { + sleep(cfg.duration); + stop = 1; + } for (uint64_t i = 0; i < cfg.threads; i++) { - thread *t = &threads[i]; + thread* t = &threads[i]; pthread_join(t->thread, NULL); complete += t->complete; - bytes += t->bytes; + bytes += t->bytes; errors.connect += t->errors.connect; - errors.read += t->errors.read; - errors.write += t->errors.write; + errors.read += t->errors.read; + errors.write += t->errors.write; errors.timeout += t->errors.timeout; - errors.status += t->errors.status; + errors.status += t->errors.status; } uint64_t runtime_us = time_us() - start; - long double runtime_s = runtime_us / 1000000.0; - long double req_per_s = complete / runtime_s; - long double bytes_per_s = bytes / runtime_s; + long double runtime_s = runtime_us / 1000000.0; + long double req_per_s = complete / runtime_s; + long double bytes_per_s = bytes / runtime_s; if (complete / cfg.connections > 0) { int64_t interval = runtime_us / (complete / cfg.connections); stats_correct(statistics.latency, interval); } - print_stats_header(); - print_stats("Latency", statistics.latency, format_time_us); - print_stats("Req/Sec", statistics.requests, format_metric); - if (cfg.latency) print_stats_latency(statistics.latency); + if (!cfg.json_format) { + print_stats_header(); + print_stats("Latency", statistics.latency, format_time_us); + print_stats("Req/Sec", statistics.requests, format_metric); + if (cfg.latency) + print_stats_latency(statistics.latency); - char *runtime_msg = format_time_us(runtime_us); + char* runtime_msg = format_time_us(runtime_us); - printf(" %"PRIu64" requests in %s, %sB read\n", complete, runtime_msg, format_binary(bytes)); - 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); - } + printf(" %" PRIu64 " requests in %s, %sB read\n", complete, runtime_msg, format_binary(bytes)); + 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); + } - 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); - printf("Transfer/sec: %10sB\n", format_binary(bytes_per_s)); + printf("Requests/sec: %9.2Lf\n", req_per_s); + printf("Transfer/sec: %10sB\n", format_binary(bytes_per_s)); - if (script_has_done(L)) { - script_summary(L, runtime_us, complete, bytes); - script_errors(L, &errors); - script_done(L, statistics.latency, statistics.requests); + if (script_has_done(L)) { + script_summary(L, runtime_us, complete, bytes); + script_errors(L, &errors); + script_done(L, statistics.latency, statistics.requests); + } + } else { + long double req_mean = stats_mean(statistics.requests); + long double req_stdev = stats_stdev(statistics.requests, req_mean); + long double req_within = stats_within_stdev(statistics.requests, req_mean, req_stdev, 1); + long double lat_mean = stats_mean(statistics.latency); + long double lat_stdev = stats_stdev(statistics.latency, lat_mean); + long double lat_within = stats_within_stdev(statistics.latency, lat_mean, lat_stdev, 1); + long double lat_perc_50 = stats_percentile(statistics.latency, 50.0) / 1000000.0; + long double lat_perc_75 = stats_percentile(statistics.latency, 75.0) / 1000000.0; + long double lat_perc_90 = stats_percentile(statistics.latency, 90.0) / 1000000.0; + long double lat_perc_95 = stats_percentile(statistics.latency, 95.0) / 1000000.0; + long double lat_perc_99 = stats_percentile(statistics.latency, 99.0) / 1000000.0; + printf("{\n"); + printf(" \"url\": \"%s\",\n", url); + printf(" \"threads\": %" PRIu64 ",\n", cfg.threads); + printf(" \"connections\": %" PRIu64 ",\n", cfg.connections); + printf(" \"duration\": %" PRIu64 ",\n", cfg.duration); + printf(" \"timeout\": %" PRIu64 ",\n", cfg.timeout); + if (cfg.script) { + printf(" \"script\": \"%s\",\n", cfg.script); + } else { + printf(" \"script\": null,\n"); + } + printf(" \"runtime_us\": %" PRIu64 ",\n", runtime_us); + printf(" \"bytes\": %" PRIu64 ",\n", bytes); + printf(" \"bytes_per_sec\": %Lf,\n", bytes_per_s); + printf(" \"requests_count\": %" PRIu64 ",\n", statistics.requests->count); + printf(" \"requests_per_sec\": %Lf,\n", req_per_s); + printf(" \"requests_mean\": %Lf,\n", req_mean); + printf(" \"requests_stdev\": %Lf,\n", req_stdev); + printf(" \"requests_min\": %" PRIu64 ",\n", statistics.requests->min); + printf(" \"requests_max\": %" PRIu64 ",\n", statistics.requests->max); + printf(" \"requests_within_stdev\": %.2Lf,\n", req_within); + printf(" \"latency_mean\": %Lf,\n", lat_mean / 1000000.0); + printf(" \"latency_stdev\": %Lf,\n", lat_stdev / 1000000.0); + printf(" \"latency_min\": %f,\n", statistics.latency->min / 1000000.0); + printf(" \"latency_max\": %f,\n", statistics.latency->max / 1000000.0); + printf(" \"latency_within_stdev\": %.2Lf,\n", lat_within); + printf(" \"latency_percentile_50\": %.2Lf,\n", lat_perc_50); + printf(" \"latency_percentile_75\": %.2Lf,\n", lat_perc_75); + printf(" \"latency_percentile_90\": %.2Lf,\n", lat_perc_90); + printf(" \"latency_percentile_95\": %.2Lf,\n", lat_perc_95); + printf(" \"latency_percentile_99\": %.2Lf,\n", lat_perc_99); + printf(" \"errors_connect\": %d,\n", errors.connect); + printf(" \"errors_read\": %d,\n", errors.read); + printf(" \"errors_write\": %d,\n", errors.write); + printf(" \"errors_timeout\": %d,\n", errors.timeout); + printf(" \"errors_status\": %d\n", errors.status); + printf("}"); } return 0; } -void *thread_main(void *arg) { - thread *thread = arg; +void* thread_main(void* arg) +{ + thread* thread = arg; - char *request = NULL; + char* request = NULL; size_t length = 0; if (!cfg.dynamic) { @@ -210,18 +294,18 @@ void *thread_main(void *arg) { } thread->cs = zcalloc(thread->connections * sizeof(connection)); - connection *c = thread->cs; + connection* c = thread->cs; for (uint64_t i = 0; i < thread->connections; i++, c++) { c->thread = thread; - c->ssl = cfg.ctx ? SSL_new(cfg.ctx) : NULL; + c->ssl = cfg.ctx ? SSL_new(cfg.ctx) : NULL; c->request = request; - c->length = length; + c->length = length; c->delayed = cfg.delay; connect_socket(thread, c); } - aeEventLoop *loop = thread->loop; + aeEventLoop* loop = thread->loop; aeCreateTimeEvent(loop, RECORD_INTERVAL_MS, record_rate, thread, NULL); thread->start = time_us(); @@ -233,9 +317,10 @@ void *thread_main(void *arg) { return NULL; } -static int connect_socket(thread *thread, connection *c) { - struct addrinfo *addr = thread->addr; - struct aeEventLoop *loop = thread->loop; +static int connect_socket(thread* thread, connection* c) +{ + struct addrinfo* addr = thread->addr; + struct aeEventLoop* loop = thread->loop; int fd, flags; fd = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol); @@ -244,7 +329,8 @@ static int connect_socket(thread *thread, connection *c) { fcntl(fd, F_SETFL, flags | O_NONBLOCK); if (connect(fd, addr->ai_addr, addr->ai_addrlen) == -1) { - if (errno != EINPROGRESS) goto error; + if (errno != EINPROGRESS) + goto error; } flags = 1; @@ -257,46 +343,51 @@ static int connect_socket(thread *thread, connection *c) { return fd; } - error: +error: thread->errors.connect++; close(fd); return -1; } -static int reconnect_socket(thread *thread, connection *c) { +static int reconnect_socket(thread* thread, connection* c) +{ aeDeleteFileEvent(thread->loop, c->fd, AE_WRITABLE | AE_READABLE); sock.close(c); close(c->fd); return connect_socket(thread, c); } -static int record_rate(aeEventLoop *loop, long long id, void *data) { - thread *thread = data; +static int record_rate(aeEventLoop* loop, long long id, void* data) +{ + thread* thread = data; if (thread->requests > 0) { uint64_t elapsed_ms = (time_us() - thread->start) / 1000; - uint64_t requests = (thread->requests / (double) elapsed_ms) * 1000; + uint64_t requests = (thread->requests / (double)elapsed_ms) * 1000; stats_record(statistics.requests, requests); thread->requests = 0; - thread->start = time_us(); + thread->start = time_us(); } - if (stop) aeStop(loop); + if (stop) + aeStop(loop); return RECORD_INTERVAL_MS; } -static int delay_request(aeEventLoop *loop, long long id, void *data) { - connection *c = data; +static int delay_request(aeEventLoop* loop, long long id, void* data) +{ + connection* c = data; c->delayed = false; aeCreateFileEvent(loop, c->fd, AE_WRITABLE, socket_writeable, c); return AE_NOMORE; } -static int header_field(http_parser *parser, const char *at, size_t len) { - connection *c = parser->data; +static int header_field(http_parser* parser, const char* at, size_t len) +{ + connection* c = parser->data; if (c->state == VALUE) { *c->headers.cursor++ = '\0'; c->state = FIELD; @@ -305,8 +396,9 @@ static int header_field(http_parser *parser, const char *at, size_t len) { return 0; } -static int header_value(http_parser *parser, const char *at, size_t len) { - connection *c = parser->data; +static int header_value(http_parser* parser, const char* at, size_t len) +{ + connection* c = parser->data; if (c->state == FIELD) { *c->headers.cursor++ = '\0'; c->state = VALUE; @@ -315,15 +407,17 @@ static int header_value(http_parser *parser, const char *at, size_t len) { return 0; } -static int response_body(http_parser *parser, const char *at, size_t len) { - connection *c = parser->data; +static int response_body(http_parser* parser, const char* at, size_t len) +{ + connection* c = parser->data; buffer_append(&c->body, at, len); return 0; } -static int response_complete(http_parser *parser) { - connection *c = parser->data; - thread *thread = c->thread; +static int response_complete(http_parser* parser) +{ + connection* c = parser->data; + thread* thread = c->thread; uint64_t now = time_us(); int status = parser->status_code; @@ -355,17 +449,21 @@ static int response_complete(http_parser *parser) { http_parser_init(parser, HTTP_RESPONSE); - done: +done: return 0; } -static void socket_connected(aeEventLoop *loop, int fd, void *data, int mask) { - connection *c = data; +static void socket_connected(aeEventLoop* loop, int fd, void* data, int mask) +{ + connection* c = data; switch (sock.connect(c, cfg.host)) { - case OK: break; - case ERROR: goto error; - case RETRY: return; + case OK: + break; + case ERROR: + goto error; + case RETRY: + return; } http_parser_init(&c->parser, HTTP_RESPONSE); @@ -376,14 +474,15 @@ static void socket_connected(aeEventLoop *loop, int fd, void *data, int mask) { return; - error: +error: c->thread->errors.connect++; reconnect_socket(c->thread, c); } -static void socket_writeable(aeEventLoop *loop, int fd, void *data, int mask) { - connection *c = data; - thread *thread = c->thread; +static void socket_writeable(aeEventLoop* loop, int fd, void* data, int mask) +{ + connection* c = data; + thread* thread = c->thread; if (c->delayed) { uint64_t delay = script_delay(thread->L); @@ -396,18 +495,21 @@ static void socket_writeable(aeEventLoop *loop, int fd, void *data, int mask) { if (cfg.dynamic) { script_request(thread->L, &c->request, &c->length); } - c->start = time_us(); + c->start = time_us(); c->pending = cfg.pipeline; } - char *buf = c->request + c->written; - size_t len = c->length - c->written; + char* buf = c->request + c->written; + size_t len = c->length - c->written; size_t n; switch (sock.write(c, buf, len, &n)) { - case OK: break; - case ERROR: goto error; - case RETRY: return; + case OK: + break; + case ERROR: + goto error; + case RETRY: + return; } c->written += n; @@ -418,43 +520,51 @@ static void socket_writeable(aeEventLoop *loop, int fd, void *data, int mask) { return; - error: +error: thread->errors.write++; reconnect_socket(thread, c); } -static void socket_readable(aeEventLoop *loop, int fd, void *data, int mask) { - connection *c = data; +static void socket_readable(aeEventLoop* loop, int fd, void* data, int mask) +{ + connection* c = data; size_t n; do { switch (sock.read(c, &n)) { - case OK: break; - case ERROR: goto error; - case RETRY: return; + case OK: + break; + case ERROR: + goto error; + case RETRY: + return; } - if (http_parser_execute(&c->parser, &parser_settings, c->buf, n) != n) goto error; - if (n == 0 && !http_body_is_final(&c->parser)) goto error; + if (http_parser_execute(&c->parser, &parser_settings, c->buf, n) != n) + goto error; + if (n == 0 && !http_body_is_final(&c->parser)) + goto error; c->thread->bytes += n; } while (n == RECVBUF && sock.readable(c) > 0); return; - error: +error: c->thread->errors.read++; reconnect_socket(c->thread, c); } -static uint64_t time_us() { +static uint64_t time_us() +{ struct timeval t; gettimeofday(&t, NULL); return (t.tv_sec * 1000000) + t.tv_usec; } -static char *copy_url_part(char *url, struct http_parser_url *parts, enum http_parser_url_fields field) { - char *part = NULL; +static char* copy_url_part(char* url, struct http_parser_url* parts, enum http_parser_url_fields field) +{ + char* part = NULL; if (parts->field_set & (1 << field)) { uint16_t off = parts->field_data[field].off; @@ -468,64 +578,81 @@ static char *copy_url_part(char *url, struct http_parser_url *parts, enum http_p static struct option longopts[] = { { "connections", required_argument, NULL, 'c' }, - { "duration", required_argument, NULL, 'd' }, - { "threads", required_argument, NULL, 't' }, - { "script", required_argument, NULL, 's' }, - { "header", required_argument, NULL, 'H' }, - { "latency", no_argument, NULL, 'L' }, - { "timeout", required_argument, NULL, 'T' }, - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, 'v' }, - { NULL, 0, NULL, 0 } + { "duration", required_argument, NULL, 'd' }, + { "threads", required_argument, NULL, 't' }, + { "requests", required_argument, NULL, 'r' }, + { "script", required_argument, NULL, 's' }, + { "header", required_argument, NULL, 'H' }, + { "latency", no_argument, NULL, 'L' }, + { "timeout", required_argument, NULL, 'T' }, + { "json-format", no_argument, NULL, 'j' }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'v' }, + { NULL, 0, NULL, 0 } }; -static int parse_args(struct config *cfg, char **url, struct http_parser_url *parts, char **headers, int argc, char **argv) { - char **header = headers; +static int parse_args(struct config* cfg, char** url, struct http_parser_url* parts, char** headers, int argc, char** argv) +{ + char** header = headers; int c; memset(cfg, 0, sizeof(struct config)); - cfg->threads = 2; + cfg->threads = 2; cfg->connections = 10; - cfg->duration = 10; - cfg->timeout = SOCKET_TIMEOUT_MS; + cfg->requests = 0; + cfg->duration = 10; + cfg->timeout = SOCKET_TIMEOUT_MS; + cfg->json_format = false; - while ((c = getopt_long(argc, argv, "t:c:d:s:H:T:Lrv?", longopts, NULL)) != -1) { + while ((c = getopt_long(argc, argv, "t:c:r:d:s:H:T:Lrv?", longopts, NULL)) != -1) { switch (c) { - case 't': - if (scan_metric(optarg, &cfg->threads)) return -1; - break; - case 'c': - if (scan_metric(optarg, &cfg->connections)) return -1; - break; - case 'd': - if (scan_time(optarg, &cfg->duration)) return -1; - break; - case 's': - cfg->script = optarg; - break; - case 'H': - *header++ = optarg; - break; - case 'L': - cfg->latency = true; - break; - case 'T': - if (scan_time(optarg, &cfg->timeout)) return -1; - cfg->timeout *= 1000; - break; - case 'v': - printf("wrk %s [%s] ", VERSION, aeGetApiName()); - printf("Copyright (C) 2012 Will Glozer\n"); - break; - case 'h': - case '?': - case ':': - default: + case 't': + if (scan_metric(optarg, &cfg->threads)) return -1; + break; + case 'c': + if (scan_metric(optarg, &cfg->connections)) + return -1; + break; + case 'r': + if (scan_metric(optarg, &cfg->requests)) + return -1; + break; + case 'd': + if (scan_time(optarg, &cfg->duration)) + return -1; + break; + case 's': + cfg->script = optarg; + break; + case 'H': + *header++ = optarg; + break; + case 'L': + cfg->latency = true; + break; + case 'j': + cfg->json_format = true; + break; + case 'T': + if (scan_time(optarg, &cfg->timeout)) + return -1; + cfg->timeout *= 1000; + break; + case 'v': + printf("wrk %s [%s] ", VERSION, aeGetApiName()); + printf("Copyright (C) 2012 Will Glozer\n"); + break; + case 'h': + case '?': + case ':': + default: + return -1; } } - if (optind == argc || !cfg->threads || !cfg->duration) return -1; + if (optind == argc || !cfg->threads || !cfg->duration) + return -1; if (!script_parse_url(argv[optind], parts)) { fprintf(stderr, "invalid URL: %s\n", argv[optind]); @@ -537,22 +664,32 @@ static int parse_args(struct config *cfg, char **url, struct http_parser_url *pa return -1; } - *url = argv[optind]; + if (cfg->requests > 0) + cfg->duration = 0; + + if (cfg->requests > 0 && cfg->requests < cfg->threads) + cfg->threads = cfg->requests; + + *url = argv[optind]; *header = NULL; return 0; } -static void print_stats_header() { +static void print_stats_header() +{ printf(" Thread Stats%6s%11s%8s%12s\n", "Avg", "Stdev", "Max", "+/- Stdev"); } -static void print_units(long double n, char *(*fmt)(long double), int width) { - char *msg = fmt(n); +static void print_units(long double n, char* (*fmt)(long double), int width) +{ + char* msg = fmt(n); int len = strlen(msg), pad = 2; - if (isalpha(msg[len-1])) pad--; - if (isalpha(msg[len-2])) pad--; + if (isalpha(msg[len - 1])) + pad--; + if (isalpha(msg[len - 2])) + pad--; width -= pad; printf("%*.*s%.*s", width, width, msg, pad, " "); @@ -560,20 +697,22 @@ static void print_units(long double n, char *(*fmt)(long double), int width) { free(msg); } -static void print_stats(char *name, stats *stats, char *(*fmt)(long double)) { +static void print_stats(char* name, stats* stats, char* (*fmt)(long double)) +{ uint64_t max = stats->max; - long double mean = stats_mean(stats); + long double mean = stats_mean(stats); long double stdev = stats_stdev(stats, mean); printf(" %-10s", name); - print_units(mean, fmt, 8); + print_units(mean, fmt, 8); print_units(stdev, fmt, 10); - print_units(max, fmt, 9); + print_units(max, fmt, 9); printf("%8.2Lf%%\n", stats_within_stdev(stats, mean, stdev, 1)); } -static void print_stats_latency(stats *stats) { - long double percentiles[] = { 50.0, 75.0, 90.0, 99.0 }; +static void print_stats_latency(stats* stats) +{ + long double percentiles[] = { 50.0, 75.0, 90.0, 95.0, 99.0 }; printf(" Latency Distribution\n"); for (size_t i = 0; i < sizeof(percentiles) / sizeof(long double); i++) { long double p = percentiles[i]; diff --git a/src/wrk.h b/src/wrk.h index 2d0ac84e..0aa9aa03 100644 --- a/src/wrk.h +++ b/src/wrk.h @@ -36,6 +36,7 @@ typedef struct { lua_State *L; errors errors; struct connection *cs; + uint64_t complete_stop; } thread; typedef struct {