Read-only investigation of JUCE 8.0.15 at /private/tmp/smartgrid-juce-8.0.15/modules, JUCE 8.0.2 at /Users/joyo/JUCE/modules, and the app worktree /Users/joyo/.codex/worktrees/e18e/theallelectricsmartgrid. No source changes, builds, tests, device operations, network operations, or recording control were performed. This report was the only file written.
The strongest supported conclusion is that routine JUCE repaint and CPU-label updates do not acquire the JUCE audio callback locks or request audio-session changes. UI work can compete for execution/resources. JUCE also has deferred audio recovery paths on the message thread, but the current-trial log reviewed in the addendum provides strong counterevidence that the observed driver restarts invoked those paths. They must not be presented as the active recovery mechanism for these faults. The addendum also establishes that native input AudioUnitRender is skipped in these output-only trials.
The parent supplied these observations, which were not remeasured here: old JUCE UI-on had 17 USB-error/restart gaps in 382.55 seconds; old UI-off had one in 960 seconds; new JUCE UI-on had five in 168.192 seconds; new UI-off had at least one analog gap in more than four minutes and reproduced periodic holes. Exposures were sequential. Native callbacks were 48 kHz / 512 samples. For sporadic events, the kernel endpoint 0x82 transaction failure preceded restart/resume by about 0.3 seconds, while measured app DSP was short. During earlier periodic blanking, app callback arrivals remained regular at approximately 10.667 ms, without additional long or missing-callback intervals. The app’s dsp_us measurement ends before the callback’s device/xrun query and INFO formatting/enqueue tail; it is not a full native callback duration.
In 8.0.15, AudioDeviceManager::getCpuUsage() simply returns loadMeasurer.getLoadAsProportion(). The load getter clamps an atomic load of cpuUsageProportion; [the field is std::atomic
The load measurer does have a spinlock, but reset takes that lock, and the audio-side registerRenderTime uses a try-lock. On contention the measurement is omitted rather than waiting. CPU reads do not take this spinlock at all. There is no JUCE mutex path from repeated CPU-label polling to a blocked audio callback. Ordinary atomic/cache traffic remains possible; the source supplies no evidence that it is material to these faults.
This is unchanged from 8.0.2: old getCpuUsage makes the same call, and the entire juce_AudioProcessLoadMeasurer.cpp files have identical SHA-256 56bf1b824ed0526d25878fd744109129eb53f8430c441fc50a920e3038d2da9b.
getCpuUsage is also a limited diagnostic. The manager callback acquires audioCallbackLock at line 1091 before creating its load timer at line 1097. It measures the callbacks within that scope, not native callback arrival latency, native input rendering, waiting for the manager lock, or subsequent output level work. The filter uses 0.2 smoothing and reports xruns from timed blocks; the displayed proportion is capped at 1.0. Thus a low CPU display does not exclude delivery failures outside the timed region.
Discriminator: a full native callback timeline and lock ownership would distinguish unmeasured waiting from app DSP. An alleged getCpuUsage mutex stall would need evidence beyond these sources, because the proposed JUCE lock is absent.
| Lock | Audio path | Other owners and consequence |
|---|---|---|
iOS device Pimpl::callbackLock |
process tries it at 1100. | Held by open/start/stop, audio error delivery, status/route/inter-app changes, and restart. If unavailable, the callback skips app processing and zeros the entire native output buffer at 1142–1145. |
AudioDeviceManager::audioCallbackLock |
Blocking ScopedLock at 1091, held across registered audio callbacks. | Callback add/remove, start/stop/error delivery, selected device-manager operations, and any caller explicitly taking getAudioCallbackLock(). This is not the message-manager lock. |
AudioSourcePlayer::readLock |
Blocking lock at 81, held through source->getNextAudioBlock at 151. | setSource takes it briefly to replace the source pointer. The app only calls setSource in OpenAudioDevice/CloseAudioDevice, not its timer or paint request. |
The normal nesting is native try-lock → manager lock → source-player lock → app. The inspected app source contains no call to getAudioCallbackLock(). The timer’s UI-enabled branch does not add/remove audio callbacks or replace the source. This does not rule out application-owned shared data or locks in drawing/state code; the parent is investigating that separately.
The native try-lock silence behavior already exists in 8.0.2 process at 880–925. Source-player locking is unchanged; its .cpp diff contains only a comment punctuation change. The old manager likewise locks before starting its timer.
At the observed 512 native frames, a native lock miss skips a whole app callback and blanks roughly 10.667 ms. It is therefore counterindicated for the earlier periodic partial holes with regular app callback arrivals. It remains a possible contributor around a sporadic restart, but the source does not show ordinary painting acquiring this lock.
The native xrun recorder compares sample-time continuity and runs before input rendering and the try-lock. Consecutive callbacks which arrive on time but take the silence branch need not increase its count. The manager’s aggregate xrun count adds the native count to the load measurer’s count; neither is a guarantee against every form of silence. Actual regular app arrivals are stronger counterevidence to skipped app callbacks than a stable native xrun count alone.
Discriminator: native process entry/exit, native try-lock success, manager/source-player lock wait duration, and app entry/exit for each native sample-time would separate a skipped whole callback, blocked manager dispatch, and silence created or delivered while app callbacks still run.
The app starts a 1000 / 60 millisecond timer, which is an integer 16 ms request, not an exact display-synchronized 60 Hz timer. JUCE posts CallTimersMessage, which calls timer callbacks on message delivery; it unlocks its timer-list lock before invoking the callback.
The app’s UI branch calls SetDisplayMode, updates the CPU label, and repaints. Label::setText repaints when text changes; dontSendNotification bypasses its change-listener call. The app additionally requests a full main-component repaint every UI-enabled timer callback.
The JUCE repaint chain is concrete and contains no audio-session request:
deferredRepaints. Only an off-message-thread request, or one made during drawing, posts AsyncRepaintMessage. The ordinary timer repaint is not necessarily another posted message per call.UI-off hides child components and suppresses these repeated requests, but does not destroy the native view or its display link. Its timer still performs state interchange, log drain, and platform/device polling. The 1 Hz current-rate/block getters simply read stored values; latency getters read AVAudioSession properties, without setting preferences or holding the JUCE callback lock. These polls do not call the new sample-rate probing routine, and they remain in both UI conditions.
There is an optional Metal-backed CoreGraphics path, guarded by JUCE_COREGRAPHICS_RENDER_WITH_MULTIPLE_PAINT_CALLS. No definition enabling this macro was found in the inspected JUCE tree, app source, generated JuceLibraryCode, .jucer, or iOS Xcode project; it should not be asserted as the active rendering path without the actual build command/binary. The unguarded fallback is CALayer. Even if enabled externally, iOS passes renderSync=false. The renderer returns without painting if a previous blit is pending and puts the potentially blocking nextDrawable call on a Metal scheduled handler. Its synchronous wait branch is not the one selected by this iOS caller. The renderer files are byte-identical between versions (SHA-256 e30e99d1649d19e76bb362e03103e1bb6e9c3fbc789a1382a59110925b65359e). The old repaint/CoreGraphics chain has the same structure at old UIView peer 2163 and 2283.
Resource competition remains a plausible inference, rather than a demonstrated fault mechanism: full-tree painting, text/layout changes, CoreGraphics rasterization, backing-store/compositor work, allocations, and cache/memory traffic add work while native audio deadlines continue. Source alone does not show that this work starves USB transactions, creates a driver error, or materially lengthens the native callback. The low measured app DSP time neither establishes nor excludes delays outside that measurement. The parent’s app-state investigation is needed to assess any stronger direct app coupling.
Discriminator: work that occurs before the first USB error is relevant to triggering it. UI paint duration, scheduling/memory pressure, and complete native callback timing should be related to the first endpoint failure rather than only to restart duration. A longer message-thread backlog after the error would support delayed recovery, not explain the initiating USB transaction failure.
JUCE registers AVAudioSession interruption, media-services and route observers. Their Objective-C handlers directly forward to JUCE; the notification delivery thread is not specified by these source handlers.
For route changes, handleRouteChange holds the native callback lock and invokes an audio-device error callback if running. Route reasons other than category change / route-configuration change set hardwareInfoNeedsUpdating and trigger an async update. A stream-format sample-rate mismatch also triggers an async update. AsyncUpdater posts one coalesced message whose message callback calls handleAsyncUpdate. On iOS, MessageQueue installs a run-loop source and delivers messages through messageCallback, just as timer messages use that event machinery.
The audio async handler calls restart. restart takes the native callback lock, disposes the AudioUnit and emits audioDeviceStopped, applies rate/buffer targets, refreshes hardware, recreates the AudioUnit, emits audioDeviceAboutToStart, and starts it. Long UI work can postpone when that message runs. This is a direct route from UI workload to recovery latency, with no need to posit an audio lock held throughout painting. Notifications can also coalesce while the message is pending, so episode counts are not necessarily counts of independent initiating faults.
There are material version changes in recovery/setup, despite unchanged steady callback locking. Old restart queried/applied settings before disposing the AudioUnit; the new version disposes first. New setAudioSessionActive(true) on iOS 18+ temporarily creates/starts a substitute RemoteIO unit and waits for its callback, up to 1000 ms. This path is used for activation/status handling, not every repaint or every restart. Status handling holds the native callback lock during activation. Actual route/status/format reasons would be needed to determine which recovery path a measured event takes; an OS/driver restart is not automatically a JUCE Pimpl::restart().
Finally, when active input channels are available, the native callback calls input AudioUnitRender before the try-lock. This call is skipped in the output-only trials, as established below. The callback proceeds to the app when the lock/callback allow it and returns its status to its caller; there is no err != noErr branch here that directly calls restart. The USB driver behavior is outside this JUCE source.
Discriminator: timestamp the first endpoint error separately from JUCE route/status/stream notifications, async-update enqueue and entry, stop/about-to-start, and the first subsequent native/app callback. A consistent UI-related increase only in enqueue-to-restart or restart-to-resume time supports recovery coupling. Establishing UI as the trigger requires a reproducible pre-error change and an initiating path; neither has been demonstrated by this read-only investigation.
The sequential UI-on/off observations justify investigating UI dependence but do not isolate rendering, state updates, label work, elapsed time, temperature, or platform state. UI-off still producing sporadic and periodic faults is counterevidence to a claim that UI-on is necessary. The retained endpoint-error-before-recovery ordering is stronger evidence about the failure boundary than the absence of timed DSP overruns.
This addendum qualifies the generic possibilities above using the actual trial configuration and the collected JUCE 8.0.15 UI-on r2 app log. It uses read-only source inspection and log parsing, with no new experiment or device operation.
Native input rendering is excluded from the unmeasured work in these trials. The parent verified active_inputs=0 in both trials. The UI-on log independently confirms requested_inputs=0, active_inputs=0 at line 13, and its periodic session rows continue to report inputs=0. areInputChannelsAvailable() requires both accessible inputs and numActiveChannels > 0. Therefore useInput is false and the conditional AudioUnitRender call at 1095–1096 should not execute for this configuration. Describing native input rendering as a possible unmeasured callback cost for these particular output-only trials would be incorrect. Callback scheduling/delivery, locks, output-side native work, and the app’s query/logging tail remain outside dsp_us. Input disabled at this JUCE boundary does not explain why the USB driver reports endpoint 0x82 transaction failures; those failures must not be attributed to a JUCE input-render call that the configured branch skips.
The observed driver recoveries strongly argue against JUCE Pimpl::restart() and its deferred message-thread recovery path. The entire app log contains exactly one prepareToPlay entry, at startup, 12:21:01, line 15. It contains 21,163 callback records with IDs 1 through 21,163 in uninterrupted numerical order, with no resets. This is stronger than an absent lifecycle log alone: MainComponent::prepareToPlay logs unconditionally at line 41 and calls diagnostics.Reset at line 45, and Reset sets the callback sequence back to zero. No such post-startup reset is visible.
The successful JUCE restart chain would instead be Pimpl::restart → audioDeviceAboutToStart at 1371, through AudioDeviceManager::audioDeviceAboutToStartInt, into AudioSourcePlayer::audioDeviceAboutToStart/prepareToPlay, which calls the attached app source’s prepareToPlay. The ordinary running configuration has a non-null callback and attached source. The observed continuous sequence plus the single startup preparation contradicts that lifecycle having been repeated for the matched driver recoveries.
The app log’s matched native-xrun increments are:
| App log entry | Callback | Native xr | App arrival gap |
|---|---|---|---|
| 12:22:14, line 7093 | 6789 | 1 → 2 | 219.296 ms |
| 12:22:19, line 7607 | 7283 | 2 → 3 | 217.546 ms |
| 12:22:20, line 7652 | 7324 | 3 → 4 | 221.283 ms |
| 12:23:03, line 11900 | 11400 | 4 → 5 | 225.218 ms |
| 12:23:49, line 16329 | 15649 | 5 → 6 | 224.037 ms |
The full log also has an earlier xr 0 → 1 at 12:21:52, line 4968, with a 222.240 ms arrival gap, preceding the five-event matched window supplied by the parent. xr here comes directly from the device’s getXRunCount(), so it is the native sample-time-discontinuity counter, not the manager’s aggregate DSP-load xrun count.
Zero callback-sequence holes mean no missing numbered records or preparation resets in this log; they do not mean uninterrupted real-time delivery. Observe increments the sequence only when an app callback actually executes, and the arrival-gap columns explicitly show approximately 0.22-second interruptions. This distinction preserves the earlier, separate observation that periodic partial holes occurred with regular arrival timing.
For this trial, JUCE deferred-restart latency remains an available architectural path, not an evidence-supported explanation for the measured recovery interval. The directly supported pattern is interrupted native/app delivery followed by resumption of the existing app callback lifecycle and continuing sequence, without a new app preparation. The driver may restart internally while its client object remains intact. The current sources/logs do not identify a UI-trigger mechanism for the initial USB fault, nor show that the observed driver recovery waits for JUCE’s main-thread restart handler. Any future attribution to that handler requires new direct evidence that it ran.