Browse Source

Lwan: Add Lua benchmarks (#5567)

Leandro A. F. Pereira 5 years ago
parent
commit
1686e63cf9

+ 2 - 0
frameworks/C/lwan/Makefile

@@ -5,6 +5,7 @@ CFLAGS = -mtune=native -march=native -O3 -fno-plt -flto -ffat-lto-objects -DNDEB
 	-I /lwan/src/lib \
 	`pkg-config mariadb --cflags` \
 	`pkg-config sqlite3 --cflags` \
+	`pkg-config luajit --cflags` \
 	-fauto-profile=/lwan/src/gcda/techempower.gcov
 
 LDFLAGS = -mtune=native -march=native -O3 -flto -ffat-lto-objects -Wl,-z,now,-z,relro \
@@ -12,6 +13,7 @@ LDFLAGS = -mtune=native -march=native -O3 -flto -ffat-lto-objects -Wl,-z,now,-z,
 	-Wl,-whole-archive /lwan/build/src/lib/liblwan.a -Wl,-no-whole-archive \
 	`pkg-config mariadb --libs` \
 	`pkg-config sqlite3 --libs` \
+	`pkg-config luajit --libs` \
 	-lpthread \
 	-lz
 

+ 26 - 7
frameworks/C/lwan/README.md

@@ -3,20 +3,22 @@
 This test is based on of the [Lwan](https://lwan.ws) web-server project,
 an experimental high-performance web server.
 
-### Test Type Implementation Source Code
+# C version
+
+## Source code
 
 * [JSON](src/techempower.c)
-* [PLAINTEXT](src/techempower.c)
+* [Plain text](src/techempower.c)
 * [DB](src/techempower.c)
-* [QUERY](src/techempower.c)
-* [FORTUNES](src/techempower.c)
+* [Query](src/techempower.c)
+* [Fortunes](src/techempower.c)
 
 ## Test URLs
 ### JSON
 
 http://localhost:8080/json
 
-### PLAINTEXT
+### Plaintext
 
 http://localhost:8080/plaintext
 
@@ -24,10 +26,27 @@ http://localhost:8080/plaintext
 
 http://localhost:8080/db
 
-### QUERY
+### Query
 
 http://localhost:8080/query?queries=
 
-### FORTUNES
+### Fortunes
 
 http://localhost:8080/fortunes
+
+# Lua version
+
+## Source code
+
+* [JSON](src/techempower.conf)
+* [Plaintext](src/techempower.conf)
+
+## Test URLs
+### JSON
+
+http://localhost:8080/lua.json
+
+### Plaintext
+
+http://localhost:8080/lua.plaintext
+

+ 19 - 0
frameworks/C/lwan/benchmark_config.json

@@ -23,6 +23,25 @@
         "display_name": "Lwan",
         "notes": "",
         "versus": "None"
+      },
+      "lua": {
+        "json_url": "/lua.json",
+        "plaintext_url": "/lua.plaintext",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Micro",
+        "database": "mysql",
+        "framework": "Lwan",
+        "language": "Lua",
+        "flavor": "Lua",
+        "orm": "Raw",
+        "platform": "Lwan",
+        "webserver": "Lwan",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "Lwan",
+        "notes": "",
+        "versus": "None"
       }
     }
   ]

+ 35 - 0
frameworks/C/lwan/lwan-lua.dockerfile

@@ -0,0 +1,35 @@
+FROM ubuntu:19.10
+
+RUN apt update -yqq && \
+	apt install -yqq \
+		git pkg-config build-essential cmake zlib1g-dev \
+		libsqlite3-dev libmariadbclient-dev wget
+
+ADD ./ /lwan
+WORKDIR /lwan
+
+RUN mkdir mimalloc && \
+    wget https://github.com/microsoft/mimalloc/archive/acb03c54971c4b0a43a6d17ea55a9d5feb88972f.tar.gz -O - | tar xz --strip-components=1 -C mimalloc && \
+    cd mimalloc && mkdir build && cd build && \
+    CFLAGS="-flto -ffat-lto-objects" cmake .. -DCMAKE_BUILD_TYPE=Release -DMI_SECURE=OFF && make -j install
+
+RUN mkdir luajit && \
+    wget http://luajit.org/download/LuaJIT-2.0.5.tar.gz -O - | tar xz --strip-components=1 -C luajit && \
+    cd luajit && \
+    PREFIX=/usr CFLAGS="-O3 -mtune=native -march=native -flto -ffat-lto-objects" make -j install
+
+RUN wget https://github.com/lpereira/lwan/archive/b61c4ff17a516c9045abdd614db191072f0fd19b.tar.gz -O - | tar xz --strip-components=1 && \
+    mkdir build && cd build && \
+    cmake /lwan -DCMAKE_BUILD_TYPE=Release -DUSE_ALTERNATIVE_MALLOC=mimalloc && \
+    make lwan-static
+
+RUN make clean && make
+
+ENV LD_LIBRARY_PATH=/usr/local/lib:/usr/lib
+ENV USE_MYSQL=1
+ENV MYSQL_USER=benchmarkdbuser
+ENV MYSQL_PASS=benchmarkdbpass
+ENV MYSQL_DB=hello_world
+ENV MYSQL_HOST=tfb-database
+
+CMD ["./techempower"]

+ 14 - 6
frameworks/C/lwan/lwan.dockerfile

@@ -1,23 +1,31 @@
 FROM ubuntu:19.10
 
-RUN apt update -yqq
-RUN apt install -yqq \
-	git pkg-config build-essential cmake zlib1g-dev \
-	libsqlite3-dev libmariadbclient-dev wget
+RUN apt update -yqq && \
+	apt install -yqq \
+		git pkg-config build-essential cmake zlib1g-dev \
+		libsqlite3-dev libmariadbclient-dev wget
 
 ADD ./ /lwan
 WORKDIR /lwan
 
 RUN mkdir mimalloc && \
     wget https://github.com/microsoft/mimalloc/archive/acb03c54971c4b0a43a6d17ea55a9d5feb88972f.tar.gz -O - | tar xz --strip-components=1 -C mimalloc && \
-    cd mimalloc && mkdir build && cd build && CFLAGS="-flto -ffat-lto-objects" cmake .. -DCMAKE_BUILD_TYPE=Release -DMI_SECURE=OFF && make -j install && cd ../.. && \
-    wget https://github.com/lpereira/lwan/archive/e3c03b6a975a7206a1b6e5d78a99abf5e7be1647.tar.gz -O - | tar xz --strip-components=1 && \
+    cd mimalloc && mkdir build && cd build && \
+    CFLAGS="-flto -ffat-lto-objects" cmake .. -DCMAKE_BUILD_TYPE=Release -DMI_SECURE=OFF && make -j install
+
+RUN mkdir luajit && \
+    wget http://luajit.org/download/LuaJIT-2.0.5.tar.gz -O - | tar xz --strip-components=1 -C luajit && \
+    cd luajit && \
+    PREFIX=/usr CFLAGS="-O3 -mtune=native -march=native -flto -ffat-lto-objects" make -j install
+
+RUN wget https://github.com/lpereira/lwan/archive/b61c4ff17a516c9045abdd614db191072f0fd19b.tar.gz -O - | tar xz --strip-components=1 && \
     mkdir build && cd build && \
     cmake /lwan -DCMAKE_BUILD_TYPE=Release -DUSE_ALTERNATIVE_MALLOC=mimalloc && \
     make lwan-static
 
 RUN make clean && make
 
+ENV LD_LIBRARY_PATH=/usr/local/lib:/usr/lib
 ENV USE_MYSQL=1
 ENV MYSQL_USER=benchmarkdbuser
 ENV MYSQL_PASS=benchmarkdbpass

+ 16 - 12
frameworks/C/lwan/src/json.c

@@ -645,7 +645,7 @@ static int json_escape_internal(const char *str,
         if (escaped) {
             char bytes[2] = {'\\', escaped};
 
-            if (unescaped - cur) {
+            if (cur - unescaped) {
                 ret |= append_bytes(unescaped, (size_t)(cur - unescaped), data);
                 unescaped = cur + 1;
             }
@@ -654,8 +654,8 @@ static int json_escape_internal(const char *str,
         }
     }
 
-    if (unescaped - cur)
-        return ret | append_bytes(unescaped, (size_t)(cur - unescaped), data);
+    if (cur - unescaped)
+        ret |= append_bytes(unescaped, (size_t)(cur - unescaped), data);
 
     return ret;
 }
@@ -839,15 +839,18 @@ static int encode(const struct json_obj_descr *descr,
     }
 }
 
-static int encode_key_value(const struct json_obj_descr *descr,
-                            const void *val,
-                            json_append_bytes_t append_bytes,
-                            void *data,
-                            bool escape_key)
+static inline int encode_key(const struct json_obj_descr *descr,
+                             json_append_bytes_t append_bytes,
+                             void *data,
+                             bool escape_key)
 {
     int ret;
 
     if (!escape_key) {
+        /* Keys are encoded twice in the descriptor; once without quotes and
+         * the trailing comma, and one with.  Doing it like so cuts some
+         * indirect calls to append_bytes(), which in turn also potentially
+         * cuts some branches in most implementations of it.  */
         ret = append_bytes(descr->field_name + descr->field_name_len,
                            descr->field_name_len + 3 /* 3=len('"":') */, data);
     } else {
@@ -855,7 +858,7 @@ static int encode_key_value(const struct json_obj_descr *descr,
         ret |= append_bytes(":", 1, data);
     }
 
-    return ret | encode(descr, val, append_bytes, data, escape_key);
+    return ret;
 }
 
 int json_obj_encode_full(const struct json_obj_descr *descr,
@@ -876,12 +879,13 @@ int json_obj_encode_full(const struct json_obj_descr *descr,
          * branches.  */
 
         for (size_t i = 1; i < descr_len; i++) {
-            ret |= encode_key_value(&descr[i], val, append_bytes, data,
-                                    escape_key);
+            ret |= encode_key(&descr[i], append_bytes, data, escape_key);
+            ret |= encode(&descr[i], val, append_bytes, data, escape_key);
             ret |= append_bytes(",", 1, data);
         }
 
-        ret |= encode_key_value(&descr[0], val, append_bytes, data, escape_key);
+        ret |= encode_key(&descr[0], append_bytes, data, escape_key);
+        ret |= encode(&descr[0], val, append_bytes, data, escape_key);
     }
 
     return ret | append_bytes("}", 1, data);

+ 400 - 0
frameworks/C/lwan/src/json.lua

@@ -0,0 +1,400 @@
+--
+-- json.lua
+--
+-- Copyright (c) 2019 rxi
+--
+-- 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 json = { _version = "0.1.2" }
+
+-------------------------------------------------------------------------------
+-- Encode
+-------------------------------------------------------------------------------
+
+local encode
+
+local escape_char_map = {
+  [ "\\" ] = "\\\\",
+  [ "\"" ] = "\\\"",
+  [ "\b" ] = "\\b",
+  [ "\f" ] = "\\f",
+  [ "\n" ] = "\\n",
+  [ "\r" ] = "\\r",
+  [ "\t" ] = "\\t",
+}
+
+local escape_char_map_inv = { [ "\\/" ] = "/" }
+for k, v in pairs(escape_char_map) do
+  escape_char_map_inv[v] = k
+end
+
+
+local function escape_char(c)
+  return escape_char_map[c] or string.format("\\u%04x", c:byte())
+end
+
+
+local function encode_nil(val)
+  return "null"
+end
+
+
+local function encode_table(val, stack)
+  local res = {}
+  stack = stack or {}
+
+  -- Circular reference?
+  if stack[val] then error("circular reference") end
+
+  stack[val] = true
+
+  if rawget(val, 1) ~= nil or next(val) == nil then
+    -- Treat as array -- check keys are valid and it is not sparse
+    local n = 0
+    for k in pairs(val) do
+      if type(k) ~= "number" then
+        error("invalid table: mixed or invalid key types")
+      end
+      n = n + 1
+    end
+    if n ~= #val then
+      error("invalid table: sparse array")
+    end
+    -- Encode
+    for i, v in ipairs(val) do
+      table.insert(res, encode(v, stack))
+    end
+    stack[val] = nil
+    return "[" .. table.concat(res, ",") .. "]"
+
+  else
+    -- Treat as an object
+    for k, v in pairs(val) do
+      if type(k) ~= "string" then
+        error("invalid table: mixed or invalid key types")
+      end
+      table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
+    end
+    stack[val] = nil
+    return "{" .. table.concat(res, ",") .. "}"
+  end
+end
+
+
+local function encode_string(val)
+  return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
+end
+
+
+local function encode_number(val)
+  -- Check for NaN, -inf and inf
+  if val ~= val or val <= -math.huge or val >= math.huge then
+    error("unexpected number value '" .. tostring(val) .. "'")
+  end
+  return string.format("%.14g", val)
+end
+
+
+local type_func_map = {
+  [ "nil"     ] = encode_nil,
+  [ "table"   ] = encode_table,
+  [ "string"  ] = encode_string,
+  [ "number"  ] = encode_number,
+  [ "boolean" ] = tostring,
+}
+
+
+encode = function(val, stack)
+  local t = type(val)
+  local f = type_func_map[t]
+  if f then
+    return f(val, stack)
+  end
+  error("unexpected type '" .. t .. "'")
+end
+
+
+function json.encode(val)
+  return ( encode(val) )
+end
+
+
+-------------------------------------------------------------------------------
+-- Decode
+-------------------------------------------------------------------------------
+
+local parse
+
+local function create_set(...)
+  local res = {}
+  for i = 1, select("#", ...) do
+    res[ select(i, ...) ] = true
+  end
+  return res
+end
+
+local space_chars   = create_set(" ", "\t", "\r", "\n")
+local delim_chars   = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
+local escape_chars  = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
+local literals      = create_set("true", "false", "null")
+
+local literal_map = {
+  [ "true"  ] = true,
+  [ "false" ] = false,
+  [ "null"  ] = nil,
+}
+
+
+local function next_char(str, idx, set, negate)
+  for i = idx, #str do
+    if set[str:sub(i, i)] ~= negate then
+      return i
+    end
+  end
+  return #str + 1
+end
+
+
+local function decode_error(str, idx, msg)
+  local line_count = 1
+  local col_count = 1
+  for i = 1, idx - 1 do
+    col_count = col_count + 1
+    if str:sub(i, i) == "\n" then
+      line_count = line_count + 1
+      col_count = 1
+    end
+  end
+  error( string.format("%s at line %d col %d", msg, line_count, col_count) )
+end
+
+
+local function codepoint_to_utf8(n)
+  -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
+  local f = math.floor
+  if n <= 0x7f then
+    return string.char(n)
+  elseif n <= 0x7ff then
+    return string.char(f(n / 64) + 192, n % 64 + 128)
+  elseif n <= 0xffff then
+    return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
+  elseif n <= 0x10ffff then
+    return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
+                       f(n % 4096 / 64) + 128, n % 64 + 128)
+  end
+  error( string.format("invalid unicode codepoint '%x'", n) )
+end
+
+
+local function parse_unicode_escape(s)
+  local n1 = tonumber( s:sub(3, 6),  16 )
+  local n2 = tonumber( s:sub(9, 12), 16 )
+  -- Surrogate pair?
+  if n2 then
+    return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
+  else
+    return codepoint_to_utf8(n1)
+  end
+end
+
+
+local function parse_string(str, i)
+  local has_unicode_escape = false
+  local has_surrogate_escape = false
+  local has_escape = false
+  local last
+  for j = i + 1, #str do
+    local x = str:byte(j)
+
+    if x < 32 then
+      decode_error(str, j, "control character in string")
+    end
+
+    if last == 92 then -- "\\" (escape char)
+      if x == 117 then -- "u" (unicode escape sequence)
+        local hex = str:sub(j + 1, j + 5)
+        if not hex:find("%x%x%x%x") then
+          decode_error(str, j, "invalid unicode escape in string")
+        end
+        if hex:find("^[dD][89aAbB]") then
+          has_surrogate_escape = true
+        else
+          has_unicode_escape = true
+        end
+      else
+        local c = string.char(x)
+        if not escape_chars[c] then
+          decode_error(str, j, "invalid escape char '" .. c .. "' in string")
+        end
+        has_escape = true
+      end
+      last = nil
+
+    elseif x == 34 then -- '"' (end of string)
+      local s = str:sub(i + 1, j - 1)
+      if has_surrogate_escape then
+        s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape)
+      end
+      if has_unicode_escape then
+        s = s:gsub("\\u....", parse_unicode_escape)
+      end
+      if has_escape then
+        s = s:gsub("\\.", escape_char_map_inv)
+      end
+      return s, j + 1
+
+    else
+      last = x
+    end
+  end
+  decode_error(str, i, "expected closing quote for string")
+end
+
+
+local function parse_number(str, i)
+  local x = next_char(str, i, delim_chars)
+  local s = str:sub(i, x - 1)
+  local n = tonumber(s)
+  if not n then
+    decode_error(str, i, "invalid number '" .. s .. "'")
+  end
+  return n, x
+end
+
+
+local function parse_literal(str, i)
+  local x = next_char(str, i, delim_chars)
+  local word = str:sub(i, x - 1)
+  if not literals[word] then
+    decode_error(str, i, "invalid literal '" .. word .. "'")
+  end
+  return literal_map[word], x
+end
+
+
+local function parse_array(str, i)
+  local res = {}
+  local n = 1
+  i = i + 1
+  while 1 do
+    local x
+    i = next_char(str, i, space_chars, true)
+    -- Empty / end of array?
+    if str:sub(i, i) == "]" then
+      i = i + 1
+      break
+    end
+    -- Read token
+    x, i = parse(str, i)
+    res[n] = x
+    n = n + 1
+    -- Next token
+    i = next_char(str, i, space_chars, true)
+    local chr = str:sub(i, i)
+    i = i + 1
+    if chr == "]" then break end
+    if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
+  end
+  return res, i
+end
+
+
+local function parse_object(str, i)
+  local res = {}
+  i = i + 1
+  while 1 do
+    local key, val
+    i = next_char(str, i, space_chars, true)
+    -- Empty / end of object?
+    if str:sub(i, i) == "}" then
+      i = i + 1
+      break
+    end
+    -- Read key
+    if str:sub(i, i) ~= '"' then
+      decode_error(str, i, "expected string for key")
+    end
+    key, i = parse(str, i)
+    -- Read ':' delimiter
+    i = next_char(str, i, space_chars, true)
+    if str:sub(i, i) ~= ":" then
+      decode_error(str, i, "expected ':' after key")
+    end
+    i = next_char(str, i + 1, space_chars, true)
+    -- Read value
+    val, i = parse(str, i)
+    -- Set
+    res[key] = val
+    -- Next token
+    i = next_char(str, i, space_chars, true)
+    local chr = str:sub(i, i)
+    i = i + 1
+    if chr == "}" then break end
+    if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
+  end
+  return res, i
+end
+
+
+local char_func_map = {
+  [ '"' ] = parse_string,
+  [ "0" ] = parse_number,
+  [ "1" ] = parse_number,
+  [ "2" ] = parse_number,
+  [ "3" ] = parse_number,
+  [ "4" ] = parse_number,
+  [ "5" ] = parse_number,
+  [ "6" ] = parse_number,
+  [ "7" ] = parse_number,
+  [ "8" ] = parse_number,
+  [ "9" ] = parse_number,
+  [ "-" ] = parse_number,
+  [ "t" ] = parse_literal,
+  [ "f" ] = parse_literal,
+  [ "n" ] = parse_literal,
+  [ "[" ] = parse_array,
+  [ "{" ] = parse_object,
+}
+
+
+parse = function(str, idx)
+  local chr = str:sub(idx, idx)
+  local f = char_func_map[chr]
+  if f then
+    return f(str, idx)
+  end
+  decode_error(str, idx, "unexpected character '" .. chr .. "'")
+end
+
+
+function json.decode(str)
+  if type(str) ~= "string" then
+    error("expected argument of type string, got " .. type(str))
+  end
+  local res, idx = parse(str, next_char(str, 1, space_chars, true))
+  idx = next_char(str, idx, space_chars, true)
+  if idx <= #str then
+    decode_error(str, idx, "trailing garbage")
+  end
+  return res
+end
+
+
+return json

+ 1 - 13
frameworks/C/lwan/src/techempower.c

@@ -25,6 +25,7 @@
 #include "lwan-private.h"
 #include "lwan-config.h"
 #include "lwan-template.h"
+#include "lwan-mod-lua.h"
 
 #include "database.h"
 #include "json.h"
@@ -359,18 +360,6 @@ LWAN_HANDLER(quit_lwan)
 
 int main(void)
 {
-    static const struct lwan_url_map url_map[] = {
-        /* Routes for the TWFB benchmark: */
-        {.prefix = "/json", .handler = LWAN_HANDLER_REF(json)},
-        {.prefix = "/db", .handler = LWAN_HANDLER_REF(db)},
-        {.prefix = "/queries", .handler = LWAN_HANDLER_REF(queries)},
-        {.prefix = "/plaintext", .handler = LWAN_HANDLER_REF(plaintext)},
-        {.prefix = "/fortunes", .handler = LWAN_HANDLER_REF(fortunes)},
-        /* Routes for the test harness: */
-        {.prefix = "/quit-lwan", .handler = LWAN_HANDLER_REF(quit_lwan)},
-        {.prefix = "/hello", .handler = LWAN_HANDLER_REF(plaintext)},
-        {.prefix = NULL},
-    };
     struct lwan l;
 
     lwan_init(&l);
@@ -410,7 +399,6 @@ int main(void)
     if (!fortune_tpl)
         lwan_status_critical("Could not compile fortune templates");
 
-    lwan_set_url_map(&l, url_map);
     lwan_main_loop(&l);
 
     lwan_tpl_free(fortune_tpl);

+ 26 - 0
frameworks/C/lwan/techempower.conf

@@ -1,2 +1,28 @@
 listener *:8080 {
+    # For main TWFB benchmarks
+    &plaintext /plaintext
+    &json /json
+    &db /db
+    &queries /queries
+    &fortunes /fortunes
+
+    # For Lua version of TWFB benchmarks
+    lua /lua. {
+	default_type = text/plain
+	cache period = 1h
+        script='''local json = require "src/json"
+
+                function handle_get_plaintext(req)
+                    req:set_response("Hello, World!")
+                end
+
+                function handle_get_json(req)
+                    req:set_headers({['Content-Type']='application/json'})
+                    req:set_response(json.encode({message="Hello, World!"}))
+                end'''
+    }
+
+    # For test harness
+    &quit_lwan /quit-lwan
+    &plaintext /hello
 }