diff --git a/notes/MOONSHOT-CPU-PIPEASIO-TELEMETRY.md b/notes/MOONSHOT-CPU-PIPEASIO-TELEMETRY.md new file mode 100644 index 00000000..e0fe3bf7 --- /dev/null +++ b/notes/MOONSHOT-CPU-PIPEASIO-TELEMETRY.md @@ -0,0 +1,68 @@ +# PipeASIO callback timing + +Date: 26 August 2026 + +## Use + +Use the timing report to find where each audio callback spends time. Run normal +CPU comparisons with the default setting. + +Start one controlled timing run with: + +```sh +env PIPEASIO_TELEMETRY=on ableton-live +``` + +Set `PIPEASIO_TELEMETRY=on` to start telemetry. The default path performs zero +telemetry clock reads. + +The driver reports results once per second. It measures these 3 parts: + +- work in PipeASIO before Live receives the buffer +- time inside Live's buffer callback +- PipeASIO work after Live returns the buffer + +Each part reports elapsed time and callback thread CPU time. The report includes +`p50`, `p95`, `p99`, and the largest value. + +The cycle record separates Live's time-info and legacy callback paths. Its +`muted` value counts admitted callbacks silenced after a buffer-size mismatch, +while `dropped` counts timing samples discarded when the fixed queue is full. + +Long elapsed time with little CPU use often means that the thread waited. High +CPU time shows work on the callback thread. + +## Audio safety rules + +The callback writes fixed numeric samples into a fixed-size queue. A full queue +discards samples and counts each discarded sample. + +A separate worker copies, sorts, and summarises the samples once per second. +The audio callback continues during that work. + +The output write returns immediately when a pipe reaches capacity. The driver +discards that report. Slow storage can delay the separate worker while the +callback continues. + +During shutdown, the driver removes the shared timing state before it closes +or frees that state. + +## Measurement limits + +Each measured callback performs 8 clock reads: 4 `CLOCK_MONOTONIC` reads and 4 +`CLOCK_THREAD_CPUTIME_ID` reads. On the review host, the thread CPU clock cost +137.5 ns per read and the monotonic clock cost 12.1 ns per read. This adds about +598 ns to each measured callback. Use default mode for before and after CPU +results. + +Use timing reports with the 30-second benchmark, PipeWire error counts, and +Live's CPU value. Add a listening result when you assess audible crackle. + +## Release checks + +The 2 patches apply after PipeASIO patch 0013. Run the unit, ABI, minimal driver, +ASan, UBSan, and TSan tests with the normal runtime build. + +Run PipeWire tests at 32, 64, 128, and 256 frames. Compare default and enabled +timing runs to measure the added work. Use a slow report destination to check +discarded reports and audio deadlines. diff --git a/patches/SERIES.sha256 b/patches/SERIES.sha256 index abd06809..20269016 100644 --- a/patches/SERIES.sha256 +++ b/patches/SERIES.sha256 @@ -104,3 +104,5 @@ adf7f8446f8b4e60ab7df8bf433f50fcfd65ddf123c731027e513f4057ad2207 pipeasio/0010- bea289bb6f78f4b217813c4457c54bf2e4224fb74bb0435ac17bbb50a1db5a53 pipeasio/0011-controlpanel-dialog-off-the-host-gui-thread.patch 8542e3eb5f4e77559783b85bc741c1c21e99027e2e3989a1456909819385227e pipeasio/0012-recover-selected-routes-after-hotplug.patch ec509d3559896a4c5f877ef4a8ba562bedbbb69e44891d18d475892a65d8a704 pipeasio/0013-avoid-redundant-output-fallback-publish.patch +9979b4946153336604951fec83acfe3c264af0d5fe65d5c3da71757e31ab366d pipeasio/0014-callback-phase-telemetry.patch +54833329e71f9b15b15fe0c854464126a686ce64404b1c8bc070a8525a42065d pipeasio/0015-nonblocking-telemetry-reporting.patch diff --git a/patches/pipeasio/0014-callback-phase-telemetry.patch b/patches/pipeasio/0014-callback-phase-telemetry.patch new file mode 100644 index 00000000..be7157ce --- /dev/null +++ b/patches/pipeasio/0014-callback-phase-telemetry.patch @@ -0,0 +1,728 @@ +From f3744749bb6dbe4ac3c07ad52fe5671ed5076cab Mon Sep 17 00:00:00 2001 +From: Telemetry Prototype +Date: Mon, 24 Aug 2026 01:24:35 +0200 +Subject: [PATCH 1/2] asio: add opt-in callback phase telemetry + +Live's deadline meter and Linux process CPU cannot identify where a callback +period is spent. Add an environment-only diagnostic that records both +monotonic wall time and callback-thread CPU time around the pre-host, +host-callback, and post-host phases. + +PIPEASIO_TELEMETRY=on is deliberately off by default because the clock reads +add measurement overhead. The audio callback performs no allocation, logging, +formatting, signalling, locks, or blocking synchronization: it publishes fixed +samples to a bounded lock-free SPSC ring, dropping rather than waiting when +full. The existing lifecycle worker sorts and emits p50/p95/p99/max aggregates +once per second. WoW64 rejects the option because its PipeWire and host +callbacks run on different threads. + +Verification: the normal driver build and all 12 non-integration tests pass. +The new ring/percentile/concurrency test passes ASan, UBSan, and TSan. The +WoW64 ABI test passes; a full 32-bit build was unavailable because the cross +compiler is not installed. A live probe was unavailable because this test +environment has no PipeWire daemon. +--- + README.md | 24 +++ + include/pipeasio_telemetry.h | 106 +++++++++++++ + src/asio.c | 282 ++++++++++++++++++++++++++++++++++- + tests/asio_probe/run.sh | 3 +- + tests/asio_probe/run32.sh | 3 +- + tests/unit/CMakeLists.txt | 4 + + tests/unit/test_telemetry.c | 120 +++++++++++++++ + 7 files changed, 535 insertions(+), 7 deletions(-) + create mode 100644 include/pipeasio_telemetry.h + create mode 100644 tests/unit/test_telemetry.c + +diff --git a/README.md b/README.md +index c20c4df..1373bf3 100644 +--- a/README.md ++++ b/README.md +@@ -548,6 +548,30 @@ takes effect the next time the host starts the driver. + + Env: `PIPEASIO_RT_PRIORITY` (`off`/`on`). The environment overrides the file. + ++### Callback timing diagnostic ++ ++`PIPEASIO_TELEMETRY=on` records callback timing for a 64-bit host. The ++environment controls the setting for one launch. Each accepted audio cycle ++with matching buffer sizes records wall time and callback CPU time in 3 ++phases: ++ ++1. `entry_to_live`: time from callback entry to Live's audio call. ++2. `live_callback`: time that Live uses for its audio callback. ++3. `live_to_outputs`: time from Live's return until PipeASIO queues all output. ++ ++The audio callback stores samples in a fixed area with 8192 entries. ++A full area drops the next sample, and audio continues. The lifecycle worker ++reports once per second. It sorts at most 8192 samples. It writes the 50th, ++95th and 99th percentiles as p50, p95 and p99. It writes the largest value as ++maximum, plus the dropped sample count, to stderr. Clock reads and fixed sample ++writes add CPU work. Use telemetry during controlled measurement ++runs. ++ ++PipeASIO supports this report for 64-bit hosts. For a 32-bit WoW64 host, ++PipeASIO keeps telemetry off. PipeWire and Live callbacks use different ++threads there. A single CPU phase report would combine different thread ++clocks. ++ + ## Performance + + A few knobs affect xrun-free, low-latency operation: +diff --git a/include/pipeasio_telemetry.h b/include/pipeasio_telemetry.h +new file mode 100644 +index 0000000..7525921 +--- /dev/null ++++ b/include/pipeasio_telemetry.h +@@ -0,0 +1,106 @@ ++/* SPDX-License-Identifier: GPL-3.0-or-later */ ++#pragma once ++ ++/* ++ * Full storage drops the next sample so audio continues. ++ * ++ * The PipeWire callback adds samples to fixed storage. ++ * The lifecycle worker reads samples, sorts them and writes the report. ++ * Each worker updates its own position and keeps one-way ownership. ++ * The storage has 8192 entries. ++ */ ++ ++#include ++#include ++#include ++#include ++ ++#define PIPEASIO_TELEMETRY_PHASES 3u ++#define PIPEASIO_TELEMETRY_CAPACITY 8192u ++ ++enum pipeasio_telemetry_phase ++{ ++ PIPEASIO_TELEMETRY_PRE_LIVE, ++ PIPEASIO_TELEMETRY_LIVE, ++ PIPEASIO_TELEMETRY_POST_LIVE ++}; ++ ++#define PIPEASIO_TELEMETRY_TIME_INFO 0x1u ++ ++typedef struct pipeasio_telemetry_sample ++{ ++ uint64_t wall_ns[PIPEASIO_TELEMETRY_PHASES]; ++ uint64_t cpu_ns[PIPEASIO_TELEMETRY_PHASES]; ++ uint32_t flags; ++} pipeasio_telemetry_sample; ++ ++typedef struct pipeasio_telemetry_ring ++{ ++ _Atomic uint64_t write_seq; ++ _Atomic uint64_t read_seq; ++ _Atomic uint64_t dropped; ++ pipeasio_telemetry_sample slots[PIPEASIO_TELEMETRY_CAPACITY]; ++} pipeasio_telemetry_ring; ++ ++static inline void ++pipeasio_telemetry_ring_init(pipeasio_telemetry_ring *ring) ++{ ++ atomic_init(&ring->write_seq, 0); ++ atomic_init(&ring->read_seq, 0); ++ atomic_init(&ring->dropped, 0); ++} ++ ++/* The audio thread adds a sample or counts one drop when storage is full. */ ++static inline bool ++pipeasio_telemetry_ring_publish(pipeasio_telemetry_ring *ring, ++ const pipeasio_telemetry_sample *sample) ++{ ++ uint64_t write = atomic_load_explicit(&ring->write_seq, memory_order_relaxed); ++ uint64_t read = atomic_load_explicit(&ring->read_seq, memory_order_acquire); ++ if (write - read >= PIPEASIO_TELEMETRY_CAPACITY) ++ { ++ atomic_fetch_add_explicit(&ring->dropped, 1, memory_order_relaxed); ++ return false; ++ } ++ ring->slots[write % PIPEASIO_TELEMETRY_CAPACITY] = *sample; ++ atomic_store_explicit(&ring->write_seq, write + 1, memory_order_release); ++ return true; ++} ++ ++/* The report worker owns each listed sample until it marks the sample read. */ ++static inline size_t ++pipeasio_telemetry_ring_snapshot(const pipeasio_telemetry_ring *ring, uint64_t *first) ++{ ++ uint64_t read = atomic_load_explicit(&ring->read_seq, memory_order_relaxed); ++ uint64_t write = atomic_load_explicit(&ring->write_seq, memory_order_acquire); ++ uint64_t count = write - read; ++ if (count > PIPEASIO_TELEMETRY_CAPACITY) ++ count = PIPEASIO_TELEMETRY_CAPACITY; ++ if (first) ++ *first = read; ++ return (size_t)count; ++} ++ ++static inline const pipeasio_telemetry_sample * ++pipeasio_telemetry_ring_at(const pipeasio_telemetry_ring *ring, uint64_t sequence) ++{ ++ return &ring->slots[sequence % PIPEASIO_TELEMETRY_CAPACITY]; ++} ++ ++static inline void ++pipeasio_telemetry_ring_consume(pipeasio_telemetry_ring *ring, uint64_t first, size_t count) ++{ ++ atomic_store_explicit(&ring->read_seq, first + count, memory_order_release); ++} ++ ++/* A sorted sample set with at least one value uses the nearest-rank position. */ ++static inline size_t ++pipeasio_telemetry_percentile_index(size_t count, unsigned percentile) ++{ ++ if (!count) ++ return 0; ++ if (percentile > 100) ++ percentile = 100; ++ size_t rank = (count * (size_t)percentile + 99u) / 100u; ++ return rank ? rank - 1u : 0u; ++} +diff --git a/src/asio.c b/src/asio.c +index 7612767..fc38757 100644 +--- a/src/asio.c ++++ b/src/asio.c +@@ -71,6 +71,9 @@ + #include "pipeasio_rate_state.h" + #include "pipeasio_rt.h" + #include "pipeasio_admission_gate.h" ++#ifndef PIPEASIO_WOW64_PE ++#include "pipeasio_telemetry.h" ++#endif + #ifdef PIPEASIO_WOW64_PE + #include "pipeasio_wow64_pe.h" + #endif +@@ -273,6 +276,27 @@ typedef struct IOChannel + bool active; + } IOChannel; + ++#ifndef PIPEASIO_WOW64_PE ++typedef struct pipeasio_telemetry_state ++{ ++ pipeasio_telemetry_ring ring; ++} pipeasio_telemetry_state; ++ ++typedef struct pipeasio_telemetry_cycle ++{ ++ pipeasio_telemetry_state *state; ++ struct timespec entry_wall; ++ struct timespec entry_cpu; ++ struct timespec pre_wall; ++ struct timespec pre_cpu; ++ struct timespec post_wall; ++ struct timespec post_cpu; ++ bool valid; ++ bool live_returned; ++ uint32_t flags; ++} pipeasio_telemetry_cycle; ++#endif ++ + typedef struct IPipeASIOImpl + { + /* COM and lifetime state */ +@@ -287,9 +311,15 @@ typedef struct IPipeASIOImpl + HANDLE worker; + DWORD worker_tid; + HMODULE module_pin; +- _Atomic uint32_t gate_owner_seq; +- _Atomic uint32_t lifecycle_waiters; +- _Atomic uint32_t stop_generation; ++#ifndef PIPEASIO_WOW64_PE ++ _Atomic(pipeasio_telemetry_state *) telemetry; ++ /* The PipeWire callback sets this pointer while Live processes one block. ++ * The report worker reads completed samples from fixed storage. */ ++ pipeasio_telemetry_cycle *telemetry_cycle; ++#endif ++ _Atomic uint32_t gate_owner_seq; ++ _Atomic uint32_t lifecycle_waiters; ++ _Atomic uint32_t stop_generation; + + /* The app's main window handle on windows, 0 on OS/X */ + HWND sys_ref; +@@ -697,6 +727,143 @@ gate_leave_test_barrier(void) + } + #endif + ++#ifndef PIPEASIO_WOW64_PE ++/* Monotonic time records the full deadline. Thread CPU time records work on ++ * the callback thread. Wall time also includes sleep in Live. These clock ++ * reads occur while PIPEASIO_TELEMETRY is on. */ ++static bool ++telemetry_now(struct timespec *wall, struct timespec *cpu) ++{ ++ return clock_gettime(CLOCK_MONOTONIC, wall) == 0 ++ && clock_gettime(CLOCK_THREAD_CPUTIME_ID, cpu) == 0; ++} ++ ++static uint64_t ++telemetry_delta_ns(const struct timespec *start, const struct timespec *end) ++{ ++ if (end->tv_sec < start->tv_sec ++ || (end->tv_sec == start->tv_sec && end->tv_nsec < start->tv_nsec)) ++ return 0; ++ return (uint64_t)(end->tv_sec - start->tv_sec) * 1000000000ull ++ + (uint64_t)(end->tv_nsec - start->tv_nsec); ++} ++ ++static void ++telemetry_cycle_begin(pipeasio_telemetry_state *state, pipeasio_telemetry_cycle *cycle) ++{ ++ memset(cycle, 0, sizeof(*cycle)); ++ if (!state) ++ return; ++ cycle->state = state; ++ cycle->valid = telemetry_now(&cycle->entry_wall, &cycle->entry_cpu); ++} ++ ++static void ++telemetry_cycle_before_live(pipeasio_telemetry_cycle *cycle, bool time_info) ++{ ++ if (!cycle || !cycle->valid) ++ return; ++ cycle->valid = telemetry_now(&cycle->pre_wall, &cycle->pre_cpu); ++ if (time_info) ++ cycle->flags |= PIPEASIO_TELEMETRY_TIME_INFO; ++} ++ ++static void ++telemetry_cycle_after_live(pipeasio_telemetry_cycle *cycle) ++{ ++ if (!cycle || !cycle->valid) ++ return; ++ cycle->valid = telemetry_now(&cycle->post_wall, &cycle->post_cpu); ++ cycle->live_returned = cycle->valid; ++} ++ ++/* The audio thread completes each sample with fixed calculations and one ++ * write to the report storage. */ ++static void ++telemetry_cycle_finish(pipeasio_telemetry_cycle *cycle) ++{ ++ struct timespec output_wall; ++ struct timespec output_cpu; ++ pipeasio_telemetry_sample sample = { 0 }; ++ if (!cycle || !cycle->state || !cycle->valid || !cycle->live_returned ++ || !telemetry_now(&output_wall, &output_cpu)) ++ return; ++ sample.wall_ns[PIPEASIO_TELEMETRY_PRE_LIVE] ++ = telemetry_delta_ns(&cycle->entry_wall, &cycle->pre_wall); ++ sample.cpu_ns[PIPEASIO_TELEMETRY_PRE_LIVE] ++ = telemetry_delta_ns(&cycle->entry_cpu, &cycle->pre_cpu); ++ sample.wall_ns[PIPEASIO_TELEMETRY_LIVE] ++ = telemetry_delta_ns(&cycle->pre_wall, &cycle->post_wall); ++ sample.cpu_ns[PIPEASIO_TELEMETRY_LIVE] = telemetry_delta_ns(&cycle->pre_cpu, &cycle->post_cpu); ++ sample.wall_ns[PIPEASIO_TELEMETRY_POST_LIVE] ++ = telemetry_delta_ns(&cycle->post_wall, &output_wall); ++ sample.cpu_ns[PIPEASIO_TELEMETRY_POST_LIVE] = telemetry_delta_ns(&cycle->post_cpu, &output_cpu); ++ sample.flags = cycle->flags; ++ pipeasio_telemetry_ring_publish(&cycle->state->ring, &sample); ++} ++ ++static int ++telemetry_compare_u64(const void *left, const void *right) ++{ ++ uint64_t a = *(const uint64_t *)left; ++ uint64_t b = *(const uint64_t *)right; ++ return (a > b) - (a < b); ++} ++ ++/* The report worker owns the current batch while it sorts values. ++ * It writes six summaries. New samples use free storage or increase the ++ * dropped count. */ ++static void ++telemetry_emit(IPipeASIOImpl *This) ++{ ++ static const char *const phase_name[PIPEASIO_TELEMETRY_PHASES] ++ = { "entry_to_live", "live_callback", "live_to_outputs" }; ++ pipeasio_telemetry_state *state = atomic_load_explicit(&This->telemetry, memory_order_acquire); ++ uint64_t first; ++ size_t count; ++ uint64_t values[PIPEASIO_TELEMETRY_CAPACITY]; ++ uint64_t dropped; ++ size_t time_info = 0; ++ if (!state) ++ return; ++ count = pipeasio_telemetry_ring_snapshot(&state->ring, &first); ++ dropped = atomic_exchange_explicit(&state->ring.dropped, 0, memory_order_relaxed); ++ if (!count) ++ { ++ if (dropped) ++ PIPEASIO_LOG("[pipeasio telemetry] ", "cycles=0 dropped=%llu\n", ++ (unsigned long long)dropped); ++ return; ++ } ++ for (size_t i = 0; i < count; ++i) ++ if (pipeasio_telemetry_ring_at(&state->ring, first + i)->flags ++ & PIPEASIO_TELEMETRY_TIME_INFO) ++ ++time_info; ++ PIPEASIO_LOG("[pipeasio telemetry] ", "cycles=%llu time_info=%llu legacy=%llu dropped=%llu\n", ++ (unsigned long long)count, (unsigned long long)time_info, ++ (unsigned long long)(count - time_info), (unsigned long long)dropped); ++ for (unsigned clock_kind = 0; clock_kind < 2; ++clock_kind) ++ for (unsigned phase = 0; phase < PIPEASIO_TELEMETRY_PHASES; ++phase) ++ { ++ for (size_t i = 0; i < count; ++i) ++ { ++ const pipeasio_telemetry_sample *sample ++ = pipeasio_telemetry_ring_at(&state->ring, first + i); ++ values[i] = clock_kind ? sample->cpu_ns[phase] : sample->wall_ns[phase]; ++ } ++ qsort(values, count, sizeof(values[0]), telemetry_compare_u64); ++ PIPEASIO_LOG("[pipeasio telemetry] ", ++ "%s_ns phase=%s p50=%llu p95=%llu p99=%llu max=%llu\n", ++ clock_kind ? "thread_cpu" : "monotonic_wall", phase_name[phase], ++ (unsigned long long)values[pipeasio_telemetry_percentile_index(count, 50)], ++ (unsigned long long)values[pipeasio_telemetry_percentile_index(count, 95)], ++ (unsigned long long)values[pipeasio_telemetry_percentile_index(count, 99)], ++ (unsigned long long)values[count - 1]); ++ } ++ pipeasio_telemetry_ring_consume(&state->ring, first, count); ++} ++#endif ++ + bool + pipeasio_host_call_begin(void *owner, pipeasio_host_call_kind kind, pipeasio_host_call_token *token) + { +@@ -804,11 +971,27 @@ pipeasio_host_call_process(pipeasio_host_call_token *token, int32_t buffer_index + This->host_time.sampleRate + = (double)atomic_load_explicit(&This->host_sample_rate, memory_order_acquire); + This->host_time.flags = 0x7; ++#ifndef PIPEASIO_WOW64_PE ++ if (This->telemetry_cycle) ++ telemetry_cycle_before_live(This->telemetry_cycle, true); ++#endif + cb->swapBuffersWithTimeInfo(&This->host_time, buffer_index, 1); ++#ifndef PIPEASIO_WOW64_PE ++ if (This->telemetry_cycle) ++ telemetry_cycle_after_live(This->telemetry_cycle); ++#endif + } + else + { ++#ifndef PIPEASIO_WOW64_PE ++ if (This->telemetry_cycle) ++ telemetry_cycle_before_live(This->telemetry_cycle, false); ++#endif + cb->swapBuffers(buffer_index, 1); ++#ifndef PIPEASIO_WOW64_PE ++ if (This->telemetry_cycle) ++ telemetry_cycle_after_live(This->telemetry_cycle); ++#endif + } + atomic_store_explicit(&This->host_num_samples, samples + add_samples, memory_order_relaxed); + } +@@ -1329,6 +1512,13 @@ destroy_driver_resources(IPipeASIOImpl *This) + This->input_channel = NULL; + This->output_channel = NULL; + } ++#ifndef PIPEASIO_WOW64_PE ++ telemetry_emit(This); ++ pipeasio_telemetry_state *telemetry ++ = atomic_exchange_explicit(&This->telemetry, NULL, memory_order_acq_rel); ++ if (telemetry) ++ HeapFree(GetProcessHeap(), 0, telemetry); ++#endif + } + + static DWORD WINAPI +@@ -1338,7 +1528,20 @@ lifecycle_worker(void *arg) + + for (;;) + { ++#ifndef PIPEASIO_WOW64_PE ++ DWORD wait_result = WaitForSingleObject( ++ This->work_event, ++ atomic_load_explicit(&This->telemetry, memory_order_acquire) ? 1000 : INFINITE); ++ if (wait_result == WAIT_TIMEOUT) ++ { ++ telemetry_emit(This); ++ continue; ++ } ++ if (wait_result != WAIT_OBJECT_0) ++ continue; ++#else + WaitForSingleObject(This->work_event, INFINITE); ++#endif + bool completed = false; + if (atomic_load_explicit(&This->host_driver_state, memory_order_acquire) == Stopping) + while (!drain_gate(This, &This->host_gate, This->host_idle, 0)) +@@ -3105,7 +3308,14 @@ OutputReady(LPPIPEASIO iface) + static inline int + process_callback(audio_nframes_t nframes, void *arg) + { +- IPipeASIOImpl *This = (IPipeASIOImpl *)arg; ++ IPipeASIOImpl *This = (IPipeASIOImpl *)arg; ++#ifndef PIPEASIO_WOW64_PE ++ pipeasio_telemetry_cycle cycle; ++ pipeasio_telemetry_state *telemetry ++ = atomic_load_explicit(&This->telemetry, memory_order_acquire); ++ if (telemetry) ++ telemetry_cycle_begin(telemetry, &cycle); ++#endif + pipeasio_host_call_token token; + bool admitted = pipeasio_host_call_begin(This, PIPEASIO_HOST_PROCESS, &token); + int half = 0; +@@ -3148,8 +3358,13 @@ process_callback(audio_nframes_t nframes, void *arg) + else + memset(destination, 0, sizeof(*destination) * nframes); + } +- ++#ifndef PIPEASIO_WOW64_PE ++ This->telemetry_cycle = telemetry && cycle.valid ? &cycle : NULL; ++#endif + pipeasio_host_call_process(&token, half, nframes, audio_get_time_nsec(This->audio_client)); ++#ifndef PIPEASIO_WOW64_PE ++ This->telemetry_cycle = NULL; ++#endif + } + + /* In fail-silent mismatch mode the backend owns the graph buffer and +@@ -3164,6 +3379,12 @@ process_callback(audio_nframes_t nframes, void *arg) + audio_port_publish_output(This->output_channel[i].port, source, nframes, admitted, + This->output_channel[i].active); + } ++#ifndef PIPEASIO_WOW64_PE ++ /* During a buffer mismatch, the graph owns and queues the silent block. ++ * Telemetry resumes output timing when the callback owns the output. */ ++ if (admitted && !muted && telemetry) ++ telemetry_cycle_finish(&cycle); ++#endif + if (admitted) + This->host_buffer_index = half ? 0 : 1; + pipeasio_host_call_end(&token); +@@ -3352,6 +3573,54 @@ configure_driver(IPipeASIOImpl *This) + if (read_environment("PIPEASIO_CLIENT_NAME", name_environment, sizeof(name_environment))) + lstrcpynA(This->client_name, name_environment, sizeof(This->client_name)); + ++ if (read_environment("PIPEASIO_TELEMETRY", environment_variable, sizeof(environment_variable))) ++ { ++ int enabled = pipeasio_env_bool(environment_variable); ++#ifdef PIPEASIO_WOW64_PE ++ if (enabled == 1) ++ WARN("PIPEASIO_TELEMETRY is unavailable for 32-bit WoW64 hosts: " ++ "the PipeWire callback and Live callback use different threads, so " ++ "CLOCK_THREAD_CPUTIME_ID deltas would be invalid\n"); ++ else if (enabled < 0) ++ WARN("PIPEASIO_TELEMETRY \"%s\" is not a recognised boolean; keeping off\n", ++ environment_variable); ++#else ++ if (enabled == 1 && !atomic_load_explicit(&This->telemetry, memory_order_acquire)) ++ { ++ pipeasio_telemetry_state *state = HeapAlloc(GetProcessHeap(), 0, sizeof(*state)); ++ if (!state) ++ WARN("PIPEASIO_TELEMETRY requested but its bounded ring could not be allocated; " ++ "keeping telemetry off\n"); ++ else ++ { ++ pipeasio_telemetry_ring_init(&state->ring); ++ if (!atomic_is_lock_free(&This->telemetry) ++ || !atomic_is_lock_free(&state->ring.write_seq) ++ || !atomic_is_lock_free(&state->ring.read_seq) ++ || !atomic_is_lock_free(&state->ring.dropped)) ++ { ++ WARN("PIPEASIO_TELEMETRY requires lock-free pointer and 64-bit atomics; " ++ "keeping off\n"); ++ HeapFree(GetProcessHeap(), 0, state); ++ } ++ else ++ { ++ atomic_store_explicit(&This->telemetry, state, memory_order_release); ++ PIPEASIO_LOG("[pipeasio telemetry] ", ++ "enabled for 64-bit host; capacity=%u, interval_ms=1000\n", ++ PIPEASIO_TELEMETRY_CAPACITY); ++ /* Wake the lifecycle worker when telemetry starts. ++ * Its next wait uses the one-second report interval. */ ++ SetEvent(This->work_event); ++ } ++ } ++ } ++ else if (enabled < 0) ++ WARN("PIPEASIO_TELEMETRY \"%s\" is not a recognised boolean; keeping off\n", ++ environment_variable); ++#endif ++ } ++ + return; + } + +@@ -3390,6 +3659,9 @@ PipeASIOCreateInstance(REFIID riid, LPVOID *ppobj) + atomic_init(&pobj->lifecycle_waiters, 0); + atomic_init(&pobj->stop_generation, 0); + atomic_init(&pobj->gate_owner_seq, 1); ++#ifndef PIPEASIO_WOW64_PE ++ atomic_init(&pobj->telemetry, NULL); ++#endif + InitializeSRWLock(&pobj->lifecycle_lock); + pobj->host_version = 92; + pipeasio_gate_init(&pobj->method_gate, true); +diff --git a/tests/asio_probe/run.sh b/tests/asio_probe/run.sh +index 1770c17..3627e2c 100755 +--- a/tests/asio_probe/run.sh ++++ b/tests/asio_probe/run.sh +@@ -109,7 +109,8 @@ for _cfg_var in PIPEASIO_NUMBER_INPUTS PIPEASIO_NUMBER_OUTPUTS \ + PIPEASIO_OUTPUT_DEVICE PIPEASIO_INPUT_DEVICE \ + PIPEASIO_CLIENT_NAME PIPEASIO_RT_PRIORITY \ + PIPEASIO_CONNECT_TO_HARDWARE \ +- PIPEASIO_ALLOW_QUANTUM_MISMATCH PIPEASIO_REALTIME; do ++ PIPEASIO_ALLOW_QUANTUM_MISMATCH PIPEASIO_REALTIME \ ++ PIPEASIO_TELEMETRY; do + case " ${PROBE_ENV_KEEP:-} " in + *" $_cfg_var "*) ;; + *) unset "$_cfg_var" ;; +diff --git a/tests/asio_probe/run32.sh b/tests/asio_probe/run32.sh +index 8320ba0..d917fa6 100755 +--- a/tests/asio_probe/run32.sh ++++ b/tests/asio_probe/run32.sh +@@ -94,7 +94,8 @@ for _cfg_var in PIPEASIO_NUMBER_INPUTS PIPEASIO_NUMBER_OUTPUTS \ + PIPEASIO_OUTPUT_DEVICE PIPEASIO_INPUT_DEVICE \ + PIPEASIO_CLIENT_NAME PIPEASIO_RT_PRIORITY \ + PIPEASIO_CONNECT_TO_HARDWARE \ +- PIPEASIO_ALLOW_QUANTUM_MISMATCH PIPEASIO_REALTIME; do ++ PIPEASIO_ALLOW_QUANTUM_MISMATCH PIPEASIO_REALTIME \ ++ PIPEASIO_TELEMETRY; do + case " ${PROBE_ENV_KEEP:-} " in + *" $_cfg_var "*) ;; + *) unset "$_cfg_var" ;; +diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt +index 07cc360..04a2bfd 100644 +--- a/tests/unit/CMakeLists.txt ++++ b/tests/unit/CMakeLists.txt +@@ -38,3 +38,7 @@ pipeasio_add_unit_test(test_handle_table test_handle_table.c + ${CMAKE_SOURCE_DIR}/src/wow64/handle_table.c) + target_link_libraries(test_handle_table PRIVATE Threads::Threads) + set_tests_properties(test_handle_table PROPERTIES TIMEOUT 10) ++ ++pipeasio_add_unit_test(test_telemetry test_telemetry.c) ++target_link_libraries(test_telemetry PRIVATE Threads::Threads) ++set_tests_properties(test_telemetry PROPERTIES TIMEOUT 10) +diff --git a/tests/unit/test_telemetry.c b/tests/unit/test_telemetry.c +new file mode 100644 +index 0000000..0d70ef0 +--- /dev/null ++++ b/tests/unit/test_telemetry.c +@@ -0,0 +1,120 @@ ++/* SPDX-License-Identifier: GPL-3.0-or-later */ ++#include "pipeasio_telemetry.h" ++#include "test_helpers.h" ++ ++#include ++#include ++ ++#define CONCURRENT_SAMPLES 100000u ++ ++typedef struct concurrent_test ++{ ++ pipeasio_telemetry_ring ring; ++ atomic_bool done; ++ uint64_t consumed; ++ bool ordered; ++} concurrent_test; ++ ++static pipeasio_telemetry_sample ++sample_with_value(uint64_t value) ++{ ++ pipeasio_telemetry_sample sample = { 0 }; ++ sample.wall_ns[0] = value; ++ return sample; ++} ++ ++static void * ++producer(void *arg) ++{ ++ concurrent_test *test = arg; ++ for (uint64_t i = 1; i <= CONCURRENT_SAMPLES; ++i) ++ { ++ pipeasio_telemetry_sample sample = sample_with_value(i); ++ pipeasio_telemetry_ring_publish(&test->ring, &sample); ++ } ++ atomic_store_explicit(&test->done, true, memory_order_release); ++ return NULL; ++} ++ ++static void * ++consumer(void *arg) ++{ ++ concurrent_test *test = arg; ++ uint64_t last = 0; ++ test->ordered = true; ++ for (;;) ++ { ++ uint64_t first; ++ size_t count = pipeasio_telemetry_ring_snapshot(&test->ring, &first); ++ for (size_t i = 0; i < count; ++i) ++ { ++ uint64_t value = pipeasio_telemetry_ring_at(&test->ring, first + i)->wall_ns[0]; ++ if (value <= last) ++ test->ordered = false; ++ last = value; ++ } ++ if (count) ++ { ++ test->consumed += count; ++ pipeasio_telemetry_ring_consume(&test->ring, first, count); ++ } ++ else if (atomic_load_explicit(&test->done, memory_order_acquire)) ++ break; ++ else ++ sched_yield(); ++ } ++ return NULL; ++} ++ ++int ++main(void) ++{ ++ TEST_GROUP("nearest-rank percentile indices") ++ { ++ EXPECT_EQ(pipeasio_telemetry_percentile_index(1, 50), 0); ++ EXPECT_EQ(pipeasio_telemetry_percentile_index(100, 50), 49); ++ EXPECT_EQ(pipeasio_telemetry_percentile_index(100, 95), 94); ++ EXPECT_EQ(pipeasio_telemetry_percentile_index(100, 99), 98); ++ EXPECT_EQ(pipeasio_telemetry_percentile_index(3, 100), 2); ++ } ++ ++ TEST_GROUP("bounded FIFO and drop accounting") ++ { ++ pipeasio_telemetry_ring ring; ++ pipeasio_telemetry_ring_init(&ring); ++ for (uint64_t i = 0; i < PIPEASIO_TELEMETRY_CAPACITY; ++i) ++ { ++ pipeasio_telemetry_sample sample = sample_with_value(i + 1); ++ EXPECT_TRUE(pipeasio_telemetry_ring_publish(&ring, &sample)); ++ } ++ pipeasio_telemetry_sample extra = sample_with_value(UINT64_MAX); ++ EXPECT_TRUE(!pipeasio_telemetry_ring_publish(&ring, &extra)); ++ EXPECT_EQ(atomic_load_explicit(&ring.dropped, memory_order_relaxed), 1); ++ uint64_t first; ++ size_t count = pipeasio_telemetry_ring_snapshot(&ring, &first); ++ EXPECT_EQ(count, PIPEASIO_TELEMETRY_CAPACITY); ++ EXPECT_EQ(pipeasio_telemetry_ring_at(&ring, first)->wall_ns[0], 1); ++ EXPECT_EQ(pipeasio_telemetry_ring_at(&ring, first + count - 1)->wall_ns[0], ++ PIPEASIO_TELEMETRY_CAPACITY); ++ pipeasio_telemetry_ring_consume(&ring, first, count); ++ EXPECT_EQ(pipeasio_telemetry_ring_snapshot(&ring, NULL), 0); ++ } ++ ++ TEST_GROUP("concurrent SPSC publication") ++ { ++ concurrent_test test = { 0 }; ++ pthread_t producer_thread; ++ pthread_t consumer_thread; ++ pipeasio_telemetry_ring_init(&test.ring); ++ atomic_init(&test.done, false); ++ EXPECT_EQ(pthread_create(&consumer_thread, NULL, consumer, &test), 0); ++ EXPECT_EQ(pthread_create(&producer_thread, NULL, producer, &test), 0); ++ EXPECT_EQ(pthread_join(producer_thread, NULL), 0); ++ EXPECT_EQ(pthread_join(consumer_thread, NULL), 0); ++ uint64_t dropped = atomic_load_explicit(&test.ring.dropped, memory_order_relaxed); ++ EXPECT_TRUE(test.ordered); ++ EXPECT_EQ(test.consumed + dropped, CONCURRENT_SAMPLES); ++ } ++ ++ return test_report(); ++} +-- +2.55.0 + diff --git a/patches/pipeasio/0015-nonblocking-telemetry-reporting.patch b/patches/pipeasio/0015-nonblocking-telemetry-reporting.patch new file mode 100644 index 00000000..c9defa08 --- /dev/null +++ b/patches/pipeasio/0015-nonblocking-telemetry-reporting.patch @@ -0,0 +1,324 @@ +From 9227f00c4137b74adfc3d98fa14b0a38bef7c528 Mon Sep 17 00:00:00 2001 +From: Codex Audit +Date: Mon, 24 Aug 2026 02:00:00 +0200 +Subject: [PATCH 2/2] asio: keep telemetry reporting nonblocking +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Callback samples use a bounded ring, while periodic reports previously inherited blocking stderr behaviour. + +Open a separate nonblocking report descriptor for pipes and terminals. Share stderr’s file offset for regular files so ordinary logs preserve report order. Drop pipe reports on backpressure. + +Count fail-silent buffer-mismatch cycles as muted work in each telemetry interval. Also correct the documented buffer size and real-time environment option. + +Verification: the normal 64-bit driver build and all 11 non-integration tests pass. The standalone telemetry test passes 8,217 checks. +--- + README.md | 24 ++++++--- + include/pipeasio_telemetry.h | 2 + + src/asio.c | 98 ++++++++++++++++++++++++++++-------- + tests/unit/test_telemetry.c | 41 +++++++++++++++ + 4 files changed, 136 insertions(+), 29 deletions(-) + +diff --git a/README.md b/README.md +index 1373bf3..baf97ee 100644 +--- a/README.md ++++ b/README.md +@@ -508,10 +508,11 @@ one more buffer period but keeps a device-driven quantum from stalling the graph + Env: `PIPEASIO_FOLLOW_DEVICE_CLOCK` (`on`/`off`). + + ### buffer_size +-The preferred size returned by `GetBufferSize()`. Must be a power of two within +-[32, 8192]. Out-of-range values fall back to 1024. The floor is 32 rather than 16 +-because hosts mishandle smaller ASIO buffers (Max/MSP crashes on them), and +-RS_ASIO rounds every request up to a multiple of 32 regardless. ++PipeASIO reports `buffer_size` as the preferred buffer size. It accepts every ++whole value from 32 through 8192. Values outside this range use the current ++safe setting. The minimum is 32. Max/MSP crashes with smaller ASIO ++buffers. RS_ASIO rounds every request to a multiple of 32. ++ + Env: `PIPEASIO_PREFERRED_BUFFERSIZE`. + + A size the hardware does not support makes PipeWire reject the request or insert +@@ -546,7 +547,11 @@ Useful as a diagnostic for the scheduling-related xruns in + [#4](https://github.com/M0n7y5/pipeasio/issues/4), not as a fix for them. It + takes effect the next time the host starts the driver. + +-Env: `PIPEASIO_RT_PRIORITY` (`off`/`on`). The environment overrides the file. ++Env: `PIPEASIO_REALTIME` accepts a strict boolean for one launch. Any ++supplied value takes priority. A value outside the boolean forms prints a ++warning and uses the file setting. ++`PIPEASIO_RT_PRIORITY` (`off`/`on`) provides compatibility for launches ++that omit `PIPEASIO_REALTIME`. + + ### Callback timing diagnostic + +@@ -563,9 +568,12 @@ The audio callback stores samples in a fixed area with 8192 entries. + A full area drops the next sample, and audio continues. The lifecycle worker + reports once per second. It sorts at most 8192 samples. It writes the 50th, + 95th and 99th percentiles as p50, p95 and p99. It writes the largest value as +-maximum, plus the dropped sample count, to stderr. Clock reads and fixed sample +-writes add CPU work. Use telemetry during controlled measurement +-runs. ++maximum, plus the dropped sample count, through a separate stderr output. ++The cycle record also counts silent mismatch cycles as `muted`. ++When that output is full, PipeASIO drops the current report. ++Stop and Destroy continue. Clock reads and fixed sample ++writes add CPU work. Use telemetry during ++controlled measurement runs. + + PipeASIO supports this report for 64-bit hosts. For a 32-bit WoW64 host, + PipeASIO keeps telemetry off. PipeWire and Live callbacks use different +diff --git a/include/pipeasio_telemetry.h b/include/pipeasio_telemetry.h +index 7525921..69dadaf 100644 +--- a/include/pipeasio_telemetry.h ++++ b/include/pipeasio_telemetry.h +@@ -14,9 +14,11 @@ + #include + #include + #include ++#include + + #define PIPEASIO_TELEMETRY_PHASES 3u + #define PIPEASIO_TELEMETRY_CAPACITY 8192u ++#define PIPEASIO_TELEMETRY_OUTPUT_FLAGS (O_WRONLY | O_NONBLOCK | O_CLOEXEC | O_APPEND) + + enum pipeasio_telemetry_phase + { +diff --git a/src/asio.c b/src/asio.c +index fc38757..9aafb96 100644 +--- a/src/asio.c ++++ b/src/asio.c +@@ -280,6 +280,8 @@ typedef struct IOChannel + typedef struct pipeasio_telemetry_state + { + pipeasio_telemetry_ring ring; ++ _Atomic uint64_t muted_cycles; ++ int output_fd; + } pipeasio_telemetry_state; + + typedef struct pipeasio_telemetry_cycle +@@ -728,6 +730,41 @@ gate_leave_test_barrier(void) + #endif + + #ifndef PIPEASIO_WOW64_PE ++/* A regular file shares stderr's offset so ordinary logs and telemetry remain ++ * ordered. Pipes and terminals use a separate nonblocking description. */ ++static int ++telemetry_open_report_fd(void) ++{ ++ struct stat st; ++ if (fstat(STDERR_FILENO, &st) != 0) ++ return -1; ++ if (S_ISFIFO(st.st_mode) || S_ISCHR(st.st_mode)) ++ return open("/proc/self/fd/2", PIPEASIO_TELEMETRY_OUTPUT_FLAGS); ++ return dup(STDERR_FILENO); ++} ++ ++/* A separate stderr output keeps the lifecycle worker responsive. ++ * A full output drops the current report. Stop and Destroy ++ * continue. */ ++static void ++telemetry_log(pipeasio_telemetry_state *state, const char *format, ...) ++{ ++ static const char prefix[] = "[pipeasio telemetry] "; ++ char buffer[1024]; ++ va_list args; ++ int length; ++ size_t used = sizeof(prefix) - 1; ++ ++ memcpy(buffer, prefix, used); ++ va_start(args, format); ++ length = vsnprintf(buffer + used, sizeof(buffer) - used, format, args); ++ va_end(args); ++ if (length <= 0) ++ return; ++ used += (size_t)length < sizeof(buffer) - used ? (size_t)length : sizeof(buffer) - used - 1; ++ (void)write(state->output_fd, buffer, used); ++} ++ + /* Monotonic time records the full deadline. Thread CPU time records work on + * the callback thread. Wall time also includes sleep in Live. These clock + * reads occur while PIPEASIO_TELEMETRY is on. */ +@@ -823,25 +860,31 @@ telemetry_emit(IPipeASIOImpl *This) + size_t count; + uint64_t values[PIPEASIO_TELEMETRY_CAPACITY]; + uint64_t dropped; ++ uint64_t muted; + size_t time_info = 0; + if (!state) + return; + count = pipeasio_telemetry_ring_snapshot(&state->ring, &first); + dropped = atomic_exchange_explicit(&state->ring.dropped, 0, memory_order_relaxed); ++ muted = atomic_exchange_explicit(&state->muted_cycles, 0, memory_order_relaxed); + if (!count) + { +- if (dropped) +- PIPEASIO_LOG("[pipeasio telemetry] ", "cycles=0 dropped=%llu\n", +- (unsigned long long)dropped); ++ if (dropped || muted) ++ telemetry_log(state, ++ "cycles=%llu time_info=0 legacy=0 muted=%llu dropped=%llu\n", ++ (unsigned long long)muted, (unsigned long long)muted, ++ (unsigned long long)dropped); + return; + } + for (size_t i = 0; i < count; ++i) + if (pipeasio_telemetry_ring_at(&state->ring, first + i)->flags + & PIPEASIO_TELEMETRY_TIME_INFO) + ++time_info; +- PIPEASIO_LOG("[pipeasio telemetry] ", "cycles=%llu time_info=%llu legacy=%llu dropped=%llu\n", +- (unsigned long long)count, (unsigned long long)time_info, +- (unsigned long long)(count - time_info), (unsigned long long)dropped); ++ telemetry_log(state, ++ "cycles=%llu time_info=%llu legacy=%llu muted=%llu dropped=%llu\n", ++ (unsigned long long)(count + muted), (unsigned long long)time_info, ++ (unsigned long long)(count - time_info), (unsigned long long)muted, ++ (unsigned long long)dropped); + for (unsigned clock_kind = 0; clock_kind < 2; ++clock_kind) + for (unsigned phase = 0; phase < PIPEASIO_TELEMETRY_PHASES; ++phase) + { +@@ -852,13 +895,13 @@ telemetry_emit(IPipeASIOImpl *This) + values[i] = clock_kind ? sample->cpu_ns[phase] : sample->wall_ns[phase]; + } + qsort(values, count, sizeof(values[0]), telemetry_compare_u64); +- PIPEASIO_LOG("[pipeasio telemetry] ", +- "%s_ns phase=%s p50=%llu p95=%llu p99=%llu max=%llu\n", +- clock_kind ? "thread_cpu" : "monotonic_wall", phase_name[phase], +- (unsigned long long)values[pipeasio_telemetry_percentile_index(count, 50)], +- (unsigned long long)values[pipeasio_telemetry_percentile_index(count, 95)], +- (unsigned long long)values[pipeasio_telemetry_percentile_index(count, 99)], +- (unsigned long long)values[count - 1]); ++ telemetry_log( ++ state, "%s_ns phase=%s p50=%llu p95=%llu p99=%llu max=%llu\n", ++ clock_kind ? "thread_cpu" : "monotonic_wall", phase_name[phase], ++ (unsigned long long)values[pipeasio_telemetry_percentile_index(count, 50)], ++ (unsigned long long)values[pipeasio_telemetry_percentile_index(count, 95)], ++ (unsigned long long)values[pipeasio_telemetry_percentile_index(count, 99)], ++ (unsigned long long)values[count - 1]); + } + pipeasio_telemetry_ring_consume(&state->ring, first, count); + } +@@ -1517,7 +1560,10 @@ destroy_driver_resources(IPipeASIOImpl *This) + pipeasio_telemetry_state *telemetry + = atomic_exchange_explicit(&This->telemetry, NULL, memory_order_acq_rel); + if (telemetry) ++ { ++ close(telemetry->output_fd); + HeapFree(GetProcessHeap(), 0, telemetry); ++ } + #endif + } + +@@ -3380,10 +3426,13 @@ process_callback(audio_nframes_t nframes, void *arg) + This->output_channel[i].active); + } + #ifndef PIPEASIO_WOW64_PE +- /* During a buffer mismatch, the graph owns and queues the silent block. +- * Telemetry resumes output timing when the callback owns the output. */ +- if (admitted && !muted && telemetry) +- telemetry_cycle_finish(&cycle); ++ if (admitted && telemetry) ++ { ++ if (muted) ++ atomic_fetch_add_explicit(&telemetry->muted_cycles, 1, memory_order_relaxed); ++ else ++ telemetry_cycle_finish(&cycle); ++ } + #endif + if (admitted) + This->host_buffer_index = half ? 0 : 1; +@@ -3594,21 +3643,28 @@ configure_driver(IPipeASIOImpl *This) + else + { + pipeasio_telemetry_ring_init(&state->ring); ++ atomic_init(&state->muted_cycles, 0); + if (!atomic_is_lock_free(&This->telemetry) + || !atomic_is_lock_free(&state->ring.write_seq) + || !atomic_is_lock_free(&state->ring.read_seq) +- || !atomic_is_lock_free(&state->ring.dropped)) ++ || !atomic_is_lock_free(&state->ring.dropped) ++ || !atomic_is_lock_free(&state->muted_cycles)) + { + WARN("PIPEASIO_TELEMETRY requires lock-free pointer and 64-bit atomics; " + "keeping off\n"); + HeapFree(GetProcessHeap(), 0, state); + } ++ else if ((state->output_fd = telemetry_open_report_fd()) < 0) ++ { ++ WARN("PIPEASIO_TELEMETRY could not open a nonblocking report descriptor; " ++ "keeping off\n"); ++ HeapFree(GetProcessHeap(), 0, state); ++ } + else + { + atomic_store_explicit(&This->telemetry, state, memory_order_release); +- PIPEASIO_LOG("[pipeasio telemetry] ", +- "enabled for 64-bit host; capacity=%u, interval_ms=1000\n", +- PIPEASIO_TELEMETRY_CAPACITY); ++ telemetry_log(state, "enabled for 64-bit host; capacity=%u, interval_ms=1000\n", ++ PIPEASIO_TELEMETRY_CAPACITY); + /* Wake the lifecycle worker when telemetry starts. + * Its next wait uses the one-second report interval. */ + SetEvent(This->work_event); +diff --git a/tests/unit/test_telemetry.c b/tests/unit/test_telemetry.c +index 0d70ef0..b7f4263 100644 +--- a/tests/unit/test_telemetry.c ++++ b/tests/unit/test_telemetry.c +@@ -4,6 +4,10 @@ + + #include + #include ++#include ++#include ++#include ++#include + + #define CONCURRENT_SAMPLES 100000u + +@@ -116,5 +120,42 @@ main(void) + EXPECT_EQ(test.consumed + dropped, CONCURRENT_SAMPLES); + } + ++ TEST_GROUP("regular report descriptor shares the stderr file offset") ++ { ++ static const char original_text[] = "original-content\n"; ++ static const char report_text[] = "telemetry-report\n"; ++ static const char ordinary_text[] = "ordinary-log\n"; ++ char path[] = "/tmp/pipeasio-telemetry-XXXXXX"; ++ char contents[sizeof(original_text) + sizeof(report_text) ++ + sizeof(ordinary_text)] = { 0 }; ++ int original = mkstemp(path); ++ int report = -1; ++ ++ EXPECT_TRUE(original >= 0); ++ if (original >= 0) ++ { ++ EXPECT_EQ(write(original, original_text, sizeof(original_text) - 1), ++ (ssize_t)sizeof(original_text) - 1); ++ report = dup(original); ++ EXPECT_TRUE(report >= 0); ++ if (report >= 0) ++ EXPECT_EQ(write(report, report_text, sizeof(report_text) - 1), ++ (ssize_t)sizeof(report_text) - 1); ++ EXPECT_EQ(write(original, ordinary_text, sizeof(ordinary_text) - 1), ++ (ssize_t)sizeof(ordinary_text) - 1); ++ EXPECT_EQ(lseek(original, 0, SEEK_SET), 0); ++ EXPECT_EQ(read(original, contents, sizeof(contents) - 1), ++ (ssize_t)(sizeof(original_text) + sizeof(report_text) ++ + sizeof(ordinary_text) - 3)); ++ EXPECT_TRUE(!strcmp(contents, ++ "original-content\ntelemetry-report\nordinary-log\n")); ++ } ++ if (report >= 0) ++ close(report); ++ if (original >= 0) ++ close(original); ++ unlink(path); ++ } ++ + return test_report(); + } +-- +2.55.0 + diff --git a/scripts/build-audit.sh b/scripts/build-audit.sh index ff7e272f..5a3ad6ea 100755 --- a/scripts/build-audit.sh +++ b/scripts/build-audit.sh @@ -11,7 +11,7 @@ say() { printf '%s\n' "$*"; } fail() { printf '!! %s\n' "$*" >&2; exit 1; } readonly REQUIRED_WINE_TAIL='0107-win32u-restore-offscreen-client-content-on-expose.patch' -readonly REQUIRED_PIPEASIO_TAIL='pipeasio/0013-avoid-redundant-output-fallback-publish.patch' +readonly REQUIRED_PIPEASIO_TAIL='pipeasio/0015-nonblocking-telemetry-reporting.patch' check_required_series_tails() { @@ -398,6 +398,8 @@ pipeasio/0009|ascii|lib/wine/x86_64-unix/pipeasio.dll.so|pipeasio-honest-realtim pipeasio/0010|wide|bin/pipeasio-settings|pick a preset or type any value pipeasio/0011|ascii|lib/wine/x86_64-unix/pipeasio.dll.so|pipeasio-ableton-controlpanel pipeasio/0012|ascii|lib/wine/x86_64-unix/pipeasio.dll.so|pipeasio-reliable-hotplug +pipeasio/0014|ascii|lib/wine/x86_64-unix/pipeasio.dll.so|enabled for 64-bit host; capacity=%u, interval_ms=1000 +pipeasio/0015|ascii|lib/wine/x86_64-unix/pipeasio.dll.so|PIPEASIO_TELEMETRY could not open a nonblocking report descriptor ' # 0010's source marker (pipeasio-any-buffer-size-panel) is a comment and does # not reach the panel binary; its fingerprint is the tooltip literal above,