[{"data":1,"prerenderedAt":24},["ShallowReactive",2],{"project-omx-clap-core-thread-pool":3},{"page":4,"others":20},{"slug":5,"src":6,"title":7,"source":8,"toc":9,"html":19},"thread-pool","docs\u002Fthread-pool.md","The CLAP thread pool","https:\u002F\u002Fgithub.com\u002FFreeMixer\u002Fomx-clap-core\u002Fblob\u002Fd53f42b8689dc42669552b8518feef626ea9c828\u002Fdocs\u002Fthread-pool.md",[10,13,16],{"id":11,"title":12},"two-paths","Two paths",{"id":14,"title":15},"the-gate-per-plugin-by-its-qualification","The gate: per plugin, by its qualification",{"id":17,"title":18},"measured","Measured","\u003Ch1 id=\"the-clap-thread-pool\">The CLAP thread pool\u003C\u002Fh1>\n\u003Cp>\u003Ccode>clap.thread-pool\u003C\u002Fcode> lets a plugin split one \u003Ccode>process()\u003C\u002Fcode> into tasks: inside \u003Ccode>process()\u003C\u002Fcode> it calls the host's\n\u003Ccode>request_exec(num_tasks)\u003C\u002Fcode>, and the host calls the plugin's \u003Ccode>exec(plugin, task_index)\u003C\u002Fcode> once for every index from 0 to\n\u003Ccode>num_tasks - 1\u003C\u002Fcode> before \u003Ccode>request_exec\u003C\u002Fcode> returns. A plugin that splits its voices into tasks then uses as many cores as\nthe host gives it. This page states how libomx-clap-core runs those tasks, when it spreads them across cores, and what\nwas measured (#395).\u003C\u002Fp>\n\u003Cp>\u003Cstrong>What it gains, and for which plugins.\u003C\u002Fstrong> The pool spreads voices only for a plugin that splits them into tasks. Surge\nXT 1.4.0 does not offer \u003Ccode>clap.thread-pool\u003C\u002Fcode> (no \u003Ccode>CLAP_EXT_THREAD_POOL\u003C\u002Fcode> in its extension list, no \u003Ccode>request_exec\u003C\u002Fcode> in its\nsource), and none of the 489 plugin descriptors on the Raspberry Pi 5 of the measurement mentions the extension. For\nevery other plugin the cores are used by placement: one worker per instrument or chain, pinned across the cores\n(plugin-hostd#226, not yet built). That, not the pool, is what the Pi gains from first. The pool is the host behaviour\nthe extension specifies, and the gate by qualification stands for the plugins that use it.\u003C\u002Fp>\n\u003Ch2 id=\"two-paths\">Two paths\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>Serial.\u003C\u002Fstrong> Every task runs on the calling audio thread, one after the other, before \u003Ccode>request_exec\u003C\u002Fcode> returns. The\nextension allows it, it is real-time safe, and it is what an instance gets unless the pool is granted to it.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Pooled.\u003C\u002Fstrong> The process holds one pool of helper threads, shared by every instance granted it:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Size.\u003C\u002Fstrong> N is the number of cores in the worker's CPU affinity mask when the pool is made (\u003Ccode>sched_getaffinity\u003C\u002Fcode>, so\na worker that plugin-hostd placed on cores gets exactly those); the pool has N - 1 helpers (at most\n\u003Ccode>OMX_CLAP_THREAD_POOL_HELPERS_MAX\u003C\u002Fcode>), and the audio thread is the N-th worker. A mask of one core gives no pool.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Placement.\u003C\u002Fstrong> Each helper is pinned to one core of the mask, every core but the mask's first, which is left to the\naudio thread. A helper is named \u003Ccode>omx-pool-&lt;core&gt;\u003C\u002Fcode> (ps, \u003Ccode>\u002Fproc\u002F&lt;pid&gt;\u002Ftask\u002F*\u002Fcomm\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Priority.\u003C\u002Fstrong> A helper runs at the audio thread's scheduling: \u003Ccode>SCHED_FIFO\u003C\u002Fcode> at the audio thread's real-time priority\n(omx-clap-host reads it from JACK, \u003Ccode>jack_client_real_time_priority\u003C\u002Fcode>), or the normal class when the audio thread is not\nreal-time. When the priority cannot be set (no \u003Ccode>CAP_SYS_NICE\u003C\u002Fcode>, an \u003Ccode>RLIMIT_RTPRIO\u003C\u002Fcode> below it) no pool is made and the\ninstance runs serial, with one log line naming why: \u003Ccode>thread pool: serial: SCHED_FIFO &lt;p&gt; refused (&lt;errno&gt;)\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Lifetime.\u003C\u002Fstrong> The pool is made by the first activate of an instance granted it and ended by the last deactivate; its\nthreads are created and joined on the control thread only. Nothing is allocated after the activate.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Parking.\u003C\u002Fstrong> Between requests a helper spins for a few microseconds, then sleeps on a futex. A request wakes the\nsleepers with one \u003Ccode>FUTEX_WAKE\u003C\u002Fcode>, only when some helper is asleep.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Handing out.\u003C\u002Fstrong> The request is published as one 64-bit word: a generation, the task count and the next index. The\nhelpers and the calling audio thread take indices from it with one compare-and-swap each, so a task is taken\nexactly once and no lock is held anywhere. The audio thread takes tasks too: a helper that has not woken yet is never\nwaited for, the audio thread runs the tasks it would have taken.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Completion.\u003C\u002Fstrong> \u003Ccode>request_exec\u003C\u002Fcode> returns when every task has finished. The audio thread spins on the completion count\nfor the length of one block (\u003Ccode>max_block \u002F rate\u003C\u002Fcode>), then sleeps on the completion futex, which the helper that finishes\nthe last task wakes. A wait that runs past the block is a \u003Cstrong>late\u003C\u002Fstrong> event: it is counted in the process's pool\n(\u003Ccode>omx_clap_host_thread_pool\u003C\u002Fcode>) and omx-clap-host adds it to the \u003Ccode>xruns &lt;count&gt;\u003C\u002Fcode> line it writes on its feedback port,\nso plugin-hostd attributes it as it does an xrun. A task a helper has already started is still waited for: returning\nbefore it ends would hand \u003Ccode>process()\u003C\u002Fcode> back to the plugin while its own task still runs, which the extension forbids.\nTasks nobody has started are never waited for.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Exclusion.\u003C\u002Fstrong> One request holds the pool at a time. A second audio thread that asks while another request holds it\nruns its tasks serial. A request made from inside a task, on a helper or on the audio thread, is refused, as before.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>The role.\u003C\u002Fstrong> Inside a task, a helper answers \u003Ccode>clap.thread-check\u003C\u002Fcode>'s \u003Ccode>is_audio_thread\u003C\u002Fcode> true for the instance whose task\nit runs: the helpers are audio threads of that instance for the length of the task.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Limits.\u003C\u002Fstrong> A request of one task, or of more than 65535, runs serial.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"the-gate-per-plugin-by-its-qualification\">The gate: per plugin, by its qualification\u003C\u002Fh2>\n\u003Cp>The pool is never a global switch. A plugin gets helper threads only when its qualification measured it real-time safe\n(\u003Ccode>hosting.rt-safety.*\u003C\u002Fcode> clean, with the interposer watching the helper threads too) and faster with the pool at that\nquantum; plugin-hostd carries that verdict in the plugin's pin as \u003Ccode>pool: cores|off\u003C\u002Fcode>. An unmeasured plugin, or one with\nviolations, runs serial.\u003C\u002Fp>\n\u003Cp>In the library the gate is one call per instance, made while it is inactive:\n\u003Ccode>omx_clap_host_thread_pool_use(in, use, rt_priority)\u003C\u002Fcode>. An instance never told to use the pool runs serial.\u003C\u002Fp>\n\u003Cp>omx-clap-host takes the verdict on its command line, \u003Ccode>--thread-pool=qualified|off|on\u003C\u002Fcode>:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>qualified\u003C\u002Fcode> (the default): an instance gets the pool only when the host was told its qualification grants it. The\nhost has no per-instance verdict input yet, so \u003Ccode>qualified\u003C\u002Fcode> runs every instance serial until plugin-hostd hands the\nverdict over.\u003C\u002Fli>\n\u003Cli>\u003Ccode>on\u003C\u002Fcode>: every instance that implements \u003Ccode>clap.thread-pool\u003C\u002Fcode> gets the pool. plugin-hostd passes it to the worker of a\nplugin whose pin says \u003Ccode>pool: cores\u003C\u002Fcode>; a pinned plugin runs in a worker of its own, so the worker's switch is that\nplugin's verdict.\u003C\u002Fli>\n\u003Cli>\u003Ccode>off\u003C\u002Fcode>: every instance runs serial.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"measured\">Measured\u003C\u002Fh2>\n\u003Cp>Raspberry Pi 5 (4 cores, governor \u003Ccode>powersave\u003C\u002Fcode>, kernel 6.18 PREEMPT), idle (no stress run), 2026-10-29. Surge XT offers\nno \u003Ccode>clap.thread-pool\u003C\u002Fcode>, and no plugin on the device does, so this is \u003Cstrong>the fixture plugin\u003C\u002Fstrong>, \u003Ccode>tests\u002Ffake_pool.clap\u003C\u002Fcode>: 16\ntasks a block, each rendering 8, 24 or 48 sine voices over the block. The bench (\u003Ccode>tests\u002Fthread_pool_bench.c\u003C\u002Fcode>) is the\naudio thread at SCHED_FIFO 70 (jackd's \u003Ccode>-P 70\u003C\u002Fcode>) on core 0, the pool's three helpers on cores 1 to 3 at the same\npriority, each block started at its period for 10 s; the branch was built natively on the Pi with its gcc. Per-block\ncost of \u003Ccode>omx_clap_host_run\u003C\u002Fcode> in microseconds, 48000 Hz:\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>voices\u002Ftask\u003C\u002Fth>\n\u003Cth>frames\u003C\u002Fth>\n\u003Cth>budget\u003C\u002Fth>\n\u003Cth>off mean\u003C\u002Fth>\n\u003Cth>off p99\u003C\u002Fth>\n\u003Cth>on mean\u003C\u002Fth>\n\u003Cth>on p99\u003C\u002Fth>\n\u003Cth>speed-up\u003C\u002Fth>\n\u003Cth>helper busy\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>8\u003C\u002Ftd>\n\u003Ctd>128\u003C\u002Ftd>\n\u003Ctd>2667\u003C\u002Ftd>\n\u003Ctd>263.6\u003C\u002Ftd>\n\u003Ctd>277.1\u003C\u002Ftd>\n\u003Ctd>81.7\u003C\u002Ftd>\n\u003Ctd>86.8\u003C\u002Ftd>\n\u003Ctd>3.2x\u003C\u002Ftd>\n\u003Ctd>2.5% each\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>8\u003C\u002Ftd>\n\u003Ctd>256\u003C\u002Ftd>\n\u003Ctd>5333\u003C\u002Ftd>\n\u003Ctd>527.9\u003C\u002Ftd>\n\u003Ctd>534.8\u003C\u002Ftd>\n\u003Ctd>155.9\u003C\u002Ftd>\n\u003Ctd>164.6\u003C\u002Ftd>\n\u003Ctd>3.4x\u003C\u002Ftd>\n\u003Ctd>2.5% each\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>8\u003C\u002Ftd>\n\u003Ctd>512\u003C\u002Ftd>\n\u003Ctd>10667\u003C\u002Ftd>\n\u003Ctd>1035.3\u003C\u002Ftd>\n\u003Ctd>1050.2\u003C\u002Ftd>\n\u003Ctd>289.5\u003C\u002Ftd>\n\u003Ctd>296.0\u003C\u002Ftd>\n\u003Ctd>3.6x\u003C\u002Ftd>\n\u003Ctd>2.4% each\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>24\u003C\u002Ftd>\n\u003Ctd>128\u003C\u002Ftd>\n\u003Ctd>2667\u003C\u002Ftd>\n\u003Ctd>780.6\u003C\u002Ftd>\n\u003Ctd>793.2\u003C\u002Ftd>\n\u003Ctd>212.9\u003C\u002Ftd>\n\u003Ctd>221.1\u003C\u002Ftd>\n\u003Ctd>3.7x\u003C\u002Ftd>\n\u003Ctd>7.4% each\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>24\u003C\u002Ftd>\n\u003Ctd>256\u003C\u002Ftd>\n\u003Ctd>5333\u003C\u002Ftd>\n\u003Ctd>1583.4\u003C\u002Ftd>\n\u003Ctd>1591.1\u003C\u002Ftd>\n\u003Ctd>418.7\u003C\u002Ftd>\n\u003Ctd>425.0\u003C\u002Ftd>\n\u003Ctd>3.8x\u003C\u002Ftd>\n\u003Ctd>7.4% each\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>24\u003C\u002Ftd>\n\u003Ctd>512\u003C\u002Ftd>\n\u003Ctd>10667\u003C\u002Ftd>\n\u003Ctd>3242.2\u003C\u002Ftd>\n\u003Ctd>3269.9\u003C\u002Ftd>\n\u003Ctd>845.6\u003C\u002Ftd>\n\u003Ctd>858.0\u003C\u002Ftd>\n\u003Ctd>3.8x\u003C\u002Ftd>\n\u003Ctd>7.6% each\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>48\u003C\u002Ftd>\n\u003Ctd>128\u003C\u002Ftd>\n\u003Ctd>2667\u003C\u002Ftd>\n\u003Ctd>1660.9\u003C\u002Ftd>\n\u003Ctd>1683.1\u003C\u002Ftd>\n\u003Ctd>432.3\u003C\u002Ftd>\n\u003Ctd>439.0\u003C\u002Ftd>\n\u003Ctd>3.8x\u003C\u002Ftd>\n\u003Ctd>15.6% each\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>48\u003C\u002Ftd>\n\u003Ctd>256\u003C\u002Ftd>\n\u003Ctd>5333\u003C\u002Ftd>\n\u003Ctd>3335.5\u003C\u002Ftd>\n\u003Ctd>3362.7\u003C\u002Ftd>\n\u003Ctd>857.9\u003C\u002Ftd>\n\u003Ctd>863.2\u003C\u002Ftd>\n\u003Ctd>3.9x\u003C\u002Ftd>\n\u003Ctd>15.6% each\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>48\u003C\u002Ftd>\n\u003Ctd>512\u003C\u002Ftd>\n\u003Ctd>10667\u003C\u002Ftd>\n\u003Ctd>6603.3\u003C\u002Ftd>\n\u003Ctd>6649.5\u003C\u002Ftd>\n\u003Ctd>1686.6\u003C\u002Ftd>\n\u003Ctd>1699.2\u003C\u002Ftd>\n\u003Ctd>3.9x\u003C\u002Ftd>\n\u003Ctd>15.4% each\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>No block went over its budget and no wait was late in any run. &quot;Helper busy&quot; is each helper's time inside tasks over\nthe run's wall time. The 512-frame load of 48 voices, 6.6 ms of the 10.7 ms block serial, takes 1.7 ms on the pool.\nThe raw per-block costs, the summary and the device are in \u003Ccode>docs\u002Fevidence\u002Fthread-pool-pi5-2026-10-29\u002F\u003C\u002Fcode>\n(\u003Ccode>measure.sh\u003C\u002Fcode> reruns it).\u003C\u002Fp>\n\u003Cp>Not measured: a real plugin that requests tasks (none on the device does), the pool under JACK's own thread rather\nthan the bench's, voices until the first xrun, and 96 kHz. The fixture's tasks are equal in size and touch no shared\nstate, which is the pool's best case.\u003C\u002Fp>\n",[21],{"slug":22,"title":23},"","README.md",1793954996573]