Skip to content

Commit f34d956

Browse files
rjmholtJamesWTruher
authored andcommitted
Increase lock granularity for CommandInfo cache (#1166)
* Redo command info cache changes * Fix comment typo Co-Authored-By: rjmholt <[email protected]> * Trigger CI
1 parent a68b139 commit f34d956

File tree

2 files changed

+131
-50
lines changed

2 files changed

+131
-50
lines changed

Engine/CommandInfoCache.cs

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Concurrent;
6+
using System.Management.Automation;
7+
using System.Linq;
8+
9+
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer
10+
{
11+
/// <summary>
12+
/// Provides threadsafe caching around CommandInfo lookups with `Get-Command -Name ...`.
13+
/// </summary>
14+
internal class CommandInfoCache
15+
{
16+
private readonly ConcurrentDictionary<CommandLookupKey, Lazy<CommandInfo>> _commandInfoCache;
17+
18+
private readonly Helper _helperInstance;
19+
20+
/// <summary>
21+
/// Create a fresh command info cache instance.
22+
/// </summary>
23+
public CommandInfoCache(Helper pssaHelperInstance)
24+
{
25+
_commandInfoCache = new ConcurrentDictionary<CommandLookupKey, Lazy<CommandInfo>>();
26+
_helperInstance = pssaHelperInstance;
27+
}
28+
29+
/// <summary>
30+
/// Retrieve a command info object about a command.
31+
/// </summary>
32+
/// <param name="commandName">Name of the command to get a commandinfo object for.</param>
33+
/// <param name="aliasName">The alias of the command to be used in the cache key. If not given, uses the command name.</param>
34+
/// <param name="commandTypes">What types of command are needed. If omitted, all types are retrieved.</param>
35+
/// <returns></returns>
36+
public CommandInfo GetCommandInfo(string commandName, string aliasName = null, CommandTypes? commandTypes = null)
37+
{
38+
if (string.IsNullOrWhiteSpace(commandName))
39+
{
40+
return null;
41+
}
42+
43+
// If alias name is given, we store the entry under that, but search with the command name
44+
var key = new CommandLookupKey(aliasName ?? commandName, commandTypes);
45+
46+
// Atomically either use PowerShell to query a command info object, or fetch it from the cache
47+
return _commandInfoCache.GetOrAdd(key, new Lazy<CommandInfo>(() => GetCommandInfoInternal(commandName, commandTypes))).Value;
48+
}
49+
50+
/// <summary>
51+
/// Retrieve a command info object about a command.
52+
/// </summary>
53+
/// <param name="commandName">Name of the command to get a commandinfo object for.</param>
54+
/// <param name="commandTypes">What types of command are needed. If omitted, all types are retrieved.</param>
55+
/// <returns></returns>
56+
[Obsolete("Alias lookup is expensive and should not be relied upon for command lookup")]
57+
public CommandInfo GetCommandInfoLegacy(string commandOrAliasName, CommandTypes? commandTypes = null)
58+
{
59+
string commandName = _helperInstance.GetCmdletNameFromAlias(commandOrAliasName);
60+
61+
return string.IsNullOrEmpty(commandName)
62+
? GetCommandInfo(commandOrAliasName, commandTypes: commandTypes)
63+
: GetCommandInfo(commandName, aliasName: commandOrAliasName, commandTypes: commandTypes);
64+
}
65+
66+
/// <summary>
67+
/// Get a CommandInfo object of the given command name
68+
/// </summary>
69+
/// <returns>Returns null if command does not exists</returns>
70+
private static CommandInfo GetCommandInfoInternal(string cmdName, CommandTypes? commandType)
71+
{
72+
using (var ps = System.Management.Automation.PowerShell.Create())
73+
{
74+
ps.AddCommand("Get-Command")
75+
.AddParameter("Name", cmdName)
76+
.AddParameter("ErrorAction", "SilentlyContinue");
77+
78+
if (commandType != null)
79+
{
80+
ps.AddParameter("CommandType", commandType);
81+
}
82+
83+
return ps.Invoke<CommandInfo>()
84+
.FirstOrDefault();
85+
}
86+
}
87+
88+
private struct CommandLookupKey : IEquatable<CommandLookupKey>
89+
{
90+
private readonly string Name;
91+
92+
private readonly CommandTypes CommandTypes;
93+
94+
internal CommandLookupKey(string name, CommandTypes? commandTypes)
95+
{
96+
Name = name;
97+
CommandTypes = commandTypes ?? CommandTypes.All;
98+
}
99+
100+
public bool Equals(CommandLookupKey other)
101+
{
102+
return CommandTypes == other.CommandTypes
103+
&& Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase);
104+
}
105+
106+
public override int GetHashCode()
107+
{
108+
// Algorithm from https://stackoverflow.com/questions/1646807/quick-and-simple-hash-code-combinations
109+
unchecked
110+
{
111+
int hash = 17;
112+
hash = hash * 31 + Name.ToUpperInvariant().GetHashCode();
113+
hash = hash * 31 + CommandTypes.GetHashCode();
114+
return hash;
115+
}
116+
}
117+
}
118+
}
119+
}

Engine/Helper.cs

+12-50
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ public class Helper
2424

2525
private CommandInvocationIntrinsics invokeCommand;
2626
private IOutputWriter outputWriter;
27-
private Object getCommandLock = new object();
2827
private readonly static Version minSupportedPSVersion = new Version(3, 0);
2928
private Dictionary<string, Dictionary<string, object>> ruleArguments;
3029
private PSVersionTable psVersionTable;
31-
private Dictionary<CommandLookupKey, CommandInfo> commandInfoCache;
30+
31+
private readonly Lazy<CommandInfoCache> _commandInfoCacheLazy;
3232

3333
#endregion
3434

@@ -100,14 +100,20 @@ internal set
100100
private string[] functionScopes = new string[] { "global:", "local:", "script:", "private:"};
101101

102102
private string[] variableScopes = new string[] { "global:", "local:", "script:", "private:", "variable:", ":"};
103+
104+
/// <summary>
105+
/// Store of command info objects for commands. Memoizes results.
106+
/// </summary>
107+
private CommandInfoCache CommandInfoCache => _commandInfoCacheLazy.Value;
108+
103109
#endregion
104110

105111
/// <summary>
106112
/// Initializes the Helper class.
107113
/// </summary>
108114
private Helper()
109115
{
110-
116+
_commandInfoCacheLazy = new Lazy<CommandInfoCache>(() => new CommandInfoCache(pssaHelperInstance: this));
111117
}
112118

113119
/// <summary>
@@ -123,7 +129,7 @@ private Helper()
123129
/// </param>
124130
public Helper(
125131
CommandInvocationIntrinsics invokeCommand,
126-
IOutputWriter outputWriter)
132+
IOutputWriter outputWriter) : this()
127133
{
128134
this.invokeCommand = invokeCommand;
129135
this.outputWriter = outputWriter;
@@ -140,10 +146,6 @@ public void Initialize()
140146
KeywordBlockDictionary = new Dictionary<String, List<Tuple<int, int>>>(StringComparer.OrdinalIgnoreCase);
141147
VariableAnalysisDictionary = new Dictionary<Ast, VariableAnalysis>();
142148
ruleArguments = new Dictionary<string, Dictionary<string, object>>(StringComparer.OrdinalIgnoreCase);
143-
if (commandInfoCache == null)
144-
{
145-
commandInfoCache = new Dictionary<CommandLookupKey, CommandInfo>();
146-
}
147149

148150
IEnumerable<CommandInfo> aliases = this.invokeCommand.GetCommands("*", CommandTypes.Alias, true);
149151

@@ -677,7 +679,6 @@ private CommandInfo GetCommandInfoInternal(string cmdName, CommandTypes? command
677679
}
678680

679681
/// <summary>
680-
681682
/// Legacy method, new callers should use <see cref="GetCommandInfo"/> instead.
682683
/// Given a command's name, checks whether it exists. It does not use the passed in CommandTypes parameter, which is a bug.
683684
/// But existing method callers are already depending on this behaviour and therefore this could not be simply fixed.
@@ -689,30 +690,7 @@ private CommandInfo GetCommandInfoInternal(string cmdName, CommandTypes? command
689690
[Obsolete]
690691
public CommandInfo GetCommandInfoLegacy(string name, CommandTypes? commandType = null)
691692
{
692-
if (string.IsNullOrWhiteSpace(name))
693-
{
694-
return null;
695-
}
696-
697-
// check if it is an alias
698-
string cmdletName = Helper.Instance.GetCmdletNameFromAlias(name);
699-
if (string.IsNullOrWhiteSpace(cmdletName))
700-
{
701-
cmdletName = name;
702-
}
703-
704-
var key = new CommandLookupKey(name, commandType);
705-
lock (getCommandLock)
706-
{
707-
if (commandInfoCache.ContainsKey(key))
708-
{
709-
return commandInfoCache[key];
710-
}
711-
712-
var commandInfo = GetCommandInfoInternal(cmdletName, commandType);
713-
commandInfoCache.Add(key, commandInfo);
714-
return commandInfo;
715-
}
693+
return CommandInfoCache.GetCommandInfoLegacy(commandOrAliasName: name, commandTypes: commandType);
716694
}
717695

718696
/// <summary>
@@ -723,23 +701,7 @@ public CommandInfo GetCommandInfoLegacy(string name, CommandTypes? commandType =
723701
/// <returns></returns>
724702
public CommandInfo GetCommandInfo(string name, CommandTypes? commandType = null)
725703
{
726-
if (string.IsNullOrWhiteSpace(name))
727-
{
728-
return null;
729-
}
730-
731-
var key = new CommandLookupKey(name, commandType);
732-
lock (getCommandLock)
733-
{
734-
if (commandInfoCache.ContainsKey(key))
735-
{
736-
return commandInfoCache[key];
737-
}
738-
739-
var commandInfo = GetCommandInfoInternal(name, commandType);
740-
commandInfoCache.Add(key, commandInfo);
741-
return commandInfo;
742-
}
704+
return CommandInfoCache.GetCommandInfo(name, commandTypes: commandType);
743705
}
744706

745707
/// <summary>

0 commit comments

Comments
 (0)