Browse Source

[tools] Initial import of wrk stress tester

This uses a couple extern libraries, but all are MIT licensed.
Erick Tryzelaar 10 năm trước cách đây
mục cha
commit
b327979

+ 56 - 0
tools/wrk-scripts/README.md

@@ -0,0 +1,56 @@
+Scripts to stress test various parts of hue using
+[wrk](https://github.com/wg/wrk). Follow
+[these instructions](https://github.com/wg/wrk/wiki/Installing-Wrk-on-Linux)
+to install `wrk`.
+
+Usage
+-----
+
+To run:
+
+```
+% env LUA_PATH='./?.lua' wrk \
+  -s ./stress-hue.lua \
+  --duration 30s \
+  --threads 5 \
+  --connections 10 \
+  --timeout 5s \
+  --latency \
+  http://localhost:8000/about \
+  -- \
+  --session 1234...
+```
+
+This will start 5 threads each accessing that URL 2 at a time (connections /
+threads) for 30 seconds, with the sockets timing after 5 seconds if there's a
+problem with the specified session cookie. Once finished, `wrk` will print out
+the latency statistics:
+
+```
+Running 30s test @ http://localhost:8000/about
+  5 threads and 10 connections
+  Thread Stats   Avg      Stdev     Max   +/- Stdev
+    Latency   692.50ms  180.14ms 983.15ms   80.90%
+    Req/Sec     6.59     17.64   111.00     88.79%
+  Latency Distribution
+     50%  779.78ms
+     75%  779.78ms
+     90%  779.78ms
+     99%  779.78ms
+  902 requests in 30.02s, 37.89MB read
+  Socket errors: connect 0, read 1, write 0, timeout 0
+Requests/sec:     30.05
+Transfer/sec:      1.26MB
+```
+
+If instead you want to create a unique user for each thread, you can use Hue's
+demo mode by adding this to the `hue.ini`:
+
+```
+[desktop]
+...
+demo=true
+...
+```
+
+Then just remove the `--session ...` argument.

+ 1054 - 0
tools/wrk-scripts/lib/argparse.lua

@@ -0,0 +1,1054 @@
+-- The MIT License (MIT)
+
+-- Copyright (c) 2013 - 2015 Peter Melnichenko
+
+-- 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.
+
+
+local function deep_update(t1, t2)
+   for k, v in pairs(t2) do
+      if type(v) == "table" then
+         v = deep_update({}, v)
+      end
+
+      t1[k] = v
+   end
+
+   return t1
+end
+
+-- A property is a tuple {name, callback}.
+-- properties.args is number of properties that can be set as arguments
+-- when calling an object.
+local function new_class(prototype, properties, parent)
+   -- Class is the metatable of its instances.
+   local class = {}
+   class.__index = class
+
+   if parent then
+      class.__prototype = deep_update(deep_update({}, parent.__prototype), prototype)
+   else
+      class.__prototype = prototype
+   end
+
+   local names = {}
+
+   -- Create setter methods and fill set of property names. 
+   for _, property in ipairs(properties) do
+      local name, callback = property[1], property[2]
+
+      class[name] = function(self, value)
+         if not callback(self, value) then
+            self["_" .. name] = value
+         end
+
+         return self
+      end
+
+      names[name] = true
+   end
+
+   function class.__call(self, ...)
+      -- When calling an object, if the first argument is a table,
+      -- interpret keys as property names, else delegate arguments
+      -- to corresponding setters in order.
+      if type((...)) == "table" then
+         for name, value in pairs((...)) do
+            if names[name] then
+               self[name](self, value)
+            end
+         end
+      else
+         local nargs = select("#", ...)
+
+         for i, property in ipairs(properties) do
+            if i > nargs or i > properties.args then
+               break
+            end
+
+            local arg = select(i, ...)
+
+            if arg ~= nil then
+               self[property[1]](self, arg)
+            end
+         end
+      end
+
+      return self
+   end
+
+   -- If indexing class fails, fallback to its parent.
+   local class_metatable = {}
+   class_metatable.__index = parent
+
+   function class_metatable.__call(self, ...)
+      -- Calling a class returns its instance.
+      -- Arguments are delegated to the instance.
+      local object = deep_update({}, self.__prototype)
+      setmetatable(object, self)
+      return object(...)
+   end
+
+   return setmetatable(class, class_metatable)
+end
+
+local function typecheck(name, types, value)
+   for _, type_ in ipairs(types) do
+      if type(value) == type_ then
+         return true
+      end
+   end
+
+   error(("bad property '%s' (%s expected, got %s)"):format(name, table.concat(types, " or "), type(value)))
+end
+
+local function typechecked(name, ...)
+   local types = {...}
+   return {name, function(_, value) typecheck(name, types, value) end}
+end
+
+local multiname = {"name", function(self, value)
+   typecheck("name", {"string"}, value)
+
+   for alias in value:gmatch("%S+") do
+      self._name = self._name or alias
+      table.insert(self._aliases, alias)
+   end
+
+   -- Do not set _name as with other properties.
+   return true
+end}
+
+local function parse_boundaries(str)
+   if tonumber(str) then
+      return tonumber(str), tonumber(str)
+   end
+
+   if str == "*" then
+      return 0, math.huge
+   end
+
+   if str == "+" then
+      return 1, math.huge
+   end
+
+   if str == "?" then
+      return 0, 1
+   end
+
+   if str:match "^%d+%-%d+$" then
+      local min, max = str:match "^(%d+)%-(%d+)$"
+      return tonumber(min), tonumber(max)
+   end
+
+   if str:match "^%d+%+$" then
+      local min = str:match "^(%d+)%+$"
+      return tonumber(min), math.huge
+   end
+end
+
+local function boundaries(name)
+   return {name, function(self, value)
+      typecheck(name, {"number", "string"}, value)
+
+      local min, max = parse_boundaries(value)
+
+      if not min then
+         error(("bad property '%s'"):format(name))
+      end
+
+      self["_min" .. name], self["_max" .. name] = min, max
+   end}
+end
+
+local add_help = {"add_help", function(self, value)
+   typecheck("add_help", {"boolean", "string", "table"}, value)
+
+   if self._has_help then
+      table.remove(self._options)
+      self._has_help = false
+   end
+
+   if value then
+      local help = self:flag()
+         :description "Show this help message and exit."
+         :action(function()
+            print(self:get_help())
+            os.exit(0)
+         end)
+
+      if value ~= true then
+         help = help(value)
+      end
+
+      if not help._name then
+         help "-h" "--help"
+      end
+
+      self._has_help = true
+   end
+end}
+
+local Parser = new_class({
+   _arguments = {},
+   _options = {},
+   _commands = {},
+   _mutexes = {},
+   _require_command = true,
+   _handle_options = true
+}, {
+   args = 3,
+   typechecked("name", "string"),
+   typechecked("description", "string"),
+   typechecked("epilog", "string"),
+   typechecked("usage", "string"),
+   typechecked("help", "string"),
+   typechecked("require_command", "boolean"),
+   typechecked("handle_options", "boolean"),
+   add_help
+})
+
+local Command = new_class({
+   _aliases = {}
+}, {
+   args = 3,
+   multiname,
+   typechecked("description", "string"),
+   typechecked("epilog", "string"),
+   typechecked("target", "string"),
+   typechecked("usage", "string"),
+   typechecked("help", "string"),
+   typechecked("require_command", "boolean"),
+   typechecked("handle_options", "boolean"),
+   typechecked("action", "function"),
+   add_help
+}, Parser)
+
+local Argument = new_class({
+   _minargs = 1,
+   _maxargs = 1,
+   _mincount = 1,
+   _maxcount = 1,
+   _defmode = "unused",
+   _show_default = true
+}, {
+   args = 5,
+   typechecked("name", "string"),
+   typechecked("description", "string"),
+   typechecked("default", "string"),
+   typechecked("convert", "function", "table"),
+   boundaries("args"),
+   typechecked("target", "string"),
+   typechecked("defmode", "string"),
+   typechecked("show_default", "boolean"),
+   typechecked("argname", "string", "table")
+})
+
+local Option = new_class({
+   _aliases = {},
+   _mincount = 0,
+   _overwrite = true
+}, {
+   args = 6,
+   multiname,
+   typechecked("description", "string"),
+   typechecked("default", "string"),
+   typechecked("convert", "function", "table"),
+   boundaries("args"),
+   boundaries("count"),
+   typechecked("target", "string"),
+   typechecked("defmode", "string"),
+   typechecked("show_default", "boolean"),
+   typechecked("overwrite", "boolean"),
+   typechecked("argname", "string", "table"),
+   typechecked("action", "function")
+}, Argument)
+
+function Argument:_get_argument_list()
+   local buf = {}
+   local i = 1
+
+   while i <= math.min(self._minargs, 3) do
+      local argname = self:_get_argname(i)
+
+      if self._default and self._defmode:find "a" then
+         argname = "[" .. argname .. "]"
+      end
+
+      table.insert(buf, argname)
+      i = i+1
+   end
+
+   while i <= math.min(self._maxargs, 3) do
+      table.insert(buf, "[" .. self:_get_argname(i) .. "]")
+      i = i+1
+
+      if self._maxargs == math.huge then
+         break
+      end
+   end
+
+   if i < self._maxargs then
+      table.insert(buf, "...")
+   end
+
+   return buf
+end
+
+function Argument:_get_usage()
+   local usage = table.concat(self:_get_argument_list(), " ")
+
+   if self._default and self._defmode:find "u" then
+      if self._maxargs > 1 or (self._minargs == 1 and not self._defmode:find "a") then
+         usage = "[" .. usage .. "]"
+      end
+   end
+
+   return usage
+end
+
+function Argument:_get_type()
+   if self._maxcount == 1 then
+      if self._maxargs == 0 then
+         return "flag"
+      elseif self._maxargs == 1 and (self._minargs == 1 or self._mincount == 1) then
+         return "arg"
+      else
+         return "multiarg"
+      end
+   else
+      if self._maxargs == 0 then
+         return "counter"
+      elseif self._maxargs == 1 and self._minargs == 1 then
+         return "multicount"
+      else
+         return "twodimensional"
+      end
+   end
+end
+
+-- Returns placeholder for `narg`-th argument. 
+function Argument:_get_argname(narg)
+   local argname = self._argname or self:_get_default_argname()
+
+   if type(argname) == "table" then
+      return argname[narg]
+   else
+      return argname
+   end
+end
+
+function Argument:_get_default_argname()
+   return "<" .. self._name .. ">"
+end
+
+function Option:_get_default_argname()
+   return "<" .. self:_get_default_target() .. ">"
+end
+
+-- Returns label to be shown in the help message. 
+function Argument:_get_label()
+   return self._name
+end
+
+function Option:_get_label()
+   local variants = {}
+   local argument_list = self:_get_argument_list()
+   table.insert(argument_list, 1, nil)
+
+   for _, alias in ipairs(self._aliases) do
+      argument_list[1] = alias
+      table.insert(variants, table.concat(argument_list, " "))
+   end
+
+   return table.concat(variants, ", ")
+end
+
+function Command:_get_label()
+   return table.concat(self._aliases, ", ")
+end
+
+function Argument:_get_description()
+   if self._default and self._show_default then
+      if self._description then
+         return ("%s (default: %s)"):format(self._description, self._default)
+      else
+         return ("default: %s"):format(self._default)
+      end
+   else
+      return self._description or ""
+   end
+end
+
+function Command:_get_description()
+   return self._description or ""
+end
+
+function Option:_get_usage()
+   local usage = self:_get_argument_list()
+   table.insert(usage, 1, self._name)
+   usage = table.concat(usage, " ")
+
+   if self._mincount == 0 or self._default then
+      usage = "[" .. usage .. "]"
+   end
+
+   return usage
+end
+
+function Option:_get_default_target()
+   local res
+
+   for _, alias in ipairs(self._aliases) do
+      if alias:sub(1, 1) == alias:sub(2, 2) then
+         res = alias:sub(3)
+         break
+      end
+   end
+
+   res = res or self._name:sub(2)
+   return (res:gsub("-", "_"))
+end
+
+function Option:_is_vararg()
+   return self._maxargs ~= self._minargs
+end
+
+function Parser:_get_fullname()
+   local parent = self._parent
+   local buf = {self._name}
+
+   while parent do
+      table.insert(buf, 1, parent._name)
+      parent = parent._parent
+   end
+
+   return table.concat(buf, " ")
+end
+
+function Parser:_update_charset(charset)
+   charset = charset or {}
+
+   for _, command in ipairs(self._commands) do
+      command:_update_charset(charset)
+   end
+
+   for _, option in ipairs(self._options) do
+      for _, alias in ipairs(option._aliases) do
+         charset[alias:sub(1, 1)] = true
+      end
+   end
+
+   return charset
+end
+
+function Parser:argument(...)
+   local argument = Argument(...)
+   table.insert(self._arguments, argument)
+   return argument
+end
+
+function Parser:option(...)
+   local option = Option(...)
+
+   if self._has_help then
+      table.insert(self._options, #self._options, option)
+   else
+      table.insert(self._options, option)
+   end
+
+   return option
+end
+
+function Parser:flag(...)
+   return self:option():args(0)(...)
+end
+
+function Parser:command(...)
+   local command = Command():add_help(true)(...)
+   command._parent = self
+   table.insert(self._commands, command)
+   return command
+end
+
+function Parser:mutex(...)
+   local options = {...}
+
+   for i, option in ipairs(options) do
+      assert(getmetatable(option) == Option, ("bad argument #%d to 'mutex' (Option expected)"):format(i))
+   end
+
+   table.insert(self._mutexes, options)
+   return self
+end
+
+local max_usage_width = 70
+local usage_welcome = "Usage: "
+
+function Parser:get_usage()
+   if self._usage then
+      return self._usage
+   end
+
+   local lines = {usage_welcome .. self:_get_fullname()}
+
+   local function add(s)
+      if #lines[#lines]+1+#s <= max_usage_width then
+         lines[#lines] = lines[#lines] .. " " .. s
+      else
+         lines[#lines+1] = (" "):rep(#usage_welcome) .. s
+      end
+   end
+
+   -- This can definitely be refactored into something cleaner
+   local mutex_options = {}
+   local vararg_mutexes = {}
+
+   -- First, put mutexes which do not contain vararg options and remember those which do
+   for _, mutex in ipairs(self._mutexes) do
+      local buf = {}
+      local is_vararg = false
+
+      for _, option in ipairs(mutex) do
+         if option:_is_vararg() then
+            is_vararg = true
+         end
+
+         table.insert(buf, option:_get_usage())
+         mutex_options[option] = true
+      end
+
+      local repr = "(" .. table.concat(buf, " | ") .. ")"
+
+      if is_vararg then
+         table.insert(vararg_mutexes, repr)
+      else
+         add(repr)
+      end
+   end
+
+   -- Second, put regular options
+   for _, option in ipairs(self._options) do
+      if not mutex_options[option] and not option:_is_vararg() then
+         add(option:_get_usage())
+      end
+   end
+
+   -- Put positional arguments
+   for _, argument in ipairs(self._arguments) do
+      add(argument:_get_usage())
+   end
+
+   -- Put mutexes containing vararg options
+   for _, mutex_repr in ipairs(vararg_mutexes) do
+      add(mutex_repr)
+   end
+
+   for _, option in ipairs(self._options) do
+      if not mutex_options[option] and option:_is_vararg() then
+         add(option:_get_usage())
+      end
+   end
+
+   if #self._commands > 0 then
+      if self._require_command then
+         add("<command>")
+      else
+         add("[<command>]")
+      end
+
+      add("...")
+   end
+
+   return table.concat(lines, "\n")
+end
+
+local margin_len = 3
+local margin_len2 = 25
+local margin = (" "):rep(margin_len)
+local margin2 = (" "):rep(margin_len2)
+
+local function make_two_columns(s1, s2)
+   if s2 == "" then
+      return margin .. s1
+   end
+
+   s2 = s2:gsub("\n", "\n" .. margin2)
+
+   if #s1 < (margin_len2-margin_len) then
+      return margin .. s1 .. (" "):rep(margin_len2-margin_len-#s1) .. s2
+   else
+      return margin .. s1 .. "\n" .. margin2 .. s2
+   end
+end
+
+function Parser:get_help()
+   if self._help then
+      return self._help
+   end
+
+   local blocks = {self:get_usage()}
+   
+   if self._description then
+      table.insert(blocks, self._description)
+   end
+
+   local labels = {"Arguments:", "Options:", "Commands:"}
+
+   for i, elements in ipairs{self._arguments, self._options, self._commands} do
+      if #elements > 0 then
+         local buf = {labels[i]}
+
+         for _, element in ipairs(elements) do
+            table.insert(buf, make_two_columns(element:_get_label(), element:_get_description()))
+         end
+
+         table.insert(blocks, table.concat(buf, "\n"))
+      end
+   end
+
+   if self._epilog then
+      table.insert(blocks, self._epilog)
+   end
+
+   return table.concat(blocks, "\n\n")
+end
+
+local function get_tip(context, wrong_name)
+   local context_pool = {}
+   local possible_name
+   local possible_names = {}
+
+   for name in pairs(context) do
+      for i=1, #name do
+         possible_name = name:sub(1, i-1) .. name:sub(i+1)
+
+         if not context_pool[possible_name] then
+            context_pool[possible_name] = {}
+         end
+
+         table.insert(context_pool[possible_name], name)
+      end
+   end
+
+   for i=1, #wrong_name+1 do
+      possible_name = wrong_name:sub(1, i-1) .. wrong_name:sub(i+1)
+
+      if context[possible_name] then
+         possible_names[possible_name] = true
+      elseif context_pool[possible_name] then
+         for _, name in ipairs(context_pool[possible_name]) do
+            possible_names[name] = true
+         end
+      end
+   end
+
+   local first = next(possible_names)
+   if first then
+      if next(possible_names, first) then
+         local possible_names_arr = {}
+
+         for name in pairs(possible_names) do
+            table.insert(possible_names_arr, "'" .. name .. "'")
+         end
+
+         table.sort(possible_names_arr)
+         return "\nDid you mean one of these: " .. table.concat(possible_names_arr, " ") .. "?"
+      else
+         return "\nDid you mean '" .. first .. "'?"
+      end
+   else
+      return ""
+   end
+end
+
+local function plural(x)
+   if x == 1 then
+      return ""
+   end
+
+   return "s"
+end
+
+-- Compatibility with strict.lua and other checkers:
+local default_cmdline = rawget(_G, "arg") or {}
+
+function Parser:_parse(args, errhandler)
+   args = args or default_cmdline
+   local parser
+   local charset
+   local options = {}
+   local arguments = {}
+   local commands
+   local option_mutexes = {}
+   local used_mutexes = {}
+   local opt_context = {}
+   local com_context
+   local result = {}
+   local invocations = {}
+   local passed = {}
+   local cur_option
+   local cur_arg_i = 1
+   local cur_arg
+   local targets = {}
+   local handle_options = true
+
+   local function error_(fmt, ...)
+      return errhandler(parser, fmt:format(...))
+   end
+
+   local function assert_(assertion, ...)
+      return assertion or error_(...)
+   end
+
+   local function convert(element, data)
+      if element._convert then
+         local ok, err
+
+         if type(element._convert) == "function" then
+            ok, err = element._convert(data)
+         else
+            ok = element._convert[data]
+         end
+
+         assert_(ok ~= nil, "%s", err or "malformed argument '" .. data .. "'")
+         data = ok
+      end
+
+      return data
+   end
+
+   local invoke, pass, close
+
+   function invoke(element)
+      local overwrite = false
+
+      if invocations[element] == element._maxcount then
+         if element._overwrite then
+            overwrite = true
+         else
+            error_("option '%s' must be used at most %d time%s", element._name, element._maxcount, plural(element._maxcount))
+         end
+      else
+         invocations[element] = invocations[element]+1
+      end
+
+      passed[element] = 0
+      local type_ = element:_get_type()
+      local target = targets[element]
+
+      if type_ == "flag" then
+         result[target] = true
+      elseif type_ == "multiarg" then
+         result[target] = {}
+      elseif type_ == "counter" then
+         if not overwrite then
+            result[target] = result[target]+1
+         end
+      elseif type_ == "multicount" then
+         if overwrite then
+            table.remove(result[target], 1)
+         end
+      elseif type_ == "twodimensional" then
+         table.insert(result[target], {})
+
+         if overwrite then
+            table.remove(result[target], 1)
+         end
+      end
+
+      if element._maxargs == 0 then
+         close(element)
+      end
+   end
+
+   function pass(element, data)
+      passed[element] = passed[element]+1
+      data = convert(element, data)
+      local type_ = element:_get_type()
+      local target = targets[element]
+
+      if type_ == "arg" then
+         result[target] = data
+      elseif type_ == "multiarg" or type_ == "multicount" then
+         table.insert(result[target], data)
+      elseif type_ == "twodimensional" then
+         table.insert(result[target][#result[target]], data)
+      end
+
+      if passed[element] == element._maxargs then
+         close(element)
+      end
+   end
+
+   local function complete_invocation(element)
+      while passed[element] < element._minargs do
+         pass(element, element._default)
+      end
+   end
+
+   function close(element)
+      if passed[element] < element._minargs then
+         if element._default and element._defmode:find "a" then
+            complete_invocation(element)
+         else
+            error_("too few arguments")
+         end
+      else
+         if element == cur_option then
+            cur_option = nil
+         elseif element == cur_arg then
+            cur_arg_i = cur_arg_i+1
+            cur_arg = arguments[cur_arg_i]
+         end
+      end
+   end
+
+   local function switch(p)
+      parser = p
+
+      for _, option in ipairs(parser._options) do
+         table.insert(options, option)
+
+         for _, alias in ipairs(option._aliases) do
+            opt_context[alias] = option
+         end
+
+         local type_ = option:_get_type()
+         targets[option] = option._target or option:_get_default_target()
+
+         if type_ == "counter" then
+            result[targets[option]] = 0
+         elseif type_ == "multicount" or type_ == "twodimensional" then
+            result[targets[option]] = {}
+         end
+
+         invocations[option] = 0
+      end
+
+      for _, mutex in ipairs(parser._mutexes) do
+         for _, option in ipairs(mutex) do
+            if not option_mutexes[option] then
+               option_mutexes[option] = {mutex}
+            else
+               table.insert(option_mutexes[option], mutex)
+            end
+         end
+      end
+
+      for _, argument in ipairs(parser._arguments) do
+         table.insert(arguments, argument)
+         invocations[argument] = 0
+         targets[argument] = argument._target or argument._name
+         invoke(argument)
+      end
+
+      handle_options = parser._handle_options
+      cur_arg = arguments[cur_arg_i]
+      commands = parser._commands
+      com_context = {}
+
+      for _, command in ipairs(commands) do
+         targets[command] = command._target or command._name
+
+         for _, alias in ipairs(command._aliases) do
+            com_context[alias] = command
+         end
+      end
+   end
+
+   local function get_option(name)
+      return assert_(opt_context[name], "unknown option '%s'%s", name, get_tip(opt_context, name))
+   end
+
+   local function do_action(element)
+      if element._action then
+         element._action()
+      end
+   end
+
+   local function handle_argument(data)
+      if cur_option then
+         pass(cur_option, data)
+      elseif cur_arg then
+         pass(cur_arg, data)
+      else
+         local com = com_context[data]
+
+         if not com then
+            if #commands > 0 then
+               error_("unknown command '%s'%s", data, get_tip(com_context, data))
+            else
+               error_("too many arguments")
+            end
+         else
+            result[targets[com]] = true
+            do_action(com)
+            switch(com)
+         end
+      end
+   end
+
+   local function handle_option(data)
+      if cur_option then
+         close(cur_option)
+      end
+
+      cur_option = opt_context[data]
+
+      if option_mutexes[cur_option] then
+         for _, mutex in ipairs(option_mutexes[cur_option]) do
+            if used_mutexes[mutex] and used_mutexes[mutex] ~= cur_option then
+               error_("option '%s' can not be used together with option '%s'", data, used_mutexes[mutex]._name)
+            else
+               used_mutexes[mutex] = cur_option
+            end
+         end
+      end
+
+      do_action(cur_option)
+      invoke(cur_option)
+   end
+
+   local function mainloop()
+
+      for _, data in ipairs(args) do
+         local plain = true
+         local first, name, option
+
+         if handle_options then
+            first = data:sub(1, 1)
+            if charset[first] then
+               if #data > 1 then
+                  plain = false
+                  if data:sub(2, 2) == first then
+                     if #data == 2 then
+                        if cur_option then
+                           close(cur_option)
+                        end
+
+                        handle_options = false
+                     else
+                        local equal = data:find "="
+                        if equal then
+                           name = data:sub(1, equal-1)
+                           option = get_option(name)
+                           assert_(option._maxargs > 0, "option '%s' does not take arguments", name)
+
+                           handle_option(data:sub(1, equal-1))
+                           handle_argument(data:sub(equal+1))
+                        else
+                           get_option(data)
+                           handle_option(data)
+                        end
+                     end
+                  else
+                     for i = 2, #data do
+                        name = first .. data:sub(i, i)
+                        option = get_option(name)
+                        handle_option(name)
+
+                        if i ~= #data and option._minargs > 0 then
+                           handle_argument(data:sub(i+1))
+                           break
+                        end
+                     end
+                  end
+               end
+            end
+         end
+
+         if plain then
+            handle_argument(data)
+         end
+      end
+   end
+
+   switch(self)
+   charset = parser:_update_charset()
+   mainloop()
+
+   if cur_option then
+      close(cur_option)
+   end
+
+   while cur_arg do
+      if passed[cur_arg] == 0 and cur_arg._default and cur_arg._defmode:find "u" then
+         complete_invocation(cur_arg)
+      else
+         close(cur_arg)
+      end
+   end
+
+   if parser._require_command and #commands > 0 then
+      error_("a command is required")
+   end
+
+   for _, option in ipairs(options) do
+      if invocations[option] == 0 then
+         if option._default and option._defmode:find "u" then
+            invoke(option)
+            complete_invocation(option)
+            close(option)
+         end
+      end
+
+      if invocations[option] < option._mincount then
+         if option._default and option._defmode:find "a" then
+            while invocations[option] < option._mincount do
+               invoke(option)
+               close(option)
+            end
+         else
+            error_("option '%s' must be used at least %d time%s", option._name, option._mincount, plural(option._mincount))
+         end
+      end
+   end
+
+   return result
+end
+
+function Parser:error(msg)
+   io.stderr:write(("%s\n\nError: %s\n"):format(self:get_usage(), msg))
+   os.exit(1)
+end
+
+function Parser:parse(args)
+   return self:_parse(args, Parser.error)
+end
+
+function Parser:pparse(args)
+   local errmsg
+   local ok, result = pcall(function()
+      return self:_parse(args, function(_, err)
+         errmsg = err
+         return error()
+      end)
+   end)
+
+   if ok then
+      return true, result
+   else
+      assert(errmsg, result)
+      return false, errmsg
+   end
+end
+
+return function(...)
+   return Parser(default_cmdline[0]):add_help(true)(...)
+end

+ 107 - 0
tools/wrk-scripts/lib/cookie.lua

@@ -0,0 +1,107 @@
+-- The MIT License (MIT)
+-- 
+-- Copyright (c) 2014 Cyril David <cyx@cyx.is>
+-- 
+-- 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.
+
+local uri = require("lib/uri-f570bf7")
+
+local insert = table.insert
+local concat = table.concat
+
+local format = string.format
+local gmatch = string.gmatch
+local sub    = string.sub
+local gsub   = string.gsub
+local find   = string.find
+
+-- identity function
+local function identity(val)
+	return val
+end
+
+-- trim and remove double quotes
+local function clean(str)
+	  local s = gsub(str, "^%s*(.-)%s*$", "%1")
+	  
+	  if sub(s, 1, 1) == '"' then
+		  s = sub(s, 2, -2)
+	  end
+
+	  return s
+end
+
+-- given a unix timestamp, return a utc string
+local function to_utc_string(time)
+	return os.date("%a, %d %b %Y %H:%I:%S GMT", time)
+end
+
+local CODEX = {
+	{"max_age", "Max-Age=%d", identity},
+	{"domain", "Domain=%s", identity},
+	{"path", "Path=%s", identity},
+	{"expires", "Expires=%s", to_utc_string},
+	{"http_only", "HttpOnly", identity},
+	{"secure", "Secure", identity},
+}
+
+local function build(dict, options)
+	options = options or {}
+
+	local res = {}
+
+	for k, v in pairs(dict) do
+		insert(res, format("%s=%s", k, uri.encode(v)))
+	end
+
+	for _, tuple in ipairs(CODEX) do
+		local key, template, fn = unpack(tuple)
+		local val = options[key]
+
+		if val then
+			insert(res, format(template, fn(val)))
+		end
+	end
+
+	return concat(res, "; ")
+end
+
+local function parse(data)
+	local res = {}
+
+	for pair in gmatch(data, "[^;]+") do
+		local eq = find(pair, "=")
+
+		if eq then
+			local key = clean(sub(pair, 1, eq-1))
+			local val = clean(sub(pair, eq+1))
+
+			if not res[key] then
+				res[key] = uri.decode(val)
+			end
+		end
+	end
+
+	return res
+end
+
+return {
+	build = build,
+	parse = parse
+}

+ 11 - 0
tools/wrk-scripts/lib/copy.lua

@@ -0,0 +1,11 @@
+local copy = {}
+
+copy.shallow_copy = function(t)
+  local t2 = {}
+  for k,v in pairs(t) do
+    t2[k] = v
+  end
+  return t2
+end
+
+return copy

+ 49 - 0
tools/wrk-scripts/lib/equal-5bb8dbf.lua

@@ -0,0 +1,49 @@
+-- The MIT License (MIT)
+-- 
+-- Copyright (c) 2014, Cyril David <cyx@cyx.is>
+-- 
+-- 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.
+
+local function equal(x, y)
+	local t1, t2 = type(x), type(y)
+
+	-- Shortcircuit if types not equal.
+	if t1 ~= t2 then return false end
+
+	-- For primitive types, direct comparison works.
+	if t1 ~= 'table' and t2 ~= 'table' then return x == y end
+
+	-- Since we have two tables, make sure both have the same
+	-- length so we can avoid looping over different length arrays.
+	if #x ~= #y then return false end
+
+	-- Case 1: check over all keys of x
+	for k,v in pairs(x) do
+		if not equal(v, y[k]) then return false end
+	end
+
+	-- Case 2: check over `y` this time.
+	for k,v in pairs(y) do
+		if not equal(v, x[k]) then return false end
+	end
+
+	return true
+end
+
+return equal

+ 73 - 0
tools/wrk-scripts/lib/hue.lua

@@ -0,0 +1,73 @@
+local argparse = require("lib/argparse")
+local cookie = require("lib/cookie")
+local uri = require("lib/uri-f570bf7")
+local inspect = require("lib/inspect")
+
+local hue = {}
+
+hue.parse_args = function(args)
+	local parser = argparse()
+	parser:option "-X" "--method"
+		:description "HTTP method"
+	parser:option "-s" "--session"
+		:description "session cookie"
+
+	parser:flag "-v" "--verbose"
+		:description "print verbosely"
+
+	local args = parser:parse(args)
+
+	if args["method"] then
+		wrk.method = args["method"]
+	end
+
+	if args["session"] then
+		wrk.headers["Cookie"] = cookie.build({
+			sessionid=args["session"]
+		})
+	end
+
+	hue.verbose = args.verbose
+end
+
+hue.handle_redirect = function(status, location)
+	local location = uri.parse(location)
+
+	wrk.scheme = location.scheme
+	wrk.host = location.host
+	wrk.port = location.port
+	wrk.path = location.path
+
+	if location.query then
+		wrk.path = wrk.path .. "?" .. location.query
+	end
+
+	if location.fragment then
+		wrk.path = wrk.path .. "#" .. location.fragment
+	end
+
+	if status == 303 then
+		wrk.method = "GET"
+	end
+end
+
+hue.set_cookies = function(set_cookie)
+	local req_cookie = cookie.parse(wrk.headers["Cookie"] or "")
+	local rep_cookie = cookie.parse(set_cookie)
+
+	for key, val in pairs(rep_cookie) do
+		-- ignore the cookie metadata
+		if key ~= "Path" and key ~= "expires" and key ~= "HttpOnly" and key ~= "Max-Age" then
+			req_cookie[key] = val
+		end
+
+		-- Make sure we handle the 
+		if key == "csrftoken" then
+			wrk.headers["X-CSRFToken"] = val
+		end
+	end
+
+	wrk.headers["Cookie"] = cookie.build(req_cookie)
+end
+
+return hue

+ 328 - 0
tools/wrk-scripts/lib/inspect.lua

@@ -0,0 +1,328 @@
+local inspect ={
+  _VERSION = 'inspect.lua 3.0.0',
+  _URL     = 'http://github.com/kikito/inspect.lua',
+  _DESCRIPTION = 'human-readable representations of tables',
+  _LICENSE = [[
+    MIT LICENSE
+
+    Copyright (c) 2013 Enrique García Cota
+
+    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.
+  ]]
+}
+
+inspect.KEY       = setmetatable({}, {__tostring = function() return 'inspect.KEY' end})
+inspect.METATABLE = setmetatable({}, {__tostring = function() return 'inspect.METATABLE' end})
+
+-- Apostrophizes the string if it has quotes, but not aphostrophes
+-- Otherwise, it returns a regular quoted string
+local function smartQuote(str)
+  if str:match('"') and not str:match("'") then
+    return "'" .. str .. "'"
+  end
+  return '"' .. str:gsub('"', '\\"') .. '"'
+end
+
+local controlCharsTranslation = {
+  ["\a"] = "\\a",  ["\b"] = "\\b", ["\f"] = "\\f",  ["\n"] = "\\n",
+  ["\r"] = "\\r",  ["\t"] = "\\t", ["\v"] = "\\v"
+}
+
+local function escape(str)
+  local result = str:gsub("\\", "\\\\"):gsub("(%c)", controlCharsTranslation)
+  return result
+end
+
+local function isIdentifier(str)
+  return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" )
+end
+
+local function isSequenceKey(k, length)
+  return type(k) == 'number'
+     and 1 <= k
+     and k <= length
+     and math.floor(k) == k
+end
+
+local defaultTypeOrders = {
+  ['number']   = 1, ['boolean']  = 2, ['string'] = 3, ['table'] = 4,
+  ['function'] = 5, ['userdata'] = 6, ['thread'] = 7
+}
+
+local function sortKeys(a, b)
+  local ta, tb = type(a), type(b)
+
+  -- strings and numbers are sorted numerically/alphabetically
+  if ta == tb and (ta == 'string' or ta == 'number') then return a < b end
+
+  local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb]
+  -- Two default types are compared according to the defaultTypeOrders table
+  if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb]
+  elseif dta     then return true  -- default types before custom ones
+  elseif dtb     then return false -- custom types after default ones
+  end
+
+  -- custom types are sorted out alphabetically
+  return ta < tb
+end
+
+local function getNonSequentialKeys(t)
+  local keys, length = {}, #t
+  for k,_ in pairs(t) do
+    if not isSequenceKey(k, length) then table.insert(keys, k) end
+  end
+  table.sort(keys, sortKeys)
+  return keys
+end
+
+local function getToStringResultSafely(t, mt)
+  local __tostring = type(mt) == 'table' and rawget(mt, '__tostring')
+  local str, ok
+  if type(__tostring) == 'function' then
+    ok, str = pcall(__tostring, t)
+    str = ok and str or 'error: ' .. tostring(str)
+  end
+  if type(str) == 'string' and #str > 0 then return str end
+end
+
+local maxIdsMetaTable = {
+  __index = function(self, typeName)
+    rawset(self, typeName, 0)
+    return 0
+  end
+}
+
+local idsMetaTable = {
+  __index = function (self, typeName)
+    local col = setmetatable({}, {__mode = "kv"})
+    rawset(self, typeName, col)
+    return col
+  end
+}
+
+local function countTableAppearances(t, tableAppearances)
+  tableAppearances = tableAppearances or setmetatable({}, {__mode = "k"})
+
+  if type(t) == 'table' then
+    if not tableAppearances[t] then
+      tableAppearances[t] = 1
+      for k,v in pairs(t) do
+        countTableAppearances(k, tableAppearances)
+        countTableAppearances(v, tableAppearances)
+      end
+      countTableAppearances(getmetatable(t), tableAppearances)
+    else
+      tableAppearances[t] = tableAppearances[t] + 1
+    end
+  end
+
+  return tableAppearances
+end
+
+local copySequence = function(s)
+  local copy, len = {}, #s
+  for i=1, len do copy[i] = s[i] end
+  return copy, len
+end
+
+local function makePath(path, ...)
+  local keys = {...}
+  local newPath, len = copySequence(path)
+  for i=1, #keys do
+    newPath[len + i] = keys[i]
+  end
+  return newPath
+end
+
+local function processRecursive(process, item, path)
+  if item == nil then return nil end
+
+  local processed = process(item, path)
+  if type(processed) == 'table' then
+    local processedCopy = {}
+    local processedKey
+
+    for k,v in pairs(processed) do
+      processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY))
+      if processedKey ~= nil then
+        processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey))
+      end
+    end
+
+    local mt  = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE))
+    setmetatable(processedCopy, mt)
+    processed = processedCopy
+  end
+  return processed
+end
+
+
+-------------------------------------------------------------------
+
+local Inspector = {}
+local Inspector_mt = {__index = Inspector}
+
+function Inspector:puts(...)
+  local args   = {...}
+  local buffer = self.buffer
+  local len    = #buffer
+  for i=1, #args do
+    len = len + 1
+    buffer[len] = tostring(args[i])
+  end
+end
+
+function Inspector:down(f)
+  self.level = self.level + 1
+  f()
+  self.level = self.level - 1
+end
+
+function Inspector:tabify()
+  self:puts(self.newline, string.rep(self.indent, self.level))
+end
+
+function Inspector:alreadyVisited(v)
+  return self.ids[type(v)][v] ~= nil
+end
+
+function Inspector:getId(v)
+  local tv = type(v)
+  local id = self.ids[tv][v]
+  if not id then
+    id              = self.maxIds[tv] + 1
+    self.maxIds[tv] = id
+    self.ids[tv][v] = id
+  end
+  return id
+end
+
+function Inspector:putKey(k)
+  if isIdentifier(k) then return self:puts(k) end
+  self:puts("[")
+  self:putValue(k)
+  self:puts("]")
+end
+
+function Inspector:putTable(t)
+  if t == inspect.KEY or t == inspect.METATABLE then
+    self:puts(tostring(t))
+  elseif self:alreadyVisited(t) then
+    self:puts('<table ', self:getId(t), '>')
+  elseif self.level >= self.depth then
+    self:puts('{...}')
+  else
+    if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end
+
+    local nonSequentialKeys = getNonSequentialKeys(t)
+    local length            = #t
+    local mt                = getmetatable(t)
+    local toStringResult    = getToStringResultSafely(t, mt)
+
+    self:puts('{')
+    self:down(function()
+      if toStringResult then
+        self:puts(' -- ', escape(toStringResult))
+        if length >= 1 then self:tabify() end
+      end
+
+      local count = 0
+      for i=1, length do
+        if count > 0 then self:puts(',') end
+        self:puts(' ')
+        self:putValue(t[i])
+        count = count + 1
+      end
+
+      for _,k in ipairs(nonSequentialKeys) do
+        if count > 0 then self:puts(',') end
+        self:tabify()
+        self:putKey(k)
+        self:puts(' = ')
+        self:putValue(t[k])
+        count = count + 1
+      end
+
+      if mt then
+        if count > 0 then self:puts(',') end
+        self:tabify()
+        self:puts('<metatable> = ')
+        self:putValue(mt)
+      end
+    end)
+
+    if #nonSequentialKeys > 0 or mt then -- result is multi-lined. Justify closing }
+      self:tabify()
+    elseif length > 0 then -- array tables have one extra space before closing }
+      self:puts(' ')
+    end
+
+    self:puts('}')
+  end
+end
+
+function Inspector:putValue(v)
+  local tv = type(v)
+
+  if tv == 'string' then
+    self:puts(smartQuote(escape(v)))
+  elseif tv == 'number' or tv == 'boolean' or tv == 'nil' then
+    self:puts(tostring(v))
+  elseif tv == 'table' then
+    self:putTable(v)
+  else
+    self:puts('<',tv,' ',self:getId(v),'>')
+  end
+end
+
+-------------------------------------------------------------------
+
+function inspect.inspect(root, options)
+  options       = options or {}
+
+  local depth   = options.depth   or math.huge
+  local newline = options.newline or '\n'
+  local indent  = options.indent  or '  '
+  local process = options.process
+
+  if process then
+    root = processRecursive(process, root, {})
+  end
+
+  local inspector = setmetatable({
+    depth            = depth,
+    buffer           = {},
+    level            = 0,
+    ids              = setmetatable({}, idsMetaTable),
+    maxIds           = setmetatable({}, maxIdsMetaTable),
+    newline          = newline,
+    indent           = indent,
+    tableAppearances = countTableAppearances(root)
+  }, Inspector_mt)
+
+  inspector:putValue(root)
+
+  return table.concat(inspector.buffer)
+end
+
+setmetatable(inspect, { __call = function(_, ...) return inspect.inspect(...) end })
+
+return inspect
+

+ 1114 - 0
tools/wrk-scripts/lib/old/argparse.lua

@@ -0,0 +1,1114 @@
+local Parser, Command, Argument, Option
+
+local function require_30log()
+-------------------------------------------------------------------------------
+local assert       = assert
+local pairs        = pairs
+local type         = type
+local tostring     = tostring
+local setmetatable = setmetatable
+
+local baseMt     = {}
+local _instances = setmetatable({},{__mode='k'})
+local _classes   = setmetatable({},{__mode='k'})
+local class
+
+local function deep_copy(t, dest, aType)
+  local t = t or {}
+  local r = dest or {}
+  
+  for k,v in pairs(t) do
+    if aType and type(v)==aType then 
+      r[k] = v 
+    elseif not aType then
+      if type(v) == 'table' and k ~= "__index" then 
+        r[k] = deep_copy(v) 
+      else
+        r[k] = v
+      end
+    end
+  end
+  
+  return r
+end
+
+local function instantiate(self,...)
+  assert(_classes[self], 'new() should be called from a class.')
+  local instance = deep_copy(self)
+  _instances[instance] = tostring(instance)
+  setmetatable(instance,self)
+  
+  if self.__init then
+    if type(self.__init) == 'table' then
+      deep_copy(self.__init, instance)
+    else
+      self.__init(instance, ...)
+    end
+  end
+  
+  return instance
+end
+
+local function extends(self,extra_params)
+  local heir = {}
+  _classes[heir] = tostring(heir)
+  deep_copy(extra_params, deep_copy(self, heir))
+  heir.__index = heir
+  heir.super = self
+  return setmetatable(heir,self)
+end
+
+baseMt = {
+  __call = function (self,...)
+    return self:new(...)
+  end,
+  
+  __tostring = function(self,...)
+    if _instances[self] then
+      return 
+        ('object(of %s):<%s>')
+          :format((rawget(getmetatable(self),'__name') or '?'), _instances[self])
+    end
+    
+    return
+      _classes[self] and 
+      ('class(%s):<%s>')
+        :format((rawget(self,'__name') or '?'),_classes[self]) or 
+      self      
+  end
+}
+
+class = function(attr)
+  local c = deep_copy(attr)
+  _classes[c] = tostring(c)
+  c.include = function(self,include)
+    assert(_classes[self], 'Mixins can only be used on classes.')
+    return deep_copy(include, self, 'function')
+  end
+  
+  c.new = instantiate
+  c.extends = extends
+  c.__index = c
+  c.__call = baseMt.__call
+  c.__tostring = baseMt.__tostring
+  
+  c.is = function(self, kind)
+    local super
+    while true do 
+      super = getmetatable(super or self)
+      if super == kind or super == nil then break end
+    end
+    return kind and (super == kind)
+  end
+  return setmetatable(c, baseMt)
+end
+
+return class
+-------------------------------------------------------------------------------
+end
+
+do -- Create classes with setters
+   local class = require_30log()
+
+   local function add_setters(cl, fields)
+      for field, setter in pairs(fields) do
+         cl[field] = function(self, value)
+            setter(self, value)
+            self["_"..field] = value
+            return self
+         end
+      end
+
+      cl.__init = function(self, ...)
+         return self(...)
+      end
+
+      cl.__call = function(self, ...)
+         local name_or_options
+
+         for i=1, select("#", ...) do
+            name_or_options = select(i, ...)
+
+            if type(name_or_options) == "string" then
+               if self._aliases then
+                  table.insert(self._aliases, name_or_options)
+               end
+
+               if not self._aliases or not self._name then
+                  self._name = name_or_options
+               end
+            elseif type(name_or_options) == "table" then
+               for field in pairs(fields) do
+                  if name_or_options[field] ~= nil then
+                     self[field](self, name_or_options[field])
+                  end
+               end
+            end
+         end
+
+         return self
+      end
+
+      return cl
+   end
+
+   local typecheck = setmetatable({}, {
+      __index = function(self, type_)
+         local typechecker_factory = function(field)
+            return function(_, value)
+               if type(value) ~= type_ then
+                  error(("bad field '%s' (%s expected, got %s)"):format(field, type_, type(value)))
+               end
+            end
+         end
+
+         self[type_] = typechecker_factory
+         return typechecker_factory
+      end
+   })
+
+   local function aliased_name(self, name)
+      typecheck.string "name" (self, name)
+
+      table.insert(self._aliases, name)
+   end
+
+   local function aliased_aliases(self, aliases)
+      typecheck.table "aliases" (self, aliases)
+
+      if not self._name then
+         self._name = aliases[1]
+      end
+   end
+
+   local function parse_boundaries(boundaries)
+      if tonumber(boundaries) then
+         return tonumber(boundaries), tonumber(boundaries)
+      end
+
+      if boundaries == "*" then
+         return 0, math.huge
+      end
+
+      if boundaries == "+" then
+         return 1, math.huge
+      end
+
+      if boundaries == "?" then
+         return 0, 1
+      end
+
+      if boundaries:match "^%d+%-%d+$" then
+         local min, max = boundaries:match "^(%d+)%-(%d+)$"
+         return tonumber(min), tonumber(max)
+      end
+
+      if boundaries:match "^%d+%+$" then
+         local min = boundaries:match "^(%d+)%+$"
+         return tonumber(min), math.huge
+      end
+   end
+
+   local function boundaries(field)
+      return function(self, value)
+         local min, max = parse_boundaries(value)
+
+         if not min then
+            error(("bad field '%s'"):format(field))
+         end
+
+         self["_min"..field], self["_max"..field] = min, max
+      end
+   end
+
+   local function convert(self, value)
+      if type(value) ~= "function" then
+         if type(value) ~= "table" then
+            error(("bad field 'convert' (function or table expected, got %s)"):format(type(value)))
+         end
+      end
+   end
+
+   local function argname(self, value)
+      if type(value) ~= "string" then
+         if type(value) ~= "table" then
+            error(("bad field 'argname' (string or table expected, got %s)"):format(type(value)))
+         end
+      end
+   end
+
+   local function add_help(self, param)
+      if self._has_help then
+         table.remove(self._options)
+         self._has_help = false
+      end
+
+      if param then
+         local help = self:flag()
+            :description "Show this help message and exit. "
+            :action(function()
+               io.stdout:write(self:get_help() .. "\r\n")
+               os.exit(0)
+            end)(param)
+
+         if not help._name then
+            help "-h" "--help"
+         end
+
+         self._has_help = true
+      end
+   end
+
+   Parser = add_setters(class {
+      __name = "Parser",
+      _arguments = {},
+      _options = {},
+      _commands = {},
+      _mutexes = {},
+      _require_command = true
+   }, {
+      name = typecheck.string "name",
+      description = typecheck.string "description",
+      epilog = typecheck.string "epilog",
+      require_command = typecheck.boolean "require_command",
+      usage = typecheck.string "usage",
+      help = typecheck.string "help",
+      add_help = add_help
+   })
+
+   Command = add_setters(Parser:extends {
+      __name = "Command",
+      _aliases = {}
+   }, {
+      name = aliased_name,
+      aliases = aliased_aliases,
+      description = typecheck.string "description",
+      epilog = typecheck.string "epilog",
+      target = typecheck.string "target",
+      require_command = typecheck.boolean "require_command",
+      action = typecheck["function"] "action",
+      usage = typecheck.string "usage",
+      help = typecheck.string "help",
+      add_help = add_help
+   })
+
+   Argument = add_setters(class {
+      __name = "Argument",
+      _minargs = 1,
+      _maxargs = 1,
+      _mincount = 1,
+      _maxcount = 1,
+      _defmode = "unused",
+      _show_default = true
+   }, {
+      name = typecheck.string "name",
+      description = typecheck.string "description",
+      target = typecheck.string "target",
+      args = boundaries "args",
+      default = typecheck.string "default",
+      defmode = typecheck.string "defmode",
+      convert = convert,
+      argname = argname,
+      show_default = typecheck.boolean "show_default"
+   })
+
+   Option = add_setters(Argument:extends {
+      __name = "Option",
+      _aliases = {},
+      _mincount = 0,
+      _overwrite = true
+   }, {
+      name = aliased_name,
+      aliases = aliased_aliases,
+      description = typecheck.string "description",
+      target = typecheck.string "target",
+      args = boundaries "args",
+      count = boundaries "count",
+      default = typecheck.string "default",
+      defmode = typecheck.string "defmode",
+      convert = convert,
+      overwrite = typecheck.boolean "overwrite",
+      action = typecheck["function"] "action",
+      argname = argname,
+      show_default = typecheck.boolean "show_default"
+   })
+end
+
+function Argument:_get_argument_list()
+   local buf = {}
+   local i = 1
+
+   while i <= math.min(self._minargs, 3) do
+      local argname = self:_get_argname(i)
+
+      if self._default and self._defmode:find "a" then
+         argname = "[" .. argname .. "]"
+      end
+
+      table.insert(buf, argname)
+      i = i+1
+   end
+
+   while i <= math.min(self._maxargs, 3) do
+      table.insert(buf, "[" .. self:_get_argname(i) .. "]")
+      i = i+1
+
+      if self._maxargs == math.huge then
+         break
+      end
+   end
+
+   if i < self._maxargs then
+      table.insert(buf, "...")
+   end
+
+   return buf
+end
+
+function Argument:_get_usage()
+   local usage = table.concat(self:_get_argument_list(), " ")
+
+   if self._default and self._defmode:find "u" then
+      if self._maxargs > 1 or (self._minargs == 1 and not self._defmode:find "a") then
+         usage = "[" .. usage .. "]"
+      end
+   end
+
+   return usage
+end
+
+function Argument:_get_type()
+   if self._maxcount == 1 then
+      if self._maxargs == 0 then
+         return "flag"
+      elseif self._maxargs == 1 and (self._minargs == 1 or self._mincount == 1) then
+         return "arg"
+      else
+         return "multiarg"
+      end
+   else
+      if self._maxargs == 0 then
+         return "counter"
+      elseif self._maxargs == 1 and self._minargs == 1 then
+         return "multicount"
+      else
+         return "twodimensional"
+      end
+   end
+end
+
+-- Returns placeholder for `narg`-th argument. 
+function Argument:_get_argname(narg)
+   local argname = self._argname or self:_get_default_argname()
+
+   if type(argname) == "table" then
+      return argname[narg]
+   else
+      return argname
+   end
+end
+
+function Argument:_get_default_argname()
+   return "<" .. self._name .. ">"
+end
+
+function Option:_get_default_argname()
+   return "<" .. self:_get_default_target() .. ">"
+end
+
+-- Returns label to be shown in the help message. 
+function Argument:_get_label()
+   return self._name
+end
+
+function Option:_get_label()
+   local variants = {}
+   local argument_list = self:_get_argument_list()
+   table.insert(argument_list, 1, nil)
+
+   for _, alias in ipairs(self._aliases) do
+      argument_list[1] = alias
+      table.insert(variants, table.concat(argument_list, " "))
+   end
+
+   return table.concat(variants, ", ")
+end
+
+function Command:_get_label()
+   return table.concat(self._aliases, ", ")
+end
+
+function Argument:_get_description()
+   if self._default and self._show_default then
+      if self._description then
+         return ("%s (default: %s)"):format(self._description, self._default)
+      else
+         return ("default: %s"):format(self._default)
+      end
+   else
+      return self._description or ""
+   end
+end
+
+function Command:_get_description()
+   return self._description or ""
+end
+
+function Option:_get_usage()
+   local usage = self:_get_argument_list()
+   table.insert(usage, 1, self._name)
+   usage = table.concat(usage, " ")
+
+   if self._mincount == 0 or self._default then
+      usage = "[" .. usage .. "]"
+   end
+
+   return usage
+end
+
+function Option:_get_default_target()
+   local res
+
+   for _, alias in ipairs(self._aliases) do
+      if alias:sub(1, 1) == alias:sub(2, 2) then
+         res = alias:sub(3)
+         break
+      end
+   end
+
+   res = res or self._name:sub(2)
+   return (res:gsub("-", "_"))
+end
+
+function Option:_is_vararg()
+   return self._maxargs ~= self._minargs
+end
+
+function Parser:_get_fullname()
+   local parent = self._parent
+   local buf = {self._name}
+
+   while parent do
+      table.insert(buf, 1, parent._name)
+      parent = parent._parent
+   end
+
+   return table.concat(buf, " ")
+end
+
+function Parser:_update_charset(charset)
+   charset = charset or {}
+
+   for _, command in ipairs(self._commands) do
+      command:_update_charset(charset)
+   end
+
+   for _, option in ipairs(self._options) do
+      for _, alias in ipairs(option._aliases) do
+         charset[alias:sub(1, 1)] = true
+      end
+   end
+
+   return charset
+end
+
+function Parser:argument(...)
+   local argument = Argument:new(...)
+   table.insert(self._arguments, argument)
+   return argument
+end
+
+function Parser:option(...)
+   local option = Option:new(...)
+
+   if self._has_help then
+      table.insert(self._options, #self._options, option)
+   else
+      table.insert(self._options, option)
+   end
+
+   return option
+end
+
+function Parser:flag(...)
+   return self:option():args(0)(...)
+end
+
+function Parser:command(...)
+   local command = Command:new():add_help(true)(...)
+   command._parent = self
+   table.insert(self._commands, command)
+   return command
+end
+
+function Parser:mutex(...)
+   local options = {...}
+
+   for i, option in ipairs(options) do
+      assert(getmetatable(option) == Option, ("bad argument #%d to 'mutex' (Option expected)"):format(i))
+   end
+
+   table.insert(self._mutexes, options)
+   return self
+end
+
+local max_usage_width = 70
+local usage_welcome = "Usage: "
+
+function Parser:get_usage()
+   if self._usage then
+      return self._usage
+   end
+
+   local lines = {usage_welcome .. self:_get_fullname()}
+
+   local function add(s)
+      if #lines[#lines]+1+#s <= max_usage_width then
+         lines[#lines] = lines[#lines] .. " " .. s
+      else
+         lines[#lines+1] = (" "):rep(#usage_welcome) .. s
+      end
+   end
+
+   -- This can definitely be refactored into something cleaner
+   local mutex_options = {}
+   local vararg_mutexes = {}
+
+   -- First, put mutexes which do not contain vararg options and remember those which do
+   for _, mutex in ipairs(self._mutexes) do
+      local buf = {}
+      local is_vararg = false
+
+      for _, option in ipairs(mutex) do
+         if option:_is_vararg() then
+            is_vararg = true
+         end
+
+         table.insert(buf, option:_get_usage())
+         mutex_options[option] = true
+      end
+
+      local repr = "(" .. table.concat(buf, " | ") .. ")"
+
+      if is_vararg then
+         table.insert(vararg_mutexes, repr)
+      else
+         add(repr)
+      end
+   end
+
+   -- Second, put regular options
+   for _, option in ipairs(self._options) do
+      if not mutex_options[option] and not option:_is_vararg() then
+         add(option:_get_usage())
+      end
+   end
+
+   -- Put positional arguments
+   for _, argument in ipairs(self._arguments) do
+      add(argument:_get_usage())
+   end
+
+   -- Put mutexes containing vararg options
+   for _, mutex_repr in ipairs(vararg_mutexes) do
+      add(mutex_repr)
+   end
+
+   for _, option in ipairs(self._options) do
+      if not mutex_options[option] and option:_is_vararg() then
+         add(option:_get_usage())
+      end
+   end
+
+   if #self._commands > 0 then
+      if self._require_command then
+         add("<command>")
+      else
+         add("[<command>]")
+      end
+
+      add("...")
+   end
+
+   return table.concat(lines, "\r\n")
+end
+
+local margin_len = 3
+local margin_len2 = 25
+local margin = (" "):rep(margin_len)
+local margin2 = (" "):rep(margin_len2)
+
+local function make_two_columns(s1, s2)
+   if s2 == "" then
+      return margin .. s1
+   end
+
+   s2 = s2:gsub("[\r\n][\r\n]?", function(sub)
+      if #sub == 1 or sub == "\r\n" then
+         return "\r\n" .. margin2
+      else
+         return "\r\n\r\n" .. margin2
+      end
+   end)
+
+   if #s1 < (margin_len2-margin_len) then
+      return margin .. s1 .. (" "):rep(margin_len2-margin_len-#s1) .. s2
+   else
+      return margin .. s1 .. "\r\n" .. margin2 .. s2
+   end
+end
+
+function Parser:get_help()
+   if self._help then
+      return self._help
+   end
+
+   local blocks = {self:get_usage()}
+   
+   if self._description then
+      table.insert(blocks, self._description)
+   end
+
+   local labels = {"Arguments: ", "Options: ", "Commands: "}
+
+   for i, elements in ipairs{self._arguments, self._options, self._commands} do
+      if #elements > 0 then
+         local buf = {labels[i]}
+
+         for _, element in ipairs(elements) do
+            table.insert(buf, make_two_columns(element:_get_label(), element:_get_description()))
+         end
+
+         table.insert(blocks, table.concat(buf, "\r\n"))
+      end
+   end
+
+   if self._epilog then
+      table.insert(blocks, self._epilog)
+   end
+
+   return table.concat(blocks, "\r\n\r\n")
+end
+
+local function get_tip(context, wrong_name)
+   local context_pool = {}
+   local possible_name
+   local possible_names = {}
+
+   for name in pairs(context) do
+      for i=1, #name do
+         possible_name = name:sub(1, i-1) .. name:sub(i+1)
+
+         if not context_pool[possible_name] then
+            context_pool[possible_name] = {}
+         end
+
+         table.insert(context_pool[possible_name], name)
+      end
+   end
+
+   for i=1, #wrong_name+1 do
+      possible_name = wrong_name:sub(1, i-1) .. wrong_name:sub(i+1)
+
+      if context[possible_name] then
+         possible_names[possible_name] = true
+      elseif context_pool[possible_name] then
+         for _, name in ipairs(context_pool[possible_name]) do
+            possible_names[name] = true
+         end
+      end
+   end
+
+   local first = next(possible_names)
+   if first then
+      if next(possible_names, first) then
+         local possible_names_arr = {}
+
+         for name in pairs(possible_names) do
+            table.insert(possible_names_arr, "'" .. name .. "'")
+         end
+
+         table.sort(possible_names_arr)
+         return "\r\nDid you mean one of these: " .. table.concat(possible_names_arr, " ") .. "?"
+      else
+         return "\r\nDid you mean '" .. first .. "'?"
+      end
+   else
+      return ""
+   end
+end
+
+local function plural(x)
+   if x == 1 then
+      return ""
+   end
+
+   return "s"
+end
+
+-- Compatibility with strict.lua and other checkers:
+local default_cmdline = rawget(_G, "arg") or {}
+
+function Parser:_parse(args, errhandler)
+   args = args or default_cmdline
+   local parser
+   local charset
+   local options = {}
+   local arguments = {}
+   local commands
+   local option_mutexes = {}
+   local used_mutexes = {}
+   local opt_context = {}
+   local com_context
+   local result = {}
+   local invocations = {}
+   local passed = {}
+   local cur_option
+   local cur_arg_i = 1
+   local cur_arg
+   local targets = {}
+
+   local function error_(fmt, ...)
+      return errhandler(parser, fmt:format(...))
+   end
+
+   local function assert_(assertion, ...)
+      return assertion or error_(...)
+   end
+
+   local function convert(element, data)
+      if element._convert then
+         local ok, err
+
+         if type(element._convert) == "function" then
+            ok, err = element._convert(data)
+         else
+            ok, err = element._convert[data]
+         end
+
+         assert_(ok ~= nil, "%s", err or "malformed argument '" .. data .. "'")
+         data = ok
+      end
+
+      return data
+   end
+
+   local invoke, pass, close
+
+   function invoke(element)
+      local overwrite = false
+
+      if invocations[element] == element._maxcount then
+         if element._overwrite then
+            overwrite = true
+         else
+            error_("option '%s' must be used at most %d time%s", element._name, element._maxcount, plural(element._maxcount))
+         end
+      else
+         invocations[element] = invocations[element]+1
+      end
+
+      passed[element] = 0
+      local type_ = element:_get_type()
+      local target = targets[element]
+
+      if type_ == "flag" then
+         result[target] = true
+      elseif type_ == "multiarg" then
+         result[target] = {}
+      elseif type_ == "counter" then
+         if not overwrite then
+            result[target] = result[target]+1
+         end
+      elseif type_ == "multicount" then
+         if overwrite then
+            table.remove(result[target], 1)
+         end
+      elseif type_ == "twodimensional" then
+         table.insert(result[target], {})
+
+         if overwrite then
+            table.remove(result[target], 1)
+         end
+      end
+
+      if element._maxargs == 0 then
+         close(element)
+      end
+   end
+
+   function pass(element, data)
+      passed[element] = passed[element]+1
+      data = convert(element, data)
+      local type_ = element:_get_type()
+      local target = targets[element]
+
+      if type_ == "arg" then
+         result[target] = data
+      elseif type_ == "multiarg" or type_ == "multicount" then
+         table.insert(result[target], data)
+      elseif type_ == "twodimensional" then
+         table.insert(result[target][#result[target]], data)
+      end
+
+      if passed[element] == element._maxargs then
+         close(element)
+      end
+   end
+
+   local function complete_invocation(element)
+      while passed[element] < element._minargs do
+         pass(element, element._default)
+      end
+   end
+
+   function close(element)
+      if passed[element] < element._minargs then
+         if element._default and element._defmode:find "a" then
+            complete_invocation(element)
+         else
+            error_("too few arguments")
+         end
+      else
+         if element == cur_option then
+            cur_option = nil
+         elseif element == cur_arg then
+            cur_arg_i = cur_arg_i+1
+            cur_arg = arguments[cur_arg_i]
+         end
+      end
+   end
+
+   local function switch(p)
+      parser = p
+
+      for _, option in ipairs(parser._options) do
+         table.insert(options, option)
+
+         for _, alias in ipairs(option._aliases) do
+            opt_context[alias] = option
+         end
+
+         local type_ = option:_get_type()
+         targets[option] = option._target or option:_get_default_target()
+
+         if type_ == "counter" then
+            result[targets[option]] = 0
+         elseif type_ == "multicount" or type_ == "twodimensional" then
+            result[targets[option]] = {}
+         end
+
+         invocations[option] = 0
+      end
+
+      for _, mutex in ipairs(parser._mutexes) do
+         for _, option in ipairs(mutex) do
+            if not option_mutexes[option] then
+               option_mutexes[option] = {mutex}
+            else
+               table.insert(option_mutexes[option], mutex)
+            end
+         end
+      end
+
+      for _, argument in ipairs(parser._arguments) do
+         table.insert(arguments, argument)
+         invocations[argument] = 0
+         targets[argument] = argument._target or argument._name
+         invoke(argument)
+      end
+
+      cur_arg = arguments[cur_arg_i]
+      commands = parser._commands
+      com_context = {}
+
+      for _, command in ipairs(commands) do
+         targets[command] = command._target or command._name
+
+         for _, alias in ipairs(command._aliases) do
+            com_context[alias] = command
+         end
+      end
+   end
+
+   local function get_option(name)
+      return assert_(opt_context[name], "unknown option '%s'%s", name, get_tip(opt_context, name))
+   end
+
+   local function do_action(element)
+      if element._action then
+         element._action()
+      end
+   end
+
+   local function handle_argument(data)
+      if cur_option then
+         pass(cur_option, data)
+      elseif cur_arg then
+         pass(cur_arg, data)
+      else
+         local com = com_context[data]
+
+         if not com then
+            if #commands > 0 then
+               error_("unknown command '%s'%s", data, get_tip(com_context, data))
+            else
+               error_("too many arguments")
+            end
+         else
+            result[targets[com]] = true
+            do_action(com)
+            switch(com)
+         end
+      end
+   end
+
+   local function handle_option(data)
+      if cur_option then
+         close(cur_option)
+      end
+
+      cur_option = opt_context[data]
+
+      if option_mutexes[cur_option] then
+         for _, mutex in ipairs(option_mutexes[cur_option]) do
+            if used_mutexes[mutex] and used_mutexes[mutex] ~= cur_option then
+               error_("option '%s' can not be used together with option '%s'", data, used_mutexes[mutex]._name)
+            else
+               used_mutexes[mutex] = cur_option
+            end
+         end
+      end
+
+      do_action(cur_option)
+      invoke(cur_option)
+   end
+
+   local function mainloop()
+      local handle_options = true
+
+      for _, data in ipairs(args) do
+         local plain = true
+         local first, name, option
+
+         if handle_options then
+            first = data:sub(1, 1)
+            if charset[first] then
+               if #data > 1 then
+                  plain = false
+                  if data:sub(2, 2) == first then
+                     if #data == 2 then
+                        if cur_option then
+                           close(cur_option)
+                        end
+
+                        handle_options = false
+                     else
+                        local equal = data:find "="
+                        if equal then
+                           name = data:sub(1, equal-1)
+                           option = get_option(name)
+                           assert_(option._maxargs > 0, "option '%s' does not take arguments", name)
+
+                           handle_option(data:sub(1, equal-1))
+                           handle_argument(data:sub(equal+1))
+                        else
+                           get_option(data)
+                           handle_option(data)
+                        end
+                     end
+                  else
+                     for i = 2, #data do
+                        name = first .. data:sub(i, i)
+                        option = get_option(name)
+                        handle_option(name)
+
+                        if i ~= #data and option._minargs > 0 then
+                           handle_argument(data:sub(i+1))
+                           break
+                        end
+                     end
+                  end
+               end
+            end
+         end
+
+         if plain then
+            handle_argument(data)
+         end
+      end
+   end
+
+   switch(self)
+   charset = parser:_update_charset()
+   mainloop()
+
+   if cur_option then
+      close(cur_option)
+   end
+
+   while cur_arg do
+      if passed[cur_arg] == 0 and cur_arg._default and cur_arg._defmode:find "u" then
+         complete_invocation(cur_arg)
+      else
+         close(cur_arg)
+      end
+   end
+
+   if parser._require_command and #commands > 0 then
+      error_("a command is required")
+   end
+
+   for _, option in ipairs(options) do
+      if invocations[option] == 0 then
+         if option._default and option._defmode:find "u" then
+            invoke(option)
+            complete_invocation(option)
+            close(option)
+         end
+      end
+
+      if invocations[option] < option._mincount then
+         if option._default and option._defmode:find "a" then
+            while invocations[option] < option._mincount do
+               invoke(option)
+               close(option)
+            end
+         else
+            error_("option '%s' must be used at least %d time%s", option._name, option._mincount, plural(option._mincount))
+         end
+      end
+   end
+
+   return result
+end
+
+function Parser:error(msg)
+   io.stderr:write(("%s\r\n\r\nError: %s\r\n"):format(self:get_usage(), msg))
+   os.exit(1)
+end
+
+function Parser:parse(args)
+   return self:_parse(args, Parser.error)
+end
+
+function Parser:pparse(args)
+   local errmsg
+   local ok, result = pcall(function()
+      return self:_parse(args, function(_, err)
+         errmsg = err
+         return error()
+      end)
+   end)
+
+   if ok then
+      return true, result
+   else
+      assert(errmsg, result)
+      return false, errmsg
+   end
+end
+
+return function(...)
+   return Parser(default_cmdline[0]):add_help(true)(...)
+end

+ 296 - 0
tools/wrk-scripts/lib/uri-f570bf7.lua

@@ -0,0 +1,296 @@
+-- The MIT License (MIT)
+-- 
+-- Copyright (c) 2014 Cyril David <cyx@cyx.is>
+-- Copyright (c) 2011-2013 Bertrand Mansion <bmansion@mamasam.com>
+-- 
+-- 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.
+
+-- == list of known and common scheme ports
+--
+-- @see http://www.iana.org/assignments/uri-schemes.html
+--
+local SERVICES = {
+	acap     = 674,
+	cap      = 1026,
+	dict     = 2628,
+	ftp      = 21,
+	gopher   = 70,
+	http     = 80,
+	https    = 443,
+	iax      = 4569,
+	icap     = 1344,
+	imap     = 143,
+	ipp      = 631,
+	ldap     = 389,
+	mtqp     = 1038,
+	mupdate  = 3905,
+	news     = 2009,
+	nfs      = 2049,
+	nntp     = 119,
+	rtsp     = 554,
+	sip      = 5060,
+	snmp     = 161,
+	telnet   = 23,
+	tftp     = 69,
+	vemmi    = 575,
+	afs      = 1483,
+	jms      = 5673,
+	rsync    = 873,
+	prospero = 191,
+	videotex = 516
+}
+
+local LEGAL = {
+	["-"] = true, ["_"] = true, ["."] = true, ["!"] = true,
+	["~"] = true, ["*"] = true, ["'"] = true, ["("] = true,
+	[")"] = true, [":"] = true, ["@"] = true, ["&"] = true,
+	["="] = true, ["+"] = true, ["$"] = true, [","] = true,
+	[";"] = true -- can be used for parameters in path
+}
+
+-- aggressive caching of methods
+local gsub   = string.gsub
+local char   = string.char
+local byte   = string.byte
+local upper  = string.upper
+local lower  = string.lower
+local format = string.format
+
+-- forward declaration of helper utilities
+local util = {}
+
+local function decode(str)
+	local str = gsub(str, "+", " ")
+
+	return (gsub(str, "%%(%x%x)", function(c)
+			return char(tonumber(c, 16))
+	end))
+end
+
+local function encode(str)
+	return (gsub(str, "([^A-Za-z0-9%_%.%-%~])", function(v)
+			return upper(format("%%%02x", byte(v)))
+	end))
+end
+
+-- Build a URL given a table with the fields:
+--
+--	- scheme
+--	- user
+--	- password
+--	- host
+--	- port
+--	- path
+--	- query
+--	- fragment
+--
+-- Example:
+--
+--	local url = uri.build({
+--		scheme = "http",
+--		host = "example.com",
+--		path = "/some/path"
+--	})
+--
+--	assert(url == "http://example.com/some/path")
+--
+local function build(uri)
+	local url = ""
+
+	if uri.path then
+		local path = uri.path
+		
+		gsub(path, "([^/]+)", function (s) return util.encode_segment(s) end)
+
+		url = url .. tostring(path)
+	end
+
+	if uri.query then
+		local qstring = tostring(uri.query)
+		if qstring ~= "" then
+			url = url .. "?" .. qstring
+		end
+	end
+
+	if uri.host then
+		local authority = uri.host
+
+		if uri.port and uri.scheme and SERVICES[uri.scheme] ~= uri.port then
+			authority = authority .. ":" .. uri.port
+		end
+
+		local userinfo
+
+		if uri.user and uri.user ~= "" then
+			userinfo = encode(uri.user)
+
+			if uri.password then
+				userinfo = userinfo .. ":" .. encode(uri.password)
+			end
+		end
+
+		if userinfo and userinfo ~= "" then
+			authority = userinfo .. "@" .. authority
+		end
+
+		if authority then
+			if url ~= "" then
+				url = "//" .. authority .. "/" .. gsub(url, "^/+", "")
+			else
+				url = "//" .. authority
+			end
+		end
+	end
+
+	if uri.scheme then
+		url = uri.scheme .. ":" .. url
+	end
+
+	if uri.fragment then
+		url = url .. "#" .. uri.fragment
+	end
+
+	return url
+end
+
+-- Parse the url into the designated parts.
+--
+-- Depending on the url, the following parts will be available:
+--
+--	- scheme
+--	- userinfo
+--	- user
+--	- password
+--	- authority
+--	- host
+--	- port
+--	- path
+--      - query
+--      - fragment
+--
+-- Usage:
+--
+--     local u = uri.parse("http://john:monkey@example.com/some/path#h1")
+--
+--     assert(u.host == "example.com")
+--     assert(u.scheme == "http")
+--     assert(u.user == "john")
+--     assert(u.password == "monkey")
+--     assert(u.path == "/some/path")
+--     assert(u.fragment == "h1")
+--
+local function parse(url)
+	local uri = { query = nil }
+
+	util.set_authority(uri, "")
+
+	local url = tostring(url or "")
+
+	url = gsub(url, "#(.*)$", function(v)
+		uri.fragment = v
+		return ""
+	end)
+
+	url = gsub(url, "^([%w][%w%+%-%.]*)%:", function(v)
+		uri.scheme = lower(v)
+		return ""
+	end)
+
+	url = gsub(url, "%?(.*)", function(v)
+		uri.query = v
+		return ""
+	end)
+
+	url = gsub(url, "^//([^/]*)", function(v)
+		util.set_authority(uri, v)
+		return ""
+	end)
+
+	uri.path = decode(url)
+
+	return uri
+end
+
+function util.encode_segment(s)
+	local function encode_legal(c)
+		if LEGAL[c] then
+			return c
+		end
+
+		return encode(c)
+	end
+
+	return gsub(s, "([^a-zA-Z0-9])", encode_legal)
+end
+
+-- set the authority part of the url
+--
+-- The authority is parsed to find the user, password, port and host if available.
+-- @param authority The string representing the authority
+-- @return a string with what remains after the authority was parsed
+function util.set_authority(uri, authority)
+	uri.authority = authority
+	uri.port = nil
+	uri.host = nil
+	uri.userinfo = nil
+	uri.user = nil
+	uri.password = nil
+
+	authority = gsub(authority, "^([^@]*)@", function(v)
+		uri.userinfo = decode(v)
+		return ""
+	end)
+
+	authority = gsub(authority, "^%[[^%]]+%]", function(v)
+		-- ipv6
+		uri.host = v
+		return ""
+	end)
+
+	authority = gsub(authority, ":([^:]*)$", function(v)
+		uri.port = tonumber(v)
+		return ""
+	end)
+
+	if authority ~= "" and not uri.host then
+		uri.host = lower(authority)
+	end
+
+	if uri.userinfo then
+		local userinfo = uri.userinfo
+
+		userinfo = gsub(userinfo, ":([^:]*)$", function(v)
+				uri.password = v
+				return ""
+		end)
+
+		uri.user = userinfo
+	end
+
+	return authority
+end
+
+local uri = {
+	build = build,
+	parse = parse,
+	encode = encode,
+	decode = decode
+}
+
+return uri

+ 47 - 0
tools/wrk-scripts/stress-hue.lua

@@ -0,0 +1,47 @@
+-- This test repeatably GETs the same url.
+
+local hue = require("lib/hue")
+local inspect = require("lib/inspect")
+
+function init(args)
+	hue.parse_args(args)
+end
+
+function request()
+	request_data = {
+		method = wrk.method,
+		path = wrk.path,
+		headers = wrk.headers,
+		body = wrk.body,
+	}
+
+	return wrk.format()
+end
+
+function response(status, headers, body)
+	data = {
+		request = request_data,
+		response = {
+			status = status,
+			headers = headers,
+		}
+	}
+
+	if headers["Set-Cookie"] then
+		hue.set_cookies(headers["Set-Cookie"])
+	end
+
+	if status == 301 or status == 302 or status == 303 or status == 307 or status == 308 then
+		hue.handle_redirect(status, headers["Location"])
+	elseif status >= 399 then
+		-- log the error
+		data.response.body = body
+	end
+
+	if hue.verbose then
+		print(
+			inspect.inspect(data)
+			..
+			"\n------------------------------------------------------")
+	end
+end