The arena

The memory model, and the rules it imposes on your code

# The arena

This is the one page to read before writing Askr code. Everything else in
the framework follows from it.

## The model

A worker owns **one arena** and calls `Reset` before each request.
Everything allocated during the request is freed in a single operation. No
per-object cleanup, no refcounting, no garbage collector.

```pascal nocheck
type
  TGreeting = class(TArenaObject)      // inherits arena allocation
    Name: TStr;
    constructor Create(const AName: TStr);
  end;

G := TGreeting.Create(Req.Query('name'));   // in the arena, constructor runs
// no try/finally, no Free
```

`Reset` does not release the memory back to the OS — it rewinds the
allocation pointer. The block is reserved once and reused for every request
after it. That is why memory is flat under load, and it is measured: two
tests assert that `BytesReserved` levels off after warm-up, in the arena
alone and across 500 requests through the server. Those are premise tests.
If they stop holding, the model is wrong, not the test.

## The rules

**Anything that must outlive the current statement inherits `TArenaObject`
and is created inside a `UseArena` block.** The host sets the arena; your
code does not touch it.

**Never call `Free` on an arena object.** It is a no-op, but writing it
signals that the author believes in the wrong model.

**The destructor never runs on an arena object.** This is the rule that
surprises people, so read the next part carefully.

## What is cleaned up, and what leaks

Destructors do not run. But `string`, dynamic array and interface fields
**are** cleaned: `NewInstance` checks `ClassNeedsFinalization` and registers
`CleanupInstance` as a `Defer`. `TSession`, `TQuery` and `TErrors` depend on
that.

A field that points at an *object* — a `TStringList`, a `TList` — **leaks**,
because only a destructor would free it, and that never runs.

```pascal
type
  TGood = class(TArenaObject)
    Name: string;          // cleaned via Defer
    Items: array of Int64; // cleaned via Defer
  end;

  TBad = class(TArenaObject)
    List: TStringList;     // LEAKS — nothing frees it
  end;
```

**Prefer `TStr` anyway.** Every field that needs finalisation costs one
`Defer` entry per object per request. `TStr` is a slice — a pointer and a
length — and costs nothing.

```pascal
type
  TBetter = class(TArenaObject)
    Name: TStr;            // no Defer entry at all
  end;
```

`ClassNeedsFinalization` walks the whole inheritance chain. Free Pascal
emits one init table per class covering only that class's own fields, so a
subclass that inherits a `string` field has an empty table of its own.
Checking only the class itself would leak one string per such model per
request.

## Creating objects

```pascal
Customer := Arena.New<TCustomer>;
```

`Arena.New<T>` runs the constructor, sets up the VMT, and puts the object in
**that** arena even when another one is ambient. The type parameter is bound
to `TArenaObject` on purpose: an arbitrary `TObject` would land on the heap
without the call site noticing.

`Arena.Owns(P)` says whether a pointer actually lives in the arena's memory.
Use it in tests instead of assuming.

## Cleanup you schedule yourself

```pascal
Arena.Defer(@CloseHandle, Pointer(H));
```

`Defer` is the answer to destructors never running. The deferred functions
run in reverse order on `Reset`, `Rewind` and `Destroy`. A function that
raises is swallowed deliberately — a half-finished `Reset` is worse than a
lost error message.

## Marks and rewinding

```pascal
Mark := A.Mark;
try
  { scratch allocations }
finally
  A.Rewind(Mark);
end;
```

Used where the framework builds SQL text and parameters that are not needed
after the call.

> **Allocate parameters BEFORE `Arena.Mark` when the code rewinds
> afterwards.** If they sit after the mark, the next allocations overwrite
> the values while they are being read. That hit `LoadRelation` and produced
> a bug that did not show up in the tests.

## Reading the numbers

| Property | What it counts |
|---|---|
| `BytesLive` | Allocated right now |
| `BytesReserved` | Held by the arena, across resets |
| `HighWaterMark` | The most that was ever live |
| `ResetCount` | Requests this worker has served |

The welcome page in a new project shows all four for the worker that served
it. That is the whole memory model in one screen.

## The three copies that cannot be skipped

Three places in the framework copy data across an arena boundary. None of
them is an optimisation to remove:

**`Cache.Put` copies out of the caller's arena.** The value must outlive the
request that stored it.

**`Cache.Get` copies out into the caller's arena.** Returning a pointer into
the cache would let an eviction leave a dangling pointer, and the value
would outlive the request.

**`Queue.Push` copies out; the queue worker copies in.** When a controller
pushes, the payload lives in the request arena. The request is finished long
before the job runs — the arena has been reset and the memory handed to a
new request. A job pointing there would read another user's data.

```
request arena  ->  heap (in Push, while the caller still owns the bytes)
               ->  worker arena (in the worker, before the handler is called)
```

If you touch any of them, run the arena tests. They zero the arena and fill
it with garbage before reading back.

## Things that live outside the arena on purpose

**`TPgConnection` and the other connections.** They live on the heap across
requests. A connection is not a request-scoped object and must never become
one.

**The worker's read buffer.** It must survive `Reset` for keep-alive and
pipelining to work. The request head is copied into the arena before
parsing, so the buffer can grow when the body arrives without leaving the
slices in `TRequest` dangling.

**Uploaded file content.** It is a slice into the read buffer, valid for the
duration of the request. See [File uploads](uploads.md).

## `TStr`

A slice: a pointer and a length into memory someone else owns. The parser
copies nothing — every field on a request is a slice into the buffer the
bytes arrived in.

```pascal nocheck
function TStr.IsEmpty: Boolean;
function TStr.ToString: string;          { copies out; use at the edges }
function TStr.EqualsStr(const S: string): Boolean;
function TStr.SameTextStr(const S: string): Boolean;   { ASCII case-insensitive }
function TStr.StartsWithStr(const S: string): Boolean;
function TStr.IndexOfByte(B: Byte; StartAt: SizeInt = 0): SizeInt;
function TStr.IndexOfStr(const Needle: string; StartAt: SizeInt = 0): SizeInt;
function TStr.Slice(Start: SizeInt; Count: SizeInt = -1): TStr;
function TStr.TrimSpace: TStr;
function TStr.SplitAt(B: Byte; out Left, Right: TStr): Boolean;
function TStr.ToInt64(out V: Int64): Boolean;
```

`ToString` copies into a heap string. Use it at the edges — logging, a model
property, an exception message — not in a loop.

> `TStr.SplitAt` can take `Self` as an out parameter —
> `Rest.SplitAt(B, Item, Rest)` is the usual call — because both halves are
> worked out before either is written.

## Background work

PRD rule two: **background jobs never borrow the request's arena, they get
their own.** The queue worker resets its arena between jobs, exactly as HTTP
workers do between requests. A job handler is written the same way a
controller is.