[{"data":1,"prerenderedAt":36},["ShallowReactive",2],{"note-the-grouping-planner-en":3},{"slug":4,"order":5,"title":6,"summary":7,"measured":8,"topic":9,"toc":10,"html":35},"the-grouping-planner",50,"\"The grouping planner: the fewest workers that pins and history allow\"","Given a strip's plugins in signal order, the planner puts consecutive plugins that may share a process into one worker and leaves the others alone, judged by their pins, their cost and what they did on this machine; it stays off by default until the benchmark shows it wins.","The design of plugin-hostd's grouping, October 2026. Grouping is off by default until the benchmark publishes a win on a Raspberry Pi 5 and on a desktop.","host",[11,14,17,20,23,26,29,32],{"id":12,"title":13},"the-problem","The problem",{"id":15,"title":16},"the-planner","The planner",{"id":18,"title":19},"the-verbs","The verbs",{"id":21,"title":22},"the-history-record","The history record",{"id":24,"title":25},"how-qualification-uses-it","How qualification uses it",{"id":27,"title":28},"in-zynthian","In Zynthian",{"id":30,"title":31},"the-benchmark-that-decides-the-default","The benchmark that decides the default",{"id":33,"title":34},"failure-modes","Failure modes","\u003Ch2 id=\"the-problem\">The problem\u003C\u002Fh2>\n\u003Cp>plugin-hostd can run many plugins in one process, which saves CPU and memory. A chain is all-or-nothing, though: a\ncontroller declares the members of a strip as one chain, and one member that may not share a process (no qualification\nverdict, or a verdict of \u003Ccode>isolated\u003C\u002Fcode>) makes the whole strip run one process per plugin. Nothing is learned from what a plugin\ndid on the machine either.\u003C\u002Fp>\n\u003Cp>The planner answers a different question from a chain. A chain is the controller's statement: these run together, in this\norder. A plan is the daemon's answer to: given these, in this order, which of them may run together. It ends in the \u003Ccode>chain\u003C\u002Fcode>\nand \u003Ccode>add\u003C\u002Fcode> calls the daemon already has; it adds no new way to run a plugin.\u003C\u002Fp>\n\u003Ch2 id=\"the-planner\">The planner\u003C\u002Fh2>\n\u003Cp>A pure function in plugin-hostd: the same inputs give the same plan, byte for byte, and it reads no clock and no file while\nit decides.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Inputs\u003C\u002Fstrong>: the members in signal order (instance, uri, format); for each, its pin (files, layout, verdict or none), its\ncost (\u003Ccode>coreFractionP95\u003C\u002Fcode> at the daemon's rate and quantum) and latency from the qualification record when there is one, and\nits history on this host class; and the host class and settings (\u003Ccode>group_max\u003C\u002Fcode>, \u003Ccode>group_cost_max\u003C\u002Fcode>, \u003Ccode>grouping\u003C\u002Fcode>,\n\u003Ccode>require_pins\u003C\u002Fcode>, \u003Ccode>pin_mode\u003C\u002Fcode>).\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Output\u003C\u002Fstrong>: a list of segments in signal order. A segment is a run of consecutive members that share one worker, or one\nmember alone. A segment of two or more is declared as a chain; a segment of one runs in a worker of its own. Every member\nleft alone gets one reason code.\u003C\u002Fp>\n\u003Cp>Segments are runs of consecutive members because a chain is one callback in the declared order, and a member left out of\nthe middle must keep its place in the signal path. So \u003Ccode>A B C D E\u003C\u002Fcode> with \u003Ccode>C\u003C\u002Fcode> unfit becomes \u003Ccode>[A B] [C] [D E]\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Rules, in this order; the first that matches a member decides it.\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>reason\u003C\u002Fth>\n\u003Cth>when\u003C\u002Fth>\n\u003Cth>the member\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>refused\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>pin verdict \u003Ccode>refused\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>is not added at all (-516)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>no-pin\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>no pin, \u003Ccode>pin_mode strict\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>is not added (-508)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>no-verdict\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>a pin without a verdict (a learned pin)\u003C\u002Ftd>\n\u003Ctd>runs alone\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>verdict-isolated\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>verdict \u003Ccode>isolated\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>runs alone\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>format-world\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>its format pins only in a world of its own, and the chain's union world cannot be built\u003C\u002Ftd>\n\u003Ctd>runs alone\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>history-crashed\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>the history marks it crashed\u003C\u002Ftd>\n\u003Ctd>runs alone\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>history-xruns\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>the history marks it as raising xruns when grouped\u003C\u002Ftd>\n\u003Ctd>runs alone\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>cost-over\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>its cost alone is over \u003Ccode>group_cost_max\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>runs alone\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>format-mixed\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>its program differs from the open segment's and the two do not both run chains in one client\u003C\u002Ftd>\n\u003Ctd>starts a new segment\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>group-full\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>the open segment holds \u003Ccode>group_max\u003C\u002Fcode> members\u003C\u002Ftd>\n\u003Ctd>starts a new segment\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>group-cost\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>its cost takes the open segment over \u003Ccode>group_cost_max\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>starts a new segment\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>A member no rule matches joins the open segment. A plugin with no history is judged by its pin and cost alone: no history\nis neutral, or a plugin could never earn one. \u003Ccode>group_cost_max\u003C\u002Fcode> is a fraction of one core; a segment is one callback, so its\nmembers' costs add. A member with no known cost counts as the median of the segment's known costs (zero when none is\nknown), and the reasons say \u003Ccode>cost-assumed\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>A member left alone only because it was next to an unfit one may share a pool with such members of other chains. A pool\nsaves resident memory, not wake-ups: each member stays a JACK client. Two chains never share a chain worker.\u003C\u002Fp>\n\u003Ch2 id=\"the-verbs\">The verbs\u003C\u002Fh2>\n\u003Cp>The plan runs in the daemon, so that Zynthian's engine (a Python controller) and OpenMixer's console reach the same answer\nwithout carrying the rules.\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>plan &lt;chain&gt; &lt;instance&gt; ...\u003C\u002Fcode> applies the plan: the segments become chains and the lone members run alone. The reply\nlists the segments as \u003Ccode>i,i,i;i;i,i\u003C\u002Fcode>. A chain planned again keeps the running workers of segments that did not change.\u003C\u002Fli>\n\u003Cli>\u003Ccode>plan_get &lt;chain&gt;\u003C\u002Fcode> reads back the last plan and the reason of each member left alone; a \u003Ccode>plan_reason\u003C\u002Fcode> event carries the\nsame when a plan changes.\u003C\u002Fli>\n\u003Cli>\u003Ccode>plan_clear &lt;chain&gt;\u003C\u002Fcode> gives every member a worker of its own and forgets the plan.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Each segment's worker is pinned to the least-loaded audio core by the \u003Ccode>placement\u003C\u002Fcode> setting; \u003Ccode>plan\u003C\u002Fcode> and \u003Ccode>plan_get\u003C\u002Fcode> report the\ncore of each member.\u003C\u002Fp>\n\u003Cp>\u003Ccode>grouping\u003C\u002Fcode> is a setting, \u003Ccode>off\u003C\u002Fcode> or \u003Ccode>on\u003C\u002Fcode>. Under \u003Ccode>off\u003C\u002Fcode>, \u003Ccode>plan\u003C\u002Fcode> answers the plan without applying it, with the reason\n\u003Ccode>grouping-off\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Ch2 id=\"the-history-record\">The history record\u003C\u002Fh2>\n\u003Cp>What each plugin did on this machine, kept per plugin, per binary and per host class: the key is the uri, the SHA-256 of\nthe first pinned file, and the host class (architecture, core band, clock band). A changed plugin starts a fresh record.\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>key\u003C\u002Fth>\n\u003Cth>meaning\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>first\u003C\u002Fcode>, \u003Ccode>last\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>the first and last window, UTC seconds\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>windows_solo\u003C\u002Fcode>, \u003Ccode>windows_grouped\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>windows run in a worker of its own, or in a worker of two or more\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>cpu_p95_solo\u003C\u002Fcode>, \u003Ccode>cpu_max_solo\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>percent of one core over a window, windows run alone only\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>pss_mb_solo\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>the worker's proportional memory, MiB, largest window run alone\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>xrun_windows_solo\u003C\u002Fcode>, \u003Ccode>xrun_windows_grouped\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>windows in which JACK counted at least one xrun\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>crashes_solo\u003C\u002Fcode>, \u003Ccode>crashes_grouped\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>worker deaths attributed to the plugin\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>restarts_grouped\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>worker deaths while it was a member and not the culprit\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>quarantines\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>times it was quarantined\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>last_crash\u003C\u002Fcode>, \u003Ccode>last_crash_grouped\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>UTC seconds\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>CPU is kept per plugin only for windows run alone: a group's worker time cannot be split among its members honestly. A\nwindow is 60 seconds; a window with the plugin up less than half of it is not counted.\u003C\u002Fp>\n\u003Cp>The file is \u003Ccode>$XDG_STATE_HOME\u002Fplugin-hostd\u002Fhistory\u003C\u002Fcode> (else \u003Ccode>~\u002F.local\u002Fstate\u002Fplugin-hostd\u002Fhistory\u003C\u002Fcode>), mode 0600, written to a\ntemporary file and renamed. Only the daemon writes it, from its idle tick and when the supervisor attributes a death. The\nplanner reads a summary of it: \u003Ccode>crashed\u003C\u002Fcode> when a crash falls within 30 days, \u003Ccode>raises-xruns\u003C\u002Fcode> when, after at least 30 grouped\nwindows, the share of grouped xrun windows exceeds the share solo by 0.05.\u003C\u002Fp>\n\u003Cp>The history is local. It holds URIs, digests and counters: no audio, no path, no user, session or preset name. The daemon\nnever sends it anywhere, and \u003Ccode>history_clear\u003C\u002Fcode> deletes it.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Xruns.\u003C\u002Fstrong> JACK reports an xrun to a client, not to a plugin, and the daemon holds no JACK client. The worker does, so it\nreports \u003Ccode>xruns &lt;count&gt;\u003C\u002Fcode> on its feedback port, and the daemon counts a window's xruns against every member of that worker. A\ngroup's xrun window is a suspicion of each member, not proof against one: that is why the planner compares a plugin's grouped\nshare with its own solo share rather than reading an absolute count.\u003C\u002Fp>\n\u003Ch2 id=\"how-qualification-uses-it\">How qualification uses it\u003C\u002Fh2>\n\u003Cp>\u003Ccode>plugin-qualify adjust --history &lt;file&gt;\u003C\u002Fcode> writes a second verdict beside the measured one, for the history's host class,\nwith its reason. The measured verdict never changes.\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>move\u003C\u002Fth>\n\u003Cth>when\u003C\u002Fth>\n\u003Cth>reason\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>in-process\u003C\u002Fcode> to \u003Ccode>isolated\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>crashed, or raises xruns grouped, on this class\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>history.crashed\u003C\u002Fcode>, \u003Ccode>history.xruns-grouped\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>isolated\u003C\u002Fcode> to \u003Ccode>refused\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>three or more solo crashes within 30 days\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>history.crashed-alone\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>isolated\u003C\u002Fcode> to \u003Ccode>in-process\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>the verdict was decided by cost alone, six hours run alone with no crash or restart, and the cost under the profile's budget\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>history.steady\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>A promotion never lifts a verdict decided by real-time safety, a missing feature, a crash in the prober or an \u003Ccode>unknown\u003C\u002Fcode>:\nthose are measurements of the plugin, not of this machine. A \u003Ccode>refused\u003C\u002Fcode> verdict is never promoted. The history can only make\nthe planner more careful than the pin, never less.\u003C\u002Fp>\n\u003Ch2 id=\"in-zynthian\">In Zynthian\u003C\u002Fh2>\n\u003Cp>Zynthian's engine sends \u003Ccode>plan\u003C\u002Fcode> with a strip's members instead of \u003Ccode>chain\u003C\u002Fcode> when the daemon offers it. One plugin that may not\nshare no longer ungroups the strip, the engine shows the reason of a plugin left alone next to its verdict in the plugin\nmenu, and a restarted daemon makes the same plan from the same inputs.\u003C\u002Fp>\n\u003Ch2 id=\"the-benchmark-that-decides-the-default\">The benchmark that decides the default\u003C\u002Fh2>\n\u003Cp>The chains-against-processes scenarios of the \u003Ccode>omx-bench\u002F1\u003C\u002Fcode> harness, on a Raspberry Pi 5 and on a desktop, in four modes\nwith the same plugins: \u003Ccode>individual\u003C\u002Fcode> (one process per plugin), \u003Ccode>chain\u003C\u002Fcode> (the whole strip as one chain), \u003Ccode>plan\u003C\u002Fcode> (through the\nplanner, with at least one plugin per strip that may not share) and \u003Ccode>pool\u003C\u002Fcode>. Each scenario also runs under a fixed stress\nload: busy processes pinned to the host's cores plus a fixed memory touch.\u003C\u002Fp>\n\u003Cp>\u003Ccode>grouping\u003C\u002Fcode> turns on by default only when, on both machines, at 8 and 16 plugins and every listed rate and quantum, each row\nrepeated at least three times:\u003C\u002Fp>\n\u003Col>\n\u003Cli>without stress, \u003Ccode>plan\u003C\u002Fcode> has fewer processes than \u003Ccode>individual\u003C\u002Fcode>, and no more memory, CPU or xruns;\u003C\u002Fli>\n\u003Cli>with stress, \u003Ccode>plan\u003C\u002Fcode> has no more xruns than \u003Ccode>individual\u003C\u002Fcode>, and fewer over the whole matrix;\u003C\u002Fli>\n\u003Cli>\u003Ccode>plan\u003C\u002Fcode> has no more xruns than \u003Ccode>chain\u003C\u002Fcode> where a chain is possible, and recovers from a killed worker no slower than\n\u003Ccode>chain\u003C\u002Fcode> beyond the measured spread.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>If the numbers lose, grouping stays off and the numbers are published anyway.\u003C\u002Fp>\n\u003Ch2 id=\"failure-modes\">Failure modes\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>\u003Cstrong>A member crashes\u003C\u002Fstrong>: the named culprit is quarantined and moved to a worker of its own, the crash enters the history,\nand the next plan leaves it alone (\u003Ccode>history-crashed\u003C\u002Fcode>). A death that names no culprit splits the members; the plan is made\nagain after ten minutes of steady running.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Xruns in a group\u003C\u002Fstrong>: once the share is over the margin, the next plan leaves the costliest members alone. A plan never\nchanges by itself mid-cycle; it applies at the next declaration, quarantine or \u003Ccode>plan\u003C\u002Fcode> call.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>An unreadable history\u003C\u002Fstrong> is neither read nor saved, and the planner runs on pins and costs alone\n(\u003Ccode>history-unavailable\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>\u003Cstrong>A worker that does not report xruns\u003C\u002Fstrong> leaves the xrun windows at zero, and nothing is marked \u003Ccode>raises-xruns\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>A plan the daemon cannot make\u003C\u002Fstrong> leaves the members as they were and answers the refusal's code, with the reason\n\u003Ccode>plan-refused:&lt;code&gt;\u003C\u002Fcode>.\u003C\u002Fli>\n\u003C\u002Ful>\n",1793954995830]