[{"data":1,"prerenderedAt":30},["ShallowReactive",2],{"note-one-jack-client-for-a-chain-en":3},{"slug":4,"order":5,"title":6,"summary":7,"measured":8,"topic":9,"toc":10,"html":29},"one-jack-client-for-a-chain",130,"\"One JACK client for a chain: what it saves and what it does not\"","Running N plugins in series inside one JACK client, instead of N clients linked in series, is bit-identical and cuts the cycle's end time by 30 to 56 % at the median and the host's CPU by a third or more, with no xrun and no worse tail; the saving is the wake-ups, not the plugins.","A 4-CPU Fedora 44 container on a shared machine, PipeWire 1.6.9 on its dummy driver, 48 and 96 kHz at 64 and 128 frames, October 2026; omx-plugins 0.12.0 and stock LV2 and CLAP plugins.","core",[11,14,17,20,23,26],{"id":12,"title":13},"the-question","The question",{"id":15,"title":16},"how-the-chain-runs","How the chain runs",{"id":18,"title":19},"bit-identity","Bit identity",{"id":21,"title":22},"a-chain-of-lv2-plugins-needs-a-union-world","A chain of LV2 plugins needs a union world",{"id":24,"title":25},"the-bench","The bench",{"id":27,"title":28},"what-was-not-measured","What was not measured","\u003Ch2 id=\"the-question\">The question\u003C\u002Fh2>\n\u003Cp>omx-clap-host gives every plugin its own JACK client, and every client is a node of the graph with its own wake-up per\ncycle. The question was whether N plugins in series in one client are bit-identical with the same N plugins in N clients\nlinked in series, and whether the bench's numbers get better, with nothing getting worse. This is the measurement behind the\nchain of the host library (see the \u003Ca href=\"\u002Fnotes\u002Fchains-plugins-in-series-in-one-worker\">chains note\u003C\u002Fa> for how plugin-hostd uses it).\u003C\u002Fp>\n\u003Ch2 id=\"how-the-chain-runs\">How the chain runs\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>The head is an instance with its own client, ports and process callback. The members open no client and register no port:\ntheir cycles run inside the head's.\u003C\u002Fli>\n\u003Cli>A member is accepted only when its main inputs are as wide as the previous member's outputs and its main outputs as wide as\nthe head's. A plugin with a note input or output is not accepted as a member. A member's auxiliary inputs read silence.\u003C\u002Fli>\n\u003Cli>The head's process callback runs the head, then each member in order, each inside its own cycle gate, handing each output\nbuffer to the next; the last member writes the head's output ports. Between them the outputs are 64-byte aligned blocks the\nhost holds.\u003C\u002Fli>\n\u003Cli>Every member's audio thread is the head's; its block size and transport follow the head's.\u003C\u002Fli>\n\u003Cli>The head's ports report the sum of every member's latency on top of its own, recomputed when any member's latency changes.\u003C\u002Fli>\n\u003Cli>Bypass, parameters, meters, pins, presets and state stay per instance, by the member's own number.\u003C\u002Fli>\n\u003Cli>Removing a member unlinks it inside a gate of the chain's own, between cycles; removing the head frees every member with it.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"bit-identity\">Bit identity\u003C\u002Fh2>\n\u003Cp>One host with the plugins in N clients in series, one with them in a chain, both fed the same deterministic noise and\nrecorded from the same cycles, left and right: every pair compared sample by sample, and the latency each arm's last output\nreports compared. A control pass first runs two N-client arms against each other, so a plugin that is not deterministic\nbetween two instances of itself shows there, not as the chain's difference. 48 kHz, quantum 1024, 96 000 samples per side:\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>plugins\u003C\u002Fth>\n\u003Cth>format\u003C\u002Fth>\n\u003Cth>pins\u003C\u002Fth>\n\u003Cth>control (series against series)\u003C\u002Fth>\n\u003Cth>chain against series\u003C\u002Fth>\n\u003Cth>latency series \u002F chain\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>a test fixture: passthrough, sidechain, passthrough\u003C\u002Ftd>\n\u003Ctd>CLAP\u003C\u002Ftd>\n\u003Ctd>no\u003C\u002Ftd>\n\u003Ctd>identical\u003C\u002Ftd>\n\u003Ctd>identical\u003C\u002Ftd>\n\u003Ctd>192 \u002F 192\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>omx-eq8, omx-comp, omx-deesser, omx-delay\u003C\u002Ftd>\n\u003Ctd>CLAP\u003C\u002Ftd>\n\u003Ctd>no\u003C\u002Ftd>\n\u003Ctd>identical\u003C\u002Ftd>\n\u003Ctd>identical\u003C\u002Ftd>\n\u003Ctd>0 \u002F 0\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>omx comp, delay, deesser, eq8, reverb and an LSP compressor (6)\u003C\u002Ftd>\n\u003Ctd>CLAP\u003C\u002Ftd>\n\u003Ctd>no\u003C\u002Ftd>\n\u003Ctd>identical\u003C\u002Ftd>\n\u003Ctd>identical\u003C\u002Ftd>\n\u003Ctd>0 \u002F 0\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>the 6 and Dragonfly Room (7)\u003C\u002Ftd>\n\u003Ctd>CLAP\u003C\u002Ftd>\n\u003Ctd>no\u003C\u002Ftd>\n\u003Ctd>differs at sample 0, -29.1 dB\u003C\u002Ftd>\n\u003Ctd>differs at sample 0, -31.0 dB\u003C\u002Ftd>\n\u003Ctd>0 \u002F 0\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>omx comp, delay, deesser, eq8, reverb, ZamCompX2, caps CompressX2 (7)\u003C\u002Ftd>\n\u003Ctd>LV2\u003C\u002Ftd>\n\u003Ctd>yes\u003C\u002Ftd>\n\u003Ctd>identical\u003C\u002Ftd>\n\u003Ctd>identical\u003C\u002Ftd>\n\u003Ctd>0 \u002F 0\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>the 7 and caps PlateX2 (8)\u003C\u002Ftd>\n\u003Ctd>LV2\u003C\u002Ftd>\n\u003Ctd>yes\u003C\u002Ftd>\n\u003Ctd>differs at sample 0, -24.5 dB\u003C\u002Ftd>\n\u003Ctd>differs at sample 0, -26.8 dB\u003C\u002Ftd>\n\u003Ctd>0 \u002F 0\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cul>\n\u003Cli>The two reverbs, Dragonfly Room and caps PlateX2, are not deterministic between two instances of themselves (two\none-client-per-plugin arms differ at about -30 dB), so with either in the set no arm can be judged sample by sample: the\nchain's difference is the control's size. Every other plugin of the table is bit-identical through the chain.\u003C\u002Fli>\n\u003Cli>The compressor rounded one ulp apart (-142 dB) between two instances now and then, chain or not; with the blocks 64-byte\naligned the chain was identical.\u003C\u002Fli>\n\u003Cli>The latency the chain reports is the members' sum: three passthroughs of 64 frames report 192.\u003C\u002Fli>\n\u003Cli>Sabotaged, the test goes red: with the members skipped the output differs at sample 0, and with the latency sum dropped the\nchain reports 64 against 192.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"a-chain-of-lv2-plugins-needs-a-union-world\">A chain of LV2 plugins needs a union world\u003C\u002Fh2>\n\u003Cp>plugin-hostd narrows a worker's \u003Ccode>LV2_PATH\u003C\u002Fcode> to the plugin's own bundle, and an LV2 plugin is found on that path when it is\nadded. A chain's members are added in the head's process, with the head's \u003Ccode>LV2_PATH\u003C\u002Fcode>: in a world of the first plugin's bundle\nalone, the second member answers \u003Ccode>resp -101\u003C\u002Fcode> (not found). A chain of LV2 plugins therefore needs its worker's \u003Ccode>LV2_PATH\u003C\u002Fcode> to be\nthe union of its members' bundles, and nothing wider. The pins need nothing new: each member's add checks its own layout, and\nwith pins on, every member of the 7- and 8-plugin LV2 chains refused a wrong pin and took its right one; the pinned chain was\nbit-identical with the pinned series.\u003C\u002Fp>\n\u003Ch2 id=\"the-bench\">The bench\u003C\u002Fh2>\n\u003Cp>The arms interleaved inside each repetition, three repetitions of 30 s, medians; series \u002F chain, change = (chain - series) \u002F\nseries. The container shared its machine (load average about 3 on 4 threads), and that load landed on both arms alike.\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>rate\u003C\u002Fth>\n\u003Cth>quantum\u003C\u002Fth>\n\u003Cth>plugins\u003C\u002Fth>\n\u003Cth>cycle end p50 µs\u003C\u002Fth>\n\u003Cth>change\u003C\u002Fth>\n\u003Cth>cycle end p99 µs\u003C\u002Fth>\n\u003Cth>change\u003C\u002Fth>\n\u003Cth>overruns\u003C\u002Fth>\n\u003Cth>host CPU %\u003C\u002Fth>\n\u003Cth>RSS MiB\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>96000\u003C\u002Ftd>\n\u003Ctd>64\u003C\u002Ftd>\n\u003Ctd>7 CLAP\u003C\u002Ftd>\n\u003Ctd>336.8 \u002F 187.6\u003C\u002Ftd>\n\u003Ctd>-44.3 %\u003C\u002Ftd>\n\u003Ctd>776.0 \u002F 570.5\u003C\u002Ftd>\n\u003Ctd>-26.5 %\u003C\u002Ftd>\n\u003Ctd>7390 \u002F 241\u003C\u002Ftd>\n\u003Ctd>20.8 \u002F 12.4\u003C\u002Ftd>\n\u003Ctd>42.4 \u002F 34.7\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>96000\u003C\u002Ftd>\n\u003Ctd>128\u003C\u002Ftd>\n\u003Ctd>7 CLAP\u003C\u002Ftd>\n\u003Ctd>461.3 \u002F 261.1\u003C\u002Ftd>\n\u003Ctd>-43.4 %\u003C\u002Ftd>\n\u003Ctd>1256.7 \u002F 693.7\u003C\u002Ftd>\n\u003Ctd>-44.8 %\u003C\u002Ftd>\n\u003Ctd>172 \u002F 71\u003C\u002Ftd>\n\u003Ctd>16.6 \u002F 10.0\u003C\u002Ftd>\n\u003Ctd>42.5 \u002F 34.4\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>48000\u003C\u002Ftd>\n\u003Ctd>64\u003C\u002Ftd>\n\u003Ctd>7 CLAP\u003C\u002Ftd>\n\u003Ctd>452.7 \u002F 257.1\u003C\u002Ftd>\n\u003Ctd>-43.2 %\u003C\u002Ftd>\n\u003Ctd>1181.8 \u002F 679.2\u003C\u002Ftd>\n\u003Ctd>-42.5 %\u003C\u002Ftd>\n\u003Ctd>113 \u002F 86\u003C\u002Ftd>\n\u003Ctd>14.6 \u002F 8.6\u003C\u002Ftd>\n\u003Ctd>41.9 \u002F 33.9\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>48000\u003C\u002Ftd>\n\u003Ctd>128\u003C\u002Ftd>\n\u003Ctd>7 CLAP\u003C\u002Ftd>\n\u003Ctd>687.4 \u002F 345.5\u003C\u002Ftd>\n\u003Ctd>-49.7 %\u003C\u002Ftd>\n\u003Ctd>1557.3 \u002F 857.5\u003C\u002Ftd>\n\u003Ctd>-44.9 %\u003C\u002Ftd>\n\u003Ctd>23 \u002F 4\u003C\u002Ftd>\n\u003Ctd>11.7 \u002F 7.8\u003C\u002Ftd>\n\u003Ctd>42.0 \u002F 34.3\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>96000\u003C\u002Ftd>\n\u003Ctd>64\u003C\u002Ftd>\n\u003Ctd>8 LV2\u003C\u002Ftd>\n\u003Ctd>391.2 \u002F 171.1\u003C\u002Ftd>\n\u003Ctd>-56.3 %\u003C\u002Ftd>\n\u003Ctd>799.7 \u002F 545.3\u003C\u002Ftd>\n\u003Ctd>-31.8 %\u003C\u002Ftd>\n\u003Ctd>9222 \u002F 98\u003C\u002Ftd>\n\u003Ctd>22.1 \u002F 10.4\u003C\u002Ftd>\n\u003Ctd>29.4 \u002F 20.2\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>96000\u003C\u002Ftd>\n\u003Ctd>128\u003C\u002Ftd>\n\u003Ctd>8 LV2\u003C\u002Ftd>\n\u003Ctd>491.4 \u002F 223.0\u003C\u002Ftd>\n\u003Ctd>-54.6 %\u003C\u002Ftd>\n\u003Ctd>1204.4 \u002F 649.1\u003C\u002Ftd>\n\u003Ctd>-46.1 %\u003C\u002Ftd>\n\u003Ctd>102 \u002F 18\u003C\u002Ftd>\n\u003Ctd>16.4 \u002F 9.6\u003C\u002Ftd>\n\u003Ctd>29.4 \u002F 20.2\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>48000\u003C\u002Ftd>\n\u003Ctd>64\u003C\u002Ftd>\n\u003Ctd>8 LV2\u003C\u002Ftd>\n\u003Ctd>457.7 \u002F 217.6\u003C\u002Ftd>\n\u003Ctd>-52.5 %\u003C\u002Ftd>\n\u003Ctd>1146.0 \u002F 631.5\u003C\u002Ftd>\n\u003Ctd>-44.9 %\u003C\u002Ftd>\n\u003Ctd>44 \u002F 14\u003C\u002Ftd>\n\u003Ctd>14.1 \u002F 8.0\u003C\u002Ftd>\n\u003Ctd>29.2 \u002F 19.9\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>48000\u003C\u002Ftd>\n\u003Ctd>128\u003C\u002Ftd>\n\u003Ctd>8 LV2\u003C\u002Ftd>\n\u003Ctd>583.3 \u002F 282.5\u003C\u002Ftd>\n\u003Ctd>-51.6 %\u003C\u002Ftd>\n\u003Ctd>1402.0 \u002F 721.6\u003C\u002Ftd>\n\u003Ctd>-48.5 %\u003C\u002Ftd>\n\u003Ctd>8 \u002F 3\u003C\u002Ftd>\n\u003Ctd>8.1 \u002F 5.3\u003C\u002Ftd>\n\u003Ctd>29.3 \u002F 20.1\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>With four plugins (omx-eq8, omx-comp, omx-deesser, omx-delay, CLAP):\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>rate\u003C\u002Fth>\n\u003Cth>quantum\u003C\u002Fth>\n\u003Cth>cycle end p50 µs\u003C\u002Fth>\n\u003Cth>change\u003C\u002Fth>\n\u003Cth>cycle end p99 µs\u003C\u002Fth>\n\u003Cth>change\u003C\u002Fth>\n\u003Cth>overruns\u003C\u002Fth>\n\u003Cth>host CPU %\u003C\u002Fth>\n\u003Cth>RSS MiB\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>96000\u003C\u002Ftd>\n\u003Ctd>64\u003C\u002Ftd>\n\u003Ctd>239.2 \u002F 166.1\u003C\u002Ftd>\n\u003Ctd>-30.6 %\u003C\u002Ftd>\n\u003Ctd>685.6 \u002F 541.5\u003C\u002Ftd>\n\u003Ctd>-21.0 %\u003C\u002Ftd>\n\u003Ctd>1175 \u002F 98\u003C\u002Ftd>\n\u003Ctd>11.8 \u002F 7.9\u003C\u002Ftd>\n\u003Ctd>15.8 \u002F 12.1\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>96000\u003C\u002Ftd>\n\u003Ctd>128\u003C\u002Ftd>\n\u003Ctd>272.9 \u002F 173.4\u003C\u002Ftd>\n\u003Ctd>-36.5 %\u003C\u002Ftd>\n\u003Ctd>852.7 \u002F 599.8\u003C\u002Ftd>\n\u003Ctd>-29.7 %\u003C\u002Ftd>\n\u003Ctd>17 \u002F 9\u003C\u002Ftd>\n\u003Ctd>7.4 \u002F 4.5\u003C\u002Ftd>\n\u003Ctd>15.6 \u002F 12.2\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>48000\u003C\u002Ftd>\n\u003Ctd>64\u003C\u002Ftd>\n\u003Ctd>262.9 \u002F 184.0\u003C\u002Ftd>\n\u003Ctd>-30.0 %\u003C\u002Ftd>\n\u003Ctd>860.0 \u002F 605.0\u003C\u002Ftd>\n\u003Ctd>-29.6 %\u003C\u002Ftd>\n\u003Ctd>11 \u002F 12\u003C\u002Ftd>\n\u003Ctd>7.4 \u002F 3.8\u003C\u002Ftd>\n\u003Ctd>15.6 \u002F 12.1\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>48000\u003C\u002Ftd>\n\u003Ctd>128\u003C\u002Ftd>\n\u003Ctd>444.2 \u002F 273.4\u003C\u002Ftd>\n\u003Ctd>-38.5 %\u003C\u002Ftd>\n\u003Ctd>1019.0 \u002F 678.2\u003C\u002Ftd>\n\u003Ctd>-33.4 %\u003C\u002Ftd>\n\u003Ctd>4 \u002F 5\u003C\u002Ftd>\n\u003Ctd>5.7 \u002F 3.6\u003C\u002Ftd>\n\u003Ctd>15.6 \u002F 12.2\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cul>\n\u003Cli>The cycle's end falls 30 to 56 % at the median and 21 to 49 % at the 99th percentile; repetition by repetition, the\nchain's worst is below the series' best in every row.\u003C\u002Fli>\n\u003Cli>No xrun in any run of either arm. Overruns fall in ten rows of twelve; in two four-plugin rows the chain's median is one\noverrun higher (12 against 11, 5 against 4, out of about 22 500 and 11 250 cycles), within the spread of the repetitions.\u003C\u002Fli>\n\u003Cli>The chain's one node waits for its wake-up as long as the series' first node, or longer, and then runs the whole series\nwithout another wake-up: that is where the saving is.\u003C\u002Fli>\n\u003Cli>The host's CPU falls by a third or more, and its memory by 4 to 9 MiB: N-1 JACK clients fewer, their threads and buffers.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"what-was-not-measured\">What was not measured\u003C\u002Fh2>\n\u003Cp>The container had no \u003Ccode>\u002Fdev\u002Fcpu_dma_latency\u003C\u002Fcode>, so no CPU idle-state hold was in effect (see \u003Ca href=\"\u002Fnotes\u002Fcpu-idle-states-and-plugin-hosting\">CPU idle states and plugin\nhosting\u003C\u002Fa>), and its real-time limit was 0, so PipeWire's threads ran\n\u003Ccode>SCHED_OTHER\u003C\u002Fcode>: the figures are larger and noisier than a real-time graph's, the same for both arms. Whether part of the\nsaving is the idle-state exit stays open until the same bench runs on a machine with the device, at each hold.\u003C\u002Fp>\n",1793954995900]