TaggerMainThreadManager.PerformWorkOnMainThreadAsync allocates unnecessarily on the main-thread fast path
- Dominant language
- C#
- Stars
- 20.7k
- Forks
- 4.3k
- PR merge metrics
- PR metrics pending
Description
## Summary
Internal VS telemetry identifies `TaggerMainThreadManager.PerformWorkOnMainThreadAsync` as a top allocation hotspot on the main thread during editor tag recomputation. The method allocates a `TaskCompletionSource` on every call, even when already on the main thread where the action executes synchronously. With multiple active taggers per document (classification, outlining, diagnostics, inheritance margin, etc.), each calling this method on every recompute cycle, the per-call allocation overhead (88-128 bytes) contributes to GC pressure on the UI thread during typing.
## Current behavior
The method unconditionally allocates a `new TaskCompletionSource()` before checking `IsOnMainThread`. On the main-thread path, the action is executed synchronously and the TCS result is set immediately, but the allocation has already occurred.
## Proposed fix
Split into a non-async fast path and an async slow path:
- **Fast path** (on main thread): Run the action directly, return the result as a synchronous `ValueTask` — zero heap allocations.
- **Slow path** (off main thread): Unchanged behavior — allocate TCS and enqueue for batched execution.
The method is intentionally kept non-async so the compiler doesn't generate a state machine for the fast path (async elision pattern, matching `GetCompilationSlowAsync` elsewhere in the repo).
## Benchmark results
| Method | Before Mean | After Mean | CPU Reduction | Before Alloc | After Alloc | Alloc Reduction |
|--------|------------|------------|---------------|-------------|------------|-----------------|
| ReturnsNull | 601.9 ns | 345.2 ns | -43% | 88 B | 0 B | -100% |
| ReturnsValue | 789.7 ns | 544.1 ns | -31% | 128 B | 40 B | -69% |
Benchmark: `TaggerMainThreadManagerBenchmarks` in `src/Tools/IdeBenchmarks`, exercising `PerformWorkOnMainThreadAsync` on the main thread with `[CpuUsageDiagnoser]`.
Contributor guide
Research direction
Start at TaggerMainThreadManager.PerformWorkOnMainThreadAsync and compare the async-elision pattern used by GetCompilationSlowAsync elsewhere in the repository. Run TaggerMainThreadManagerBenchmarks in src/Tools/IdeBenchmarks, focusing on the main-thread fast path and its allocation measurements. Done means the fast path has no heap allocation while off-main-thread batched execution remains unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- performance
- Issue type
- Refactor
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100