From first task to a measured configuration.

Prepare a version-matched GNAT runtime, opt ordinary Ada tasks into the lightweight lane, then size their stacks and budget concurrency, execution groups, deadlines, and cooperative CPU work.

STEP 01

Start with an exact toolchain.

Flyology is an Alire crate and requires Alire 2.1 or newer. The crate accepts GNAT 13 through 16 as a dependency range, but runtime preparation supports only the host and release pairs listed below.

macOS / AArch64
13.2.2, 14.1.3, 14.2.1, and 16.1.0
Linux / AArch64
16.1.0
Linux / x86-64
13.2.2, 14.1.3, 14.2.1, 15.1.2, 15.3.1, and 16.1.0
STEP 02

Build the public library.

Clone the repository, let Alire select a supported toolchain, and build the crate. Applications must link against a prepared runtime; stock GNARL alone is not sufficient.

shell
git clone https://github.com/flyology-ada/flyology.git
cd flyology
alr build
STEP 03

Prepare a version-matched runtime.

The preparation script copies the active compiler's installed runtime sources into ignored build output, applies the exact patch family, compiles Flyology policy into the runtime, and builds a static RTS.

native default
./scripts/prepare-rts.sh

The default is native. This is the compatibility configuration: undesignated tasks stay on stock pthread-backed execution, and no event machinery starts until a lightweight task is activated.

lightweight project default with four shared groups
FLYOLOGY_DEFAULT=lightweight \
FLYOLOGY_LOOP_POOL_SIZE=4 \
  ./scripts/prepare-rts.sh
FLYOLOGY_RTS_DIR
Writes the generated runtime outside the default build/rts. Give each materially different configuration its own directory when comparing builds.
FLYOLOGY_DEFAULT
Selects native or lightweight for tasks that do not carry an explicit designation.
FLYOLOGY_LOOP_POOL_SIZE
Selects 1 .. 128 shared groups for automatic round-robin placement. Groups and their pthreads still start lazily.
Loop placement
FLYOLOGY_LOOP_PLACEMENT and FLYOLOGY_LOOP_PLACEMENT_MAP request Linux strict logical-CPU binding or a supported Darwin advisory tag. This places loop pthreads; it does not change task-to-group assignment.
STEP 04

Designate one ordinary Ada task.

Use GNAT Task_Info to select the execution model at task creation. The obsolete-feature warning is deliberate; Flyology projects retain other warnings while using -gnatwJ for this mechanism.

worker.ads
with Flyology;

package Worker is
   task Connection is
      pragma Task_Info (Flyology.Lightweight_Task);
   end Connection;
end Worker;

Explicit Flyology.Native_Task and Flyology.Lightweight_Task values override the prepared project default. Flyology.Project_Default requests that default explicitly, which is useful on discriminated task types.

build the application with the prepared RTS
alr exec -- gprbuild \
  --RTS="$PWD/build/rts" \
  -P path/to/application.gpr
STEP 05

Size each task stack deliberately.

Ada's Storage_Size is the stack-size control for both native and lightweight tasks. Put the pragma in the task or task-type declaration, use a named constant so the choice is reviewable, and size for the deepest real call path: local objects, recursion, exception propagation, and foreign calls all consume stack.

a task type with a 64 KiB requested stack
Worker_Stack_Size : constant := 64 * 1_024;

task type Worker (Kind : Flyology.Execution_Model) is
   pragma Task_Info (Kind);
   pragma Storage_Size (Worker_Stack_Size);
end Worker;

The requested value is not the task's complete memory footprint or necessarily the final usable mapping size. GNARL adds its conservative alternate-signal-stack allowance, and Flyology rounds a lightweight stack to the host page size. Lightweight stacks are placed in guarded arenas; each slot carries at least 64 KiB of inaccessible guard address space. Native tasks instead retain the host pthread implementation and its platform minimums.

Flyology.Observability.Stack_Pool reports the effective lightweight result. Live_Usable_Bytes is the aggregate usable size after GNARL adjustment and page rounding; Reserved_Bytes is the exact virtual address space held by live arenas, including guards. Reserved address space is not resident memory, so measure process RSS separately.

inspect the lightweight stack pool
declare
   Pool : constant Flyology.Observability.Stack_Pool_Snapshot :=
     Flyology.Observability.Stack_Pool;
begin
   Ada.Text_IO.Put_Line
     ("live=" & Pool.Live_Stacks'Image
      & " usable bytes=" & Pool.Live_Usable_Bytes'Image
      & " reserved bytes=" & Pool.Reserved_Bytes'Image);
end;

If every live lightweight task in a sample uses the same declaration, divide Live_Usable_Bytes by Live_Stacks to obtain its effective usable size. With mixed task sizes that quotient is only an average. The current structured-server generic owns its handler task type and uses the compiler/runtime default stack size; Storage_Size applies directly to task types declared by the application.

STEP 06

Choose an execution group.

A lightweight task without a specific Ada CPU is placed into the configured shared pool by deterministic round robin. A CPU aspect selects an exact Flyology execution group instead.

explicit shared groups
task Parser with CPU => 1 is
   pragma Task_Info (Flyology.Lightweight_Task);
end Parser;

task Writer with CPU => 2 is
   pragma Task_Info (Flyology.Lightweight_Task);
end Writer;

Tasks in one group share a stable loop pthread and schedule cooperatively. Separate groups use separate pthreads and may execute in parallel. Shared group identifiers are 0 .. 127; dedicated group identifiers are 128 .. 255.

Migration is an explicit safe point

A lightweight task can move to another group without changing Ada task identity, stack, locals, or exception state. Use a scoped thread pin while holding thread-affine foreign state, and use a dedicated group when exclusive pthread ownership is also required.

explicit migration
declare
   package Groups renames Flyology.Execution_Groups;
   Home : constant Groups.Group_Id := Groups.Current;
begin
   Groups.Migrate (Groups.For_CPU (2));
   Handle_Group_Owned_State;
   Groups.Migrate (Home);
end;
STEP 07

Use synchronous I/O calls.

Flyology I/O packages expose normal procedure and function calls from either lane. If a lightweight call would block, only that task waits; the loop thread remains available. A native call may block only its pthread.

task-aware I/O
Flyology.IO.Timers.Sleep_For (0.050);

Flyology.IO.Sockets.Receive
  (Socket, Buffer, Last, Timeout => 1.0);

Flyology.IO.Files.Read_At
  (File, Offset => 0, Item => Buffer, Last => Last);

Use the ownership-aware connection packages when descriptor lifetime, cancellation, and close races matter. Raw descriptor waits deliberately leave lifetime serialization to the caller.

Browse the generated API reference

STEP 08

Set resource budgets before load testing.

Flyology does not turn one number into a complete capacity plan. Bound the resources the application owns, set latency limits at operation boundaries, and then measure them together under representative peak load.

Concurrency
Use a structured server or Connections.Server capacity to cap simultaneously owned connections. A structured server creates exactly Capacity handler tasks for the duration of Serve, including while idle.
Stack memory
For homogeneous lightweight tasks, plan usable stack bytes as peak live tasks multiplied by the observed effective bytes per task. Read Reserved_Bytes for guarded virtual address space and measure RSS for resident memory.
Parallelism
FLYOLOGY_LOOP_POOL_SIZE bounds automatic shared groups, not task count. Start near the CPU parallelism the workload can use and compare measurements; each created group adds pthread, poller, scheduler, and kernel-queue resources.
Latency
Use finite I/O deadlines, a bounded shutdown drain, and checkpoints in CPU loops. These are separate limits: cancellation is not a timeout, and a fairness quantum is not a hard execution deadline.

Bound admission, not just the listen backlog

The structured-server capacity is both the maximum active handlers and an eager task-resource budget. Excess work remains in the kernel listen backlog instead of becoming an unbounded user-space task or connection queue.

bounded structured service
Handler_Capacity : constant Positive := 256;

Server : aliased HTTP.Server (Capacity => Handler_Capacity);

HTTP.Serve
  (Server, Listener, State, Drain_Timeout => 5.0);

Give an operation one meaningful deadline

Receive_Exactly and Send_All apply one timeout across the whole multi-step operation rather than restarting it for every partial transfer. DNS resolution similarly applies its overall timeout across family queries, retry rounds, and transport fallback. Prefer finite deadlines at service boundaries; reserve Infinite for scopes whose shutdown and cancellation path is otherwise explicit.

Add checkpoints to bounded units of CPU work

A CPU-bound lightweight task can monopolize its group until it reaches a runtime suspension, executes delay 0.0, or calls a fairness checkpoint. Priorities choose among ready fibers within a group, but they do not preempt arbitrary lightweight instructions. A new Yield_Budget defaults to a 2 ms quantum; configure it when the service needs a different cooperative slice.

time-budgeted fairness
Budget : Flyology.Fairness.Yield_Budget;

Budget.Configure (Ada.Real_Time.Microseconds (250));
while More_Work loop
   Process_One_Item;
   Budget.Checkpoint;
end loop;

Choose a quantum below the same-group scheduling delay the application can tolerate, then measure it. The value is elapsed wall-clock time between eligible yields, not reserved CPU time. Put Checkpoint after a bounded unit of work: if Process_One_Item itself can run for 50 ms, a 250 µs quantum cannot take effect until that call returns. Very small quanta also increase clock reads and scheduler transfers.

STEP 09

Observe, compare, and adjust one knob.

Take snapshots at idle, representative load, and recovery. Group snapshots are coherent but briefly hold scheduler locks and cost O(group members), so sample periodically rather than per request. Lifetime counters wrap and do not reset; compare two samples instead of treating one cumulative value as a rate.

Healthy waiting
A high Waiting count accompanied by timer, descriptor, interrupt, or file waits normally means concurrency is parked in the intended kernel-backed path.
Runnable pressure
A persistently high Ready count means runnable work is sharing the group. Compare dispatch progress, CPU use, checkpoint placement, and the result of adding a group.
Possible stall
Ready or running work without changes in dispatch or polling counters is a loop-lag signal. Stall_Watchdogs can sample this condition from a native monitor task; it diagnoses but does not preempt.
File pressure
Pending_File_Submissions shows file operations waiting behind the bounded kernel completion queue. It is backpressure, not a hidden worker-thread queue.
Stack pressure
Track live stacks, effective usable bytes, guarded virtual reservation, process RSS, and task-creation failures together. None of those values alone is a complete memory limit.
repeatable sizing and pool comparisons
./scripts/showcases.sh

# After the showcase build, choose smaller local counts if needed:
./showcases/run_connection_density.sh 1000 10000
./showcases/run_event_loop_pool.sh 1024 20 4

The density run compares lightweight and native tasks with the same requested stack size and reports RSS, virtual address space, thread count, and lightweight stack-pool counters. The pool run compares one event loop with a configured pool under repeated readiness waves. Treat either result as a measurement of that host and workload, not a universal sizing constant.

STEP 10

Run the checks for your changes.

The repository scripts are authoritative for behavioral tests, proofs, documentation, stress campaigns, and showcases.

core verification
./scripts/test.sh
./scripts/prove.sh
./scripts/docs.sh
./scripts/showcases.sh
  • test.sh runs both project defaults, runtime-preparation validation, and an external consumer.
  • stress.sh runs bounded deterministic concurrency and fault campaigns.
  • prove.sh runs the SPARK policy proof suite.
  • docs.sh generates the public GNATdoc reference with undocumented-entity warnings enabled.
STEP 11

Know when to stay native.

Flyology is experimental. Keep a task native when its correctness depends on kernel real-time scheduling, forced preemption, stock GNARL CPU affinity, unpredictable foreign blocking, or a host and compiler combination outside the verified patch matrix.

Use lightweight
High I/O concurrency with cooperative work, explicit task-aware waits, and ordinary Ada task semantics.
Use native
Blocking foreign libraries, kernel scheduling policies, independent pthread execution, or unmodified compatibility behavior.
Use dedicated
A lightweight continuation must temporarily own a stable loop pthread exclusively.