Skip to content

Commit 91f1b75

Browse files
authored
Az.Tools.Predictor refactor and performance improvement. (#13669)
* Prefix the environment variable "AzPredictor" * Refactor the code - Improve the comment and its format. - Create a concret class type to replace Tuple and ValueTuple. - Verify method parameter values. * Fix a bug that throws NullReferenceException. * Improve the telemetry. - Combine the error telemetry event with the non-error one. - Collect if the http request is canceled when we send http request. * Fix a bug that out of range is thrown. * Transform and send telemetry in a thread pool. - Refactor the telemetry and use a class for the collected data in each telemetry event. - Now we only get basic information and push them to the data flow. - A thread from thread pool handles the data, transform them, and send it. * Fix the SuggestionSource and test. - Updated the test after the refactor. - Add more test cases. - We don't set SuggestionSource on the suggestion in some cases. This is revealed in the unit tests. They're fixed. * Avoid duplicate extraction of user input. - We have two CommandLinePredictor in AzurePredictorService. The CommandLinePredictor needs to extract from the user input the command name, parameter set etc. It's duplicate if we do that in both CommandLinePredictor. Move that extraction to AzurePredictorService and the CommandLinePredictor will not need to do it. * Fix a bug that a duplicate key is in the dictionary. * Replace Newtonsoft with the built-in Json serializer. * Fix an issue that the parameter name is repeated in source text. * Ensure the http response is successful. * Collect the event in requesting to /commands. * Improve the perf in GetSuggestion. - Remove the string manipulation. - Pre-allocate the collections for the result. - Remove invariant check in "readonly" properties. * Seperate the inner class in seperate files.
1 parent 153d220 commit 91f1b75

35 files changed

+1993
-1027
lines changed
Lines changed: 149 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// ----------------------------------------------------------------------------------
1+
// ----------------------------------------------------------------------------------
22
//
33
// Copyright Microsoft Corporation
44
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,7 +13,10 @@
1313
// ----------------------------------------------------------------------------------
1414

1515
using Microsoft.Azure.PowerShell.Tools.AzPredictor.Test.Mocks;
16+
using System;
17+
using System.Collections.Generic;
1618
using System.Linq;
19+
using System.Management.Automation.Language;
1720
using System.Management.Automation.Subsystem;
1821
using System.Threading;
1922
using Xunit;
@@ -26,10 +29,36 @@ namespace Microsoft.Azure.PowerShell.Tools.AzPredictor.Test
2629
[Collection("Model collection")]
2730
public class AzPredictorServiceTests
2831
{
32+
private class PredictiveSuggestionComparer : EqualityComparer<PredictiveSuggestion>
33+
{
34+
public override bool Equals(PredictiveSuggestion first, PredictiveSuggestion second)
35+
{
36+
if ((first == null) && (second == null))
37+
{
38+
return true;
39+
}
40+
else if ((first == null) || (second == null))
41+
{
42+
return false;
43+
}
44+
45+
return string.Equals(first.SuggestionText, second.SuggestionText, StringComparison.Ordinal);
46+
}
47+
48+
public override int GetHashCode(PredictiveSuggestion suggestion)
49+
{
50+
return suggestion.SuggestionText.GetHashCode();
51+
}
52+
}
53+
2954
private readonly ModelFixture _fixture;
3055
private readonly AzPredictorService _service;
31-
private readonly Predictor _suggestionsPredictor;
32-
private readonly Predictor _commandsPredictor;
56+
private readonly CommandLinePredictor _commandBasedPredictor;
57+
private readonly CommandLinePredictor _fallbackPredictor;
58+
59+
private readonly AzPredictorService _noFallbackPredictorService;
60+
private readonly AzPredictorService _noCommandBasedPredictorService;
61+
private readonly AzPredictorService _noPredictorService;
3362

3463
/// <summary>
3564
/// Constructs a new instance of <see cref="AzPredictorServiceTests"/>
@@ -39,15 +68,36 @@ public AzPredictorServiceTests(ModelFixture fixture)
3968
{
4069
this._fixture = fixture;
4170
var startHistory = $"{AzPredictorConstants.CommandPlaceholder}{AzPredictorConstants.CommandConcatenator}{AzPredictorConstants.CommandPlaceholder}";
42-
this._suggestionsPredictor = new Predictor(this._fixture.PredictionCollection[startHistory], null);
43-
this._commandsPredictor = new Predictor(this._fixture.CommandCollection, null);
71+
this._commandBasedPredictor = new CommandLinePredictor(this._fixture.PredictionCollection[startHistory], null);
72+
this._fallbackPredictor = new CommandLinePredictor(this._fixture.CommandCollection, null);
4473

4574
this._service = new MockAzPredictorService(startHistory, this._fixture.PredictionCollection[startHistory], this._fixture.CommandCollection);
75+
76+
this._noFallbackPredictorService = new MockAzPredictorService(startHistory, this._fixture.PredictionCollection[startHistory], null);
77+
this._noCommandBasedPredictorService = new MockAzPredictorService(null, null, this._fixture.CommandCollection);
78+
this._noPredictorService = new MockAzPredictorService(null, null, null);
4679
}
4780

81+
/// <summary>
82+
/// Verify the method checks parameter values.
83+
/// </summary>
84+
[Fact]
85+
public void VerifyParameterValues()
86+
{
87+
var predictionContext = PredictionContext.Create("Get-AzContext");
88+
89+
Action actual = () => this._service.GetSuggestion(null, 1, 1, CancellationToken.None);
90+
Assert.Throws<ArgumentNullException>(actual);
91+
92+
actual = () => this._service.GetSuggestion(predictionContext.InputAst, 0, 1, CancellationToken.None);
93+
Assert.Throws<ArgumentOutOfRangeException>(actual);
94+
95+
actual = () => this._service.GetSuggestion(predictionContext.InputAst, 1, 0, CancellationToken.None);
96+
Assert.Throws<ArgumentOutOfRangeException>(actual);
97+
}
4898

4999
/// <summary>
50-
/// Verifies that the prediction comes from the suggestions list, not the command list.
100+
/// Verifies that the prediction comes from the command based list, not the fallback list.
51101
/// </summary>
52102
[Theory]
53103
[InlineData("CONNECT-AZACCOUNT")]
@@ -59,52 +109,126 @@ public AzPredictorServiceTests(ModelFixture fixture)
59109
[InlineData("new-azresourcegroup -name hello")]
60110
[InlineData("Get-AzContext -Name")]
61111
[InlineData("Get-AzContext -ErrorAction")]
62-
public void VerifyUsingSuggestion(string userInput)
112+
public void VerifyUsingCommandBasedPredictor(string userInput)
63113
{
64114
var predictionContext = PredictionContext.Create(userInput);
65-
var presentCommands = new System.Collections.Generic.Dictionary<string, int>();
66-
var expected = this._suggestionsPredictor.Query(predictionContext.InputAst, presentCommands, 1, 1, CancellationToken.None);
115+
var commandAst = predictionContext.InputAst.FindAll(p => p is CommandAst, true).LastOrDefault() as CommandAst;
116+
var commandName = (commandAst?.CommandElements?.FirstOrDefault() as StringConstantExpressionAst)?.Value;
117+
var inputParameterSet = new ParameterSet(commandAst);
118+
var rawUserInput = predictionContext.InputAst.Extent.Text;
119+
var presentCommands = new Dictionary<string, int>();
120+
var expected = this._commandBasedPredictor.GetSuggestion(commandName,
121+
inputParameterSet,
122+
rawUserInput,
123+
presentCommands,
124+
1,
125+
1,
126+
CancellationToken.None);
127+
67128
var actual = this._service.GetSuggestion(predictionContext.InputAst, 1, 1, CancellationToken.None);
68-
Assert.NotEmpty(actual);
69-
Assert.NotNull(actual.First().Item1);
70-
Assert.Equal(expected.Item1.First().Key, actual.First().Item1);
71-
Assert.Equal(PredictionSource.CurrentCommand, actual.First().Item3);
129+
Assert.NotNull(actual);
130+
Assert.True(actual.Count > 0);
131+
Assert.NotNull(actual.PredictiveSuggestions.First());
132+
Assert.NotNull(actual.PredictiveSuggestions.First().SuggestionText);
133+
Assert.Equal(expected.Count, actual.Count);
134+
Assert.Equal<PredictiveSuggestion>(expected.PredictiveSuggestions, actual.PredictiveSuggestions, new PredictiveSuggestionComparer());
135+
Assert.Equal<string>(expected.SourceTexts, actual.SourceTexts);
136+
Assert.All<SuggestionSource>(actual.SuggestionSources, (source) => Assert.Equal(SuggestionSource.CurrentCommand, source));
137+
138+
actual = this._noFallbackPredictorService.GetSuggestion(predictionContext.InputAst, 1, 1, CancellationToken.None);
139+
Assert.NotNull(actual);
140+
Assert.True(actual.Count > 0);
141+
Assert.NotNull(actual.PredictiveSuggestions.First());
142+
Assert.NotNull(actual.PredictiveSuggestions.First().SuggestionText);
143+
Assert.Equal(expected.Count, actual.Count);
144+
Assert.Equal<PredictiveSuggestion>(expected.PredictiveSuggestions, actual.PredictiveSuggestions, new PredictiveSuggestionComparer());
145+
Assert.Equal<string>(expected.SourceTexts, actual.SourceTexts);
146+
Assert.All<SuggestionSource>(actual.SuggestionSources, (source) => Assert.Equal(SuggestionSource.CurrentCommand, source));
72147
}
73148

74149
/// <summary>
75-
/// Verifies that when no prediction is in the suggestion list, we'll use the command list.
150+
/// Verifies that when no prediction is in the command based list, we'll use the fallback list.
76151
/// </summary>
77152
[Theory]
78153
[InlineData("Get-AzResource -Name hello -Pre")]
79154
[InlineData("Get-AzADServicePrincipal -ApplicationObject")]
80-
public void VerifyUsingCommand(string userInput)
155+
public void VerifyUsingFallbackPredictor(string userInput)
81156
{
82157
var predictionContext = PredictionContext.Create(userInput);
83-
var presentCommands = new System.Collections.Generic.Dictionary<string, int>();
84-
var expected = this._commandsPredictor.Query(predictionContext.InputAst, presentCommands, 1, 1, CancellationToken.None);
158+
var commandAst = predictionContext.InputAst.FindAll(p => p is CommandAst, true).LastOrDefault() as CommandAst;
159+
var commandName = (commandAst?.CommandElements?.FirstOrDefault() as StringConstantExpressionAst)?.Value;
160+
var inputParameterSet = new ParameterSet(commandAst);
161+
var rawUserInput = predictionContext.InputAst.Extent.Text;
162+
var presentCommands = new Dictionary<string, int>();
163+
var expected = this._fallbackPredictor.GetSuggestion(commandName,
164+
inputParameterSet,
165+
rawUserInput,
166+
presentCommands,
167+
1,
168+
1,
169+
CancellationToken.None);
170+
85171
var actual = this._service.GetSuggestion(predictionContext.InputAst, 1, 1, CancellationToken.None);
86-
Assert.NotEmpty(actual);
87-
Assert.NotNull(actual.First().Item1);
88-
Assert.Equal(expected.Item1.First().Key, actual.First().Item1);
89-
Assert.Equal(PredictionSource.StaticCommands, actual.First().Item3);
172+
Assert.NotNull(actual);
173+
Assert.True(actual.Count > 0);
174+
Assert.NotNull(actual.PredictiveSuggestions.First());
175+
Assert.NotNull(actual.PredictiveSuggestions.First().SuggestionText);
176+
Assert.Equal(expected.Count, actual.Count);
177+
Assert.Equal<PredictiveSuggestion>(expected.PredictiveSuggestions, actual.PredictiveSuggestions, new PredictiveSuggestionComparer());
178+
Assert.Equal<string>(expected.SourceTexts, actual.SourceTexts);
179+
Assert.All<SuggestionSource>(actual.SuggestionSources, (source) => Assert.Equal(SuggestionSource.StaticCommands, source));
180+
181+
actual = this._noCommandBasedPredictorService.GetSuggestion(predictionContext.InputAst, 1, 1, CancellationToken.None);
182+
Assert.NotNull(actual);
183+
Assert.True(actual.Count > 0);
184+
Assert.NotNull(actual.PredictiveSuggestions.First());
185+
Assert.NotNull(actual.PredictiveSuggestions.First().SuggestionText);
186+
Assert.Equal(expected.Count, actual.Count);
187+
Assert.Equal<PredictiveSuggestion>(expected.PredictiveSuggestions, actual.PredictiveSuggestions, new PredictiveSuggestionComparer());
188+
Assert.Equal<string>(expected.SourceTexts, actual.SourceTexts);
189+
Assert.All<SuggestionSource>(actual.SuggestionSources, (source) => Assert.Equal(SuggestionSource.StaticCommands, source));
90190
}
91191

92192
/// <summary>
93-
/// Verify that no prediction for the user input, meaning it's not in the prediction list or the command list.
193+
/// Verify that no prediction for the user input, meaning it's not in the command based list or the fallback list.
94194
/// </summary>
95195
[Theory]
96196
[InlineData(AzPredictorConstants.CommandPlaceholder)]
97-
[InlineData("git status")]
98197
[InlineData("Get-ChildItem")]
99198
[InlineData("new-azresourcegroup -NoExistingParam")]
100199
[InlineData("get-azaccount ")]
101-
[InlineData("Get-AzContext Name")]
102200
[InlineData("NEW-AZCONTEXT")]
103201
public void VerifyNoPrediction(string userInput)
104202
{
105203
var predictionContext = PredictionContext.Create(userInput);
106204
var actual = this._service.GetSuggestion(predictionContext.InputAst, 1, 1, CancellationToken.None);
107-
Assert.Empty(actual);
205+
Assert.Equal(0, actual.Count);
206+
207+
actual = this._noFallbackPredictorService.GetSuggestion(predictionContext.InputAst, 1, 1, CancellationToken.None);
208+
Assert.Equal(0, actual.Count);
209+
210+
actual = this._noCommandBasedPredictorService.GetSuggestion(predictionContext.InputAst, 1, 1, CancellationToken.None);
211+
Assert.Equal(0, actual.Count);
212+
213+
actual = this._noPredictorService.GetSuggestion(predictionContext.InputAst, 1, 1, CancellationToken.None);
214+
Assert.Null(actual);
215+
}
216+
217+
/// <summary>
218+
/// Verify when we cannot parse the user input correctly.
219+
/// </summary>
220+
/// <remarks>
221+
/// When we can parse them correctly, please move the InlineData to the corresponding test methods, for example, "git status"
222+
/// doesn't have any prediction so it should move to <see cref="VerifyNoPrediction"/>.
223+
/// </remarks>
224+
[Theory]
225+
[InlineData("git status")]
226+
[InlineData("Get-AzContext Name")]
227+
public void VerifyMalFormattedCommandLine(string userInput)
228+
{
229+
var predictionContext = PredictionContext.Create(userInput);
230+
Action actual = () => this._service.GetSuggestion(predictionContext.InputAst, 1, 1, CancellationToken.None);
231+
_ = Assert.Throws<InvalidOperationException>(actual);
108232
}
109233
}
110234
}

tools/Az.Tools.Predictor/Az.Tools.Predictor.Test/AzPredictorTests.cs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public AzPredictorTests(ModelFixture modelFixture)
4545
this._azPredictor = new AzPredictor(this._service, this._telemetryClient, new Settings()
4646
{
4747
SuggestionCount = 1,
48+
MaxAllowedCommandDuplicate = 1,
4849
},
4950
null);
5051
}
@@ -134,7 +135,6 @@ public void VerifySupportedCommandMasked()
134135
/// Verifies AzPredictor returns the same value as AzPredictorService for the prediction.
135136
/// </summary>
136137
[Theory]
137-
[InlineData("git status")]
138138
[InlineData("new-azresourcegroup -name hello")]
139139
[InlineData("Get-AzContext -Name")]
140140
[InlineData("Get-AzContext -ErrorAction")]
@@ -145,7 +145,8 @@ public void VerifySuggestion(string userInput)
145145
var expected = this._service.GetSuggestion(predictionContext.InputAst, 1, 1, CancellationToken.None);
146146
var actual = this._azPredictor.GetSuggestion(predictionContext, CancellationToken.None);
147147

148-
Assert.Equal(expected.Select(e => e.Item1), actual.Select(a => a.SuggestionText));
148+
Assert.Equal(expected.Count, actual.Count);
149+
Assert.Equal(expected.PredictiveSuggestions.First().SuggestionText, actual.First().SuggestionText);
149150
}
150151

151152
/// <summary>
@@ -158,6 +159,7 @@ public void VerifySuggestionOnIncompleteCommand()
158159
var localAzPredictor = new AzPredictor(this._service, this._telemetryClient, new Settings()
159160
{
160161
SuggestionCount = 7,
162+
MaxAllowedCommandDuplicate = 1,
161163
},
162164
null);
163165

@@ -169,5 +171,23 @@ public void VerifySuggestionOnIncompleteCommand()
169171

170172
Assert.Equal(expected, actual.First().SuggestionText);
171173
}
174+
175+
176+
/// <summary>
177+
/// Verify when we cannot parse the user input correctly.
178+
/// </summary>
179+
/// <remarks>
180+
/// When we can parse them correctly, please move the InlineData to the corresponding test methods, for example, "git status"
181+
/// can be moved to <see cref="VerifySuggestion"/>.
182+
/// </remarks>
183+
[Theory]
184+
[InlineData("git status")]
185+
public void VerifyMalFormattedCommandLine(string userInput)
186+
{
187+
var predictionContext = PredictionContext.Create(userInput);
188+
var actual = this._azPredictor.GetSuggestion(predictionContext, CancellationToken.None);
189+
190+
Assert.Empty(actual);
191+
}
172192
}
173193
}

0 commit comments

Comments
 (0)