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

Reduce Environment.GetEnvironmentVariables calls on start-up #49856

Closed
wants to merge 3 commits into from

Conversation

benaadams
Copy link
Member

@benaadams benaadams commented Mar 19, 2021

Caching it privately here is safer than caching in Environment as the IDictionary is writable and at this level we know its not being modified.

Then throw the cache away at Gen2 as shouldn't hold onto it forever as its only looked at at start-up; but a WeakReference<T> would have the risk of discarding too early as there are likely many allocations at startup.

Before

image

After

image

And the allocated IDictionary (HashTable) is collected

image

As well as the Gen2Callback

image

Resolves #44733
Contributes to #44598

@ghost
Copy link

ghost commented Mar 19, 2021

Tagging subscribers to this area: @maryamariyan, @safern
See info in area-owners.md if you want to be subscribed.

Issue Details

Resolves #44733

Author: benaadams
Assignees: -
Labels:

area-Extensions-Configuration

Milestone: -

private static IDictionary CreateEnvironmentVariablesCache()
{
IDictionary environmentVariables;
s_environmentVariablesCache = environmentVariables = Environment.GetEnvironmentVariables();
Copy link
Contributor

Choose a reason for hiding this comment

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

This will misbehave if someone adds/sets an environment variable before Gen2 runs (which can be anytime). I think a better fix would be to cache it inside Environment class for Windows too.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think a better fix would be to cache it inside Environment class for Windows too.

If it was in Environment would it need to do a defensive copy on return since its returning an IDictionary that is writable; but unlinked from environment vars? That would mean if it was called only once it would create 2 IDictionarys which would be a regression; could alternatively make it a live IDictionary so changes are reflected in Environment, but that might be breaking?

This will misbehave if someone adds/sets an environment variable before Gen2 runs

Might be bigger issues there?

  • The ConfigurationProvider has a Set method; however EnvironmentVariablesConfigurationProvider doesn't call Environment.SetEnvironmentVariable if that set method is called but just updates that instance's copy.

  • The ConfigurationProvider also has an IChangeToken which says to update and reload the configuration however if Environment.SetEnvironmentVariable is called it doesn't reload and fire that event (unlike with file config), nor is there currently an Environment event that it can listen to trigger that reload of configuration.

@jkotas
Copy link
Member

jkotas commented Mar 19, 2021

Does this actually improve startup performance that is the primary metric that we care about?

This is replacing one cost (redundant short term allocation that will get collected quickly) with a different cost (dragging the allocation all the way to Gen2 and only allowing it to be collected then).

@benaadams
Copy link
Member Author

benaadams commented Mar 19, 2021

This is replacing one cost (redundant short term allocation that will get collected quickly)

I assume its also the reducing the multiple copies of the keys and value environment strings that get stashed in the configuration forever; the p/invokes to get them and the parsing of the environment block to produce them? /cc @stephentoub

// Find the = separating the key and value. We skip entries that begin with =. We also skip entries that don't
// have =, which can happen on some older OSes when the environment block gets corrupted.
int i = variable.IndexOf('=');
if (i > 0)
{
// Add the key and value.
string key = new string(variable.Slice(0, i));
string value = new string(variable.Slice(i + 1));
try
{
// Add may throw if the environment block was corrupted leading to duplicate entries.
// We allow such throws and eat them (rather than proactively checking for duplication)
// to provide a non-fatal notification about the corruption.
results.Add(key, value);
}
catch (ArgumentException) { }

Mono/Linux takes a different approach; which may be what @marek-safar #49856 (comment) was referring to and caches everything in a Dictionary<string, string> using that also for getting a single variable; and then returns a copy for all

private static void EnsureEnvironmentCached()
{
if (s_environment == null)
{
Interlocked.CompareExchange(ref s_environment, GetSystemEnvironmentVariables(), null);
}
}

@jkotas
Copy link
Member

jkotas commented Mar 19, 2021

Mono/Linux takes a different approach
caches everything in a Dictionary<string, string>

CoreCLR on Linux uses similar approach, except that the cache is in the Win32 PAL. It should be possible to switch CoreCLR to a similar plan on Linux, but not on Windows.

@benaadams
Copy link
Member Author

but not on Windows.

Why wouldn't it work on Windows? (For process level environment vars)

@jkotas
Copy link
Member

jkotas commented Mar 19, 2021

Code out there depends on environment changes done via SetEnvironmentVariable Win32 API to be visible via managed APIs, and vice versa.

@ghost
Copy link

ghost commented Mar 19, 2021

Tagging subscribers to this area: @eerhardt, @maryamariyan
See info in area-owners.md if you want to be subscribed.

Issue Details

Caching it privately here is safer than caching in Environment as the IDictionary is writable and at this level we know its not being modified.

Then throw the cache away at Gen2 as shouldn't hold onto it forever as its only looked at at start-up; but a WeakReference<T> would have the risk of discarding too early as there are likely many allocations at startup.

Before

image

After

image

And the allocated IDictionary (HashTable) is collected

image

As well as the Gen2Callback

image

Resolves #44733
Contributes to #44598

Author: benaadams
Assignees: -
Labels:

area-Extensions-Hosting

Milestone: -

@benaadams
Copy link
Member Author

Code out there depends on environment changes done via SetEnvironmentVariable Win32 API to be visible via managed APIs, and vice versa.

So wouldn't be able to cache in Environment itself to address this.

This is replacing one cost (redundant short term allocation that will get collected quickly) with a different cost (dragging the allocation all the way to Gen2 and only allowing it to be collected then).

Locally (for me) one call to Environment.GetEnvironmentVariables parses and creates about 15kB of strings; the configuration will hold them forever so its about a 30kB saving, and that's without passing connection strings in Env vars which are very chunky and one of the common ways to pass them:

image

@benaadams
Copy link
Member Author

Oh the 15kb might be 3 calls; so is only a saving of 10kB (and 10Kb less parsing)

@tarekgh
Copy link
Member

tarekgh commented Apr 19, 2021

@benaadams @jkotas what you think is the next step here with this PR?

@jkotas
Copy link
Member

jkotas commented Apr 19, 2021

I think that this PR can be closed. It does not save that much if anything, and it comes with other issues.

There may be an opportunity to move the caching from Win32 PAL to C# on non-Windows, but that should be a separate PR.

@jkotas jkotas closed this Apr 19, 2021
@jkotas
Copy link
Member

jkotas commented Apr 19, 2021

@benaadams Thank you anyway for giving it a shot! Keep these coming :-)

@ghost ghost locked as resolved and limited conversation to collaborators May 19, 2021
@karelz karelz added this to the 6.0.0 milestone May 20, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Environment.GetEnvironmentVariables() called multiple times during MVC app startup
7 participants