{"id":257172,"date":"2026-09-08T09:00:22","date_gmt":"2026-09-08T16:00:22","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/visualstudio\/?p=257172"},"modified":"2026-08-27T18:14:18","modified_gmt":"2026-08-28T01:14:18","slug":"today-i-will-find-hidden-latency-across-a-distributed-net-application","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/visualstudio\/today-i-will-find-hidden-latency-across-a-distributed-net-application\/","title":{"rendered":"Today I will&#8230; find hidden latency across a distributed .NET application"},"content":{"rendered":"<p>When a distributed application feels slow, the user sees one delay. The code behind that delay may run across a web frontend, backend services, databases, and external APIs. If you profile only the frontend while the backend is slow, the profiler can show that the frontend is healthy without revealing the actual bottleneck.<\/p>\n<p>This article shows how to narrow a cross-process performance problem with Visual Studio: identify the process that owns the slow operation, capture its CPU activity, inspect the report, and add a focused measurement when CPU samples cannot explain elapsed time. GitHub Copilot Profiler Agent reinforces this workflow with detailed analysis of the captured profile and helps validate the interpretation of the evidence.<\/p>\n<p>The case study uses the <a href=\"https:\/\/aka.ms\/agentframework\/interviewcoach\">Interview Coach<\/a> sample, a .NET Aspire application with a Blazor frontend and several backend resources. The eventual cause is in an AI streaming path, but the investigation applies to any distributed .NET application where one user action crosses process boundaries.<\/p>\n<h2 id=\"one-symptom-several-processes\">One symptom, several processes<\/h2>\n<p>The Interview Coach Web UI sends a prompt to a backend agent and renders the response as a stream of updates. I tested it with this prompt:<\/p>\n<pre><code class=\"language-text\">Hi, I'm Peter. Here's my resume: https:\/\/justinyoo.github.io\/fake-resumes\/resume-peter-parker.pdf.\r\nAnd this is JD: https:\/\/justinyoo.github.io\/fake-resumes\/jd-cloud-solution-architect.pdf\r\n<\/code><\/pre>\n<p>The first complete response took roughly 1.5 to 2 minutes. Some of that time was expected: the first turn parses the PDF into Markdown and stores initial data in Cosmos DB. Even with that work in mind, the response felt slower than expected.<\/p>\n<p>One interaction crossed the Blazor Web UI, the agent, MCP servers, Cosmos DB, and a hosted model. Before looking for a slow method, I needed to establish which process was doing work and which process was waiting.<\/p>\n<p>Visual Studio&#8217;s <a href=\"https:\/\/learn.microsoft.com\/visualstudio\/profiling\/what-is-a-profiler\">Performance Profiler<\/a> was a good place to start.<\/p>\n<h2 id=\"define-the-question-before-collecting-data\">Define the question before collecting data<\/h2>\n<p>I reduced the investigation to two questions:<\/p>\n<ol>\n<li>Which process owned the slow part of the interaction?<\/li>\n<li>Once that process was isolated, was it computing or waiting?<\/li>\n<\/ol>\n<p>AppHost starts the Web UI, agent, MCP servers, and data services as separate processes. A profile of <code>InterviewCoach.AppHost<\/code> describes the orchestrator. It does not automatically describe the Blazor code in <code>InterviewCoach.WebUI<\/code>.<\/p>\n<p>CPU Usage can collect from multiple processes, which is useful for an initial system-wide view. Here, the visible slowdown occurred while the Web UI consumed and rendered updates, so I wanted a focused capture of that process. I kept the backend resources under Aspire and launched the Web UI as the Visual Studio profiling target.<\/p>\n<h2 id=\"target-the-process-that-owns-the-work\">Target the process that owns the work<\/h2>\n<p>This walkthrough assumes the sample is configured for <a href=\"https:\/\/aspire.dev\/integrations\/cloud\/azure\/local-provisioning\/\">local Azure provisioning<\/a> and already runs successfully from Visual Studio.<\/p>\n<p>Set <code>InterviewCoach.AppHost<\/code> as the startup project, select its HTTPS launch profile, and choose <strong>Debug &gt; Start Without Debugging<\/strong>. Starting without the debugger leaves AppHost running when the startup project changes later. Once the Aspire resources are ready, stop only the Aspire-managed <code>webui<\/code> resource from the Aspire dashboard. Leave the agent and its dependencies running.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/visualstudio\/wp-content\/uploads\/sites\/4\/2026\/08\/image-01-scaled.webp\" alt=\"Aspire dashboard with the backend resources running\" \/><\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/visualstudio\/wp-content\/uploads\/sites\/4\/2026\/08\/image-02-scaled.webp\" alt=\"Aspire dashboard showing the stopped Web UI\" \/><\/p>\n<p>The standalone Web UI needs a launch profile that uses its own ports and points service discovery at the running agent. In this capture, the agent&#8217;s HTTPS endpoint used port 7048. Aspire can assign a different port after a restart, so use the value shown in the dashboard.<\/p>\n<p>Add a <code>Profiler<\/code> profile under <code>profiles<\/code> in <code>src\/InterviewCoach.WebUI\/Properties\/launchSettings.json<\/code>:<\/p>\n<pre><code class=\"language-json\">\"Profiler\": {\r\n  \"commandName\": \"Project\",\r\n  \"dotnetRunMessages\": true,\r\n  \"launchBrowser\": true,\r\n  \"applicationUrl\": \"https:\/\/localhost:7201;http:\/\/localhost:5088\",\r\n  \"environmentVariables\": {\r\n    \"ASPNETCORE_ENVIRONMENT\": \"Development\",\r\n    \"Services__agent__https__0\": \"localhost:7048\"\r\n  }\r\n}\r\n<\/code><\/pre>\n<p>The Web UI addresses the agent through a logical service name:<\/p>\n<pre><code class=\"language-csharp\">client.BaseAddress = new Uri(\"https+http:\/\/agent\");\r\n<\/code><\/pre>\n<p>The environment variable maps to <code>Services:agent:https:0<\/code>. The configuration-based service discovery provider resolves <code>localhost:7048<\/code> as the HTTPS endpoint for <code>agent<\/code>. The value contains the host and port, not the <code>https:\/\/<\/code> prefix.<\/p>\n<p>In Visual Studio:<\/p>\n<ol>\n<li>Set <code>InterviewCoach.WebUI<\/code> as the startup project.<\/li>\n<li>Select the <code>Profiler<\/code> launch profile.<\/li>\n<li>Set the build configuration to <code>Release<\/code>.<\/li>\n<li>Open <strong>Debug &gt; Performance Profiler<\/strong>, or press <strong>Alt+F2<\/strong>.<\/li>\n<li>Confirm that the target is <code>InterviewCoach.WebUI<\/code>.<\/li>\n<li>Select <strong>CPU Usage<\/strong>.<\/li>\n<li>Select <strong>Start with collection paused<\/strong>.<\/li>\n<\/ol>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/visualstudio\/wp-content\/uploads\/sites\/4\/2026\/08\/image-03-scaled.webp\" alt=\"Visual Studio Performance Profiler\" \/><\/p>\n<p>Start the application. When the Web UI is ready, resume collection, submit the test prompt, and stop collection when the response finishes.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/visualstudio\/wp-content\/uploads\/sites\/4\/2026\/08\/image-04-scaled.webp\" alt=\"CPU Usage results\" \/><\/p>\n<h2 id=\"read-the-cpu-report-before-asking-copilot\">Read the CPU report before asking Copilot<\/h2>\n<p>Start with the CPU timeline. Select only the interval from submitting the prompt to receiving the final update. This removes startup and unrelated activity from the details below the chart.<\/p>\n<p>Next, open <strong>Call Tree<\/strong> and enable <strong>Just My Code<\/strong>. The columns answer different questions:<\/p>\n<ul>\n<li><strong>Total CPU<\/strong> includes CPU samples in a method and everything it called.<\/li>\n<li><strong>Self CPU<\/strong> includes samples attributed directly to that method.<\/li>\n<\/ul>\n<p>Use <strong>Expand Hot Path<\/strong> to follow the most CPU-intensive branch, then search for <code>GetStreamingResponseAsync<\/code> to find the Web UI&#8217;s streaming path. The <strong>Functions<\/strong> view is also useful for sorting application methods by Total CPU or Self CPU without navigating the entire tree.<\/p>\n<p>In this capture, the selected interval reported 6.7% CPU usage, and the Call Tree did not show a dominant application hot path that accounted for the long response. That did not prove what caused the delay. It told me something narrower: the Web UI was not CPU-bound during the selected interval.<\/p>\n<p>This distinction matters. CPU Usage samples active processor work. Time spent awaiting I\/O, a timer, a lock, or another service can make the user wait without appearing as a CPU hot path. With no CPU hot path explaining the elapsed time, the next step was to inspect the code along the streaming path for an explicit wait.<\/p>\n<h2 id=\"follow-the-evidence-into-the-streaming-loop\">Follow the evidence into the streaming loop<\/h2>\n<p>The response loop in <code>src\/InterviewCoach.WebUI\/Components\/Pages\/Chat\/Chat.razor<\/code> contained a useful clue:<\/p>\n<pre><code class=\"language-csharp\">await foreach (var update in ChatClient.GetStreamingResponseAsync(\r\n    outboundMessages,\r\n    chatOptions,\r\n    cancellationToken))\r\n{\r\n    await Task.Delay(50);\r\n\r\n    messages.AddMessages(update, filter: c =&gt; c is not TextContent);\r\n\r\n    if (update.Role == ChatRole.Assistant)\r\n    {\r\n        responseText.Text += update.Text;\r\n        ChatMessageItem.NotifyChanged(responseMessage);\r\n    }\r\n\r\n    StateHasChanged();\r\n}\r\n<\/code><\/pre>\n<p>This line pauses every streaming update:<\/p>\n<pre><code class=\"language-csharp\">await Task.Delay(50);\r\n<\/code><\/pre>\n<p>If a response arrives in 200 updates, the nominal accumulated delay is 10 seconds:<\/p>\n<pre><code class=\"language-text\">200 updates x 50 ms = 10,000 ms\r\n<\/code><\/pre>\n<p>The delay appears to pace updates before the UI refreshes. Regardless of why it was added, its cost grows linearly with the number of updates.<\/p>\n<h3 id=\"measure-the-accumulated-wait\">Measure the accumulated wait<\/h3>\n<p>CPU Usage cannot measure elapsed time spent awaiting <code>Task.Delay<\/code>. To capture that value, add <code>@using System.Diagnostics<\/code> to <code>Chat.razor<\/code> and place temporary counters around the existing loop:<\/p>\n<pre><code class=\"language-csharp\">var responseTimer = Stopwatch.StartNew();\r\nvar measuredDelay = TimeSpan.Zero;\r\nvar updateCount = 0;\r\n\r\nawait foreach (var update in ChatClient.GetStreamingResponseAsync(\r\n    outboundMessages,\r\n    chatOptions,\r\n    cancellationToken))\r\n{\r\n    updateCount++;\r\n\r\n    var delayStarted = Stopwatch.GetTimestamp();\r\n    await Task.Delay(50);\r\n    measuredDelay += Stopwatch.GetElapsedTime(delayStarted);\r\n\r\n    \/\/ Existing update handling...\r\n}\r\n\r\nLogger.LogInformation(\r\n    \"Stream completed in {ElapsedMs} ms across {UpdateCount} updates; \"\r\n    + \"artificial delay consumed {DelayMs} ms\",\r\n    responseTimer.Elapsed.TotalMilliseconds,\r\n    updateCount,\r\n    measuredDelay.TotalMilliseconds);\r\n<\/code><\/pre>\n<p>The code logs once after the response completes:<\/p>\n<pre><code class=\"language-text\">Stream completed in {{ total elapsed time }} ms across {{ number of streaming updates }} updates; artificial delay consumed {{ measured delay time }} ms\r\n<\/code><\/pre>\n<p>These are streaming updates, not model tokens. One response update does not necessarily represent one token.<\/p>\n<p><code>Task.Delay(50)<\/code> guarantees a minimum wait, not an exact resumption time. Thread scheduling and other work can make the measured interval longer than 50 ms.<\/p>\n<p>I ran the same prompt five times.<\/p>\n<h3 id=\"remove-the-delay-and-repeat\">Remove the delay and repeat<\/h3>\n<p>For comparison, I removed <code>await Task.Delay(50)<\/code> and ran the same prompt five more times. I left the timestamp calls in place temporarily so both versions produced the same log format. Without the awaited delay between them, those calls measure only instrumentation overhead.<\/p>\n<p>The live model can produce a different response and update count on every run. Total response time therefore includes model and network variation. It is useful user-experience context, but the delay counter is the measurement that isolates the Web UI&#8217;s artificial wait.<\/p>\n<h2 id=\"use-profiler-agent-to-deepen-the-analysis\">Use Profiler Agent to deepen the analysis<\/h2>\n<p>After reading the report and measuring the suspected wait, I brought up <a href=\"https:\/\/learn.microsoft.com\/visualstudio\/profiling\/profile-with-copilot-agent\">GitHub Copilot Profiler Agent<\/a> to analyze the CPU session in more detail and validate my interpretation. I gave it the method under investigation and the counter values:<\/p>\n<pre><code class=\"language-text\">@Profiler Review this CPU Usage session for InterviewCoach.WebUI. The response contained 193 streaming updates, took 84,813.9868 ms, and accumulated approximately 11,871 ms inside Task.Delay(50), or 61.5088 ms per update. Does the report show significant CPU work in GetStreamingResponseAsync, or is the elapsed time consistent with asynchronous waiting?\r\n<\/code><\/pre>\n<p>The question is deliberately narrow. Profiler Agent can help interpret the report, but the CPU profile still cannot measure the elapsed wait by itself.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/visualstudio\/wp-content\/uploads\/sites\/4\/2026\/08\/image-05-scaled.webp\" alt=\"Profiler Agent view\" \/><\/p>\n<p>Profiler Agent reached the same supporting interpretation: response processing was not CPU-bound. The direct timer remained the evidence for the accumulated delay.<\/p>\n<h2 id=\"what-the-measurements-showed\">What the measurements showed<\/h2>\n<h3 id=\"baseline-with-task.delay50\">Baseline with <code>Task.Delay(50)<\/code><\/h3>\n<table>\n<thead>\n<tr>\n<th style=\"text-align: right;\">Run<\/th>\n<th style=\"text-align: right;\">Total elapsed (ms)<\/th>\n<th style=\"text-align: right;\">Update count<\/th>\n<th style=\"text-align: right;\">Expected delay (ms)<\/th>\n<th style=\"text-align: right;\">Measured delay (ms)<\/th>\n<th style=\"text-align: right;\">Delay\/update (ms)<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"text-align: right;\">1<\/td>\n<td style=\"text-align: right;\">92386.3482<\/td>\n<td style=\"text-align: right;\">168<\/td>\n<td style=\"text-align: right;\">8400<\/td>\n<td style=\"text-align: right;\">10299.8094<\/td>\n<td style=\"text-align: right;\">61.3084<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\">2<\/td>\n<td style=\"text-align: right;\">84772.6630<\/td>\n<td style=\"text-align: right;\">188<\/td>\n<td style=\"text-align: right;\">9400<\/td>\n<td style=\"text-align: right;\">11549.6042<\/td>\n<td style=\"text-align: right;\">61.4341<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\">3<\/td>\n<td style=\"text-align: right;\">90705.5531<\/td>\n<td style=\"text-align: right;\">153<\/td>\n<td style=\"text-align: right;\">7650<\/td>\n<td style=\"text-align: right;\">9376.8390<\/td>\n<td style=\"text-align: right;\">61.2865<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\">4<\/td>\n<td style=\"text-align: right;\">79027.4327<\/td>\n<td style=\"text-align: right;\">147<\/td>\n<td style=\"text-align: right;\">7350<\/td>\n<td style=\"text-align: right;\">9028.3215<\/td>\n<td style=\"text-align: right;\">61.4172<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\">5<\/td>\n<td style=\"text-align: right;\">69061.6113<\/td>\n<td style=\"text-align: right;\">190<\/td>\n<td style=\"text-align: right;\">9500<\/td>\n<td style=\"text-align: right;\">11714.1005<\/td>\n<td style=\"text-align: right;\">61.6532<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\">AVG<\/td>\n<td style=\"text-align: right;\">83190.7217<\/td>\n<td style=\"text-align: right;\">169<\/td>\n<td style=\"text-align: right;\">8460<\/td>\n<td style=\"text-align: right;\">10393.7349<\/td>\n<td style=\"text-align: right;\">61.4199<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\">MED<\/td>\n<td style=\"text-align: right;\">84772.6630<\/td>\n<td style=\"text-align: right;\">168<\/td>\n<td style=\"text-align: right;\">8400<\/td>\n<td style=\"text-align: right;\">10299.8094<\/td>\n<td style=\"text-align: right;\">61.4172<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The five runs recorded between 9.03 and 11.71 seconds inside the artificial delay. The median accumulated delay was 10.30 seconds.<\/p>\n<p>The measured interval averaged 61.42 ms per update, about 11 ms longer than the nominal 50 ms. The accumulated wait, rather than the difference between 50 ms and 61 ms, is the performance issue.<\/p>\n<h3 id=\"after-removing-task.delay50\">After removing <code>Task.Delay(50)<\/code><\/h3>\n<table>\n<thead>\n<tr>\n<th style=\"text-align: right;\">Run<\/th>\n<th style=\"text-align: right;\">Total elapsed (ms)<\/th>\n<th style=\"text-align: right;\">Update count<\/th>\n<th style=\"text-align: right;\">Measured timer overhead (ms)<\/th>\n<th style=\"text-align: right;\">Overhead\/update (ms)<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"text-align: right;\">1<\/td>\n<td style=\"text-align: right;\">98450.7463<\/td>\n<td style=\"text-align: right;\">226<\/td>\n<td style=\"text-align: right;\">0.0262<\/td>\n<td style=\"text-align: right;\">0.0001<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\">2<\/td>\n<td style=\"text-align: right;\">69886.9730<\/td>\n<td style=\"text-align: right;\">174<\/td>\n<td style=\"text-align: right;\">0.0155<\/td>\n<td style=\"text-align: right;\">0.0001<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\">3<\/td>\n<td style=\"text-align: right;\">77264.4819<\/td>\n<td style=\"text-align: right;\">289<\/td>\n<td style=\"text-align: right;\">0.0256<\/td>\n<td style=\"text-align: right;\">0.0001<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\">4<\/td>\n<td style=\"text-align: right;\">58254.1312<\/td>\n<td style=\"text-align: right;\">163<\/td>\n<td style=\"text-align: right;\">0.0171<\/td>\n<td style=\"text-align: right;\">0.0001<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\">5<\/td>\n<td style=\"text-align: right;\">86431.5633<\/td>\n<td style=\"text-align: right;\">164<\/td>\n<td style=\"text-align: right;\">0.0257<\/td>\n<td style=\"text-align: right;\">0.0002<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\">AVG<\/td>\n<td style=\"text-align: right;\">78057.5791<\/td>\n<td style=\"text-align: right;\">203<\/td>\n<td style=\"text-align: right;\">0.0220<\/td>\n<td style=\"text-align: right;\">0.0001<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\">MED<\/td>\n<td style=\"text-align: right;\">77264.4819<\/td>\n<td style=\"text-align: right;\">174<\/td>\n<td style=\"text-align: right;\">0.0256<\/td>\n<td style=\"text-align: right;\">0.0001<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The tiny measured intervals in this table are timestamp overhead. They do not represent the model, network, rendering, or end-to-end processing time for each update.<\/p>\n<p>The median total response fell from 84.77 seconds to 77.26 seconds, a difference of about 7.51 seconds. That comparison is not a controlled benchmark because the live model produced different responses and update counts. The direct result is simpler: removing the line removed the 9 to 12 seconds of application-added waiting measured in the baseline runs.<\/p>\n<h2 id=\"remove-the-wait-not-the-stream\">Remove the wait, not the stream<\/h2>\n<p>The profiler investigation worked only after it targeted the process that owned the code. AppHost was useful for orchestrating the agent and its dependencies, while the standalone Web UI gave Visual Studio an unambiguous profiling target.<\/p>\n<p>CPU Usage then answered one part of the question: the captured response was not dominated by application CPU work. It could not expose the accumulated asynchronous wait, so a direct timer around <code>Task.Delay(50)<\/code> supplied the missing evidence.<\/p>\n<p>The measurements do not support keeping an unconditional 50 ms delay on every update. The response still streams after the delay is removed. If the UI needs deliberate pacing, that behavior should be designed and measured separately rather than tying latency to the number of incoming updates.<\/p>\n<p>One option is to consume updates immediately but coalesce UI refreshes. This example renders at most once every 50 ms and always flushes the final update:<\/p>\n<pre><code class=\"language-csharp\">\/\/ Limit UI refreshes to at most once every 50 ms without delaying stream consumption.\r\nvar renderInterval = TimeSpan.FromMilliseconds(50);\r\nvar lastRenderAt = Stopwatch.GetTimestamp();\r\nvar renderPending = false;\r\nvar textChanged = false;\r\n\r\ntry\r\n{\r\n    await foreach (var update in ChatClient.GetStreamingResponseAsync(\r\n        outboundMessages,\r\n        chatOptions,\r\n        cancellationToken))\r\n    {\r\n        messages.AddMessages(update, filter: c =&gt; c is not TextContent);\r\n        renderPending = true;\r\n\r\n        if (update.Role == ChatRole.Assistant &amp;&amp;\r\n            !string.IsNullOrEmpty(update.Text))\r\n        {\r\n            responseText.Text += update.Text;\r\n            textChanged = true;\r\n        }\r\n\r\n        \/\/ Flush pending UI changes once the render interval has elapsed.\r\n        if (Stopwatch.GetElapsedTime(lastRenderAt) &lt; renderInterval)\r\n        {\r\n            continue;\r\n        }\r\n\r\n        FlushRender();\r\n        lastRenderAt = Stopwatch.GetTimestamp();\r\n    }\r\n}\r\nfinally\r\n{\r\n    FlushRender();\r\n}\r\n\r\nvoid FlushRender()\r\n{\r\n    if (!renderPending)\r\n    {\r\n        return;\r\n    }\r\n\r\n    if (textChanged)\r\n    {\r\n        ChatMessageItem.NotifyChanged(responseMessage);\r\n    }\r\n\r\n    StateHasChanged();\r\n    renderPending = false;\r\n    textChanged = false;\r\n}\r\n<\/code><\/pre>\n<p>Unlike <code>Task.Delay(50)<\/code>, this does not pause each incoming update. Bursts are combined into fewer renders, while slower updates still appear as they arrive. The 50 ms refresh interval is a starting point, not a fixed recommendation; profile it with the expected stream rate and adjust it based on responsiveness and render cost. This coalesced version was not part of the measurements above, so it needs its own before-and-after run.<\/p>\n<p>Fifty milliseconds looked cheap in isolation. Repeated across a real response, it became roughly 10 seconds of avoidable waiting.<\/p>\n<h2 id=\"references\">References<\/h2>\n<ul>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/extensions\/service-discovery\">Service discovery in .NET<\/a><\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/visualstudio\/profiling\/cpu-usage\">Analyze performance by using CPU profiling<\/a><\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/visualstudio\/profiling\/profile-with-copilot-agent\">Profile your app with GitHub Copilot Profiler Agent<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>When a distributed application feels slow, the user sees one delay. The code behind that delay may run across a web frontend, backend services, databases, and external APIs. If you profile only the frontend while the backend is slow, the profiler can show that the frontend is healthy without revealing the actual bottleneck. This article [&hellip;]<\/p>\n","protected":false},"author":122298,"featured_media":257205,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[6888,6967,6868,155],"tags":[6924,7042,53,526,7019,6803],"class_list":["post-257172","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-copilot","category-github-copilot","category-performance","category-visual-studio","tag-github-copilot","tag-github-copilot-profiler-agent","tag-performance","tag-productivity","tag-visual-studio-2026","tag-visual-studio-profiler"],"acf":[],"blog_post_summary":"<p>When a distributed application feels slow, the user sees one delay. The code behind that delay may run across a web frontend, backend services, databases, and external APIs. If you profile only the frontend while the backend is slow, the profiler can show that the frontend is healthy without revealing the actual bottleneck. This article [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/visualstudio\/wp-json\/wp\/v2\/posts\/257172","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/visualstudio\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/visualstudio\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/visualstudio\/wp-json\/wp\/v2\/users\/122298"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/visualstudio\/wp-json\/wp\/v2\/comments?post=257172"}],"version-history":[{"count":2,"href":"https:\/\/devblogs.microsoft.com\/visualstudio\/wp-json\/wp\/v2\/posts\/257172\/revisions"}],"predecessor-version":[{"id":257185,"href":"https:\/\/devblogs.microsoft.com\/visualstudio\/wp-json\/wp\/v2\/posts\/257172\/revisions\/257185"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/visualstudio\/wp-json\/wp\/v2\/media\/257205"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/visualstudio\/wp-json\/wp\/v2\/media?parent=257172"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/visualstudio\/wp-json\/wp\/v2\/categories?post=257172"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/visualstudio\/wp-json\/wp\/v2\/tags?post=257172"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}