Browse Source

Complete mORMot2 benchmarks (Pascal language) (#7481)

* Complete mORMot benchmarks (Pascal language)

* fixed memory leak for "raw" /updates and /queries testcases

* async socket now properly closed on Linux/Epoll - this fixes `tfb --verify`; Use [email protected] release to build container (instead of latest release)

* use array itearaion feature in mORMot {#.} ... {/.} in the Mustache template

* added hsoNoStats HTTP server option

* use Ubuntu 22.04 as planned for Round22; use mORMot 4e65a791 - improved performance

* [mORMot] improved `/fortunes` - direct use from the local variables, with no temporary TDocVariant conversion

Co-authored-by: pavel.mash <[email protected]>
pavelmash 3 years ago
parent
commit
0905e4068a

+ 72 - 0
frameworks/Pascal/mormot/README.md

@@ -0,0 +1,72 @@
+# mORMot Benchmarking Test
+
+This is a framework implementation using the [mORMot2](https://github.com/synopse/mORMot2) FreePascal/Delphi framework.
+It builds using [FreePascal](https://www.freepascal.org/) compiler and developed using [Lazarus IDE](https://www.lazarus-ide.org/)
+
+### Test Type Implementation Source Code
+
+* [ORM and RAW implementation for all tests](src/raw.pas)
+
+## Important Libraries
+The tests were run with:
+* [mORMot2 latest](https://github.com/synopse/mORMot2)
+* [FreePascal 3.2.2](https://www.freepascal.org/)
+
+## Contributor tips
+For debugging purpose run Postges using Docker
+```shell
+sudo docker run --name postgres -e POSTGRES_PASSWORD=postgres -d -p 5432:5432 postgres:12
+```
+add `tfb-database` into hosts
+```shell
+echo '127.0.0.1	 tfb-database' | sudo tee -a /etc/hosts
+```
+
+Database can be initialized using scripts from [TFBDatabases repo](https://github.com/TechEmpower/TFBDatabases)
+
+```shell
+git clone https://github.com/TechEmpower/TFBDatabases.git
+cd TFBDatabases
+psql postgres://postgres:postgres@tfb-database:5432 < create-postgres-database.sql
+psql postgres://benchmarkdbuser:benchmarkdbpass@tfb-database:5432/hello_world < create-postgres.sql
+```
+
+## Test URLs
+
+### JSON
+
+http://localhost:8080/json
+
+### PLAINTEXT
+
+http://localhost:8080/plaintext
+
+`mORMot` HTTP server does not support [HTTP pipelining](https://developer.mozilla.org/en-US/docs/Web/HTTP/Connection_management_in_HTTP_1.x#http_pipelining),
+so numbers is not impressive here.
+
+### DB
+
+http://localhost:8080/db
+
+### QUERY
+
+http://localhost:8080/query?queries=
+
+### CACHED QUERY
+
+http://localhost:8080/cached_query?queries=
+
+### UPDATE
+
+http://localhost:8080/update?queries=
+
+Batch update SQL statement in case of Postgres is generated by ORM as such:
+```sql
+update table set field = v.field from (SELECT unnest(?), unnest(?)) as v(id, field) where t.id = v.id
+```
+Nested select is a reason of warning for `tfb --verify`
+
+### FORTUNES
+
+http://localhost:8080/fortunes
+

+ 53 - 0
frameworks/Pascal/mormot/benchmark_config.json

@@ -0,0 +1,53 @@
+{
+  "framework": "mormot",
+  "tests": [
+    {
+      "default": {
+        "json_url": "/json",
+        "db_url": "/db",
+        "query_url": "/queries?queries=",
+        "fortune_url": "/fortunes",
+        "update_url": "/updates?queries=",
+        "plaintext_url": "/plaintext",
+        "cached_query_url": "/cached-queries?count=",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Fullstack",
+        "database": "postgres",
+        "framework": "mormot",
+        "language": "Pascal",
+        "flavor": "None",
+        "orm": "Full",
+        "platform": "None",
+        "webserver": "None",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "mormot",
+        "notes": "",
+        "versus": "None"
+      },
+      "postgres-raw": {
+        "dockerfile": "mormot.dockerfile",
+        "db_url": "/rawdb",
+        "query_url": "/rawqueries?queries=",
+        "fortune_url": "/rawfortunes",
+        "update_url": "/rawupdates?queries=",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Fullstack",
+        "database": "postgres",
+        "framework": "mormot",
+        "language": "Pascal",
+        "flavor": "None",
+        "orm": "Raw",
+        "platform": "None",
+        "webserver": "None",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "mormot",
+        "notes": "",
+        "versus": "None"
+      }
+    }
+  ]
+}

+ 20 - 0
frameworks/Pascal/mormot/mormot.dockerfile

@@ -0,0 +1,20 @@
+FROM freepascal/fpc:3.2.2-focal-full as builder
+
+RUN apt-get update -yqq
+RUN apt-get install -yqq p7zip-full zlib1g-dev
+
+WORKDIR /build
+COPY src/ src/
+COPY setup_and_build.sh .
+
+RUN /bin/bash -c ./setup_and_build.sh
+
+FROM ubuntu:22.04
+COPY --from=builder /build/bin/fpc-x86_64-linux/raw /usr/local/bin/raw
+
+RUN apt-get update && apt-get install -yqq postgresql-client
+
+EXPOSE 8080
+CMD ["raw"]
+
+

+ 88 - 0
frameworks/Pascal/mormot/setup_and_build.sh

@@ -0,0 +1,88 @@
+#!/bin/bash
+
+# Update mORMot and static folder content from the latest [pre]release of mORMot2
+# Required tools: jq wget 7zip. On Ubuntu can be installed by
+# sudo apt install wget jq p7zip-full
+
+# On error
+err_report() {
+  >&2 echo "Error in $0 on line $1"
+  script_aborted
+}
+trap 'err_report $LINENO' ERR
+
+script_successful(){
+  echo "++Build successfully++"
+  exit 0
+}
+
+script_aborted() {
+  echo "******Build aborted******"
+  exit 1
+}
+
+set -o pipefail
+
+rm -rf ./libs
+
+# echo "Getting the latest pre-release URL..."
+# USED_TAG=$(wget -qO- https://api.github.com/repos/synopse/mORMot2/releases/latest | jq -r '.tag_name')
+USED_TAG="2.0.3780"
+
+echo "Used release tag $USED_TAG"
+URL="https://github.com/synopse/mORMot2/releases/download/$USED_TAG/mormot2static.7z"
+echo "Download statics from $URL ..."
+wget -q -O./mormot2static.7z "$URL"
+
+mkdir -p ./libs/mORMot/static
+echo "Unpacking to ./libs/mORMot/static ..."
+7za x ./mormot2static.7z -o./libs/mORMot/static
+rm -rf ./mormot2static.7z
+
+# uncomment for fixed commit URL
+URL=https://github.com//synopse/mORMot2/tarball/e567622e45caaf7056ee8ba6d1827314d945ccb2
+#URL="https://api.github.com/repos/synopse/mORMot2/tarball/$USED_TAG"
+echo "Download and unpacking mORMot sources from $URL ..."
+wget -qO- "$URL" | tar -xz -C ./libs/mORMot  --strip-components=1
+
+
+# uncomment line below to echo commands to console
+set -x
+
+# get a mORMot folder name based on this script location
+TARGET="${TARGET:-linux}"
+ARCH="${ARCH:-x86_64}"
+ARCH_TG="$ARCH-$TARGET"
+
+MSRC="./libs/mORMot/src"
+BIN="./bin"
+STATIC="./libs/mORMot/static"
+
+mkdir -p "$BIN/fpc-$ARCH_TG/.dcu"
+rm -f "$BIN"/fpc-"$ARCH_TG"/.dcu/*
+
+dest_fn=raw
+if [[ $TARGET == win* ]]; then
+  dest_fn="$dest_fn.exe"
+fi
+
+# suppress warnings
+# Warning: (5059) Function result variable does not seem to be initialized
+# Warning: (5036) Local variable XXX does not seem to be initialized
+# Warning: (5089) Local variable XXX of a managed type does not seem to be initialized
+# Warning: (5090) Variable XXX of a managed type does not seem to be initialized
+SUPRESS_WARN=-vm11047,6058,5092,5091,5060,5058,5057,5028,5024,5023,4081,4079,4055,3187,3124,3123,5059,5036,5089,5090
+
+echo "Start compiling..."
+fpc -MDelphi -Sci -Ci -O3 -g -gl -gw2 -Xg -k'-rpath=$ORIGIN' -k-L$BIN \
+  -T$TARGET -P$ARCH \
+  -veiq -v-n-h- $SUPRESS_WARN \
+  -Fi"$BIN/fpc-$ARCH_TG/.dcu" -Fi"$MSRC" \
+  -Fl"$STATIC/$ARCH-$TARGET" \
+  -Fu"$MSRC/core" -Fu"$MSRC/db" -Fu"$MSRC/rest" -Fu"$MSRC/crypt" \
+    -Fu"$MSRC/app" -Fu"$MSRC/net" -Fu"$MSRC/lib" -Fu"$MSRC/orm" -Fu"$MSRC/soa" \
+  -FU"$BIN/fpc-$ARCH_TG/.dcu" -FE"$BIN/fpc-$ARCH_TG" -o"$BIN/fpc-$ARCH_TG/$dest_fn" \
+  -dFPC_X64MM -dFPCMM_SERVER -dNOSYNDBZEOS -dNOSYNDBIBX -dFPCMM_REPORTMEMORYLEAKS \
+  -B -Se1 "./src/raw.pas" | grep "[Warning|Error|Fatal]:"
+
+script_successful

+ 152 - 0
frameworks/Pascal/mormot/src/raw.lpi

@@ -0,0 +1,152 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CONFIG>
+  <ProjectOptions>
+    <Version Value="12"/>
+    <General>
+      <Flags>
+        <MainUnitHasCreateFormStatements Value="False"/>
+        <MainUnitHasTitleStatement Value="False"/>
+        <MainUnitHasScaledStatement Value="False"/>
+      </Flags>
+      <SessionStorage Value="InProjectDir"/>
+      <Title Value="raw"/>
+      <UseAppBundle Value="False"/>
+      <ResourceType Value="res"/>
+    </General>
+    <BuildModes>
+      <Item Name="Debug" Default="True"/>
+      <Item Name="Release">
+        <CompilerOptions>
+          <Version Value="11"/>
+          <Target>
+            <Filename Value="exe/raw"/>
+          </Target>
+          <SearchPaths>
+            <IncludeFiles Value="../libs/mORMot/src;$(ProjOutDir)"/>
+            <Libraries Value="../libs/mORMot/static/$(TargetCPU)-$(TargetOS)"/>
+            <OtherUnitFiles Value="../libs/mORMot/src/core;../libs/mORMot/src/db;../libs/mORMot/src/rest;../libs/mORMot/src/app;../libs/mORMot/src/net;../libs/mORMot/src/lib;../libs/mORMot/src/orm;../libs/mORMot/src/crypt;../libs/mORMot/src/soa"/>
+            <UnitOutputDirectory Value="lib/$(TargetCPU)-$(TargetOS)"/>
+          </SearchPaths>
+          <CodeGeneration>
+            <Optimizations>
+              <OptimizationLevel Value="3"/>
+            </Optimizations>
+          </CodeGeneration>
+          <Linking>
+            <Debugging>
+              <GenerateDebugInfo Value="False"/>
+              <DebugInfoType Value="dsDwarf3"/>
+            </Debugging>
+          </Linking>
+          <Other>
+            <CustomOptions Value="-dFPC_X64MM
+-dFPCMM_SERVER
+-dFPCMM_REPORTMEMORYLEAKS
+-dNOSYNDBZEOS
+-dNOSYNDBIBX"/>
+            <OtherDefines Count="5">
+              <Define0 Value="FPC_X64MM"/>
+              <Define1 Value="FPCMM_SERVER"/>
+              <Define2 Value="FPCMM_REPORTMEMORYLEAKS"/>
+              <Define3 Value="NOSYNDBZEOS"/>
+              <Define4 Value="NOSYNDBIBX"/>
+            </OtherDefines>
+          </Other>
+        </CompilerOptions>
+      </Item>
+    </BuildModes>
+    <PublishOptions>
+      <Version Value="2"/>
+      <UseFileFilters Value="True"/>
+    </PublishOptions>
+    <RunParams>
+      <FormatVersion Value="2"/>
+    </RunParams>
+    <Units>
+      <Unit>
+        <Filename Value="raw.pas"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+    </Units>
+  </ProjectOptions>
+  <CompilerOptions>
+    <Version Value="11"/>
+    <Target>
+      <Filename Value="exe/raw"/>
+    </Target>
+    <SearchPaths>
+      <IncludeFiles Value="../libs/mORMot/src;$(ProjOutDir)"/>
+      <Libraries Value="../libs/mORMot/static/$(TargetCPU)-$(TargetOS)"/>
+      <OtherUnitFiles Value="../libs/mORMot/src/core;../libs/mORMot/src/db;../libs/mORMot/src/rest;../libs/mORMot/src/app;../libs/mORMot/src/net;../libs/mORMot/src/lib;../libs/mORMot/src/orm;../libs/mORMot/src/crypt;../libs/mORMot/src/soa"/>
+      <UnitOutputDirectory Value="lib/$(TargetCPU)-$(TargetOS)"/>
+    </SearchPaths>
+    <Linking>
+      <Debugging>
+        <DebugInfoType Value="dsDwarf3"/>
+      </Debugging>
+    </Linking>
+    <Other>
+      <CustomOptions Value="-dFPC_X64MM
+-dFPCMM_SERVER
+-dFPCMM_REPORTMEMORYLEAKS
+-dFPCMM_DEBUG
+-dNOSYNDBZEOS
+-dNOSYNDBIBX"/>
+      <OtherDefines Count="7">
+        <Define0 Value="FPC_X64MM"/>
+        <Define1 Value="FPCMM_SERVER"/>
+        <Define2 Value="FPCMM_REPORTMEMORYLEAKS"/>
+        <Define3 Value="FPCMM_DEBUG"/>
+        <Define4 Value="WITH_LOGS"/>
+        <Define5 Value="NOSYNDBZEOS"/>
+        <Define6 Value="NOSYNDBIBX"/>
+      </OtherDefines>
+    </Other>
+  </CompilerOptions>
+  <Debugging>
+    <Exceptions>
+      <Item>
+        <Name Value="EAbort"/>
+      </Item>
+      <Item>
+        <Name Value="ECodetoolError"/>
+      </Item>
+      <Item>
+        <Name Value="EFOpenError"/>
+      </Item>
+      <Item>
+        <Name Value="EInterfaceFactoryException"/>
+      </Item>
+      <Item>
+        <Name Value="EAssertionFailed"/>
+      </Item>
+      <Item>
+        <Name Value="ERttiException"/>
+      </Item>
+      <Item>
+        <Name Value="EFastReader"/>
+      </Item>
+      <Item>
+        <Name Value="RunError(216)"/>
+      </Item>
+      <Item>
+        <Name Value="ENetSock"/>
+      </Item>
+      <Item>
+        <Name Value="EThreadError"/>
+      </Item>
+      <Item>
+        <Name Value="ESynCrypto"/>
+      </Item>
+      <Item>
+        <Name Value="ERelayProtocol"/>
+      </Item>
+      <Item>
+        <Name Value="EServiceException"/>
+      </Item>
+      <Item>
+        <Name Value="EFCreateError"/>
+      </Item>
+    </Exceptions>
+  </Debugging>
+</CONFIG>

+ 603 - 0
frameworks/Pascal/mormot/src/raw.pas

@@ -0,0 +1,603 @@
+program raw;
+
+{
+TechEmpower framework benchmarks implementation
+See https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview
+}
+
+{$I mormot.defines.inc}
+
+{.$define USE_SQLITE3}
+// may be defined to use a SQLite3 database instead of external PostgresSQL DB
+// - note: /rawupdates and /rawqueries are PostgresSQL specific and will fail
+
+{.$define WITH_LOGS}
+// logging is fine for debugging, less for benchmarking ;)
+
+uses
+  {$I mormot.uses.inc} // include mormot.core.fpcx64mm
+  sysutils,
+  classes,
+  BaseUnix,
+  mormot.core.base,
+  mormot.core.os,
+  mormot.core.rtti,
+  mormot.core.log,
+  mormot.core.unicode,
+  mormot.core.text,
+  mormot.core.buffers,
+  mormot.core.json,
+  mormot.core.data,
+  mormot.core.variants,
+  mormot.core.perf,
+  mormot.core.mustache,
+  mormot.orm.core,
+  mormot.orm.sql,
+  mormot.db.core,
+  mormot.db.raw.sqlite3,
+  mormot.db.raw.sqlite3.static,
+  {$ifdef USE_SQLITE3}
+  mormot.db.sql.sqlite3,
+  {$endif USE_SQLITE3}
+  mormot.rest.sqlite3,
+  mormot.net.http,
+  mormot.net.server,
+  mormot.net.async,
+  mormot.db.sql,
+  mormot.db.sql.postgres;
+
+type
+  TMessageRec = packed record
+    message: RawUtf8;
+  end;
+  TWorldRec = packed record
+    id: integer;
+    randomNumber: integer;
+  end;
+  TWorlds = array of TWorldRec;
+  TFortune = packed record
+    id: integer;
+    message: RawUtf8;
+  end;
+  TFortunes = array of TFortune;
+
+  TOrmWorld = class(TOrm)
+  protected
+    fRandomNumber: integer;
+  published
+    property RandomNumber: integer
+      read fRandomNumber write fRandomNumber;
+  end;
+  TOrmCachedWorld = class(TOrmWorld);
+  TOrmWorldClass = class of TOrmWorld;
+  TOrmFortune = class(TOrm)
+  protected
+    fMessage: RawUtf8;
+  published
+    property Message: RawUtf8
+      read fMessage write fMessage;
+  end;
+  TOrmFortunes = array of TOrmFortune;
+
+  { TRawAsyncServer }
+
+  TRawAsyncServer = class
+  private
+    fHttpServer: THttpAsyncServer;
+    fDbPool: TSqlDBConnectionProperties;
+    fModel: TOrmModel;
+    fStore: TRestServerDB;
+    fTemplate: TSynMustache;
+  protected
+    // main HTTP routing method
+    function DoOnRequest(ctxt: THttpServerRequestAbstract): cardinal;
+    // return ?queries= parameter value. If missed or < 1 return 1, if > 500 return 500
+    function getQueriesParamValue(ctxt: THttpServerRequestAbstract;
+      const search: RawUtf8 = 'QUERIES='): integer;
+    procedure getRandomWorlds(cnt: PtrInt; out res: TWorlds);
+    {$ifdef USE_SQLITE3}
+    procedure GenerateDB;
+    {$endif USE_SQLITE3}
+  public
+    constructor Create(threadCount: integer);
+    destructor Destroy; override;
+    // those are the implementation methods
+    function json(ctxt: THttpServerRequestAbstract): cardinal;
+    function db(ctxt: THttpServerRequestAbstract): cardinal;
+    // /queries and /cached-queries endpoints are implemented in doqueries
+    function doqueries(ctxt: THttpServerRequestAbstract; orm: TOrmWorldClass;
+      const search: RawUtf8): cardinal;
+    function fortunes(ctxt: THttpServerRequestAbstract): cardinal;
+    function updates(ctxt: THttpServerRequestAbstract): cardinal;
+    function plaintext(ctxt: THttpServerRequestAbstract): cardinal;
+    function rawdb(ctxt: THttpServerRequestAbstract): cardinal;
+    function rawqueries(ctxt: THttpServerRequestAbstract): cardinal;
+    function rawfortunes(ctxt: THttpServerRequestAbstract): cardinal;
+    function rawupdates(ctxt: THttpServerRequestAbstract): cardinal;
+  end;
+
+const
+  TEXT_CONTENT_TYPE_NO_ENCODING: RawUtf8 = 'text/plain';
+  HELLO_WORLD: RawUtf8 = 'Hello, World!';
+  WORLD_COUNT = 10000;
+
+  WORLD_READ_SQL = 'select id, randomNumber from World where id=?';
+  WORLD_UPDATE_SQLN ='update World as t set randomNumber = v.r from ' +
+    '(SELECT unnest(?::NUMERIC[]), unnest(?::NUMERIC[])) as v(id, r)' +
+    ' where t.id = v.id';
+  FORTUNES_SQL = 'select id, message from Fortune';
+
+  FORTUNES_MESSAGE = 'Additional fortune added at request time.';
+  FORTUNES_TPL = '<!DOCTYPE html>' +
+                 '<html>' +
+                 '<head><title>Fortunes</title></head>' +
+                 '<body>' +
+                 '<table>' +
+                 '<tr><th>id</th><th>message</th></tr>' +
+                 '{{#.}}' +
+                 '<tr><td>{{id}}</td><td>{{message}}</td></tr>' +
+                 '{{/.}}' +
+                 '</table>' +
+                 '</body>' +
+                 '</html>';
+
+
+{ TRawAsyncServer }
+
+constructor TRawAsyncServer.Create(threadCount: integer);
+begin
+  inherited Create;
+  {$ifdef USE_SQLITE3}
+  fDbPool := TSqlDBSQLite3ConnectionProperties.Create(
+      SQLITE_MEMORY_DATABASE_NAME, '', '', '');
+  fDbPool.StatementCacheReplicates := threadcount; // shared SQlite3 connection
+  {$else}
+  fDbPool := TSqlDBPostgresConnectionProperties.Create(
+    'tfb-database:5432', 'hello_world', 'benchmarkdbuser', 'benchmarkdbpass');
+  {$endif USE_SQLITE3}
+  fModel := TOrmModel.Create([TOrmWorld, TOrmFortune, TOrmCachedWorld]);
+  OrmMapExternal(fModel, [TOrmWorld, TOrmFortune], fDbPool);
+  // CachedWorld table doesn't exists in DB, but should as read in requirements.
+  // Use world table as in other implementations.
+  OrmMapExternal(fModel, TOrmCachedWorld, fDbPool, 'world');
+  fStore := TRestServerDB.Create(fModel, SQLITE_MEMORY_DATABASE_NAME);
+  fStore.NoAjaxJson := true;
+  {$ifdef USE_SQLITE3}
+  GenerateDB;
+  {$else}
+  fStore.Server.CreateMissingTables; // create SQlite3 virtual tables
+  {$endif USE_SQLITE3}
+  if fStore.Server.Cache.SetCache(TOrmCachedWorld) then
+    fStore.Server.Cache.FillFromQuery(TOrmCachedWorld, '', []);
+  fTemplate := TSynMustache.Parse(FORTUNES_TPL);
+  fHttpServer := THttpAsyncServer.Create(
+    '8080', nil, nil, '', threadCount,
+    5 * 60 * 1000,        // 5 minutes keep alive connections
+    [hsoNoXPoweredHeader, // not needed for a benchmark
+     hsoHeadersInterning, // reduce memory contention for /plaintext and /json
+     hsoNoStats,          // disable low-level statistic counters
+     {$ifdef WITH_LOGS}
+     hsoLogVerbose,
+     {$endif WITH_LOGS}
+     hsoIncludeDateHeader // required by TPW General Test Requirements #5
+    ]);
+  fHttpServer.HttpQueueLength := 100000; // needed e.g. from wrk/ab benchmarks
+  fHttpServer.OnRequest := DoOnRequest;
+  fHttpServer.WaitStarted; // raise exception e.g. on binding issue
+end;
+
+destructor TRawAsyncServer.Destroy;
+begin
+  fHttpServer.Free;
+  fStore.Free;
+  fModel.Free;
+  fDBPool.free;
+  inherited Destroy;
+end;
+
+function TRawAsyncServer.DoOnRequest(ctxt: THttpServerRequestAbstract): cardinal;
+const
+  ROUTES: array[0..11] of RawUtf8 = (
+     // basic tests
+     '/PLAINTEXT', '/JSON',
+     // ORM tests
+     '/DB', '/QUERIES', '/FORTUNES', '/UPDATES', '/CACHED-QUERIES',
+     // raw tests
+     '/RAWDB' , '/RAWQUERIES', '/RAWFORTUNES', '/RAWUPDATES', '');
+var
+  route: PtrInt;
+begin
+  {$ifdef WITH_LOGS}
+  TSynLog.Add.Log(sllServiceCall, 'DoOnRequest % %', [ctxt.Method, ctxt.Url], self);
+  {$endif WITH_LOGS}
+  result := HTTP_NOTFOUND;
+  route := IdemPPChar(pointer(ctxt.Url), @ROUTES);
+  if (route >= 0) and
+     (ctxt.Url[length(ROUTES[route]) + 1] in [#0, '?', '/']) then
+    case route of
+      // basic tests
+      0: result := plaintext(ctxt);
+      1: result := json(ctxt);
+      // ORM tests
+      2: result := db(ctxt);
+      3: result := doqueries(ctxt, TOrmWorld, 'QUERIES=');
+      4: result := fortunes(ctxt);
+      5: result := updates(ctxt);
+      6: result := doqueries(ctxt, TOrmCachedWorld, 'COUNT=');
+      // raw tests
+      7: result := rawdb(ctxt);
+      8: result := rawqueries(ctxt);
+      9: result := rawfortunes(ctxt);
+      10: result := rawupdates(ctxt);
+    end;
+end;
+
+function RandomWorld: integer; inline;
+begin
+  result := Random32(WORLD_COUNT) + 1;
+end;
+
+{$ifdef USE_SQLITE3}
+
+const
+  _FORTUNES: array[1..12] of RawUtf8 = (
+  'fortune: No such file or directory',
+  'A computer scientist is someone who fixes things that aren''t broken.',
+  'After enough decimal places, nobody gives a damn.',
+  'A bad random number generator: 1, 1, 1, 1, 1, 4.33e+67, 1, 1, 1',
+  'A computer program does what you tell it to do, not what you want it to do.',
+  'Emacs is a nice operating system, but I prefer UNIX. — Tom Christaensen',
+  'Any program that runs right is obsolete.',
+  'A list is only as strong as its weakest link. — Donald Knuth',
+  'Feature: A bug with seniority.',
+  'Computers make very fast, very accurate mistakes.',
+  '<script>alert("This should not be displayed in a browser alert box.");</script>',
+  'フレームワークのベンチマーク');
+
+procedure TRawAsyncServer.GenerateDB;
+var
+  i: PtrInt;
+  b: TRestBatch;
+  w: TOrmWorld;
+  f: TOrmFortune;
+begin
+  fStore.Server.CreateMissingTables;
+  w := TOrmWorld.Create;
+  f := TOrmFortune.Create;
+  b := TRestBatch.Create(fStore.Orm, nil);
+  try
+    for i := 1 to WORLD_COUNT do
+    begin
+      w.IDValue := i;
+      w.RandomNumber := RandomWorld;
+      b.Add(w, true, true);
+    end;
+    for i := low(_FORTUNES) to high(_FORTUNES) do
+    begin
+      f.IDValue := i;
+      f.Message := _FORTUNES[i];
+      b.Add(f, true, true);
+    end;
+    if fStore.Orm.BatchSend(b) <> HTTP_SUCCESS then
+      raise EOrmBatchException.Create('GenerateDB failed');
+  finally
+    b.Free;
+    f.Free;
+    w.Free;
+  end;
+end;
+
+{$endif USE_SQLITE3}
+
+function TRawAsyncServer.json(ctxt: THttpServerRequestAbstract): cardinal;
+var
+  msgRec: TMessageRec;
+begin
+  msgRec.message := HELLO_WORLD;
+  ctxt.OutContentType := JSON_CONTENT_TYPE;
+  ctxt.OutContent := SaveJson(msgRec, TypeInfo(TMessageRec));
+  result := HTTP_SUCCESS;
+end;
+
+function TRawAsyncServer.plaintext(ctxt: THttpServerRequestAbstract): cardinal;
+begin
+  ctxt.OutContentType := TEXT_CONTENT_TYPE_NO_ENCODING;
+  ctxt.OutContent := HELLO_WORLD;
+  result := HTTP_SUCCESS;
+end;
+
+function TRawAsyncServer.rawdb(ctxt: THttpServerRequestAbstract): cardinal;
+var
+  conn: TSqlDBConnection;
+  stmt: ISQLDBStatement;
+begin
+  result := HTTP_SERVERERROR;
+  conn := fDbPool.ThreadSafeConnection;
+  stmt := conn.NewStatementPrepared(WORLD_READ_SQL, true, true);
+  stmt.Bind(1, RandomWorld);
+  stmt.ExecutePrepared;
+  if stmt.Step then
+  begin
+    ctxt.OutContent := FormatUtf8('{"id":%,"randomNumber":%}',
+      [stmt.ColumnInt(0), stmt.ColumnInt(1)]);
+    ctxt.OutContentType := JSON_CONTENT_TYPE;
+    result := HTTP_SUCCESS;
+    stmt.ReleaseRows;
+  end;
+  stmt := nil;
+end;
+
+function TRawAsyncServer.db(ctxt: THttpServerRequestAbstract): cardinal;
+var
+  w: TOrmWorld;
+begin
+  w := TOrmWorld.Create(fStore.Orm, RandomWorld);
+  try
+    ctxt.OutContent := FormatUtf8('{"id":%,"randomNumber":%}',
+      [w.IDValue, w.randomNumber]);
+    ctxt.OutContentType := JSON_CONTENT_TYPE;
+    result := HTTP_SUCCESS;
+  finally
+    w.Free;
+  end;
+end;
+
+function TRawAsyncServer.getQueriesParamValue(ctxt: THttpServerRequestAbstract;
+  const search: RawUtf8): integer;
+var
+  p: PUtf8Char;
+begin
+  result := 0;
+  p := PosChar(pointer(ctxt.Url), '?');
+  if p <> nil then
+    UrlDecodeInteger(p + 1, search, result);
+  if result = 0 then
+    result := 1
+  else if result > 500 then
+    result := 500;
+end;
+
+procedure TRawAsyncServer.getRandomWorlds(cnt: PtrInt; out res: TWorlds);
+var
+  conn: TSqlDBConnection;
+  stmt: ISQLDBStatement;
+  i: PtrInt;
+begin
+  SetLength(res{%H-}, cnt);
+  conn := fDbPool.ThreadSafeConnection;
+  stmt := conn.NewStatementPrepared(WORLD_READ_SQL, true, true);
+  for i := 0 to cnt - 1 do
+  begin
+    stmt.Bind(1, RandomWorld);
+    stmt.ExecutePrepared;
+    if not stmt.Step then
+      exit;
+    res[i].id := stmt.ColumnInt(0);
+    res[i].randomNumber := stmt.ColumnInt(1);
+  end;
+end;
+
+function TRawAsyncServer.rawqueries(ctxt: THttpServerRequestAbstract): cardinal;
+var
+  cnt: PtrInt;
+  res: TWorlds;
+begin
+  cnt := getQueriesParamValue(ctxt);
+  getRandomWorlds(cnt, res);
+  if res = nil then
+    exit(HTTP_SERVERERROR);
+  ctxt.OutContentType := JSON_CONTENT_TYPE;
+  ctxt.OutContent := SaveJson(res, TypeInfo(TWorlds));
+  result := HTTP_SUCCESS;
+end;
+
+function TRawAsyncServer.doqueries(ctxt: THttpServerRequestAbstract;
+  orm: TOrmWorldClass; const search: RawUtf8): cardinal;
+var
+  cnt, i: PtrInt;
+  res: TWorlds;
+  w: TOrmWorld;
+begin
+  result := HTTP_SERVERERROR;
+  cnt := getQueriesParamValue(ctxt, search);
+  SetLength(res, cnt);
+  w := orm.Create; // TOrmWorld or TOrmCachedWorld
+  try
+    for i := 0 to cnt - 1 do
+    begin
+      if not fStore.Orm.Retrieve(RandomWorld, w) then
+        exit;
+      res[i].id := w.IDValue;
+      res[i].randomNumber := w.RandomNumber;
+    end;
+  finally
+    w.Free;
+  end;
+  ctxt.OutContentType := JSON_CONTENT_TYPE;
+  ctxt.OutContent := SaveJson(res, TypeInfo(TWorlds));
+  result := HTTP_SUCCESS;
+end;
+
+function OrmFortuneCompareByMessage(const A, B): integer;
+begin
+  result := StrComp(pointer(TOrmFortune(A).Message), pointer(TOrmFortune(B).Message));
+end;
+
+function TRawAsyncServer.fortunes(ctxt: THttpServerRequestAbstract): cardinal;
+var
+  list: TOrmFortunes;
+  new: TOrmFortune;
+  arr: TDynArray;
+begin
+  result := HTTP_SERVERERROR;
+  arr.Init(TypeInfo(TOrmFortunes), list);
+  if fStore.Orm.RetrieveListObjArray(list, TOrmFortune, '', []) then
+    try
+      new := TOrmFortune.Create;
+      new.Message := FORTUNES_MESSAGE;
+      arr.Add(new);
+      arr.Sort(OrmFortuneCompareByMessage);
+      ctxt.OutContent := fTemplate.RenderDataArray(arr);
+      ctxt.OutContentType := HTML_CONTENT_TYPE;
+      result := HTTP_SUCCESS;
+    finally
+      arr.Clear;
+    end;
+end;
+
+function FortuneCompareByMessage(const A, B): integer;
+begin
+  result := StrComp(pointer(TFortune(A).message), pointer(TFortune(B).message));
+end;
+
+function TRawAsyncServer.rawfortunes(ctxt: THttpServerRequestAbstract): cardinal;
+var
+  conn: TSqlDBConnection;
+  stmt: ISQLDBStatement;
+  list: TFortunes;
+  f: TFortune;
+  arr: TDynArray;
+  n: integer;
+begin
+  result := HTTP_SERVERERROR;
+  conn := fDbPool.ThreadSafeConnection;
+  stmt := conn.NewStatementPrepared(FORTUNES_SQL, true, true);
+  stmt.ExecutePrepared;
+  arr.Init(TypeInfo(TFortunes), list, @n);
+  while stmt.Step do
+  begin
+    f.id := stmt.ColumnInt(0);
+    f.message := stmt.ColumnUtf8(1);
+    arr.Add(f);
+  end;
+  f.id := 0;
+  f.message := FORTUNES_MESSAGE;
+  arr.Add(f);
+  arr.Sort(FortuneCompareByMessage);
+  ctxt.OutContent := fTemplate.RenderDataArray(arr);
+  ctxt.OutContentType := HTML_CONTENT_TYPE;
+  result := HTTP_SUCCESS;
+end;
+
+function TRawAsyncServer.updates(ctxt: THttpServerRequestAbstract): cardinal;
+var
+  cnt, i: PtrInt;
+  res: TWorlds;
+  w: TOrmWorld;
+  b: TRestBatch;
+begin
+  result := HTTP_SERVERERROR;
+  cnt := getQueriesParamValue(ctxt);
+  SetLength(res, cnt);
+  b := TRestBatch.Create(fStore.ORM, TOrmWorld, {transrows=}0,
+    [boExtendedJson, boNoModelEncoding, boPutNoCacheFlush]);
+  w := TOrmWorld.Create;
+  try
+    for i := 0 to cnt - 1 do
+    begin
+      if not fStore.Orm.Retrieve(RandomWorld, w) then
+        exit;
+      w.RandomNumber := RandomWorld;
+      b.Update(w);
+      res[i].id := w.IDValue;
+      res[i].randomNumber := w.RandomNumber;
+    end;
+    result := fStore.Orm.BatchSend(b);
+  finally
+    w.Free;
+    b.Free;
+  end;
+  if result <> HTTP_SUCCESS then
+    exit;
+  ctxt.OutContentType := JSON_CONTENT_TYPE;
+  ctxt.OutContent := SaveJson(res, TypeInfo(TWorlds));
+end;
+
+function TRawAsyncServer.rawupdates(ctxt: THttpServerRequestAbstract): cardinal;
+var
+  cnt, i: PtrInt;
+  words: TWorlds;
+  ids, nums: TInt64DynArray;
+  conn: TSqlDBConnection;
+  stmt: ISQLDBStatement;
+begin
+  cnt := getQueriesParamValue(ctxt);
+  getRandomWorlds(cnt, words);
+  if length(words) <> cnt then
+    exit(HTTP_SERVERERROR);
+  setLength(ids, cnt);
+  setLength(nums, cnt);
+  // generate new randoms, fill parameters arrays for update
+  for i := 0 to cnt - 1 do
+  begin
+    words[i].randomNumber := RandomWorld;
+    ids[i] := words[i].id;
+    nums[i] := words[i].randomNumber;
+  end;
+  conn := fDbPool.ThreadSafeConnection;
+  //conn.StartTransaction;
+  stmt := conn.NewStatementPrepared(WORLD_UPDATE_SQLN, false);
+  stmt.BindArray(1, nums);
+  stmt.BindArray(2, ids);
+  stmt.ExecutePrepared;
+  //conn.Commit; // autocommit
+  ctxt.OutContentType := JSON_CONTENT_TYPE;
+  ctxt.OutContent := SaveJson(words, TypeInfo(TWorlds));
+  result := HTTP_SUCCESS;
+end;
+
+
+
+var
+  rawServer: TRawAsyncServer;
+  threads: integer;
+
+begin
+  {$ifdef WITH_LOGS}
+  TSynLog.Family.Level := LOG_VERBOSE; // disable logs for benchmarking
+  TSynLog.Family.HighResolutionTimestamp := true;
+  TSynLog.Family.AutoFlushTimeOut := 1;
+  {$else}
+  {$ifdef USE_SQLITE3}
+  TSynLog.Family.Level := LOG_STACKTRACE; // minimal debug logs on fatal errors
+  {$endif USE_SQLITE3}
+  {$endif WITH_LOGS}
+  TSynLog.Family.PerThreadLog := ptIdentifiedInOneFile;
+
+  Rtti.RegisterFromText([
+    TypeInfo(TMessageRec), 'message:RawUtf8',
+    TypeInfo(TWorldRec),   'id,randomNumber:integer',
+    TypeInfo(TFortune),    'id:integer message:RawUtf8']);
+
+  if (ParamCount <> 1) or
+     not TryStrToInt(ParamStr(1), threads) then
+    threads := SystemInfo.dwNumberOfProcessors * 4;
+  if threads < 16 then
+    threads := 16
+  else if threads > 64 then
+    threads := 64; // prevents too many PostgreSQL per connection forks
+
+  rawServer := TRawAsyncServer.Create(threads);
+  try
+    {$I-}
+    writeln;
+    writeln(rawServer.fHttpServer.ClassName, ' running on localhost:',
+      rawServer.fHttpServer.SockPort, '; num thread=', threads, ' db=',
+      rawServer.fDbPool.DbmsEngineName, #10);
+    {$ifdef USE_SQLITE3}
+    writeln('Press [Enter] to terminate'#10);
+    readln;
+    {$else}
+    writeln('Press Ctrl+C or use SIGTERM to terminate'#10);
+    FpPause;
+    {$endif USE_SQLITE3}
+    //TSynLog.Family.Level := LOG_VERBOSE; // enable shutdown logs for debug
+    writeln(ObjectToJsonDebug(rawServer.fHttpServer, [woDontStoreVoid, woHumanReadable]));
+    {$ifdef FPC_X64MM}
+    WriteHeapStatus(' ', 16, 8, {compileflags=}true);
+    {$endif FPC_X64MM}
+  finally
+    rawServer.Free;
+  end;
+
+end.