Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ internal static class AnalysisCache
{
private static readonly AnalysisCacheData s_cacheData = AnalysisCacheData.Get();

internal static void SerializePendingCache() => s_cacheData.SerializeIfPending();

// This dictionary shouldn't see much use, so low concurrency and capacity
private static readonly ConcurrentDictionary<string, string> s_modulesBeingAnalyzed =
new(concurrencyLevel: 1, capacity: 2, StringComparer.OrdinalIgnoreCase);
Expand Down Expand Up @@ -676,6 +678,15 @@ public void QueueSerialization()
// We want to make sure we only queue one task.
if (_saveCacheToDisk && Interlocked.Increment(ref _saveCacheToDiskQueued) == 1)
{
// A process that exits before the delayed write below (for example pwsh -Command)
// would otherwise never save its analysis, and the next process would redo it.
// The handler is static and only saves the process-wide cache, so the temporary
// instances read while merging another process's cache are not kept alive until exit.
if (Interlocked.Exchange(ref s_exitHandlerRegistered, 1) == 0)
{
AppDomain.CurrentDomain.ProcessExit += static (sender, e) => AnalysisCache.SerializePendingCache();
}

Task.Run(async delegate
{
// Wait a while before assuming we've finished the updates,
Expand All @@ -691,11 +702,26 @@ public void QueueSerialization()
await Task.Delay(3000).ConfigureAwait(false);
counter2 = _saveCacheToDiskQueued;
} while (counter1 != counter2);
Serialize(s_cacheStoreLocation);
SerializeIfPending();
});
}
}

private static int s_exitHandlerRegistered;

private readonly object _serializeLock = new object();

internal void SerializeIfPending()
{
lock (_serializeLock)
{
if (_saveCacheToDisk && Volatile.Read(ref _saveCacheToDiskQueued) > 0)
{
Serialize(s_cacheStoreLocation);
}
}
}

// Remove entries that are not needed anymore, e.g. if a module was removed.
// If anything is removed, save the cache.
private void Cleanup()
Expand Down
25 changes: 25 additions & 0 deletions test/powershell/engine/Module/ModuleAnalysisCache.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

Describe "Module analysis cache" -Tags 'CI' {
BeforeAll {
Import-Module HelpersCommon
}

It "Saves the analysis of an auto-loaded module when a short-lived process exits" {
$cachePath = Join-Path $TestDrive 'ModuleAnalysisCache'
$oldCachePath = $env:PSModuleAnalysisCachePath
try {
$env:PSModuleAnalysisCachePath = $cachePath

# Get-ChildItem is not loaded in a new session, so it is found through module auto-loading.
Start-NativeExecution { & "$PSHOME/pwsh" -NoProfile -NonInteractive -Command 'Get-ChildItem -Path $PSHOME | Out-Null' }
}
finally {
$env:PSModuleAnalysisCachePath = $oldCachePath
}

$cachePath | Should -Exist
(Get-Item $cachePath).Length | Should -BeGreaterThan 0
}
}
43 changes: 43 additions & 0 deletions test/xUnit/csharp/test_AnalysisCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Management.Automation;
using System.Reflection;
using Xunit;

namespace PSTests.Parallel
{
public static class AnalysisCacheTests
{
[Fact]
public static void QueueSerializationDoesNotKeepInstanceAliveUntilExit()
{
// Serialize reads another process's cache into a temporary AnalysisCacheData, and that
// instance can queue a save from its delayed Cleanup. It must not be referenced by ProcessExit.
var instance = (AnalysisCacheData)Activator.CreateInstance(typeof(AnalysisCacheData), nonPublic: true);
FieldInfo saveToDisk = typeof(AnalysisCacheData).GetField("_saveCacheToDisk", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.NotNull(saveToDisk);

try
{
instance.QueueSerialization();

FieldInfo processExit = typeof(AppDomain).GetField("ProcessExit", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.NotNull(processExit);
var handlers = (EventHandler)processExit.GetValue(AppDomain.CurrentDomain);
Assert.NotNull(handlers);

foreach (Delegate handler in handlers.GetInvocationList())
{
Assert.NotSame(instance, handler.Target);
}
}
finally
{
// Keep the delayed save from writing this empty instance to the cache file.
saveToDisk.SetValue(instance, false);
}
}
}
}