You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Browse filesBrowse the repository at this point in the historyBrowse files
tempraturbo
committed
Speed up the request path; fix StringToBytesUTF8 destroying invalid bytes
The class registries kept only the class NAME and went through GetClass on
every lookup, and System.Classes.GetClass takes MonitorEnter on the RTL class
registry - a process-wide lock, taken a handful of times per request because
GetBestCompress takes one per registered compressor. The registration routines
already receive the class, so they keep the pointer now: compressors in an
array indexed by the enum, engines and database drivers in the Objects[] of
their definition list, storage links cached on first use.
Name comparison no longer leaves StringRAL. On Delphi SameText has no
AnsiString overload and converted both sides from UTF-8 to UTF-16 on every
call - two heap allocations per comparison, in lookups that run once per param
of every request. RALSameName compares ASCII byte by byte and hands anything
above 127 back to the RTL, so case equivalence is unchanged. It reaches the
param and route lookups, IsTyped, DecodeAuth on every engine, the JWT claims,
multipart part headers, cookies and the storage field lookups.
StringToBytesUTF8 was calling TEncoding.UTF8.GetBytes over a string that is
already UTF-8: a full round trip that also substituted U+FFFD for every
invalid sequence, which is what RALHashBase works around with HMACAsDigest.
It moves the bytes straight through now, matching BytesToStringUTF8.
FixRoute walks the string once instead of rebuilding it per pair of slashes,
HTTP method names and TFieldType names go through memoised tables instead of
RTTI, and CheckCORS stops building the allowed-method list when OPTIONS is not
allowed.
Measured on the mORMot2 sample, 70300 requests per server: 22 to 25 percent
less CPU per request and 37 to 51 percent more throughput at 50 concurrent
connections, with p99 latency down by about a third. Behaviour is unchanged -
an equivalence program compared old and new over every route shape, name,
verb, encoding header and field type.
Both sides now read the same constant (`DEFAULTCONNECTTIMEOUT`, `DEFAULTREQUESTTIMEOUT` in `RALConsts.pas`), which is the point of naming them. `DEFAULTMAXREDIRECTS` and `RALMAXTOKENTRIES` live there too; `MaxRedirects` became a published property of `TRALClient` because the engines each hardcoded a different limit (Indy 3, mORMot2 3, fpHTTP 255, netHTTP whatever `THTTPClient` defaults to) with nobody having chosen it. When adding a numeric `default`, grep the constructor.
169
169
170
170
### Runtime class registry (why linking a unit changes behavior)
171
-
Compression, crypto, and storage backends are discovered at runtime by class name, not by static reference. `RALCompress.GetCompressClass` builds the enum name (`ctBrotli`) via `GetEnumName` and looks the class up with RTL `GetClass`. Optional units self-register in their `initialization`:
171
+
Compression, crypto, and storage backends are discovered at runtime, not by static reference. Optional units self-register in their `initialization`:
172
172
```pascal
173
173
initialization
174
174
RegisterClass(TRALCompressBrotli);
175
175
RegisterCompress(TRALCompressBrotli);
176
176
```
177
-
Consequence: **an algorithm exists only if its unit is linked into the binary.**`GetSuportedCompress`/`GetAcceptCompress` derive the `Accept-Encoding` header from whatever registered. `TRALStorageLink.GetStorageClass` uses the same name-based lookup (`cStorageLinkClass`). Never assume a format is available; go through the lookup functions.
177
+
Consequence: **an algorithm exists only if its unit is linked into the binary.**`GetSuportedCompress`/`GetAcceptCompress` derive the `Accept-Encoding` header from whatever registered. Never assume a format is available; go through the lookup functions.
178
+
179
+
**The lookup keeps the class, it does not resolve a name.** It used to: `GetCompressClass` built the enum name with `GetEnumName` and asked the RTL for `GetClass(name)` — and `System.Classes.GetClass` takes `RegGroups.Lock` (`MonitorEnter`), a **process-wide** lock, on every call. `GetBestCompress` took one per registered compressor and runs several times per request, so a handful of that lock was taken on every request of every engine. `RegisterCompress`/`RegisterEngine`/`RegisterDatabase` already receive the class, so they now keep the pointer: an `array[TRALCompressType]` in `RALCompress`, the `Objects[]` of the definition list in `RALClient` and `RALDBBase`. Measured on the mORMot2 sample, taking that lock out of the hot path was worth about nine points of throughput at 50 concurrent connections — a lock costs where it is contended, not where it is counted.
180
+
181
+
`TRALStorageLink.GetStorageClass` is the exception: the storage units only call `RegisterClass`, there is no `RegisterStorage` to keep the class in, so `StorageLinkClassOf` caches the resolution on first use. Only a non-nil result is cached, so a design-time package loaded later is still found. `RegisterClass` stays mandatory for storages — that is what `GetClass` reads.
178
182
179
183
180
184
### Connection charset is chosen by the driver, not left blank
@@ -531,6 +535,18 @@ Two behaviors worth knowing before tuning: waiting for a free connection is an e
531
535
532
536
## Cross-compiler conventions
533
537
538
+
**The whole project is written in English** — identifiers, `///` doc comments and ordinary comments alike. A few older comments are in Portuguese; new code is not.
539
+
540
+
### On Delphi, every RTL string call over a `StringRAL` converts UTF-8 to UTF-16 and back
541
+
542
+
`StringRAL` is `UTF8String` on both compilers, but Delphi's RTL is UTF-16 and `System.SysUtils` has no AnsiString overloads. So `SameText`, `LowerCase`, `UpperCase`, `Trim` and `StringReplace` over a `StringRAL` convert **both** arguments and the result — two heap allocations and two transcodings per call. FPC has the overloads and converts nothing, which is the bulk of the performance difference between the two compilers on a request whose real work is small. `Pos` is the exception: it has the overload and does not convert.
543
+
544
+
Compiling the core with `dcc32` reports it: **W1057 "Implicit string cast"**, ~500 of them. The warning is on, it just drowns in the volume. `grep -c W1057` on a build log is the way to see whether an edit made it worse.
545
+
546
+
For anything on the per-request path, prefer `RALTools.RALSameName` — ASCII case-insensitive comparison byte by byte, handing anything above 127 back to `SameText` so Unicode case equivalence is unchanged. It is what the param, header, route, cookie and claim lookups use. `TRALParam.IsTyped` shows why it matters: it called `MediaType` six times and did twelve conversions per value received, on every `SetAsString`.
547
+
548
+
Same reason behind `RALFieldTypeName`/`RALNameToFieldType` (`RALDBTypes`) and the `RALMethodNames` table (`RALTools`): `GetEnumName` and `GetEnumValue` hand back a `string`, so RTTI per field or per request paid the conversion too. The caches are filled **by**`GetEnumName`, never by a hand-written table — `TFieldType` has different members across compilers and versions.
549
+
534
550
`src/base/PascalRAL.inc` is included (`{$I PascalRAL.inc}`) by essentially every unit and is the **only** place compiler/OS/framework conditionals are defined. Use the symbols it exports (`DELPHIXE7UP`, `RALWindows`, `RALLinuxFPC`, `NewDelphiAndLazarus`, `HAS_FMX`, `CPU64`, …) instead of raw `CompilerVersion` or `VERxxx` checks. The IFEND block must stay at the top of that file.
535
551
536
552
Three compile-time selectors live in `PascalRAL.inc` and change what gets compiled:
0 commit comments