Surface hidden failures: out-of-order phonemes and unhandled exceptions - #2404
Merged
stakira merged 4 commits intoSep 16, 2026
Merged
Conversation
`UPart.Update` runs a loop that silently repairs out-of-order phoneme positions (`Math.Min(position, next.position - 10)`). That safety treatment covers two very different cases: a user dragging a phoneme offset past its neighbour, which is legitimate, and a phonemizer that returned positions out of order, which is a bug. The repair is silent, so the second case is invisible: the part renders "a bit odd" and no test can see it either, because the test harness calls `Phonemizer.Process` directly and never goes through this code path. This adds a read-only ordering check on `rawPosition` -- the phonemizer output before user overrides are applied -- so only the phonemizer case is reported. One warning per part, and nothing changes about the repair itself.
`Program.InitLogging` registers `AppDomain.CurrentDomain.UnhandledException`, which can log an exception but can not keep the process alive. There is nothing registered for the two cases users actually hit: * `TaskScheduler.UnobservedTaskException` -- an exception in a `Task` nobody awaited is dropped until the finalizer runs, and then ignored by default. * `Dispatcher.UIThread.UnhandledException` -- the code base has 47 `async void` methods, 31 of them menu/button handlers. An exception escaping one of those reaches the dispatcher with no handler to mark it handled, so the process exits. The user sees the window disappear. This registers both hooks in `OnFrameworkInitializationCompleted` (where Avalonia is up and we are on the UI thread). Exceptions are logged and reported through the existing `ErrorMessageNotification` path, so the app stays alive instead of vanishing. It does not fix any root cause -- a null reference still has to be fixed where it is thrown -- it only turns "process gone" into "error reported".
`UPart.Update` runs a loop that silently repairs out-of-order phoneme positions (`Math.Min(position, next.position - 10)`). That safety treatment covers two very different cases: a user dragging a phoneme offset past its neighbour, which is legitimate, and a phonemizer that returned positions out of order, which is a bug. The repair is silent, so the second case is invisible: the part renders "a bit odd" and no test can see it either, because the test harness calls `Phonemizer.Process` directly and never goes through this code path. This adds a read-only ordering check on `rawPosition` -- the phonemizer output before user overrides are applied -- so only the phonemizer case is reported. One warning per part, and nothing changes about the repair itself.
`Program.InitLogging` registers `AppDomain.CurrentDomain.UnhandledException`, which can log an exception but can not keep the process alive. There is nothing registered for the two cases users actually hit: * `TaskScheduler.UnobservedTaskException` -- an exception in a `Task` nobody awaited is dropped until the finalizer runs, and then ignored by default. * `Dispatcher.UIThread.UnhandledException` -- the code base has 47 `async void` methods, 31 of them menu/button handlers. An exception escaping one of those reaches the dispatcher with no handler to mark it handled, so the process exits. The user sees the window disappear. This registers both hooks in `OnFrameworkInitializationCompleted` (where Avalonia is up and we are on the UI thread). Exceptions are logged and reported through the existing `ErrorMessageNotification` path, so the app stays alive instead of vanishing. It does not fix any root cause -- a null reference still has to be fixed where it is thrown -- it only turns "process gone" into "error reported".
keirokeer
added a commit
to keirokeer/OpenUtau-lunai
that referenced
this pull request
Sep 16, 2026
…nutau#2408 openutau#2411 openutau#2412 openutau#2414) Log out-of-order phonemes, clear ClassicSinger loaded on FreeMemory, add VibratoPreset default ctor, fix G2p lang replacements without IDs, and drop undefined part curves after load.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two small, unrelated changes that make hidden failures visible. Kept as two commits so they can be split into two PRs if you prefer.
1.
ustx: log when a phonemizer returns out-of-order phonemesUPart.Updatesilently repairs out-of-order phoneme positions:That repair covers two very different causes:
Because the repair is silent, the second case is invisible: the part just renders slightly odd. No test can see it either, because the test harness calls
Phonemizer.Processdirectly and never reaches this code path.The patch adds a read-only ordering check on
rawPosition, which holds the phonemizer output before user overrides are applied, so only the phonemizer case is reported. One warning per part; the repair itself is unchanged.I measured the built-in phonemizers through the test harness and found no violations across 598 phonemes / 318 note groups, so this should normally stay quiet — silence is the useful signal here.
2.
app: also handle unobserved task and unhandled UI exceptionsProgram.InitLoggingregistersAppDomain.CurrentDomain.UnhandledException, which can log an exception but cannot keep the process alive. Nothing is registered for the two cases users actually hit:TaskScheduler.UnobservedTaskException— a faultedTaskthat nobody awaited is dropped until finalization, and then ignored.Dispatcher.UIThread.UnhandledException— this code base has 47async voidmethods, 31 of them menu/button handlers. An exception escaping one reaches the dispatcher, nothing marks it handled, and the process exits: the window simply disappears.Both hooks are registered in
OnFrameworkInitializationCompleted, where Avalonia is up and we are on the UI thread. Exceptions are logged and surfaced through the existingErrorMessageNotificationpath.This does not fix any root cause. A null reference still has to be fixed where it is thrown (as #2390 did). It only turns "process gone" into "error reported", so an unforeseen exception costs the user a dialog instead of their session.
Verification
dotnet buildwith both patches applieddotnet testfull suiteNeither change alters behaviour for the existing suite; the App-level one only changes behaviour in cases that would otherwise terminate the process.
Happy to adjust or drop either part — for example lowering the new log to
Debug, or splitting this into two PRs.