Skip to content

Commit a1f94f5

Browse files
committed
Redo command info cache changes
1 parent dff9522 commit a1f94f5

File tree

2 files changed

+131
-50
lines changed

2 files changed

+131
-50
lines changed

Engine/CommandInfoCache.cs

Lines changed: 119 additions & 0 deletions
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 catch
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

Lines changed: 12 additions & 50 deletions
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

@@ -676,7 +678,6 @@ private CommandInfo GetCommandInfoInternal(string cmdName, CommandTypes? command
676678
}
677679

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

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

744706
/// <summary>

0 commit comments

Comments
 (0)