Browse Source

- thread pool auto-tuning: use 1 listening socket per logical CPU and 8 working thread pes listener socket (#7994)

- for /updates with count <=20 use 'case .. when .. then' pattern
[mORMot] update to mORMot 2.0.stable
[mORMot] improved cached queries performance

Co-authored-by: pavel.mash <[email protected]>
pavelmash 2 years ago
parent
commit
a85041b95c

+ 2 - 2
frameworks/Pascal/mormot/mormot.dockerfile

@@ -1,10 +1,10 @@
 FROM freepascal/fpc:3.2.2-focal-full as builder
 FROM freepascal/fpc:3.2.2-focal-full as builder
 
 
 RUN apt-get update -yqq
 RUN apt-get update -yqq
-RUN apt-get install -yqq p7zip-full zlib1g-dev
+RUN apt-get install -yqq zlib1g-dev
 
 
 WORKDIR /build
 WORKDIR /build
-COPY src/ src/
+COPY src/raw.* src/
 COPY setup_and_build.sh .
 COPY setup_and_build.sh .
 
 
 RUN /bin/bash -c ./setup_and_build.sh
 RUN /bin/bash -c ./setup_and_build.sh

+ 6 - 12
frameworks/Pascal/mormot/setup_and_build.sh

@@ -24,23 +24,18 @@ script_aborted() {
 set -o pipefail
 set -o pipefail
 
 
 rm -rf ./libs
 rm -rf ./libs
-
+mkdir -p ./libs/mORMot/static
 # echo "Getting the latest pre-release URL..."
 # 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=$(wget -qO- https://api.github.com/repos/synopse/mORMot2/releases/latest | jq -r '.tag_name')
-USED_TAG="2.0.4383"
+USED_TAG="2.0.stable"
 
 
 echo "Used release tag $USED_TAG"
 echo "Used release tag $USED_TAG"
-URL="https://github.com/synopse/mORMot2/releases/download/$USED_TAG/mormot2static.7z"
+URL="https://github.com/synopse/mORMot2/releases/download/$USED_TAG/mormot2static.tgz"
 echo "Download statics from $URL ..."
 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
+wget -qO- "$URL" | tar -xz -C ./libs/mORMot/static
 
 
 # uncomment for fixed commit URL
 # uncomment for fixed commit URL
-URL=https://github.com/synopse/mORMot2/tarball/0eb1a70da04481a6d478acff5183742eed1882f7
+URL=https://github.com/synopse/mORMot2/tarball/46f5360a668ccf7a7c4d538fb319b449da8a232f
 #URL="https://api.github.com/repos/synopse/mORMot2/tarball/$USED_TAG"
 #URL="https://api.github.com/repos/synopse/mORMot2/tarball/$USED_TAG"
 echo "Download and unpacking mORMot sources from $URL ..."
 echo "Download and unpacking mORMot sources from $URL ..."
 wget -qO- "$URL" | tar -xz -C ./libs/mORMot  --strip-components=1
 wget -qO- "$URL" | tar -xz -C ./libs/mORMot  --strip-components=1
@@ -72,7 +67,6 @@ fi
 # Warning: (5089) Local variable XXX of a managed type 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
 # 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
 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..."
 echo "Start compiling..."
 fpc -MDelphi -Sci -Ci -O4 -g -gl -gw2 -Xg -k'-rpath=$ORIGIN' -k-L$BIN \
 fpc -MDelphi -Sci -Ci -O4 -g -gl -gw2 -Xg -k'-rpath=$ORIGIN' -k-L$BIN \
   -T$TARGET -P$ARCH \
   -T$TARGET -P$ARCH \
@@ -82,7 +76,7 @@ fpc -MDelphi -Sci -Ci -O4 -g -gl -gw2 -Xg -k'-rpath=$ORIGIN' -k-L$BIN \
   -Fu"$MSRC/core" -Fu"$MSRC/db" -Fu"$MSRC/rest" -Fu"$MSRC/crypt" \
   -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"$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" \
   -FU"$BIN/fpc-$ARCH_TG/.dcu" -FE"$BIN/fpc-$ARCH_TG" -o"$BIN/fpc-$ARCH_TG/$dest_fn" \
-  -dFPC_LIBCMM -dNOSYNDBZEOS -dNOSYNDBIBX \
+  -dFPC_LIBCMM \
   -B -Se1 "./src/raw.pas" | grep "[Warning|Error|Fatal]:"
   -B -Se1 "./src/raw.pas" | grep "[Warning|Error|Fatal]:"
 
 
 script_successful
 script_successful

+ 272 - 211
frameworks/Pascal/mormot/src/raw.pas

@@ -1,8 +1,12 @@
 program raw;
 program raw;
 
 
 {
 {
-TechEmpower framework benchmarks implementation
-See https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview
+ ----------------------------------------------------
+   TechEmpower Framework Benchmarks implementation
+   in modern pascal and the mORMot 2 framework
+ ----------------------------------------------------
+ https://github.com/TechEmpower/FrameworkBenchmarks/wiki
+ command line optional syntax: raw [threads] [cores] [servers]
 }
 }
 
 
 {$I mormot.defines.inc}
 {$I mormot.defines.inc}
@@ -14,7 +18,6 @@ uses
   {$I mormot.uses.inc} // include mormot.core.fpcx64mm or mormot.core.fpclibcmm
   {$I mormot.uses.inc} // include mormot.core.fpcx64mm or mormot.core.fpclibcmm
   sysutils,
   sysutils,
   classes,
   classes,
-  BaseUnix,
   mormot.core.base,
   mormot.core.base,
   mormot.core.os,
   mormot.core.os,
   mormot.core.rtti,
   mormot.core.rtti,
@@ -28,16 +31,17 @@ uses
   mormot.core.perf,
   mormot.core.perf,
   mormot.core.mustache,
   mormot.core.mustache,
   mormot.orm.core,
   mormot.orm.core,
+  mormot.orm.base,
   mormot.orm.sql,
   mormot.orm.sql,
   mormot.db.core,
   mormot.db.core,
   mormot.db.raw.sqlite3,
   mormot.db.raw.sqlite3,
   mormot.db.raw.sqlite3.static,
   mormot.db.raw.sqlite3.static,
+  mormot.db.sql,
+  mormot.db.sql.postgres,
   mormot.rest.sqlite3,
   mormot.rest.sqlite3,
   mormot.net.http,
   mormot.net.http,
   mormot.net.server,
   mormot.net.server,
-  mormot.net.async,
-  mormot.db.sql,
-  mormot.db.sql.postgres;
+  mormot.net.async;
 
 
 type
 type
   // data structures
   // data structures
@@ -65,7 +69,6 @@ type
   end;
   end;
   TOrmCachedWorld = class(TOrmWorld);
   TOrmCachedWorld = class(TOrmWorld);
   TOrmWorlds = array of TOrmWorld;
   TOrmWorlds = array of TOrmWorld;
-  TOrmWorldClass = class of TOrmWorld;
   TOrmFortune = class(TOrm)
   TOrmFortune = class(TOrm)
   protected
   protected
     fMessage: RawUtf8;
     fMessage: RawUtf8;
@@ -84,11 +87,8 @@ type
     fStore: TRestServerDB;
     fStore: TRestServerDB;
     fTemplate: TSynMustache;
     fTemplate: TSynMustache;
   protected
   protected
-    // as used by rawqueries and rawupdates
-    function getRawRandomWorlds(cnt: PtrInt; out res: TWorlds): boolean;
-    // implements /queries and /cached-queries endpoints
-    function doqueries(ctxt: THttpServerRequest; orm: TOrmWorldClass;
-      const search: RawUtf8): cardinal;
+    // as used by /rawqueries and /rawupdates
+    function GetRawRandomWorlds(cnt: PtrInt; out res: TWorlds): boolean;
   public
   public
     constructor Create(threadCount: integer; flags: THttpServerOptions); reintroduce;
     constructor Create(threadCount: integer; flags: THttpServerOptions); reintroduce;
     destructor Destroy; override;
     destructor Destroy; override;
@@ -108,45 +108,44 @@ type
   end;
   end;
 
 
 const
 const
-  TEXT_CONTENT_TYPE_NO_ENCODING: RawUtf8 = 'text/plain';
   HELLO_WORLD: RawUtf8 = 'Hello, World!';
   HELLO_WORLD: RawUtf8 = 'Hello, World!';
-  WORLD_COUNT = 10000;
+  TEXT_CONTENT_TYPE_NO_ENCODING: RawUtf8 = 'text/plain';
 
 
-  WORLD_READ_SQL = 'select id,randomNumber from World where id=?';
+  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 ' +
   WORLD_UPDATE_SQLN ='update World as t set randomNumber = v.r from ' +
     '(SELECT unnest(?::bigint[]), unnest(?::bigint[]) order by 1) as v(id, r)' +
     '(SELECT unnest(?::bigint[]), unnest(?::bigint[]) order by 1) as v(id, r)' +
     ' where t.id = v.id';
     ' where t.id = v.id';
-  FORTUNES_SQL = 'select id,message from Fortune';
+  FORTUNES_SQL     = 'select id,message from Fortune';
 
 
   FORTUNES_MESSAGE = 'Additional fortune added at request time.';
   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>';
-
-
-function RandomWorld: integer; inline;
+  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>';
+
+
+function ComputeRandomWorld: integer; inline;
 begin
 begin
   result := Random32(WORLD_COUNT) + 1;
   result := Random32(WORLD_COUNT) + 1;
 end;
 end;
 
 
-function getQueriesParamValue(ctxt: THttpServerRequest;
+function GetQueriesParamValue(ctxt: THttpServerRequest;
   const search: RawUtf8 = 'QUERIES='): cardinal;
   const search: RawUtf8 = 'QUERIES='): cardinal;
 begin
 begin
-  if not ctxt.UrlParam(search, result) then
+  if not ctxt.UrlParam(search, result) or
+     (result = 0) then
     result := 1
     result := 1
   else if result > 500 then
   else if result > 500 then
-    result := 500
-  else if result < 1 then
-    result := 1;
+    result := 500;
 end;
 end;
 
 
 
 
@@ -160,10 +159,10 @@ begin
     'tfb-database:5432', 'hello_world', 'benchmarkdbuser', 'benchmarkdbpass');
     'tfb-database:5432', 'hello_world', 'benchmarkdbuser', 'benchmarkdbpass');
   // customize JSON serialization for TFB expectations
   // customize JSON serialization for TFB expectations
   TOrmWorld.OrmProps.Fields.JsonRenameProperties([
   TOrmWorld.OrmProps.Fields.JsonRenameProperties([
-    'ID', 'id',
+    'ID',           'id',
     'RandomNumber', 'randomNumber']);
     'RandomNumber', 'randomNumber']);
   TOrmCachedWorld.OrmProps.Fields.JsonRenameProperties([
   TOrmCachedWorld.OrmProps.Fields.JsonRenameProperties([
-    'ID', 'id',
+    'ID',           'id',
     'RandomNumber', 'randomNumber']);
     'RandomNumber', 'randomNumber']);
   // setup the ORM data model
   // setup the ORM data model
   fModel := TOrmModel.Create([TOrmWorld, TOrmFortune, TOrmCachedWorld]);
   fModel := TOrmModel.Create([TOrmWorld, TOrmFortune, TOrmCachedWorld]);
@@ -186,8 +185,8 @@ begin
     [hsoNoXPoweredHeader,  // not needed for a benchmark
     [hsoNoXPoweredHeader,  // not needed for a benchmark
      hsoHeadersInterning,  // reduce memory contention for /plaintext and /json
      hsoHeadersInterning,  // reduce memory contention for /plaintext and /json
      hsoNoStats,           // disable low-level statistic counters
      hsoNoStats,           // disable low-level statistic counters
-     //hsoThreadCpuAffinity, // better scaling of /plaintext in some cases
-     hsoReusePort,         // allow several processes binding on the same port
+     //hsoThreadCpuAffinity, // worse scaling on multi-servers
+     hsoThreadSmooting,    // seems a good option, even if not magical
      {$ifdef WITH_LOGS}
      {$ifdef WITH_LOGS}
      hsoLogVerbose,
      hsoLogVerbose,
      {$endif WITH_LOGS}
      {$endif WITH_LOGS}
@@ -196,8 +195,8 @@ begin
   fHttpServer.HttpQueueLength := 10000; // needed e.g. from wrk/ab benchmarks
   fHttpServer.HttpQueueLength := 10000; // needed e.g. from wrk/ab benchmarks
   // use default routing using RTTI on the TRawAsyncServer published methods
   // use default routing using RTTI on the TRawAsyncServer published methods
   fHttpServer.Route.RunMethods([urmGet], self);
   fHttpServer.Route.RunMethods([urmGet], self);
-  // writeln(fHttpServer.Route.Tree[urmGet].ToText);
-  fHttpServer.WaitStarted; // raise exception e.g. on binding issue
+  // wait for the server to be ready and raise exception e.g. on binding issue
+  fHttpServer.WaitStarted;
 end;
 end;
 
 
 destructor TRawAsyncServer.Destroy;
 destructor TRawAsyncServer.Destroy;
@@ -209,66 +208,9 @@ begin
   inherited Destroy;
   inherited Destroy;
 end;
 end;
 
 
-function TRawAsyncServer.plaintext(ctxt: THttpServerRequest): cardinal;
-begin
-  ctxt.OutContentType := TEXT_CONTENT_TYPE_NO_ENCODING;
-  ctxt.OutContent := HELLO_WORLD;
-  result := HTTP_SUCCESS;
-end;
-
-function TRawAsyncServer.json(ctxt: THttpServerRequest): cardinal;
-var
-  msgRec: TMessageRec;
-begin
-  msgRec.message := HELLO_WORLD;
-  ctxt.SetOutJson(@msgRec, TypeInfo(TMessageRec));
-  result := HTTP_SUCCESS;
-end;
+// query DB world table for /rawqueries and /rawupdates endpoints
 
 
-function TRawAsyncServer.rawdb(ctxt: THttpServerRequest): 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.SetOutJson(
-      '{"id":%,"randomNumber":%}', [stmt.ColumnInt(0), stmt.ColumnInt(1)]);
-    result := HTTP_SUCCESS;
-    stmt.ReleaseRows;
-  end;
-  stmt := nil;
-end;
-
-function TRawAsyncServer.db(ctxt: THttpServerRequest): cardinal;
-var
-  w: TOrmWorld;
-begin
-  w := TOrmWorld.Create(fStore.Orm, RandomWorld);
-  try
-    ctxt.SetOutJson(w);
-    result := HTTP_SUCCESS;
-  finally
-    w.Free;
-  end;
-end;
-
-function TRawAsyncServer.queries(ctxt: THttpServerRequest): cardinal;
-begin
-  result := doqueries(ctxt, TOrmWorld, 'QUERIES=');
-end;
-
-function TRawAsyncServer.cached_queries(ctxt: THttpServerRequest): cardinal;
-begin
-  result := doqueries(ctxt, TOrmCachedWorld, 'COUNT=');
-end;
-
-function TRawAsyncServer.getRawRandomWorlds(cnt: PtrInt; out res: TWorlds): boolean;
+function TRawAsyncServer.GetRawRandomWorlds(cnt: PtrInt; out res: TWorlds): boolean;
 var
 var
   conn: TSqlDBConnection;
   conn: TSqlDBConnection;
   stmt: ISQLDBStatement;
   stmt: ISQLDBStatement;
@@ -280,15 +222,14 @@ begin
   SetLength(res{%H-}, cnt);
   SetLength(res{%H-}, cnt);
   conn := fDbPool.ThreadSafeConnection;
   conn := fDbPool.ThreadSafeConnection;
   // specific code to use PostgresSQL pipelining mode
   // specific code to use PostgresSQL pipelining mode
-  // see test_multi_pipelines in
+  // see test_nosync in
   // https://github.com/postgres/postgres/blob/master/src/test/modules/libpq_pipeline/libpq_pipeline.c
   // https://github.com/postgres/postgres/blob/master/src/test/modules/libpq_pipeline/libpq_pipeline.c
   stmt := conn.NewStatementPrepared(WORLD_READ_SQL, true, true);
   stmt := conn.NewStatementPrepared(WORLD_READ_SQL, true, true);
-  //conn.StartTransaction;
   pConn.EnterPipelineMode;
   pConn.EnterPipelineMode;
   pStmt := TSqlDBPostgresStatement(stmt.Instance);
   pStmt := TSqlDBPostgresStatement(stmt.Instance);
   for i := 0 to cnt - 1 do
   for i := 0 to cnt - 1 do
   begin
   begin
-    pStmt.Bind(1, RandomWorld);
+    pStmt.Bind(1, ComputeRandomWorld);
     pStmt.SendPipelinePrepared;
     pStmt.SendPipelinePrepared;
     pConn.PipelineSync;
     pConn.PipelineSync;
   end;
   end;
@@ -303,42 +244,67 @@ begin
     pConn.CheckPipelineSync;
     pConn.CheckPipelineSync;
   end;
   end;
   pConn.ExitPipelineMode;
   pConn.ExitPipelineMode;
-  //conn.commit;
   result := true;
   result := true;
 end;
 end;
 
 
-function TRawAsyncServer.rawqueries(ctxt: THttpServerRequest): cardinal;
+// following methods implement the server endpoints
+
+function TRawAsyncServer.plaintext(ctxt: THttpServerRequest): cardinal;
+begin
+  ctxt.OutContent := HELLO_WORLD;
+  ctxt.OutContentType := TEXT_CONTENT_TYPE_NO_ENCODING;
+  result := HTTP_SUCCESS;
+end;
+
+function TRawAsyncServer.json(ctxt: THttpServerRequest): cardinal;
 var
 var
-  cnt: PtrInt;
-  res: TWorlds;
+  msgRec: TMessageRec;
 begin
 begin
-  cnt := getQueriesParamValue(ctxt);
-  if not getRawRandomWorlds(cnt, res) then
-    exit(HTTP_SERVERERROR);
-  ctxt.SetOutJson(@res, TypeInfo(TWorlds));
+  msgRec.message := HELLO_WORLD;
+  ctxt.SetOutJson(@msgRec, TypeInfo(TMessageRec));
   result := HTTP_SUCCESS;
   result := HTTP_SUCCESS;
 end;
 end;
 
 
-function TRawAsyncServer.doqueries(ctxt: THttpServerRequest;
-  orm: TOrmWorldClass; const search: RawUtf8): cardinal;
+function TRawAsyncServer.db(ctxt: THttpServerRequest): cardinal;
 var
 var
-  cnt, i: PtrInt;
-  res: TOrmWorlds;
+  w: TOrmWorld;
 begin
 begin
-  result := HTTP_SERVERERROR;
-  cnt := getQueriesParamValue(ctxt, search);
-  SetLength(res, cnt);
-  for i := 0 to cnt - 1 do
-  begin
-    res[i] := orm.Create; // TOrmWorld or TOrmCachedWorld
-    if not fStore.Orm.Retrieve(RandomWorld, res[i]) then
-      exit;
+  w := TOrmWorld.Create(fStore.Orm, ComputeRandomWorld);
+  try
+    ctxt.SetOutJson(w);
+    result := HTTP_SUCCESS;
+  finally
+    w.Free;
   end;
   end;
+end;
+
+function TRawAsyncServer.queries(ctxt: THttpServerRequest): cardinal;
+var
+  i: PtrInt;
+  res: TOrmWorlds;
+begin
+  SetLength(res, GetQueriesParamValue(ctxt, 'QUERIES='));
+  for i := 0 to length(res) - 1 do
+    res[i] := TOrmWorld.Create(fStore.Orm, ComputeRandomWorld);
   ctxt.SetOutJson(@res, TypeInfo(TOrmWorlds));
   ctxt.SetOutJson(@res, TypeInfo(TOrmWorlds));
   ObjArrayClear(res);
   ObjArrayClear(res);
   result := HTTP_SUCCESS;
   result := HTTP_SUCCESS;
 end;
 end;
 
 
+function TRawAsyncServer.cached_queries(ctxt: THttpServerRequest): cardinal;
+var
+  i: PtrInt;
+  res: TOrmWorlds;
+  cache: POrmCacheEntry;
+begin
+  cache := fStore.Orm.Cache.Table(TOrmCachedWorld);
+  SetLength(res, GetQueriesParamValue(ctxt, 'COUNT='));
+  for i := 0 to length(res) - 1 do
+    res[i] := cache.Get(ComputeRandomWorld);
+  ctxt.SetOutJson(@res, TypeInfo(TOrmWorlds));
+  result := HTTP_SUCCESS;
+end;
+
 function OrmFortuneCompareByMessage(const A, B): integer;
 function OrmFortuneCompareByMessage(const A, B): integer;
 begin
 begin
   result := StrComp(pointer(TOrmFortune(A).Message), pointer(TOrmFortune(B).Message));
   result := StrComp(pointer(TOrmFortune(A).Message), pointer(TOrmFortune(B).Message));
@@ -366,6 +332,66 @@ begin
     end;
     end;
 end;
 end;
 
 
+function TRawAsyncServer.updates(ctxt: THttpServerRequest): cardinal;
+var
+  i: PtrInt;
+  res: TOrmWorlds;
+  w: TOrmWorld;
+  b: TRestBatch;
+begin
+  result := HTTP_SERVERERROR;
+  SetLength(res, GetQueriesParamValue(ctxt));
+  b := TRestBatch.Create(fStore.ORM, TOrmWorld, {transrows=}0,
+    [boExtendedJson, boNoModelEncoding, boPutNoCacheFlush]);
+  try
+    for i := 0 to length(res) - 1 do
+    begin
+      w := TOrmWorld.Create;
+      res[i] := w;
+      if not fStore.Orm.Retrieve(ComputeRandomWorld, w) then
+        exit;
+      w.RandomNumber := ComputeRandomWorld;
+      b.Update(w);
+    end;
+    result := b.Send;
+    if result = HTTP_SUCCESS then
+      ctxt.SetOutJson(@res, TypeInfo(TOrmWorlds));
+  finally
+    b.Free;
+    ObjArrayClear(res);
+  end;
+end;
+
+function TRawAsyncServer.rawdb(ctxt: THttpServerRequest): cardinal;
+var
+  conn: TSqlDBConnection;
+  stmt: ISQLDBStatement;
+begin
+  result := HTTP_SERVERERROR;
+  conn := fDbPool.ThreadSafeConnection;
+  stmt := conn.NewStatementPrepared(WORLD_READ_SQL, true, true);
+  stmt.Bind(1, ComputeRandomWorld);
+  stmt.ExecutePrepared;
+  if stmt.Step then
+  begin
+    ctxt.SetOutJson(
+      '{"id":%,"randomNumber":%}', [stmt.ColumnInt(0), stmt.ColumnInt(1)]);
+    result := HTTP_SUCCESS;
+    stmt.ReleaseRows;
+  end;
+  stmt := nil;
+end;
+
+function TRawAsyncServer.rawqueries(ctxt: THttpServerRequest): cardinal;
+var
+  res: TWorlds;
+begin
+  if not GetRawRandomWorlds(GetQueriesParamValue(ctxt), res) then
+    exit(HTTP_SERVERERROR);
+  ctxt.SetOutJson(@res, TypeInfo(TWorlds));
+  result := HTTP_SUCCESS;
+end;
+
 function FortuneCompareByMessage(const A, B): integer;
 function FortuneCompareByMessage(const A, B): integer;
 begin
 begin
   result := StrComp(pointer(TFortune(A).message), pointer(TFortune(B).message));
   result := StrComp(pointer(TFortune(A).message), pointer(TFortune(B).message));
@@ -399,41 +425,51 @@ begin
   result := HTTP_SUCCESS;
   result := HTTP_SUCCESS;
 end;
 end;
 
 
-function TRawAsyncServer.updates(ctxt: THttpServerRequest): cardinal;
 var
 var
-  cnt, i: PtrInt;
-  res: TOrmWorlds;
-  w: TOrmWorld;
-  b: TRestBatch;
+  LastComputeUpdateSql: RawUtf8;
+  LastComputeUpdateSqlCnt: integer;
+  LastComputeUpdateSqlSafe: TLightLock;
+
+function ComputeUpdateSql(cnt: integer): RawUtf8;
+var
+  i: integer;
+  W: TTextWriter;
+  tmp: TTextWriterStackBuffer;
 begin
 begin
-  result := HTTP_SERVERERROR;
-  cnt := getQueriesParamValue(ctxt);
-  SetLength(res, cnt);
-  b := TRestBatch.Create(fStore.ORM, TOrmWorld, {transrows=}0,
-    [boExtendedJson, boNoModelEncoding, boPutNoCacheFlush]);
-  try
-    for i := 0 to cnt - 1 do
-    begin
-      w := TOrmWorld.Create;
-      res[i] := w;
-      if not fStore.Orm.Retrieve(RandomWorld, w) then
-        exit;
-      w.RandomNumber := RandomWorld;
-      b.Update(w);
+  LastComputeUpdateSqlSafe.Lock;
+  if cnt <> LastComputeUpdateSqlCnt then
+  begin
+    // update table set randomNumber = CASE id when ? then ? when ? then ? ...
+    // when ? then ? else randomNumber end where id in (?,?,?,?,?)
+    // - this weird syntax gives best number for TFB /rawupdates?queries=20 but
+    // seems not good for smaller or higher count - we won't include it in the
+    // ORM but only for our RAW results - as other frameworks (e.g. ntex) do
+    LastComputeUpdateSqlCnt := cnt;
+    W := TTextWriter.CreateOwnedStream(tmp);
+    try
+      W.AddShort('UPDATE world SET randomnumber = CASE id');
+      for i := 1 to cnt do
+        W.AddShort(' when ? then ?');
+      W.AddShort(' else randomNumber end where id in (');
+      repeat
+        W.Add('?', ',');
+        dec(cnt);
+      until cnt = 0;
+      W.CancelLastComma;
+      W.Add(')');
+      W.SetText(LastComputeUpdateSql);
+    finally
+      W.Free;
     end;
     end;
-    result := b.Send;
-    if result = HTTP_SUCCESS then
-      ctxt.SetOutJson(@res, TypeInfo(TOrmWorlds));
-  finally
-    b.Free;
-    ObjArrayClear(res);
   end;
   end;
+  result := LastComputeUpdateSql;
+  LastComputeUpdateSqlSafe.UnLock;
 end;
 end;
 
 
 function TRawAsyncServer.rawupdates(ctxt: THttpServerRequest): cardinal;
 function TRawAsyncServer.rawupdates(ctxt: THttpServerRequest): cardinal;
 var
 var
   cnt, i: PtrInt;
   cnt, i: PtrInt;
-  words: TWorlds;
+  res: TWorlds;
   ids, nums: TInt64DynArray;
   ids, nums: TInt64DynArray;
   conn: TSqlDBConnection;
   conn: TSqlDBConnection;
   stmt: ISQLDBStatement;
   stmt: ISQLDBStatement;
@@ -441,23 +477,36 @@ begin
   result := HTTP_SERVERERROR;
   result := HTTP_SERVERERROR;
   conn := fDbPool.ThreadSafeConnection;
   conn := fDbPool.ThreadSafeConnection;
   cnt := getQueriesParamValue(ctxt);
   cnt := getQueriesParamValue(ctxt);
-  if not getRawRandomWorlds(cnt, words) then
+  if not getRawRandomWorlds(cnt, res) then
     exit;
     exit;
-  setLength(ids{%H-}, cnt);
-  setLength(nums{%H-}, cnt);
-  // generate new randoms, fill parameters arrays for update
-  for i := 0 to cnt - 1 do
+  if cnt > 20 then
+  begin
+    setLength(ids{%H-}, cnt);
+    setLength(nums{%H-}, cnt);
+    // generate new randoms, fill parameters arrays for update
+    for i := 0 to cnt - 1 do
+    begin
+      res[i].randomNumber := ComputeRandomWorld;
+      ids[i] := res[i].id;
+      nums[i] := res[i].randomNumber;
+    end;
+    stmt := conn.NewStatementPrepared(WORLD_UPDATE_SQLN, false, true);
+    stmt.BindArray(1, ids);
+    stmt.BindArray(2, nums);
+  end
+  else
   begin
   begin
-    words[i].randomNumber := RandomWorld;
-    ids[i] := words[i].id;
-    nums[i] := words[i].randomNumber;
+    stmt := conn.NewStatementPrepared(ComputeUpdateSql(cnt), false, true);
+    for i := 0 to cnt - 1 do
+    begin
+      res[i].randomNumber := ComputeRandomWorld;
+      stmt.Bind(i * 2 + 1, res[i].id);
+      stmt.Bind(i * 2 + 2, res[i].randomNumber);
+      stmt.Bind(cnt * 2 + i + 1, res[i].id);
+    end;
   end;
   end;
-  stmt := conn.NewStatementPrepared(WORLD_UPDATE_SQLN, false, true);
-  stmt.BindArray(1, ids);
-  stmt.BindArray(2, nums);
   stmt.ExecutePrepared;
   stmt.ExecutePrepared;
-  //conn.Commit; // autocommit
-  ctxt.SetOutJson(@words, TypeInfo(TWorlds));
+  ctxt.SetOutJson(@res, TypeInfo(TWorlds));
   result := HTTP_SUCCESS;
   result := HTTP_SUCCESS;
 end;
 end;
 
 
@@ -465,74 +514,85 @@ end;
 
 
 var
 var
   rawServers: array of TRawAsyncServer;
   rawServers: array of TRawAsyncServer;
-  threads, cores, servers, i: integer;
+  threads, servers, i: integer;
   flags: THttpServerOptions;
   flags: THttpServerOptions;
 
 
+procedure ComputeExecutionContextFromParams;
+var
+  cores: integer;
+begin
+  // user specified some values at command line: raw [threads] [cores] [servers]
+  // in practice, [cores] is just ignored
+  if not TryStrToInt(ParamStr(1), threads) then
+    threads := SystemInfo.dwNumberOfProcessors * 4;
+  if not TryStrToInt(ParamStr(2), cores) then
+    cores := 16;
+  if not TryStrToInt(ParamStr(3), servers) then
+    servers := 1;
+  if threads < 2 then
+    threads := 2
+  else if threads > 256 then
+    threads := 256; // max. threads for THttpAsyncServer
+  {if SystemInfo.dwNumberOfProcessors > cores then
+    SystemInfo.dwNumberOfProcessors := cores; // for hsoThreadCpuAffinity}
+  if servers < 1 then
+    servers := 1
+  else if servers > 256 then
+    servers := 256;
+end;
+
+procedure ComputeExecutionContextFromNumberOfProcessors;
+var
+  logicalcores: integer;
 begin
 begin
+  // automatically guess best parameters depending on available CPU cores
+  logicalcores := SystemInfo.dwNumberOfProcessors;
+  if logicalcores >= 12 then
+  begin
+    // high-end CPU - scale using several listeners (one per core)
+    // see https://synopse.info/forum/viewtopic.php?pid=39263#p39263
+    servers := logicalcores;
+    threads := 8;
+  end
+  else
+  begin
+    // regular CPU - a single instance and a few threads per core
+    servers := 1;
+    threads := logicalcores * 4;
+  end;
+end;
+
+begin
+  // setup logs
   {$ifdef WITH_LOGS}
   {$ifdef WITH_LOGS}
   TSynLog.Family.Level := LOG_VERBOSE; // disable logs for benchmarking
   TSynLog.Family.Level := LOG_VERBOSE; // disable logs for benchmarking
   TSynLog.Family.HighResolutionTimestamp := true;
   TSynLog.Family.HighResolutionTimestamp := true;
+  TSynLog.Family.PerThreadLog := ptIdentifiedInOneFile;
   TSynLog.Family.AutoFlushTimeOut := 1;
   TSynLog.Family.AutoFlushTimeOut := 1;
   {$else}
   {$else}
   SynDBLog := nil; // slightly faster: no need to check log level
   SynDBLog := nil; // slightly faster: no need to check log level
   {$endif WITH_LOGS}
   {$endif WITH_LOGS}
-  TSynLog.Family.PerThreadLog := ptIdentifiedInOneFile;
 
 
+  // register some RTTI for records JSON serialization
   Rtti.RegisterFromText([
   Rtti.RegisterFromText([
     TypeInfo(TMessageRec), 'message:RawUtf8',
     TypeInfo(TMessageRec), 'message:RawUtf8',
     TypeInfo(TWorldRec),   'id,randomNumber:integer',
     TypeInfo(TWorldRec),   'id,randomNumber:integer',
     TypeInfo(TFortune),    'id:integer message:RawUtf8']);
     TypeInfo(TFortune),    'id:integer message:RawUtf8']);
 
 
-  flags := [];
+  // setup execution context
   if ParamCount > 1 then
   if ParamCount > 1 then
-  begin
-    // user specified some values at command line
-    if not TryStrToInt(ParamStr(1), threads) then
-      threads := SystemInfo.dwNumberOfProcessors * 4;
-    if threads < 2 then
-      threads := 2
-    else if threads > 256 then
-      threads := 256; // max. threads for THttpAsyncServer
-
-    if not TryStrToInt(ParamStr(2), cores) then
-      cores := 16;
-    if SystemInfo.dwNumberOfProcessors > cores then
-      SystemInfo.dwNumberOfProcessors := cores; //for hsoThreadCpuAffinity
-
-    if not TryStrToInt(ParamStr(3), servers) then
-      servers := 1;
-    if servers < 1 then
-      servers := 1
-    else if servers > 16 then
-      servers := 16;
-  end
+    ComputeExecutionContextFromParams
   else
   else
-  begin
-    // automatically sets best parameters depending on available CPU cores
-    cores := SystemInfo.dwNumberOfProcessors;
-    if cores > 12 then
-    begin
-      // hi-end CPU - scale using several listeners bound to the HW cores
-      threads := cores;
-      if cores div 4 > 6 then
-        servers := 6
-      else
-        servers := cores div 4;
-    end
-    else
-    begin
-      threads := cores * 4;
-      servers := 1;
-    end;
-  end;
-  if servers = 1 then
-    include(flags, hsoThreadSmooting); // 30% better /plaintext e.g. on i5 7300U
+    ComputeExecutionContextFromNumberOfProcessors;
+  if servers > 1 then
+    include(flags, hsoReusePort); // allow several bindings on the same port
 
 
-  // start the server instance(s), in hsoReusePort mode
+  // start the server instance(s), in hsoReusePort mode if needed
   SetLength(rawServers, servers);
   SetLength(rawServers, servers);
   for i := 0 to servers - 1 do
   for i := 0 to servers - 1 do
     rawServers[i] := TRawAsyncServer.Create(threads, flags);
     rawServers[i] := TRawAsyncServer.Create(threads, flags);
   try
   try
+    // display some information and wait for SIGTERM
     {$I-}
     {$I-}
     writeln;
     writeln;
     writeln(rawServers[0].fHttpServer.ClassName,
     writeln(rawServers[0].fHttpServer.ClassName,
@@ -542,16 +602,17 @@ begin
             ', num servers=', servers,
             ', num servers=', servers,
             ', total workers=', threads * servers,
             ', total workers=', threads * servers,
             ', db=', rawServers[0].fDbPool.DbmsEngineName);
             ', db=', rawServers[0].fDbPool.DbmsEngineName);
-    writeln('Press Ctrl+C or use SIGTERM to terminate'#10);
-    FpPause; // mandatory for the actual benchmark tool
+    writeln(' options=', GetSetName(TypeInfo(THttpServerOptions), flags));
+    writeln('Press [Enter] or Ctrl+C or send SIGTERM to terminate'#10);
+    ConsoleWaitForEnterKey;
     //TSynLog.Family.Level := LOG_VERBOSE; // enable shutdown logs for debug
     //TSynLog.Family.Level := LOG_VERBOSE; // enable shutdown logs for debug
     for i := 0 to servers - 1 do
     for i := 0 to servers - 1 do
       writeln(ObjectToJsonDebug(rawServers[i].fHttpServer,
       writeln(ObjectToJsonDebug(rawServers[i].fHttpServer,
         [woDontStoreVoid, woHumanReadable]));
         [woDontStoreVoid, woHumanReadable]));
   finally
   finally
+    // clear all server instance(s)
     ObjArrayClear(rawServers);
     ObjArrayClear(rawServers);
   end;
   end;
-
   {$ifdef FPC_X64MM}
   {$ifdef FPC_X64MM}
   WriteHeapStatus(' ', 16, 8, {compileflags=}true);
   WriteHeapStatus(' ', 16, 8, {compileflags=}true);
   {$endif FPC_X64MM}
   {$endif FPC_X64MM}