dotnet / dotnet/roslyn

TaggerMainThreadManager.PerformWorkOnMainThreadAsync allocates unnecessarily on the main-thread fast path

Open
#83,998 4 comments 0 reactions 0 assignees View on GitHub
Area-IDE
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.