plugin-hostd

docs/placement.md

The project's own text, in English. The source of this page

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_cores is a cpu list (1-3, 2,3). Empty, the default, is every cpu the daemon may run on when it starts (its affinity, so a taskset or 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, or CPUAffinity=0 in its unit). With no reserved core left (audio_cores names 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' coreFractionP95 from their qualification records at group_rate and group_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 chain or a segment of a plan, is one worker, so it takes one core. A plugin the planner leaves alone, an isolated verdict 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 by worker_env keeps that list: the policy does not move those workers, and they report no core.
  • placement none is 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> on or qualified (below). The place still holds colons, so a record is read from both ends: six fields on the left, the instances on the right.
  • plan answers resp 0 <segments> <cores> and plan_get resp 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), so 1,2;3 placed on cores 2 and 1 reads 2,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.

openmixer

A software mixing console for Linux. The desk is the software; the browser is the surface.

Pages

Licence

OpenMixer is free software under the GPL-3.0-or-later. Every package in the workspace carries the same licence.

Roland, Midas, Behringer, RME and the product names used here belong to their respective owners. OpenMixer is an independent project and is not affiliated with any of them.