Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 150 additions & 2 deletions docs/ASYNC_MIGRATION_ANALYSIS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,153 @@ This document records the audit of blocking I/O operations in ACAT as the first
of the async/await workstream (Phase 3 – Workstream C). It identifies each blocking call,
categorises it by priority, and maps it to its async replacement.

## Phase 4: Call-site adoption inventory (#278)

The following inventory was collected **before code changes**, searching `src/**/*.cs`
for the synchronous APIs listed in the issue and following instance and inherited
preference calls. Definitions, comments, and tests are not production callers.
The existing synchronous implementations are compatibility boundaries, not candidates
for deletion.

| API family | Initial callers / locations | Adoption plan |
|---|---|---|
| JSON loader | 9 calls inside `Theme`, `Abbreviations`, `Pronunciations`, `ActuatorConfig`, `PreferredWordPredictors`; 5 in `ConfigurationExamples` | Preserve sync implementations; route startup to the existing async implementations. Examples are not startup paths. |
| Global/user preferences | `AppCommon.LoadGlobalSettings`, `LoadUserPreferences`, onboarding saves; `ACATPreferences.Load`, `LoadDefaultSettings` | Add async helpers alongside the public sync methods and await from WinForms startup events. |
| PreferencesBase | 19 explicitly qualified loads: 2 command tables, 8 default extension settings wrappers (LaunchApp, Chrome, Edge, Outlook, Explorer, Camera, SAPI, TTSClient), 9 EEG settings wrappers | Preserve synchronous plugin contracts; adopt async where a task-returning load path is available. |
| Abbreviations / pronunciations | `AbbreviationsManager.Load`, manager initialization and mutation saves; `TTSManager` pronunciation initialization and mutation saves | Async manager initialization; retain immediate-result mutation contracts where necessary. |
| Actuator configuration | 8 explicit `ActuatorConfig.Load` calls in core (`ActuatorManager`, `Actuators`, switch/keyboard forms), 1 in onboarding; instance saves | Async startup loading; document immediate-result accessors and UI configuration boundaries. |
| Preferred predictors | `WordPredictors` constructor (`PreferredWordPredictors.Load`) and instance saves | Move I/O out of construction into async initialization. |
| Themes | 2 `Theme.Create` calls in `ThemeManager` constructor, 1 in `SetActiveTheme`, 2 in `ThemeRepository`; 1 `SetActiveTheme` in `Context` | Async manager startup and repository loads; retain public synchronous compatibility paths. |
| ConvAssist | `ConvAssistWordPredictor.Init`: 1 pipe creation; 3 request methods call `WriteSync`; `NamedPipeServerConvAssist.Restart`: 1 pipe creation | Use existing async initialization and request methods in task-capable paths; preserve synchronous predictor interface methods. |
| XML helpers | `PreferencesRepository.Save`: 1 `XmlFileSave`; generic `XmlFileLoad` in synchronous preference/repository helpers | Preserve sync repository methods; use existing async repository overrides in async callers. |

Initialization is order-sensitive: user/profile paths precede preference loading;
themes precede panels; actuators and agents may construct WinForms controls. Do not
run the whole initialization sequence on a worker thread. Independent file loads
may overlap, but UI work must return to the owning STA thread.

Validation baseline in this Linux sandbox: the integration test command stops at
MSB3644 (missing .NET Framework 4.8.1 reference assemblies), before compilation or
test execution. Windows validation remains necessary; this is not a test failure.

### Reproducible expression census

This census counts explicitly qualified production calls, excluding comments,
string literals, tests, and `src/Docs` samples. It includes the bodies of retained
synchronous APIs. Inherited/instance preference calls and unqualified pipe restart
calls are followed separately in the path inventory above.

| Qualified sync expression | Before adoption | Retained compatibility/example expressions |
|---|---:|---:|
| JSON `loader` / `_loader` Load/Save | 14 | 14 |
| PreferencesBase Load/Save/Reload | 19 | 19 |
| GlobalPreferences Load/Save | 1 | 1 |
| ActuatorConfig Load/Save | 9 | 5 |
| Abbreviations Load/Save | 1 | 1 |
| PreferredWordPredictors Load/Save | 1 | 1 |
| Theme.Create | 5 | 5 |
| manager.SetActiveTheme | 1 | 1 |
| namedPipe.CreatePipeServer | 1 | 1 |
| namedPipe.WriteSync | 3 | 3 |
| XmlUtils.XmlFileLoad/XmlFileSave | 2 | 2 |
| **Total** | **57** | **53** |

Four dialog load expressions are replaced outright (and their two instance saves
are also asynchronous). Other migration paths add task-returning entry points and
route production callers to them, while retaining the original expressions in
synchronous compatibility methods. Therefore a raw sync grep count alone is not
the count of I/O operations still performed by async startup.

### Application adoption results

- **17 startup call-site replacements** across ACATApp, ACATTalk, ACATConfig,
ACATConfigNext, and ACATWatch: 3 global loads, 4 user preference loads, 2 Context
initializations, 3 username saves, 3 display-setting saves, and 2 onboarding
calls. ConfigNext additionally bypasses its synchronous constructor reload using
preloaded preferences, and its cancellation reload is asynchronous.
- Startup begins after the WinForms message loop starts. Existing `[STAThread]`
`void Main` signatures remain unchanged; no async `Main` or blocking task bridges
are used. Modeless forms keep the startup task alive until `FormClosed`.
- `GlobalPreferences.LoadAsync` now also awaits the factory-default file save
before loading user settings. Previously this first write was synchronous even
through the async API. Cancellation coverage verifies that a pre-cancelled load
cannot write that file.
- Factory preference *object construction* (`LoadDefaults`/`LoadDefaultSettings`)
has no file I/O and deliberately stays synchronous.

### Manager and extension adoption results

| Migrated path | Async I/O used | Compatibility retained |
|---|---|---|
| Context → themes | ThemeManager.InitAsync, SetActiveThemeAsync, ThemeRepository.LoadAsync → Theme.CreateAsync | Lazy synchronous theme access and Init/SetActiveTheme |
| Context → actuators | ActuatorManager.LoadExtensionsAsync → Actuators.LoadAsync → ActuatorConfig.LoadAsync/SaveAsync | Plugin discovery/construction, hardware Init and immediate-result mutation APIs |
| Context → word prediction | WordPredictionManager.InitAsync → preferred predictor LoadAsync; SetActiveWordPredictorAsync → optional IAsyncInitializer | Existing IWordPredictor implementations fall back to synchronous Init |
| Context → abbreviations | AbbreviationsManager.InitAsync → Abbreviations.LoadAsync | Existing Init/Load and synchronous mutations |
| Context → TTS | SetActiveEngineAsync → SAPI/TTSClient InitAsync → culture-aware Pronunciations.LoadAsync | Engine construction/settings, plugin preference saves, legacy engine Init |
| ConvAssist startup | Settings.LoadAsync, CreatePipeServerAsync | Synchronous Init and preference access |
| ConvAssist request processing | Async word/sentence handlers and learning → existing async sends | Existing synchronous request handlers and IWordPredictor methods |
| Switch/keyboard dialogs | Four config loads and two saves → ActuatorConfig async APIs | Public form constructors/signatures unchanged |

Preferred predictor configuration and abbreviations are independent reads and
are awaited with `Task.WhenAll` in Context startup. Predictor activation still
follows configuration loading; plugin constructors, hardware initialization,
panel/widget initialization, and engine selection are dispatched to the owning
STA. The pipe response slot is shared, so its requests are serialized, **not**
parallelized.

New regression coverage exercises theme async/sync parity and fallback,
abbreviation missing/valid/malformed files, async and legacy predictor
initialization/fallback, STA construction and dispatcher error propagation,
learning selection, pipe protocol behavior, global-default save cancellation,
and dialog save rejection when the reloaded actuator is absent.

### Validation status

- `git diff --check` passes.
- Core/application/ConvAssist builds and the requested integration/performance
test commands were attempted. All stop before compilation at MSB3644 because
this Linux environment lacks the .NET Framework 4.8.1 targeting pack.
- Focused new test commands were also attempted and hit the same prerequisite.
WinForms execution, passing-test counts, and zero-new-warning semantic builds
cannot be verified here; they require Windows with the net481 developer pack.
- Existing MSB3539 output-directory warnings appear on baseline test commands;
output configuration is outside this migration's scope.
- Secret scanning of every changed/new file found no secrets. Post-commit CodeQL
was requested, but C# analysis was skipped because the database is too large;
this is not a completed clean security analysis.

### Deliberately synchronous compatibility boundaries

- The existing `Load`, `Save`, `Reload`, `Init`, theme creation/selection, and
predictor request methods remain callable. Their implementations must not call
an async method and block on its task. Existing synchronous tests continue to
exercise these APIs rather than being rewritten merely to reduce grep counts.
- Chrome, Edge, Outlook, Explorer, and LaunchApp agents read preferences in their
constructors. The type loader and external consumers expect fully initialized
instances on return. An asynchronous constructor is not available in C#;
returning an incompletely initialized agent would change that contract.
- Camera and EEG preference wrappers serve synchronous `GetPreferences`,
calibration, and hardware configuration contracts. Their nine EEG and one
Camera `PreferencesBase.Load` expressions are retained rather than starting
unobserved asynchronous work or blocking on a task.
- `CmdDescriptorTable.Load` and `CmdScopeTable.Load` are immediate-result table
accessors; `CommandManager.Init` currently performs no file I/O.
- `PreferencesRepository.Load/Save` and `ThemeRepository.Load/GetDefault` implement
`IRepository<T>`. The async repository counterparts serve asynchronous callers;
the synchronous interface is not changed.
- `ActuatorManager.GetActuatorConfig`, enable/disable mutations, and existing
synchronous collection mutation APIs return immediate results. Configuration
dialog loads and saves can instead await I/O before accepting their result.
- `ConfigurationExamples` contains five synchronous JSON calls in illustrative
immediate-result loaders/constructors and reload callbacks, not application
startup. These examples remain demonstrations of the synchronous API.
- Synchronous ConvAssist request methods remain for `IWordPredictor` consumers.
Async request processing must serialize access to the shared response slot;
parallel sends on a single pipe are not independent work.
The synchronous compatibility sends use bounded semaphore acquisition, not
`Task.Wait`; async sends await the semaphore with cancellation. No parameterless
`.Wait()`, `.Result`, or `GetAwaiter().GetResult()` task bridges are added.

---

## Blocking I/O Audit
Expand Down Expand Up @@ -113,8 +260,9 @@ opportunities:

## Remaining Work / Future Recommendations

All planned async migration items have been completed. The async API is now fully
usable across the entire codebase.
The infrastructure work described above makes the async API usable across the
codebase. Phase 4 call-site adoption and deliberately synchronous compatibility
boundaries are tracked separately at the top of this document.

Potential further optimisations:
- Replace the `Task.Run` fallback in `XmlUtils.XmlFileLoadAsync`/`XmlFileSaveAsync` with
Expand Down
Loading
Loading