Deployment: hardware, execution context, communication
Where the parts run, and how archec follows them while they run: the
deployment, the living model (archec live) and containers.
The logical model (a system) says what the parts are. Three more questions are kept
apart from it, so the same system can be deployed more than one way:
- Hardware — where things run. A processor is a component with cores
(
cores 8,core safety { type "Cortex-R52" }), anos,ram; a bus is a component withkind busand abitrate. Put them in a platformsystem. - Execution context — what runs. A
deploymentplacesbinarys on processors; a binary hasprocesses (address spaces; none on an RTOS), and they havethreads andisrs withpriority,period,deadline,core,stack. A threadrunsparts: their functions, handlers and state machine execute on it, one at a time. - Communication — how two parts talk, on ports and links:
pattern(fire-and-forgetby default,request-reply,pub-sub,broadcast: who waits and how many receive) andbuffer(queue+depth/overflow,sample,stream: what the receiver keeps), plustimeout,deadline,stale-after,reliability,topic.
deployment Vehicle "L4 stack on the vehicle" {
system Car // what runs
platform CarHw // where
binary adas "adas-stack" on adcu_soc {
process driving { core cpu2; thread drive { runs adcu; priority 80 } }
}
binary brake_fw "brake.elf" on brake_mcu { // FreeRTOS: tasks, no process
thread ctl { runs brakes; period 1ms; step regulate; priority 10 }
}
route adcu.can_brake -> brakes.can over chassis_can { priority 3 }
chain obstacle { through adcu, brakes; budget 100ms }
}
The transport of each link follows from where its ends run (same thread, same process,
same processor through ipc-latency, or another processor through its route) — a link
between two processors with no route is an error. The linter also reports parts with
behaviour that run nowhere, a core a processor does not have, a bus that does not reach
both processors, mismatched patterns, a request with no handler for it, unbounded queues
on an RTOS, and mixed safety levels in one address space. archec analyze (and the
editor) computes per-core utilisation and fixed-priority response times from wcet,
period and priority, bus load from message sizes and periods, and the worst-case
latency of each chain against its budget. See examples/autonomous-car/deployment.arche.
What the system then does is measured against it: archec gen --target c --trace (or
embassy --trace) puts trace points in the generated code — each thread's activation
and completion, messages, bus frames, chains — and archec timing main.arche <trace…>
reads the traces (or any Chrome trace JSON) and sets each thread's period, jitter and
response time, each core's load, each bus's load and each chain's latency beside the
prediction: worse than predicted means the analysis is optimistic (a wcet too small),
over a deadline or a budget is a miss. See examples/timing. Built live (make LIVE=1,
or -DARCHE_TRACE_LIVE), the C writes each trace line on stdout as it happens — the
state each part's machine enters among them (arche-trace <ns> state counter.Seen 0) —
for a running system to be followed.
Execution and memory#
A thread is an active object: it runs its parts one message at a time, from its
queue. queue 32 sizes that queue (on an RTOS a thread woken by messages should
say it), and affinity cpu1, cpu2 gives the cores it may run on when one core
does not say it. A thread may also run one port of a part: runs sink.metrics
handles that port's messages on it, not on the part's thread.
A part no thread runs can still run: place it in a process. It is then
passive (it runs in its callers' threads) or protected (one caller at a time).
execution active says it needs a thread of its own.
Memory comes from allocators. A processor says its memories (memory ddr { size 1GiB; shared }: a memory with no type and no address); an allocator in the
deployment, a binary or a process takes its blocks from one of them, or from a
parent allocator. A place then says where a part's memory comes from, per use:
hrt and lrt (its real-time data and the rest), saved and tmp (an
algorithm's working memory), comm (what its ports marshal). A dataflow port's
buffering says how big its buffers are, whether consumers copy or share them,
when they are freed and where they come from.
deployment Target {
allocator heap { kind heap; block-size 1MiB; memory cpu.ddr; shared }
binary chain on cpu {
options mccoffee { orb "rteorb" }
process main {
allocator frames { kind pool; block-size 4KiB; blocks 32; parent heap; alignment 64 }
thread work { runs chain; priority 50; queue 32; affinity cpu1, cpu2 }
thread watch { runs sink.metrics; queue 8 }
place log { execution protected; memory hrt frames }
place chain.f1 { memory saved frames; memory tmp frames }
place chain.split.input {
memory comm frames
buffering { max-size 4KiB; transfer copy; destroy after-processing; allocator frames }
}
}
}
}
The linter reports an active part no thread runs, a passive part a thread runs,
a thread that runs an out port, an allocator of another process, an allocator in
a local memory that is mapped, and overflows: a child allocator larger than one
block of its parent, more uses than blocks, a buffer larger than a block,
allocators larger than their memory. A buffer that is shared and freed once sent
is an error: its consumers would read freed memory.
options <generator> { … } on a binary (or a process) sets that generator's
options for it only, over the project's; options { … } sets them for any
generator. The generator model gives each binary and each process its options
resolved. See examples/mccoffee.
Live#
archec live main.arche runs a deployment's containers under podman (the generated C
traced live, --target containerd), follows their logs and podman stats, and keeps
the model's status as it runs: each requirement, chain, thread and container pass, fail
or pending. A chain over its budget fails the requirement it satisfies; a thread late
or a container over its memory-limit fails what the parts it runs satisfy; a runtime
monitor's arche-trace <ns> monitor REQ-1 1 fails REQ-1. --json streams the state
(one JSON per line, each item with its file and line) for the editor; --once --duration 30s is a CI gate; --replay trace.log evaluates traces already written.
Containers#
On a Linux processor whose compute gives a runtime containerd, a container holds
binaries instead of putting them straight on the processor. It names its image after
its id, and its cgroup and sandbox: the cores it may use (core, its cpuset),
memory-limit, network host|bridge|none, the host devices it sees (/dev/ttyS1), Linux
capabilities (sys_nice for policy fifo threads, all for a privileged one) and
environment variables (prop NAME = "value"). Its binaries take its on:
container driving "registry.local/l4/driving:0.3" on adcu_soc {
core cpu2, cpu3, cpu4, cpu5; memory-limit 4GiB; network host
capabilities sys_nice; prop RUST_LOG = "info"
binary adas { language rust; thread drive { runs adcu; core cpu2; priority 80; policy fifo } }
}
A container with a memory-limit counts once against its processor's ram, and its
processes are checked against that limit. A link between two containers costs the
processor's ipc-latency, or, when one of them is on a bridge network, its latency.
The linter reports containers on a non-Linux processor or one with no runtime, a thread
outside its container's cores, a real-time thread without sys_nice, a link leaving a
network none container, a container on a CAN bus without network host (SocketCAN can0
is a network interface, not a device), one on another bus (a serial line) that sees no device,
and a binary inside a container that also says on. archec gen --target containerd
writes the images and the compose files, and --target k8s the Kubernetes (k3s) manifests
that run them, each pinned to its processor's node (see Code generation).
To put every process in a container of its own without writing each one, give the
deployment packaging container: each process of a binary on a Linux processor gets a
container named after it, on that processor, with its core(s) as the cpuset, its
memory-limit, the host's network and sys_nice when one of its threads is real-time (a
binary's threads with no process share one named after the binary). Containers written out
stay as written; write one out to give it an image, a device or another network.
deployment Vehicle {
packaging container
binary adas on adcu_soc {
process driving { core cpu2; memory-limit 4GiB; thread drive { runs adcu; policy fifo } }
process island { core safety; thread guard { runs adcu.brakes_path } }
}
}
In the Arche project, the containers of the simulated deployment go to containers.yaml
(processor, cpuset cores, memoryLimit, network, latency, image, devices,
capabilities, env); a process in one, or a thread with no process, says
container: <id> and keeps its own target and core. Arche's Hardware page frames the
threads of each core by their container; the simulation runs them as before.