Created
August 12, 2022 07:12
-
-
Save synopse/83f452250ab5a7a7d0094970d25b9c4d to your computer and use it in GitHub Desktop.
latest raw for TechEmpower framework benchmarks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment