raw.pas 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. program raw;
  2. {
  3. TechEmpower framework benchmarks implementation
  4. See https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview
  5. }
  6. {$I mormot.defines.inc}
  7. {.$define WITH_LOGS}
  8. // logging is fine for debugging, less for benchmarking ;)
  9. uses
  10. {$I mormot.uses.inc} // include mormot.core.fpcx64mm or mormot.core.fpclibcmm
  11. sysutils,
  12. classes,
  13. BaseUnix,
  14. mormot.core.base,
  15. mormot.core.os,
  16. mormot.core.rtti,
  17. mormot.core.log,
  18. mormot.core.unicode,
  19. mormot.core.text,
  20. mormot.core.buffers,
  21. mormot.core.json,
  22. mormot.core.data,
  23. mormot.core.variants,
  24. mormot.core.perf,
  25. mormot.core.mustache,
  26. mormot.orm.core,
  27. mormot.orm.sql,
  28. mormot.db.core,
  29. mormot.db.raw.sqlite3,
  30. mormot.db.raw.sqlite3.static,
  31. mormot.rest.sqlite3,
  32. mormot.net.http,
  33. mormot.net.server,
  34. mormot.net.async,
  35. mormot.db.sql,
  36. mormot.db.sql.postgres;
  37. type
  38. // data structures
  39. TMessageRec = packed record
  40. message: RawUtf8;
  41. end;
  42. TWorldRec = packed record
  43. id: integer;
  44. randomNumber: integer;
  45. end;
  46. TWorlds = array of TWorldRec;
  47. TFortune = packed record
  48. id: integer;
  49. message: RawUtf8;
  50. end;
  51. TFortunes = array of TFortune;
  52. // ORM definitions
  53. TOrmWorld = class(TOrm)
  54. protected
  55. fRandomNumber: integer;
  56. published
  57. property randomNumber: integer
  58. read fRandomNumber write fRandomNumber;
  59. end;
  60. TOrmCachedWorld = class(TOrmWorld);
  61. TOrmWorldClass = class of TOrmWorld;
  62. TOrmFortune = class(TOrm)
  63. protected
  64. fMessage: RawUtf8;
  65. published
  66. property Message: RawUtf8
  67. read fMessage write fMessage;
  68. end;
  69. TOrmFortunes = array of TOrmFortune;
  70. // main server class
  71. TRawAsyncServer = class(TSynPersistent)
  72. private
  73. fHttpServer: THttpAsyncServer;
  74. fDbPool: TSqlDBConnectionProperties;
  75. fModel: TOrmModel;
  76. fStore: TRestServerDB;
  77. fTemplate: TSynMustache;
  78. protected
  79. // as used by rawqueries and rawupdates
  80. function getRawRandomWorlds(cnt: PtrInt; out res: TWorlds): boolean;
  81. // implements /queries and /cached-queries endpoints
  82. function doqueries(ctxt: THttpServerRequestAbstract; orm: TOrmWorldClass;
  83. const search: RawUtf8): cardinal;
  84. public
  85. constructor Create(threadCount: integer; flags: THttpServerOptions); reintroduce;
  86. destructor Destroy; override;
  87. published
  88. // all service URI are implemented by these published methods using RTTI
  89. function plaintext(ctxt: THttpServerRequestAbstract): cardinal;
  90. function json(ctxt: THttpServerRequestAbstract): cardinal;
  91. function db(ctxt: THttpServerRequestAbstract): cardinal;
  92. function queries(ctxt: THttpServerRequestAbstract): cardinal;
  93. function cached_queries(ctxt: THttpServerRequestAbstract): cardinal;
  94. function fortunes(ctxt: THttpServerRequestAbstract): cardinal;
  95. function updates(ctxt: THttpServerRequestAbstract): cardinal;
  96. function rawdb(ctxt: THttpServerRequestAbstract): cardinal;
  97. function rawqueries(ctxt: THttpServerRequestAbstract): cardinal;
  98. function rawfortunes(ctxt: THttpServerRequestAbstract): cardinal;
  99. function rawupdates(ctxt: THttpServerRequestAbstract): cardinal;
  100. end;
  101. const
  102. TEXT_CONTENT_TYPE_NO_ENCODING: RawUtf8 = 'text/plain';
  103. HELLO_WORLD: RawUtf8 = 'Hello, World!';
  104. WORLD_COUNT = 10000;
  105. WORLD_READ_SQL = 'select id,randomNumber from World where id=?';
  106. WORLD_UPDATE_SQLN ='update World as t set randomNumber = v.r from ' +
  107. '(SELECT unnest(?::bigint[]), unnest(?::bigint[]) order by 1) as v(id, r)' +
  108. ' where t.id = v.id';
  109. FORTUNES_SQL = 'select id,message from Fortune';
  110. FORTUNES_MESSAGE = 'Additional fortune added at request time.';
  111. FORTUNES_TPL = '<!DOCTYPE html>' +
  112. '<html>' +
  113. '<head><title>Fortunes</title></head>' +
  114. '<body>' +
  115. '<table>' +
  116. '<tr><th>id</th><th>message</th></tr>' +
  117. '{{#.}}' +
  118. '<tr><td>{{id}}</td><td>{{message}}</td></tr>' +
  119. '{{/.}}' +
  120. '</table>' +
  121. '</body>' +
  122. '</html>';
  123. function RandomWorld: integer; inline;
  124. begin
  125. result := Random32(WORLD_COUNT) + 1;
  126. end;
  127. function getQueriesParamValue(ctxt: THttpServerRequestAbstract;
  128. const search: RawUtf8 = 'QUERIES='): cardinal;
  129. begin
  130. if not ctxt.UrlParam(search, result) then
  131. result := 1
  132. else if result > 500 then
  133. result := 500
  134. else if result < 1 then
  135. result := 1;
  136. end;
  137. { TRawAsyncServer }
  138. constructor TRawAsyncServer.Create(
  139. threadCount: integer; flags: THttpServerOptions);
  140. begin
  141. inherited Create;
  142. fDbPool := TSqlDBPostgresConnectionProperties.Create(
  143. 'tfb-database:5432', 'hello_world', 'benchmarkdbuser', 'benchmarkdbpass');
  144. fModel := TOrmModel.Create([TOrmWorld, TOrmFortune, TOrmCachedWorld]);
  145. OrmMapExternal(fModel, [TOrmWorld, TOrmFortune], fDbPool);
  146. // CachedWorld table doesn't exists in DB, but should as read in requirements.
  147. // Use world table as in other implementations.
  148. OrmMapExternal(fModel, TOrmCachedWorld, fDbPool, 'world');
  149. fStore := TRestServerDB.Create(fModel, SQLITE_MEMORY_DATABASE_NAME);
  150. fStore.NoAjaxJson := true;
  151. fStore.Server.CreateMissingTables; // create SQlite3 virtual tables
  152. if fStore.Server.Cache.SetCache(TOrmCachedWorld) then
  153. fStore.Server.Cache.FillFromQuery(TOrmCachedWorld, '', []);
  154. fTemplate := TSynMustache.Parse(FORTUNES_TPL);
  155. fHttpServer := THttpAsyncServer.Create(
  156. '8080', nil, nil, '', threadCount,
  157. 5 * 60 * 1000, // 5 minutes keep alive connections
  158. [hsoNoXPoweredHeader, // not needed for a benchmark
  159. hsoHeadersInterning, // reduce memory contention for /plaintext and /json
  160. hsoNoStats, // disable low-level statistic counters
  161. //hsoThreadCpuAffinity, // better scaling of /plaintext in some cases
  162. hsoReusePort, // allow several processes binding on the same port
  163. {$ifdef WITH_LOGS}
  164. hsoLogVerbose,
  165. {$endif WITH_LOGS}
  166. hsoIncludeDateHeader // required by TPW General Test Requirements #5
  167. ] + flags);
  168. fHttpServer.HttpQueueLength := 100000; // needed e.g. from wrk/ab benchmarks
  169. fHttpServer.Route.RunMethods([urmGet], self);
  170. // writeln(fHttpServer.Route.Tree[urmGet].ToText);
  171. fHttpServer.WaitStarted; // raise exception e.g. on binding issue
  172. end;
  173. destructor TRawAsyncServer.Destroy;
  174. begin
  175. fHttpServer.Free;
  176. fStore.Free;
  177. fModel.Free;
  178. fDBPool.free;
  179. inherited Destroy;
  180. end;
  181. function TRawAsyncServer.plaintext(ctxt: THttpServerRequestAbstract): cardinal;
  182. begin
  183. ctxt.OutContentType := TEXT_CONTENT_TYPE_NO_ENCODING;
  184. ctxt.OutContent := HELLO_WORLD;
  185. result := HTTP_SUCCESS;
  186. end;
  187. function TRawAsyncServer.json(ctxt: THttpServerRequestAbstract): cardinal;
  188. var
  189. msgRec: TMessageRec;
  190. begin
  191. msgRec.message := HELLO_WORLD;
  192. ctxt.SetOutJson(SaveJson(msgRec, TypeInfo(TMessageRec)));
  193. result := HTTP_SUCCESS;
  194. end;
  195. function TRawAsyncServer.rawdb(ctxt: THttpServerRequestAbstract): cardinal;
  196. var
  197. conn: TSqlDBConnection;
  198. stmt: ISQLDBStatement;
  199. begin
  200. result := HTTP_SERVERERROR;
  201. conn := fDbPool.ThreadSafeConnection;
  202. stmt := conn.NewStatementPrepared(WORLD_READ_SQL, true, true);
  203. stmt.Bind(1, RandomWorld);
  204. stmt.ExecutePrepared;
  205. if stmt.Step then
  206. begin
  207. ctxt.SetOutJson(
  208. '{"id":%,"randomNumber":%}', [stmt.ColumnInt(0), stmt.ColumnInt(1)]);
  209. result := HTTP_SUCCESS;
  210. stmt.ReleaseRows;
  211. end;
  212. stmt := nil;
  213. end;
  214. function TRawAsyncServer.db(ctxt: THttpServerRequestAbstract): cardinal;
  215. var
  216. w: TOrmWorld;
  217. begin
  218. w := TOrmWorld.Create(fStore.Orm, RandomWorld);
  219. try
  220. ctxt.SetOutJson('{"id":%,"randomNumber":%}', [w.IDValue, w.randomNumber]);
  221. result := HTTP_SUCCESS;
  222. finally
  223. w.Free;
  224. end;
  225. end;
  226. function TRawAsyncServer.queries(ctxt: THttpServerRequestAbstract): cardinal;
  227. begin
  228. result := doqueries(ctxt, TOrmWorld, 'QUERIES=');
  229. end;
  230. function TRawAsyncServer.cached_queries(ctxt: THttpServerRequestAbstract): cardinal;
  231. begin
  232. result := doqueries(ctxt, TOrmCachedWorld, 'COUNT=');
  233. end;
  234. function TRawAsyncServer.getRawRandomWorlds(cnt: PtrInt; out res: TWorlds): boolean;
  235. var
  236. conn: TSqlDBConnection;
  237. stmt: ISQLDBStatement;
  238. pConn: TSqlDBPostgresConnection absolute conn;
  239. pStmt: TSqlDBPostgresStatement;
  240. i: PtrInt;
  241. begin
  242. result := false;
  243. SetLength(res{%H-}, cnt);
  244. conn := fDbPool.ThreadSafeConnection;
  245. if not conn.IsConnected then
  246. conn.Connect;
  247. // specific code to use PostgresSQL pipelining mode
  248. // see test_nosync in
  249. // https://github.com/postgres/postgres/blob/master/src/test/modules/libpq_pipeline/libpq_pipeline.c
  250. stmt := conn.NewStatementPrepared(WORLD_READ_SQL, true, true);
  251. //w/o transaction pg_stat_statements view returns calls-1 and tfb verify fails
  252. conn.StartTransaction;
  253. pConn.EnterPipelineMode;
  254. pStmt := (stmt as TSqlDBPostgresStatement);
  255. for i := 0 to cnt - 1 do
  256. begin
  257. stmt.Bind(1, RandomWorld);
  258. pStmt.SendPipelinePrepared;
  259. pConn.Flush;
  260. end;
  261. pConn.SendFlushRequest;
  262. pConn.Flush;
  263. for i := 0 to cnt - 1 do
  264. begin
  265. pStmt.GetPipelineResult;
  266. if not stmt.Step then
  267. exit;
  268. res[i].id := pStmt.ColumnInt(0);
  269. res[i].randomNumber := pStmt.ColumnInt(1);
  270. pStmt.ReleaseRows;
  271. end;
  272. pConn.ExitPipelineMode;
  273. conn.commit;
  274. result := true;
  275. end;
  276. function TRawAsyncServer.rawqueries(ctxt: THttpServerRequestAbstract): cardinal;
  277. var
  278. cnt: PtrInt;
  279. res: TWorlds;
  280. begin
  281. cnt := getQueriesParamValue(ctxt);
  282. if not getRawRandomWorlds(cnt, res) then
  283. exit(HTTP_SERVERERROR);
  284. ctxt.SetOutJson(SaveJson(res, TypeInfo(TWorlds)));
  285. result := HTTP_SUCCESS;
  286. end;
  287. function TRawAsyncServer.doqueries(ctxt: THttpServerRequestAbstract;
  288. orm: TOrmWorldClass; const search: RawUtf8): cardinal;
  289. var
  290. cnt, i: PtrInt;
  291. res: TWorlds;
  292. w: TOrmWorld;
  293. begin
  294. result := HTTP_SERVERERROR;
  295. cnt := getQueriesParamValue(ctxt, search);
  296. SetLength(res, cnt);
  297. w := orm.Create; // TOrmWorld or TOrmCachedWorld
  298. try
  299. for i := 0 to cnt - 1 do
  300. begin
  301. if not fStore.Orm.Retrieve(RandomWorld, w) then
  302. exit;
  303. res[i].id := w.IDValue;
  304. res[i].randomNumber := w.RandomNumber;
  305. end;
  306. finally
  307. w.Free;
  308. end;
  309. ctxt.SetOutJson(SaveJson(res, TypeInfo(TWorlds)));
  310. result := HTTP_SUCCESS;
  311. end;
  312. function OrmFortuneCompareByMessage(const A, B): integer;
  313. begin
  314. result := StrComp(pointer(TOrmFortune(A).Message), pointer(TOrmFortune(B).Message));
  315. end;
  316. function TRawAsyncServer.fortunes(ctxt: THttpServerRequestAbstract): cardinal;
  317. var
  318. list: TOrmFortunes;
  319. new: TOrmFortune;
  320. arr: TDynArray;
  321. begin
  322. result := HTTP_SERVERERROR;
  323. arr.Init(TypeInfo(TOrmFortunes), list);
  324. if fStore.Orm.RetrieveListObjArray(list, TOrmFortune, '', []) then
  325. try
  326. new := TOrmFortune.Create;
  327. new.Message := FORTUNES_MESSAGE;
  328. arr.Add(new);
  329. arr.Sort(OrmFortuneCompareByMessage);
  330. ctxt.OutContent := fTemplate.RenderDataArray(arr);
  331. ctxt.OutContentType := HTML_CONTENT_TYPE;
  332. result := HTTP_SUCCESS;
  333. finally
  334. arr.Clear;
  335. end;
  336. end;
  337. function FortuneCompareByMessage(const A, B): integer;
  338. begin
  339. result := StrComp(pointer(TFortune(A).message), pointer(TFortune(B).message));
  340. end;
  341. function TRawAsyncServer.rawfortunes(ctxt: THttpServerRequestAbstract): cardinal;
  342. var
  343. conn: TSqlDBConnection;
  344. stmt: ISQLDBStatement;
  345. list: TFortunes;
  346. arr: TDynArray;
  347. n: integer;
  348. f: ^TFortune;
  349. begin
  350. conn := fDbPool.ThreadSafeConnection;
  351. stmt := conn.NewStatementPrepared(FORTUNES_SQL, true, true);
  352. stmt.ExecutePrepared;
  353. arr.Init(TypeInfo(TFortunes), list, @n);
  354. while stmt.Step do
  355. begin
  356. f := arr.NewPtr;
  357. f.id := stmt.ColumnInt(0);
  358. f.message := stmt.ColumnUtf8(1);
  359. end;
  360. f := arr.NewPtr;
  361. f.id := 0;
  362. f.message := FORTUNES_MESSAGE;
  363. arr.Sort(FortuneCompareByMessage);
  364. ctxt.OutContent := fTemplate.RenderDataArray(arr);
  365. ctxt.OutContentType := HTML_CONTENT_TYPE;
  366. result := HTTP_SUCCESS;
  367. end;
  368. function TRawAsyncServer.updates(ctxt: THttpServerRequestAbstract): cardinal;
  369. var
  370. cnt, i: PtrInt;
  371. res: TWorlds;
  372. w: TOrmWorld;
  373. b: TRestBatch;
  374. begin
  375. result := HTTP_SERVERERROR;
  376. cnt := getQueriesParamValue(ctxt);
  377. SetLength(res, cnt);
  378. b := TRestBatch.Create(fStore.ORM, TOrmWorld, {transrows=}0,
  379. [boExtendedJson, boNoModelEncoding, boPutNoCacheFlush]);
  380. w := TOrmWorld.Create;
  381. try
  382. for i := 0 to cnt - 1 do
  383. begin
  384. if not fStore.Orm.Retrieve(RandomWorld, w) then
  385. exit;
  386. w.RandomNumber := RandomWorld;
  387. b.Update(w);
  388. res[i].id := w.IDValue;
  389. res[i].randomNumber := w.RandomNumber;
  390. end;
  391. result := fStore.Orm.BatchSend(b);
  392. finally
  393. w.Free;
  394. b.Free;
  395. end;
  396. if result <> HTTP_SUCCESS then
  397. exit;
  398. ctxt.SetOutJson(SaveJson(res, TypeInfo(TWorlds)));
  399. end;
  400. function TRawAsyncServer.rawupdates(ctxt: THttpServerRequestAbstract): cardinal;
  401. var
  402. cnt, i: PtrInt;
  403. words: TWorlds;
  404. ids, nums: TInt64DynArray;
  405. conn: TSqlDBConnection;
  406. stmt: ISQLDBStatement;
  407. begin
  408. result := HTTP_SERVERERROR;
  409. conn := fDbPool.ThreadSafeConnection;
  410. cnt := getQueriesParamValue(ctxt);
  411. if not getRawRandomWorlds(cnt, words) then
  412. exit;
  413. setLength(ids{%H-}, cnt);
  414. setLength(nums{%H-}, cnt);
  415. // generate new randoms, fill parameters arrays for update
  416. for i := 0 to cnt - 1 do
  417. begin
  418. words[i].randomNumber := RandomWorld;
  419. ids[i] := words[i].id;
  420. nums[i] := words[i].randomNumber;
  421. end;
  422. stmt := conn.NewStatementPrepared(WORLD_UPDATE_SQLN, false, true);
  423. stmt.BindArray(1, ids);
  424. stmt.BindArray(2, nums);
  425. stmt.ExecutePrepared;
  426. //conn.Commit;
  427. ctxt.SetOutJson(SaveJson(words, TypeInfo(TWorlds)));
  428. result := HTTP_SUCCESS;
  429. end;
  430. var
  431. rawServers: array of TRawAsyncServer;
  432. threads, cores, servers, i: integer;
  433. flags: THttpServerOptions;
  434. begin
  435. {$ifdef WITH_LOGS}
  436. TSynLog.Family.Level := LOG_VERBOSE; // disable logs for benchmarking
  437. TSynLog.Family.HighResolutionTimestamp := true;
  438. TSynLog.Family.AutoFlushTimeOut := 1;
  439. {$endif WITH_LOGS}
  440. TSynLog.Family.PerThreadLog := ptIdentifiedInOneFile;
  441. Rtti.RegisterFromText([
  442. TypeInfo(TMessageRec), 'message:RawUtf8',
  443. TypeInfo(TWorldRec), 'id,randomNumber:integer',
  444. TypeInfo(TFortune), 'id:integer message:RawUtf8']);
  445. flags := [];
  446. if ParamCount > 1 then
  447. begin
  448. // user specified some values at command line
  449. if not TryStrToInt(ParamStr(1), threads) then
  450. threads := SystemInfo.dwNumberOfProcessors * 4;
  451. if threads < 2 then
  452. threads := 2
  453. else if threads > 256 then
  454. threads := 256; // max. threads for THttpAsyncServer
  455. if not TryStrToInt(ParamStr(2), cores) then
  456. cores := 16;
  457. if SystemInfo.dwNumberOfProcessors > cores then
  458. SystemInfo.dwNumberOfProcessors := cores; //for hsoThreadCpuAffinity
  459. if not TryStrToInt(ParamStr(3), servers) then
  460. servers := 1;
  461. if servers < 1 then
  462. servers := 1
  463. else if servers > 16 then
  464. servers := 16;
  465. end
  466. else
  467. begin
  468. // automatically sets best parameters depending on available CPU cores
  469. cores := SystemInfo.dwNumberOfProcessors;
  470. if cores > 12 then
  471. begin
  472. // hi-end CPU - scale using several listeners bound to the HW cores
  473. threads := cores;
  474. if cores div 4 > 6 then
  475. servers := 6
  476. else
  477. servers := cores div 4;
  478. end
  479. else
  480. begin
  481. threads := cores * 4;
  482. servers := 1;
  483. end;
  484. end;
  485. if servers = 1 then
  486. include(flags, hsoThreadSmooting); // 30% better /plaintext e.g. on i5 7300U
  487. // start the server instance(s), in hsoReusePort mode
  488. SetLength(rawServers, servers);
  489. for i := 0 to servers - 1 do
  490. rawServers[i] := TRawAsyncServer.Create(threads, flags);
  491. try
  492. {$I-}
  493. writeln;
  494. writeln(rawServers[0].fHttpServer.ClassName,
  495. ' running on localhost:', rawServers[0].fHttpServer.SockPort);
  496. writeln(' num thread=', threads,
  497. ', num CPU=', SystemInfo.dwNumberOfProcessors,
  498. ', num servers=', servers,
  499. ', total workers=', threads * servers,
  500. ', db=', rawServers[0].fDbPool.DbmsEngineName);
  501. writeln('Press Ctrl+C or use SIGTERM to terminate'#10);
  502. FpPause; // mandatory for the actual benchmark tool
  503. //TSynLog.Family.Level := LOG_VERBOSE; // enable shutdown logs for debug
  504. for i := 0 to servers - 1 do
  505. writeln(ObjectToJsonDebug(rawServers[i].fHttpServer,
  506. [woDontStoreVoid, woHumanReadable]));
  507. finally
  508. for i := 0 to servers - 1 do
  509. rawServers[i].Free;
  510. end;
  511. {$ifdef FPC_X64MM}
  512. WriteHeapStatus(' ', 16, 8, {compileflags=}true);
  513. {$endif FPC_X64MM}
  514. end.