123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557 |
- program raw;
- {
- TechEmpower framework benchmarks implementation
- See https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview
- }
- {$I mormot.defines.inc}
- {.$define WITH_LOGS}
- // logging is fine for debugging, less for benchmarking ;)
- uses
- {$I mormot.uses.inc} // include mormot.core.fpcx64mm or mormot.core.fpclibcmm
- 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,
- mormot.rest.sqlite3,
- mormot.net.http,
- mormot.net.server,
- mormot.net.async,
- mormot.db.sql,
- mormot.db.sql.postgres;
- type
- // data structures
- 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;
- // ORM definitions
- 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;
- // main server class
- TRawAsyncServer = class(TSynPersistent)
- private
- fHttpServer: THttpAsyncServer;
- fDbPool: TSqlDBConnectionProperties;
- fModel: TOrmModel;
- fStore: TRestServerDB;
- fTemplate: TSynMustache;
- protected
- // as used by rawqueries and rawupdates
- function getRawRandomWorlds(cnt: PtrInt; out res: TWorlds): boolean;
- // implements /queries and /cached-queries endpoints
- function doqueries(ctxt: THttpServerRequestAbstract; orm: TOrmWorldClass;
- const search: RawUtf8): cardinal;
- public
- constructor Create(threadCount: integer; flags: THttpServerOptions); reintroduce;
- destructor Destroy; override;
- published
- // all service URI are implemented by these published methods using RTTI
- function plaintext(ctxt: THttpServerRequestAbstract): cardinal;
- function json(ctxt: THttpServerRequestAbstract): cardinal;
- function db(ctxt: THttpServerRequestAbstract): cardinal;
- function queries(ctxt: THttpServerRequestAbstract): cardinal;
- function cached_queries(ctxt: THttpServerRequestAbstract): cardinal;
- function fortunes(ctxt: THttpServerRequestAbstract): cardinal;
- function updates(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(?::bigint[]), unnest(?::bigint[]) order by 1) 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>';
- function RandomWorld: integer; inline;
- begin
- result := Random32(WORLD_COUNT) + 1;
- end;
- function getQueriesParamValue(ctxt: THttpServerRequestAbstract;
- const search: RawUtf8 = 'QUERIES='): cardinal;
- begin
- if not ctxt.UrlParam(search, result) then
- result := 1
- else if result > 500 then
- result := 500
- else if result < 1 then
- result := 1;
- end;
- { TRawAsyncServer }
- constructor TRawAsyncServer.Create(
- threadCount: integer; flags: THttpServerOptions);
- begin
- inherited Create;
- fDbPool := TSqlDBPostgresConnectionProperties.Create(
- 'tfb-database:5432', 'hello_world', 'benchmarkdbuser', 'benchmarkdbpass');
- 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;
- fStore.Server.CreateMissingTables; // create SQlite3 virtual tables
- 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
- //hsoThreadCpuAffinity, // better scaling of /plaintext in some cases
- hsoReusePort, // allow several processes binding on the same port
- {$ifdef WITH_LOGS}
- hsoLogVerbose,
- {$endif WITH_LOGS}
- hsoIncludeDateHeader // required by TPW General Test Requirements #5
- ] + flags);
- fHttpServer.HttpQueueLength := 100000; // needed e.g. from wrk/ab benchmarks
- fHttpServer.Route.RunMethods([urmGet], self);
- // writeln(fHttpServer.Route.Tree[urmGet].ToText);
- 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.plaintext(ctxt: THttpServerRequestAbstract): cardinal;
- begin
- ctxt.OutContentType := TEXT_CONTENT_TYPE_NO_ENCODING;
- ctxt.OutContent := HELLO_WORLD;
- result := HTTP_SUCCESS;
- end;
- function TRawAsyncServer.json(ctxt: THttpServerRequestAbstract): cardinal;
- var
- msgRec: TMessageRec;
- begin
- msgRec.message := HELLO_WORLD;
- ctxt.SetOutJson(SaveJson(msgRec, TypeInfo(TMessageRec)));
- 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.SetOutJson(
- '{"id":%,"randomNumber":%}', [stmt.ColumnInt(0), stmt.ColumnInt(1)]);
- 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.SetOutJson('{"id":%,"randomNumber":%}', [w.IDValue, w.randomNumber]);
- result := HTTP_SUCCESS;
- finally
- w.Free;
- end;
- end;
- function TRawAsyncServer.queries(ctxt: THttpServerRequestAbstract): cardinal;
- begin
- result := doqueries(ctxt, TOrmWorld, 'QUERIES=');
- end;
- function TRawAsyncServer.cached_queries(ctxt: THttpServerRequestAbstract): cardinal;
- begin
- result := doqueries(ctxt, TOrmCachedWorld, 'COUNT=');
- end;
- function TRawAsyncServer.getRawRandomWorlds(cnt: PtrInt; out res: TWorlds): boolean;
- var
- conn: TSqlDBConnection;
- stmt: ISQLDBStatement;
- pConn: TSqlDBPostgresConnection absolute conn;
- pStmt: TSqlDBPostgresStatement;
- i: PtrInt;
- begin
- result := false;
- SetLength(res{%H-}, cnt);
- conn := fDbPool.ThreadSafeConnection;
- if not conn.IsConnected then
- conn.Connect;
- // specific code to use PostgresSQL pipelining mode
- // see test_nosync in
- // https://github.com/postgres/postgres/blob/master/src/test/modules/libpq_pipeline/libpq_pipeline.c
- stmt := conn.NewStatementPrepared(WORLD_READ_SQL, true, true);
- //w/o transaction pg_stat_statements view returns calls-1 and tfb verify fails
- conn.StartTransaction;
- pConn.EnterPipelineMode;
- pStmt := (stmt as TSqlDBPostgresStatement);
- for i := 0 to cnt - 1 do
- begin
- stmt.Bind(1, RandomWorld);
- pStmt.SendPipelinePrepared;
- pConn.Flush;
- end;
- pConn.SendFlushRequest;
- pConn.Flush;
- for i := 0 to cnt - 1 do
- begin
- pStmt.GetPipelineResult;
- if not stmt.Step then
- exit;
- res[i].id := pStmt.ColumnInt(0);
- res[i].randomNumber := pStmt.ColumnInt(1);
- pStmt.ReleaseRows;
- end;
- pConn.ExitPipelineMode;
- conn.commit;
- result := true;
- end;
- function TRawAsyncServer.rawqueries(ctxt: THttpServerRequestAbstract): cardinal;
- var
- cnt: PtrInt;
- res: TWorlds;
- begin
- cnt := getQueriesParamValue(ctxt);
- if not getRawRandomWorlds(cnt, res) then
- exit(HTTP_SERVERERROR);
- ctxt.SetOutJson(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.SetOutJson(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;
- arr: TDynArray;
- n: integer;
- f: ^TFortune;
- begin
- conn := fDbPool.ThreadSafeConnection;
- stmt := conn.NewStatementPrepared(FORTUNES_SQL, true, true);
- stmt.ExecutePrepared;
- arr.Init(TypeInfo(TFortunes), list, @n);
- while stmt.Step do
- begin
- f := arr.NewPtr;
- f.id := stmt.ColumnInt(0);
- f.message := stmt.ColumnUtf8(1);
- end;
- f := arr.NewPtr;
- f.id := 0;
- f.message := FORTUNES_MESSAGE;
- 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.SetOutJson(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
- result := HTTP_SERVERERROR;
- conn := fDbPool.ThreadSafeConnection;
- cnt := getQueriesParamValue(ctxt);
- if not getRawRandomWorlds(cnt, words) then
- exit;
- setLength(ids{%H-}, cnt);
- setLength(nums{%H-}, 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;
- stmt := conn.NewStatementPrepared(WORLD_UPDATE_SQLN, false, true);
- stmt.BindArray(1, ids);
- stmt.BindArray(2, nums);
- stmt.ExecutePrepared;
- //conn.Commit;
- ctxt.SetOutJson(SaveJson(words, TypeInfo(TWorlds)));
- result := HTTP_SUCCESS;
- end;
- var
- rawServers: array of TRawAsyncServer;
- threads, cores, servers, i: integer;
- flags: THttpServerOptions;
- begin
- {$ifdef WITH_LOGS}
- TSynLog.Family.Level := LOG_VERBOSE; // disable logs for benchmarking
- TSynLog.Family.HighResolutionTimestamp := true;
- TSynLog.Family.AutoFlushTimeOut := 1;
- {$endif WITH_LOGS}
- TSynLog.Family.PerThreadLog := ptIdentifiedInOneFile;
- Rtti.RegisterFromText([
- TypeInfo(TMessageRec), 'message:RawUtf8',
- TypeInfo(TWorldRec), 'id,randomNumber:integer',
- TypeInfo(TFortune), 'id:integer message:RawUtf8']);
- flags := [];
- 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
- 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
- // start the server instance(s), in hsoReusePort mode
- SetLength(rawServers, servers);
- for i := 0 to servers - 1 do
- rawServers[i] := TRawAsyncServer.Create(threads, flags);
- try
- {$I-}
- writeln;
- writeln(rawServers[0].fHttpServer.ClassName,
- ' running on localhost:', rawServers[0].fHttpServer.SockPort);
- writeln(' num thread=', threads,
- ', num CPU=', SystemInfo.dwNumberOfProcessors,
- ', num servers=', servers,
- ', total workers=', threads * servers,
- ', db=', rawServers[0].fDbPool.DbmsEngineName);
- writeln('Press Ctrl+C or use SIGTERM to terminate'#10);
- FpPause; // mandatory for the actual benchmark tool
- //TSynLog.Family.Level := LOG_VERBOSE; // enable shutdown logs for debug
- for i := 0 to servers - 1 do
- writeln(ObjectToJsonDebug(rawServers[i].fHttpServer,
- [woDontStoreVoid, woHumanReadable]));
- finally
- for i := 0 to servers - 1 do
- rawServers[i].Free;
- end;
- {$ifdef FPC_X64MM}
- WriteHeapStatus(' ', 16, 8, {compileflags=}true);
- {$endif FPC_X64MM}
- end.
|