Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Environment.WorkingSet to improve performance of Process.Metrics #2286

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

danespinosa
Copy link

@danespinosa danespinosa commented Nov 1, 2024

Fixes #
Design discussion issue #

Based on traces GetProcessInfos which is called by DiagnosticsGetProcess has a big perf hit. Proposing using Environment.WorkingSet to improve the performance of this API.

Changes

Please provide a brief description of the changes here.
This PR replaces the usage of a new Process to get its working set in favor of using Environment.WorkingSet instead. See benchmark below.
image

Merge requirement checklist

  • CONTRIBUTING guidelines followed (license requirements, nullable enabled, static analysis, etc.)
  • Unit tests added/updated
  • Appropriate CHANGELOG.md files updated for non-trivial changes
  • Changes in public API reviewed (if applicable)

Based on traces GetProcessInfos which is called by DiagnosticsGetProcess has a big perf hit since the process doesn't really change caching it on a static variable will avoid this big perf hit
@danespinosa danespinosa requested a review from a team as a code owner November 1, 2024 23:49
Copy link

linux-foundation-easycla bot commented Nov 1, 2024

CLA Missing ID CLA Not Signed

@github-actions github-actions bot added the comp:instrumentation.process Things related to OpenTelemetry.Instrumentation.Process label Nov 1, 2024
static ProcessMetrics()
{
MeterInstance.CreateObservableUpDownCounter(
"process.memory.usage",
() =>
{
using var process = Diagnostics.Process.GetCurrentProcess();
return process.WorkingSet64;
return Process.WorkingSet64;
Copy link
Contributor

@haipz haipz Nov 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Process.WorkingSet64 won't change unless call Process.Refresh() before accessing the property(WorkingSet64).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right @haipz probably the class will need some redesign, e.g. maybe the ProcessInstrumentationOptions could accept a refresh rate at which we could refresh the Process. I will try to get ahold of @Yun-Ting to discuss this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An option to this is using Environment.WorkingSet which has no perf implications and displays the same data with minimal change
image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @danespinosa, I've gone down the cache path before. Check out the discussions here: #718.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for sharing @Yun-Ting i see that your implementation got updated intending to solve an issue but it really didn't solve the issue perse. Here is the commit [Instrumentation.Process, Resources.Process] Properly disposes of Sys… · open-telemetry/opentelemetry-dotnet-contrib@55c0dfb and here is the issue that explains at the bottom that actually the issue persists Properly dispose instances of System.Diagnostics.Process class · Issue #2100 · open-telemetry/opentelemetry-dotnet-contrib.

My 2 suggestions and 2 cents, could we revert that PR and bring back your implementation or can we at least update the process.WorkingSet64 call to instead calling Environment.WorkingSet? This will bring huge gains
image

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@noahfalk I was chatting with @Yun-Ting regarding the perf or this library and we were wondering if we could get your thoughts about this change.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a fine change to me.

Looking at #718 I see that it relied on some benchmarking in #717. The benchmarking tested various ways of running the Refresh() or GetCurrentProcess() in isolation and then #718 extrapolated that if the calls were fast in isolation then it would also be fast in the process metrics scenarios where we invoke various property getters on the Process object afterwards. Unforetunately that isn't the case. The property getters populate data lazily if it isn't already populated and that winds up being the expensive part of the operation. Calling GetCurrentProcess() returns a process object where the values haven't been initialized and Refresh() only invalidates the cached data but doesn't fetch new data. Calling GetCurrentProcess() or Refresh() repeatedly costs very little. Also calling WorkingSet on an initialized Process object costs very little. But the first call to WorkingSet after Refresh() or GetCurrentProcess() is comparatively very expensive:

| RefreshOnly                    |         2.017 ns |      0.0305 ns |       0.0270 ns |
| GetCurrentProcessOnly          |        47.814 ns |      0.9146 ns |       1.1567 ns |
| WorkingSetOnly                 |         1.110 ns |      0.0231 ns |       0.0216 ns |
| RefreshAndWorkingSet           | 3,241,750.985 ns | 58,170.7094 ns | 109,258.7011 ns |
| GetCurrentProcessAndWorkingSet | 3,243,749.014 ns | 63,792.7044 ns |  75,940.6834 ns |

Benchmark code:

    public class ProcessBenchmarks
    {
        private static Process s_process = Process.GetCurrentProcess();


        [Benchmark]
        public void RefreshOnly()
        {
            s_process.Refresh();
        }

        [Benchmark]
        public Process GetCurrentProcessOnly()
        {
            return Process.GetCurrentProcess();
        }

        [Benchmark]
        public long WorkingSetOnly()
        {
            return s_process.WorkingSet64;
        }

        [Benchmark]
        public long RefreshAndWorkingSet()
        {
            s_process.Refresh();
            return s_process.WorkingSet64;
        }

        [Benchmark]
        public long GetCurrentProcessAndWorkingSet()
        {
            return Process.GetCurrentProcess().WorkingSet64;
        }
    }

In the .NET source here this is the cache check. If processInfo == null the property getter will take milliseconds, if its already cached the getter will be nanoseconds. Hope that helps!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change all good to merge then?

@danespinosa danespinosa changed the title Cache Process object to avoid performance hit Use Environment.WorkingSet to improve performance of Process.Metrics Nov 7, 2024
Copy link
Contributor

This PR was marked stale due to lack of activity. It will be closed in 7 days.

@github-actions github-actions bot added the Stale label Nov 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
comp:instrumentation.process Things related to OpenTelemetry.Instrumentation.Process Stale
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants