diff --git a/CHANGES b/CHANGES index d2b4de6e..8b948363 100644 --- a/CHANGES +++ b/CHANGES @@ -1,4 +1,8 @@ -master +wrk next * The wrk global variable is the only global defined by default. * wrk.init() calls the global init(), remove calls to wrk.init(). + * Add wrk.lookup(host, port) and wrk.connect(addr) functions. + * Add setup phase that calls the global setup() for each thread. + * Allow assignment to thread.addr to specify the server address. + * Add thread:set(key, value), thread:get(key), and thread:stop(). diff --git a/Makefile b/Makefile index d956c6b1..efe5dbb3 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,7 @@ ifeq ($(TARGET), sunos) else ifeq ($(TARGET), darwin) LDFLAGS += -pagezero_size 10000 -image_base 100000000 else ifeq ($(TARGET), linux) + CFLAGS += -D_POSIX_C_SOURCE=200112L -D_BSD_SOURCE LIBS += -ldl LDFLAGS += -Wl,-E else ifeq ($(TARGET), freebsd) diff --git a/README b/README index 76bda730..906172d4 100644 --- a/README +++ b/README @@ -5,8 +5,8 @@ wrk - a HTTP benchmarking tool design with scalable event notification systems such as epoll and kqueue. An optional LuaJIT script can perform HTTP request generation, response - processing, and custom reporting. Several example scripts are located in - scripts/ + processing, and custom reporting. Details are available in SCRIPTING and + several examples are located in scripts/ Basic Usage @@ -26,65 +26,6 @@ Basic Usage Requests/sec: 748868.53 Transfer/sec: 606.33MB -Scripting - - wrk's public Lua API is: - - init = function(args) - request = function() - response = function(status, headers, body) - done = function(summary, latency, requests) - - wrk = { - scheme = "http", - host = "localhost", - port = nil, - method = "GET", - path = "/", - headers = {}, - body = nil - } - - function wrk.format(method, path, headers, body) - - wrk.format returns a HTTP request string containing the passed - parameters merged with values from the wrk table. - - The following globals are optional, and if defined must be functions: - - global init -- called when the thread is initialized - global request -- returning the HTTP message for each request - global response -- called with HTTP response data - global done -- called with results of run - - The init() function receives any extra command line arguments for the - script which must be separated from wrk arguments with "--". - - The done() function receives a table containing result data, and two - statistics objects representing the sampled per-request latency and - per-thread request rate. Duration and latency are microsecond values - and rate is measured in requests per second. - - latency.min -- minimum value seen - latency.max -- maximum value seen - latency.mean -- average value seen - latency.stdev -- standard deviation - latency:percentile(99.0) -- 99th percentile value - latency[i] -- raw sample value - - summary = { - duration = N, -- run duration in microseconds - requests = N, -- total completed requests - bytes = N, -- total bytes received - errors = { - connect = N, -- total socket connection errors - read = N, -- total socket read errors - write = N, -- total socket write errors - status = N, -- total HTTP status codes > 399 - timeout = N -- total request timeouts - } - } - Benchmarking Tips The machine running wrk must have a sufficient number of ephemeral ports @@ -93,11 +34,9 @@ Benchmarking Tips than the number of concurrent connections being tested. A user script that only changes the HTTP method, path, adds headers or - a body, will have no performance impact. If multiple HTTP requests are - necessary they should be pre-generated and returned via a quick lookup in - the request() call. Per-request actions, particularly building a new HTTP - request, and use of response() will necessarily reduce the amount of load - that can be generated. + a body, will have no performance impact. Per-request actions, particularly + building a new HTTP request, and use of response() will necessarily reduce + the amount of load that can be generated. Acknowledgements diff --git a/SCRIPTING b/SCRIPTING new file mode 100644 index 00000000..d3378663 --- /dev/null +++ b/SCRIPTING @@ -0,0 +1,112 @@ +Overview + + wrk supports executing a LuaJIT script during three distinct phases: setup, + running, and done. Each wrk thread has an independent scripting environment + and the setup & done phases execute in a separate environment which does + not participate in the running phase. + + The public Lua API consists of a global table and a number of global + functions: + + wrk = { + scheme = "http", + host = "localhost", + port = nil, + method = "GET", + path = "/", + headers = {}, + body = nil, + thread = , + } + + function wrk.format(method, path, headers, body) + + wrk.format returns a HTTP request string containing the passed parameters + merged with values from the wrk table. + + function wrk.lookup(host, service) + + wrk.lookup returns a table containing all known addresses for the host + and service pair. This corresponds to the POSIX getaddrinfo() function. + + function wrk.connect(addr) + + wrk.connect returns true if the address can be connected to, otherwise + it returns false. The address must be one returned from wrk.lookup(). + + The following globals are optional, and if defined must be functions: + + global setup -- called during thread setup + global init -- called when the thread is starting + global request -- called to generate the HTTP request + global response -- called with HTTP response data + global done -- called with results of run + +Setup + + function setup(thread) + + The setup phase begins after the target IP address has been resolved and all + threads have been initialized but not yet started. + + setup() is called once for each thread and receives a userdata object + representing the thread. + + thread.addr - get or set the thread's server address + thread:get(key) - get the value of a global in the thread's env + thread:set(key, value) - set the value of a global in the thread's env + thread:stop() - stop the thread + + Only boolean, nil, number, and string values or tables of the same may be + transfered via get()/set() and thread:stop() can only be called while the + thread is running. + +Running + + function init(args) + function request() + function response(status, headers, body) + + The running phase begins with a single call to init(), followed by + a call to request() and response() for each request cycle. + + The init() function receives any extra command line arguments for the + script which must be separated from wrk arguments with "--". + + request() returns a string containing the HTTP request. Building a new + request each time is expensive, when testing a high performance server + one solution is to pre-generate all requests in init() and do a quick + lookup in request(). + + response() is called with the HTTP response status, headers, and body. + Parsing the headers and body is expensive, so if the response global is + nil after the call to init() wrk will ignore the headers and body. + +Done + + function done(summary, latency, requests) + + The done() function receives a table containing result data, and two + statistics objects representing the sampled per-request latency and + per-thread request rate. Duration and latency are microsecond values + and rate is measured in requests per second. + + latency.min -- minimum value seen + latency.max -- maximum value seen + latency.mean -- average value seen + latency.stdev -- standard deviation + latency:percentile(99.0) -- 99th percentile value + latency[i] -- raw sample value + + summary = { + duration = N, -- run duration in microseconds + requests = N, -- total completed requests + bytes = N, -- total bytes received + errors = { + connect = N, -- total socket connection errors + read = N, -- total socket read errors + write = N, -- total socket write errors + status = N, -- total HTTP status codes > 399 + timeout = N -- total request timeouts + } + } diff --git a/scripts/addr.lua b/scripts/addr.lua new file mode 100644 index 00000000..783c1e97 --- /dev/null +++ b/scripts/addr.lua @@ -0,0 +1,22 @@ +-- example script that demonstrates use of setup() to pass +-- a random server address to each thread + +local addrs = nil + +function setup(thread) + if not addrs then + addrs = wrk.lookup(wrk.host, wrk.port or "http") + for i = #addrs, 1, -1 do + if not wrk.connect(addrs[i]) then + table.remove(addrs, i) + end + end + end + + thread.addr = addrs[math.random(#addrs)] +end + +function init(args) + local msg = "thread addr: %s" + print(msg:format(wrk.thread.addr)) +end diff --git a/scripts/setup.lua b/scripts/setup.lua new file mode 100644 index 00000000..30d72f3a --- /dev/null +++ b/scripts/setup.lua @@ -0,0 +1,38 @@ +-- example script that demonstrates use of setup() to pass +-- data to and from the threads + +local counter = 1 +local threads = {} + +function setup(thread) + thread:set("id", counter) + table.insert(threads, thread) + counter = counter + 1 +end + +function init(args) + requests = 0 + responses = 0 + + local msg = "thread %d created" + print(msg:format(id)) +end + +function request() + requests = requests + 1 + return wrk.request() +end + +function response(status, headers, body) + responses = responses + 1 +end + +function done(summary, latency, requests) + for index, thread in ipairs(threads) do + local id = thread:get("id") + local requests = thread:get("requests") + local responses = thread:get("responses") + local msg = "thread %d made %d requests and got %d responses" + print(msg:format(id, requests, responses)) + end +end diff --git a/scripts/stop.lua b/scripts/stop.lua new file mode 100644 index 00000000..16f40e4d --- /dev/null +++ b/scripts/stop.lua @@ -0,0 +1,10 @@ +-- example script that demonstrates use of thread:stop() + +local counter = 1 + +function response() + if counter == 100 then + wrk.thread:stop() + end + counter = counter + 1 +end diff --git a/src/config.h b/src/config.h index 0417b2b5..895c34f6 100644 --- a/src/config.h +++ b/src/config.h @@ -5,7 +5,6 @@ #define HAVE_KQUEUE #elif defined(__linux__) #define HAVE_EPOLL -#define _POSIX_C_SOURCE 200809L #elif defined (__sun) #define HAVE_EVPORT #endif diff --git a/src/main.h b/src/main.h index a101ae24..ab38adea 100644 --- a/src/main.h +++ b/src/main.h @@ -16,7 +16,6 @@ #include #include #include -#include #include #include diff --git a/src/script.c b/src/script.c index 0d2799d0..ee2d0607 100644 --- a/src/script.c +++ b/src/script.c @@ -15,35 +15,43 @@ typedef struct { static int script_addr_tostring(lua_State *); static int script_addr_gc(lua_State *); static int script_stats_len(lua_State *); -static int script_stats_get(lua_State *); +static int script_stats_index(lua_State *); +static int script_thread_index(lua_State *); +static int script_thread_newindex(lua_State *); static int script_wrk_lookup(lua_State *); static int script_wrk_connect(lua_State *); + static void set_fields(lua_State *, int index, const table_field *); static const struct luaL_reg addrlib[] = { - { "__tostring", script_addr_tostring }, - { "__gc" , script_addr_gc }, - { NULL, NULL } + { "__tostring", script_addr_tostring }, + { "__gc" , script_addr_gc }, + { NULL, NULL } }; static const struct luaL_reg statslib[] = { - { "__index", script_stats_get }, - { "__len", script_stats_len }, - { NULL, NULL } + { "__index", script_stats_index }, + { "__len", script_stats_len }, + { NULL, NULL } +}; + +static const struct luaL_reg threadlib[] = { + { "__index", script_thread_index }, + { "__newindex", script_thread_newindex }, + { NULL, NULL } }; -lua_State *script_create(char *scheme, char *host, char *port, char *path) { +lua_State *script_create(char *file, char *scheme, char *host, char *port, char *path) { lua_State *L = luaL_newstate(); luaL_openlibs(L); - luaL_dostring(L, "wrk = require \"wrk\""); + (void) luaL_dostring(L, "wrk = require \"wrk\""); luaL_newmetatable(L, "wrk.addr"); luaL_register(L, NULL, addrlib); - lua_pop(L, 1); - luaL_newmetatable(L, "wrk.stats"); luaL_register(L, NULL, statslib); - lua_pop(L, 1); + luaL_newmetatable(L, "wrk.thread"); + luaL_register(L, NULL, threadlib); const table_field fields[] = { { "scheme", LUA_TSTRING, scheme }, @@ -56,17 +64,15 @@ lua_State *script_create(char *scheme, char *host, char *port, char *path) { }; lua_getglobal(L, "wrk"); - set_fields(L, 1, fields); - lua_pop(L, 1); + set_fields(L, 4, fields); + lua_pop(L, 4); - return L; -} - -void script_prepare_setup(lua_State *L, char *script) { - if (script && luaL_dofile(L, script)) { + if (file && luaL_dofile(L, file)) { const char *cause = lua_tostring(L, -1); - fprintf(stderr, "%s: %s\n", script, cause); + fprintf(stderr, "%s: %s\n", file, cause); } + + return L; } bool script_resolve(lua_State *L, char *host, char *service) { @@ -83,13 +89,24 @@ bool script_resolve(lua_State *L, char *host, char *service) { return count > 0; } -struct addrinfo *script_peek_addr(lua_State *L) { +void script_push_thread(lua_State *L, thread *t) { + thread **ptr = (thread **) lua_newuserdata(L, sizeof(thread **)); + *ptr = t; + luaL_getmetatable(L, "wrk.thread"); + lua_setmetatable(L, -2); +} + +void script_setup(lua_State *L, thread *t) { + lua_getglobal(t->L, "wrk"); + script_push_thread(t->L, t); + lua_setfield(t->L, -2, "thread"); + lua_pop(t->L, 1); + lua_getglobal(L, "wrk"); - lua_getfield(L, -1, "addrs"); - lua_rawgeti(L, -1, 1); - struct addrinfo *addr = lua_touserdata(L, -1); - lua_pop(L, 3); - return addr; + lua_getfield(L, -1, "setup"); + script_push_thread(L, t); + lua_call(L, 1, 0); + lua_pop(L, 1); } void script_headers(lua_State *L, char **headers) { @@ -106,12 +123,7 @@ void script_headers(lua_State *L, char **headers) { lua_pop(L, 2); } -void script_init(lua_State *L, char *script, int argc, char **argv) { - if (script && luaL_dofile(L, script)) { - const char *cause = lua_tostring(L, -1); - fprintf(stderr, "%s: %s\n", script, cause); - } - +void script_init(lua_State *L, int argc, char **argv) { lua_getglobal(L, "wrk"); lua_getfield(L, -1, "init"); lua_newtable(L); @@ -211,21 +223,19 @@ void script_errors(lua_State *L, errors *errors) { lua_setfield(L, 1, "errors"); } -void script_done(lua_State *L, stats *latency, stats *requests) { - stats **s; +void script_push_stats(lua_State *L, stats *s) { + stats **ptr = (stats **) lua_newuserdata(L, sizeof(stats **)); + *ptr = s; + luaL_getmetatable(L, "wrk.stats"); + lua_setmetatable(L, -2); +} +void script_done(lua_State *L, stats *latency, stats *requests) { lua_getglobal(L, "done"); lua_pushvalue(L, 1); - s = (stats **) lua_newuserdata(L, sizeof(stats **)); - *s = latency; - luaL_getmetatable(L, "wrk.stats"); - lua_setmetatable(L, 4); - - s = (stats **) lua_newuserdata(L, sizeof(stats **)); - *s = requests; - luaL_getmetatable(L, "wrk.stats"); - lua_setmetatable(L, 5); + script_push_stats(L, latency); + script_push_stats(L, requests); lua_call(L, 3, 0); lua_pop(L, 1); @@ -278,6 +288,20 @@ static struct addrinfo *checkaddr(lua_State *L) { return addr; } +void script_addr_copy(struct addrinfo *src, struct addrinfo *dst) { + *dst = *src; + dst->ai_addr = zmalloc(src->ai_addrlen); + memcpy(dst->ai_addr, src->ai_addr, src->ai_addrlen); +} + +struct addrinfo *script_addr_clone(lua_State *L, struct addrinfo *addr) { + struct addrinfo *udata = lua_newuserdata(L, sizeof(*udata)); + luaL_getmetatable(L, "wrk.addr"); + lua_setmetatable(L, -2); + script_addr_copy(addr, udata); + return udata; +} + static int script_addr_tostring(lua_State *L) { struct addrinfo *addr = checkaddr(L); char host[NI_MAXHOST]; @@ -313,7 +337,7 @@ static int script_stats_percentile(lua_State *L) { return 1; } -static int script_stats_get(lua_State *L) { +static int script_stats_index(lua_State *L) { stats *s = checkstats(L); if (lua_isnumber(L, 2)) { int index = luaL_checkint(L, 2); @@ -337,6 +361,59 @@ static int script_stats_len(lua_State *L) { return 1; } +static thread *checkthread(lua_State *L) { + thread **t = luaL_checkudata(L, 1, "wrk.thread"); + luaL_argcheck(L, t != NULL, 1, "`thread' expected"); + return *t; +} + +static int script_thread_get(lua_State *L) { + thread *t = checkthread(L); + const char *key = lua_tostring(L, -1); + lua_getglobal(t->L, key); + script_copy_value(t->L, L, -1); + lua_pop(t->L, 1); + return 1; +} + +static int script_thread_set(lua_State *L) { + thread *t = checkthread(L); + const char *key = lua_tostring(L, -2); + script_copy_value(L, t->L, -1); + lua_setglobal(t->L, key); + return 0; +} + +static int script_thread_stop(lua_State *L) { + thread *t = checkthread(L); + aeStop(t->loop); + return 0; +} + +static int script_thread_index(lua_State *L) { + thread *t = checkthread(L); + const char *key = lua_tostring(L, 2); + if (!strcmp("get", key)) lua_pushcfunction(L, script_thread_get); + if (!strcmp("set", key)) lua_pushcfunction(L, script_thread_set); + if (!strcmp("stop", key)) lua_pushcfunction(L, script_thread_stop); + if (!strcmp("addr", key)) script_addr_clone(L, t->addr); + return 1; +} + +static int script_thread_newindex(lua_State *L) { + thread *t = checkthread(L); + const char *key = lua_tostring(L, -2); + if (!strcmp("addr", key)) { + struct addrinfo *addr = checkaddr(L); + if (t->addr) zfree(t->addr->ai_addr); + t->addr = zrealloc(t->addr, sizeof(*addr)); + script_addr_copy(addr, t->addr); + } else { + luaL_error(L, "cannot set '%s' on thread", luaL_typename(L, -1)); + } + return 0; +} + static int script_wrk_lookup(lua_State *L) { struct addrinfo *addrs; struct addrinfo hints = { @@ -356,13 +433,7 @@ static int script_wrk_lookup(lua_State *L) { lua_newtable(L); for (struct addrinfo *addr = addrs; addr != NULL; addr = addr->ai_next) { - struct addrinfo *udata = lua_newuserdata(L, sizeof(*udata)); - luaL_getmetatable(L, "wrk.addr"); - lua_setmetatable(L, -2); - - *udata = *addr; - udata->ai_addr = zmalloc(addr->ai_addrlen); - memcpy(udata->ai_addr, addr->ai_addr, addr->ai_addrlen); + script_addr_clone(L, addr); lua_rawseti(L, -2, index++); } @@ -381,6 +452,36 @@ static int script_wrk_connect(lua_State *L) { return 1; } +void script_copy_value(lua_State *src, lua_State *dst, int index) { + switch (lua_type(src, index)) { + case LUA_TBOOLEAN: + lua_pushboolean(dst, lua_toboolean(src, index)); + break; + case LUA_TNIL: + lua_pushnil(dst); + break; + case LUA_TNUMBER: + lua_pushnumber(dst, lua_tonumber(src, index)); + break; + case LUA_TSTRING: + lua_pushstring(dst, lua_tostring(src, index)); + break; + case LUA_TTABLE: + lua_newtable(dst); + lua_pushnil(src); + while (lua_next(src, index - 1)) { + script_copy_value(src, dst, -1); + script_copy_value(src, dst, -2); + lua_settable(dst, -3); + lua_pop(src, 1); + } + lua_pop(src, 1); + break; + default: + luaL_error(src, "cannot transfer '%s' to thread", luaL_typename(src, index)); + } +} + static void set_fields(lua_State *L, int index, const table_field *fields) { for (int i = 0; fields[i].name; i++) { table_field f = fields[i]; diff --git a/src/script.h b/src/script.h index 3baccd93..54f7f17f 100644 --- a/src/script.h +++ b/src/script.h @@ -5,28 +5,21 @@ #include #include #include -#include -#include #include #include "stats.h" +#include "wrk.h" -typedef struct { - char *buffer; - size_t length; - char *cursor; -} buffer; +lua_State *script_create(char *, char *, char *, char *, char *); -lua_State *script_create(char *, char *, char *, char *); -void script_prepare_setup(lua_State *, char *); bool script_resolve(lua_State *, char *, char *); -struct addrinfo *script_peek_addr(lua_State *); -void script_headers(lua_State *, char **); -size_t script_verify_request(lua_State *L); - -void script_init(lua_State *, char *, int, char **); +void script_setup(lua_State *, thread *); void script_done(lua_State *, stats *, stats *); + +void script_headers(lua_State *, char **); +void script_init(lua_State *, int, char **); void script_request(lua_State *, char **, size_t *); void script_response(lua_State *, int, buffer *, buffer *); +size_t script_verify_request(lua_State *L); bool script_is_static(lua_State *); bool script_want_response(lua_State *L); @@ -34,6 +27,8 @@ bool script_has_done(lua_State *L); void script_summary(lua_State *, uint64_t, uint64_t, uint64_t); void script_errors(lua_State *, errors *); +void script_copy_value(lua_State *, lua_State *, int); + void buffer_append(buffer *, const char *, size_t); void buffer_reset(buffer *); char *buffer_pushlstring(lua_State *, char *); diff --git a/src/wrk.c b/src/wrk.c index b77a8383..f075aece 100644 --- a/src/wrk.c +++ b/src/wrk.c @@ -1,6 +1,7 @@ // Copyright (C) 2012 - Will Glozer. All rights reserved. #include "wrk.h" +#include "script.h" #include "main.h" static struct config { @@ -82,14 +83,6 @@ int main(int argc, char **argv) { path = &url[parser_url.field_data[UF_PATH].off]; } - lua_State *L = script_create(schema, host, port, path); - script_prepare_setup(L, cfg.script); - if (!script_resolve(L, host, service)) { - char *msg = strerror(errno); - fprintf(stderr, "unable to connect to %s:%s %s\n", host, service, msg); - exit(1); - } - if (!strncmp("https", schema, 5)) { if ((cfg.ctx = ssl_init()) == NULL) { fprintf(stderr, "unable to initialize SSL\n"); @@ -111,19 +104,25 @@ int main(int argc, char **argv) { statistics.requests = stats_alloc(SAMPLES); thread *threads = zcalloc(cfg.threads * sizeof(thread)); - uint64_t connections = cfg.connections / cfg.threads; - uint64_t stop_at = time_us() + (cfg.duration * 1000000); + uint64_t stop_at = time_us() + (cfg.duration * 1000000); + + lua_State *L = script_create(cfg.script, schema, host, port, path); + if (!script_resolve(L, host, service)) { + char *msg = strerror(errno); + fprintf(stderr, "unable to connect to %s:%s %s\n", host, service, msg); + exit(1); + } for (uint64_t i = 0; i < cfg.threads; i++) { - thread *t = &threads[i]; + thread *t = &threads[i]; t->loop = aeCreateEventLoop(10 + cfg.connections * 3); - t->addr = script_peek_addr(L); - t->connections = connections; + t->connections = cfg.connections / cfg.threads; t->stop_at = stop_at; - t->L = script_create(schema, host, port, path); + t->L = script_create(cfg.script, schema, host, port, path); script_headers(t->L, headers); - script_init(t->L, cfg.script, argc - optind, &argv[optind]); + script_setup(L, t); + script_init(t->L, argc - optind, &argv[optind]); if (i == 0) { cfg.pipeline = script_verify_request(t->L); @@ -474,7 +473,6 @@ static void socket_writeable(aeEventLoop *loop, int fd, void *data, int mask) { reconnect_socket(thread, c); } - static void socket_readable(aeEventLoop *loop, int fd, void *data, int mask) { connection *c = data; size_t n; diff --git a/src/wrk.h b/src/wrk.h index bf473502..247fab9f 100644 --- a/src/wrk.h +++ b/src/wrk.h @@ -6,13 +6,14 @@ #include #include #include +#include #include #include +#include #include "stats.h" #include "ae.h" -#include "script.h" #include "http_parser.h" #define VERSION "3.1.2" @@ -43,6 +44,12 @@ typedef struct { struct connection *cs; } thread; +typedef struct { + char *buffer; + size_t length; + char *cursor; +} buffer; + typedef struct connection { thread *thread; http_parser parser; diff --git a/src/wrk.lua b/src/wrk.lua index cb43cde4..0229fdfe 100644 --- a/src/wrk.lua +++ b/src/wrk.lua @@ -5,7 +5,8 @@ local wrk = { method = "GET", path = "/", headers = {}, - body = nil + body = nil, + thread = nil, } function wrk.resolve(host, service) @@ -18,6 +19,13 @@ function wrk.resolve(host, service) wrk.addrs = addrs end +function wrk.setup(thread) + thread.addr = wrk.addrs[1] + if type(setup) == "function" then + setup(thread) + end +end + function wrk.init(args) if not wrk.headers["Host"] then local host = wrk.host