Core placement: one worker per chain, pinned across the audio cores
plugin-hostd runs each chain (and each plugin that may not share) in a worker process of its own. On a small machine, a Raspberry Pi 5 with four cores, the kernel and the JACK server decide where those processes run unless someone pins them. A plugin's own threads do not help: Surge XT 1.4.0 and the other plugins measured on the Pi 5 ignore CLAP's thread-pool extension, so one instrument is one audio thread. Several instruments use several cores only when each runs in a worker of its own and those workers are spread over the cores, with the daemon, the UI and the system kept off them.
The setting placement does that.
The policy
- Audio cores.
audio_coresis a cpu list (1-3,2,3). Empty, the default, is every cpu the daemon may run on when it starts (its affinity, so atasksetor a cgroup is honoured) except the lowest one. A list that names a cpu outside that set refuses the start. - The reserved cores are the daemon's cpus that are not audio cores: the lowest one by default. The daemon pins
itself and all its threads there at start. The processes it starts that are not workers, the descriptor scans and
probes, are forked from it and inherit that mask. The UI is the system's to pin there (
taskset -c 0, orCPUAffinity=0in its unit). With no reserved core left (audio_coresnames every cpu) the daemon is not pinned, and stderr says so. - Each new worker is pinned (
sched_setaffinity, in the child before it runs the worker program, so every thread it creates, its JACK or PipeWire thread included, inherits the mask) to one audio core: the one with the least load. The load of a core is the sum of the estimated costs of the workers already placed on it, when the cost of every placed worker is known. A worker's cost is the sum of its members'coreFractionP95from their qualification records atgroup_rateandgroup_quantum, as the planner reads it. When any placed worker has a member without a record, the load is the number of workers. Ties go to the lowest core. So four workers on four cpus land on cores 1, 2, 3 and 1, with the daemon on 0. - A worker keeps its core when it is respawned. A worker that is made anew (a chain formed, a group split by a death, a quarantined member moved out) is placed again.
- Chains and isolated plugins. A chain, whether declared by
chainor a segment of aplan, is one worker, so it takes one core. A plugin the planner leaves alone, anisolatedverdict among them, gets a worker of its own, placed by the same rule. Zynthian still sees one chain. worker_env. A format whose workers were given a cpu list byworker_envkeeps that list: the policy does not move those workers, and they report no core.placement noneis the behaviour before this setting. Nothing is pinned, the workers inherit the daemon's mask, and the reports say-.
The configuration
In the settings file (-c, or $XDG_CONFIG_HOME/plugin-hostd.conf):
placement spread # or none
audio_cores 1-3 # empty: every cpu the daemon may use but the lowest
and on the command line, over the file: plugin-hostd --placement=none.
What reports the core
worker_list: each record is<worker>:<pid>:<format>:<state>:<core>:<pool>:<place>:<instances>,<core>the audio core the policy pinned it to,-for none,<pool>onorqualified(below). The place still holds colons, so a record is read from both ends: six fields on the left, the instances on the right.plananswersresp 0 <segments> <cores>andplan_getresp 0 <segments> <reasons> <cores>:<cores>has the shape of<segments>, each member replaced by the core of the worker that runs it now (-for none), so1,2;3placed on cores 2 and 1 reads2,2;1.
The CLAP thread pool
omx-clap-host's pool of helper threads (--thread-pool=qualified|off|on) sizes itself from the
worker's affinity mask: N cores, N - 1 helpers. The pool is the plugin's qualification's to give, never a global switch,
and it travels in the plugin's pin as the word pool=cores (or pool=off) after the verdict:
pin_set <uri> <files> <layout> verdict=isolated pool=cores
A CLAP or lv2-clap worker of a plugin whose pin says pool=cores (and is not a refusal) is started with
--thread-pool=on, after the cpus above are set, so the mask it sizes the pool from is the one the policy gave it. The
plugin runs in a worker of its own: the planner leaves it alone (verdict-isolated), a pool: placement and a chain
are refused for it, and a respawn starts the worker with the option again. Every other worker is started with no
option, the host's qualified, which grants the pool to no plugin. The pool's waits past the block are in the worker's
xruns line, so they are attributed as any xrun is. Pins are read while require_pins is on, so the pool is too.
Measured
On a Pi 5, with two and three LV2 instruments in workers of their own, spread put the workers on cores 1-3 and the
daemon on 0, and core 0 fell from 7-9 % busy to under 2 %. Neither mode had an xrun at that load, so no xrun
gain is shown yet: the instruments use a fraction of one core. A run that fills the audio cores is still owed.