Measure task host overhead in mt - #13555
Merged
OvesN merged 11 commits intoApr 23, 2026
Merged
Conversation
OvesN
force-pushed
the
dev/veronikao/measure-task-host-overhead-in-mt
branch
3 times, most recently
from
April 16, 2026 14:46
29fe5dc to
e5caffe
Compare
…task host Remove the attribute from all built-in tasks and intrinsic tasks to route them through the out-of-proc task host in MT mode. This enables measuring task host overhead by ensuring tasks go through the TaskHost dispatch path instead of running in-process. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add four new events to MSBuildEventSource: - TaskHostDispatchStart/Stop (107/108): wraps TaskHostTask.Execute() in the main process, measuring the full task host roundtrip - TaskExecuteInHostStart/Stop (109/110): wraps task.Execute() inside the task host process, measuring actual task execution time Overhead = Dispatch time - Execute time Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
OvesN
force-pushed
the
dev/veronikao/measure-task-host-overhead-in-mt
branch
from
April 16, 2026 14:53
e5caffe to
ce341bf
Compare
Contributor
Author
|
Measured https://github.com/OrchardCMS/OrchardCore build task overhead. Command used: D:\msbuild\artifacts\bin\bootstrap\core\dotnet.exe build-server shutdown
D:\msbuild\artifacts\bin\bootstrap\core\dotnet.exe clean
PerfView.exe "/DataFile:PerfViewData.etl" /BufferSizeMB:256 /StackCompression /CircularMB:2048 /KernelEvents:None /Process:"dotnet" /ClrEvents:None /Providers:"*Microsoft-Build" /NoGui /NoNGenRundown /Merge:False /Zip:False run D:\msbuild\artifacts\bin\bootstrap\core\dotnet.exe build D:\OrchardCore\OrchardCore.slnx --no-restore -mt
|
…es and remove parse_overhead.py Keep only the ETW event additions (TaskHostDispatchStart/Stop, TaskExecuteInHostStart/Stop) for measuring task host overhead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
JanProvaznik
approved these changes
Apr 20, 2026
Contributor
There was a problem hiding this comment.
Pull request overview
Adds new ETW instrumentation to quantify out-of-proc task host overhead in MT builds by measuring (1) main-process task-host dispatch roundtrips and (2) in-task-host task.Execute() durations.
Changes:
- Added four new ETW events (IDs 107–110) to
MSBuildEventSourceto mark task-host dispatch and in-host execution start/stop. - Instrumented
TaskHostTask.Execute()(main process) with dispatch start/stop events. - Instrumented
OutOfProcTaskAppDomainWrapperBase(task host process) with in-host execution start/stop events.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/MSBuild/OutOfProcTaskAppDomainWrapperBase.cs | Emits ETW start/stop around wrappedTask.Execute() inside the task host process. |
| src/Framework/MSBuildEventSource.cs | Defines new ETW events for task-host dispatch and in-host execution timing. |
| src/Build/Instance/TaskFactories/TaskHostTask.cs | Emits ETW start/stop around the main-process task-host roundtrip. |
AR-May
approved these changes
Apr 20, 2026
rainersigwald
approved these changes
Apr 20, 2026
OvesN
enabled auto-merge (squash)
April 21, 2026 11:13
DustinCampbell
pushed a commit
to DustinCampbell/msbuild
that referenced
this pull request
Apr 23, 2026
Fixes dotnet#13372 ## Context In multi-threaded (MT) mode, tasks that are not marked with `[MSBuildMultiThreadableTask]` are routed to an out-of-proc **task host** process for isolation. This introduces overhead from IPC serialization, process communication, environment setup, assembly loading, and parameter marshaling - all on top of the actual task execution time. This PR uses ETW instrumentation to measure that overhead and provides tooling to analyze the results. ## Changes Made ### Added ETW events to `MSBuildEventSource` (events 107–110) | Event ID | Name | Location | What it measures | |----------|------|----------|------------------| | 107 | `TaskHostDispatchStart` | `TaskHostTask.Execute()` (main process) | Start of full task host roundtrip | | 108 | `TaskHostDispatchStop` | `TaskHostTask.Execute()` (main process) | End of full roundtrip (includes IPC + remote execution) | | 109 | `TaskExecuteInHostStart` | `OutOfProcTaskAppDomainWrapperBase` (task host process) | Start of actual `task.Execute()` | | 110 | `TaskExecuteInHostStop` | `OutOfProcTaskAppDomainWrapperBase` (task host process) | End of actual `task.Execute()` | --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Jan Provazník <janprovaznik@microsoft.com>
OvesN
added a commit
that referenced
this pull request
Jul 8, 2026
Fixes #14097 ## Context Out-of-proc task hosts re-send the same **build-invariant** data in every `TaskHostConfiguration` (and echo the environment back in every `TaskHostTaskComplete`). Two payloads dominate this redundancy on large `-mt` builds: 1. The **build process environment** (~6 KB), shipped forward in every config and back in every result. 2. On solution builds, the **`CurrentSolutionConfigurationContents`** global property — an XML blob describing every project's configuration (~57 KB on OrchardCore) — shipped forward in every config. It is ~99% of the global-properties payload and ~48% of all task-host config bytes. Both are invariant for the vast majority of tasks, yet re-transmitted in full to connections that already received them. ## Changes Made - **Send the environment as a delta.** Under a negotiated wire format (PacketVersion 5) the environment is sent in full only once per task-host connection; subsequent packets whose environment is unchanged carry a 1-byte "identical" marker instead of the full dictionary, on **both** the forward (config) and return (result) paths. The receiver reconstructs it from the last full environment seen on that connection. - **Deduplicate the solution configuration blob.** Solution (`.sln`/`.slnx`) builds inject `CurrentSolutionConfigurationContents` as a global property on every project. It is build-invariant (it describes the *solution*, not the project), but it is re-serialized into every `TaskHostConfiguration`. It cannot simply be dropped — a task may read it at runtime via `IBuildEngine6.GetGlobalProperties()` — so the same per-connection delta is applied (PacketVersion 65: the value is carried in its **own field**, excluded from the global-properties dictionary on the wire (so the large blob is never serialized twice), preceded by a 1-byte Full/Identical marker. It is sent once per connection, then only a marker; the task host reconstructs it from the connection baseline. - **Skip redundant per-task environment apply/restore** in the task host when the next task's environment is unchanged from the previous one. - **Bug fixes:** - *Self-clear:* feeding the reused (unchanged) environment back into `SetEnvironment` cleared it, because in `-mt` it was the driver's own backing dictionary. Fixed with a reference-equality guard. ## Testing - Round-trip serialization tests for the new Full/Identical wire forms: environment and `CurrentSolutionConfigurationContents` at PacketVersion 5 - `-mt` end-to-end regression tests: environment observed across consecutive task-host tasks, and an environment change made by one task-host task is observed by the next (verified failing before the aliasing fix, passing after). - Validated end-to-end: full build incl. net35, and OrchardCore.slnx `-mt` Rebuild succeeds. - Existing task-host test suites pass. ## Measurements **Workload:** OrchardCore (`OrchardCore.slnx`), `-t:Rebuild -mt` — 17,975 task-host task invocations. Serialized payload sizes measured with [ThProfile](2b19f76#diff-25ae7637236c1204a382324f95effa1822c33fcf7e805daea2e08d3faf185d02) (write-side stream-position deltas; deterministic, single run). Baseline = upstream `main`; patched = this PR. | Task-host IPC payload | Baseline | This PR | Saved | | --- | --- | --- | --- | | Build process environment (forward + return) | 122.0 MB | 0.1 MB | **≈ 122 MB** | | `CurrentSolutionConfigurationContents` (forward) | 647.4 MB | 1.8 MB | **≈ 646 MB** | | **Total task-host IPC** (config + result) | **≈ 1,548 MB** | **≈ 764 MB** | **≈ 785 MB (−51%)** | Per direction: | Field | Baseline | This PR | Reduction | | --- | --- | --- | --- | | `TaskHostConfiguration` total (forward) | 1,342 MB | 620 MB | **−54%** | | ↳ environment | 61.0 MB | 0.1 MB | −99.8% | | ↳ `CurrentSolutionConfigurationContents` | 647.4 MB | 1.8 MB | −99.7% | | ↳ global properties (incl. the blob) | 653.6 MB | 7.9 MB | −98.8% | | `TaskHostTaskComplete` total (return) | 206.4 MB | 143.3 MB | **−31%** (all from the environment) | The environment was ~4.6% of every config packet and ~29.7% of every result packet; `CurrentSolutionConfigurationContents` was ~48% of every config packet. After the change each carries only a 1-byte "unchanged" marker once the connection baseline is established. The remaining config payload is dominated by per-task task parameters, which are not invariant and so are not deduplicated. ### Wall-clock / overhead Sending the environment as a delta, deduplicating the solution configuration, and skipping the redundant per-task environment apply/restore did **not** measurably change total wall-clock time or task-host overhead in my measurements — the IPC savings are below the run-to-run noise floor on my machine. This is an IPC-volume optimization (fewer bytes serialized/transmitted/deserialized), not a critical-path one. I measured TaskHost overhead on OrchardCore after restore on my machine three times and took the average, using this [guide](#13555 (comment)) to compute task host overhead. (Note: in this guide all `MSBuildMultiThreadableTask` parameters were deliberately deleted; I kept them, of course.) The measurement showed no significant improvement on total time for task and overhead: Baseline: <img width="2361" height="1034" alt="baseline" src="https://github.com/user-attachments/assets/a95cfa2e-2af3-47ae-8be4-51a069a06e65" /> Patched version: <img width="2365" height="1034" alt="patched" src="https://github.com/user-attachments/assets/b9c98c88-f00d-4244-843a-a6fa6ac6e303" /> | Metric | Before → After | Delta | Surface reading | | --- | --- | --- | --- | | Total task-host time | 871,916 → 1,008,858 ms | **+136,942 ms (+15.7%)** | looks like regression | | Overhead share | 58.6% → 54.4% | **−4.2 pp (−7.2% rel.)** | looks like improvement | Both deltas are likely noise (the two metrics point in opposite directions, and `execute_ms` — task work this change cannot affect — moved by a similar magnitude, indicating machine-load drift rather than a real effect). --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

Fixes #13372
Context
In multi-threaded (MT) mode, tasks that are not marked with
[MSBuildMultiThreadableTask]are routed to an out-of-proc task host process for isolation. This introduces overhead from IPC serialization, process communication, environment setup, assembly loading, and parameter marshaling - all on top of the actual task execution time.This PR uses ETW instrumentation to measure that overhead and provides tooling to analyze the results.
Changes Made
Added ETW events to
MSBuildEventSource(events 109–112)TaskHostDispatchStartTaskHostTask.Execute()(main process)TaskHostDispatchStopTaskHostTask.Execute()(main process)TaskExecuteInHostStartOutOfProcTaskAppDomainWrapperBase(task host process)task.Execute()TaskExecuteInHostStopOutOfProcTaskAppDomainWrapperBase(task host process)task.Execute()How the events map to the execution flow
How to compute overhead
Both Stop events include
DURATION_MSEC(auto-calculated by ETW). For each task type.Dispatch events (main process) and ExecuteInHost events (task host process) are in different processes on different threads — there is no shared ActivityID linking a specific dispatch to its corresponding execute. Instead, we aggregate durations per task type:
The script validates that dispatch and execute event counts match per task type. If counts differ, a warning is printed.
CSV column definitions (
task_host_summary.csv)task_nametaskNamefield in ETW eventdispatch_countTaskHostDispatch/Stopevents for this task typeexecute_countTaskExecuteInHost/Stopevents for this task type. Should equaldispatch_count; If not equal it mean that event had a missing duration time.total_dispatch_msDURATION_MSECfrom allTaskHostDispatch/Stopevents — total wall-clock time in the main process (includes IPC + remote execution + result retrieval)total_execute_msDURATION_MSECfrom allTaskExecuteInHost/Stopevents — total time inside the task host runningtask.Execute()total_overhead_mstotal_dispatch_ms − total_execute_ms— time spent on IPC, serialization, environment setup, assembly loading, and parameter marshalingavg_dispatch_mstotal_dispatch_ms / dispatch_count— average dispatch roundtrip per invocationavg_execute_mstotal_execute_ms / execute_count— average actual task execution per invocation (uses its own count to avoid cross-dividing when counts differ)avg_overhead_mstotal_overhead_ms / dispatch_count— average overhead per invocationoverhead_pcttotal_overhead_ms / total_dispatch_ms × 100— percentage of dispatch time that is overheadHow to Collect Measurements
1. Delete all
[MSBuildMultiThreadableTask]from tasks2. Build MSBuild with these changes
.\build.cmd3. Clean up previous build
4. Collect ETW trace with PerfView
5. Export events from PerfView
Open the
.etlin PerfView → Events tab → filterMicrosoft-Build→ select event types (TaskHostDispatchStart/Stop, TaskExecuteInHostStart/stop) → Make sure that MaxRet parameter is set to all events → Load view → Save View As CSV.6. Run analysis scripts
parse_overhead.py
python parse_overhead.py # produces task_host_summary.csv