Skip to content

Commit dbac8cc

Browse files
Add PythonProfilderCommandService to support new profiler (#8150)
* set up the user input service * refactor the structure * remove tests * address feedback part 1 * update Args description * address feedback - part 2 rename file and method names * update comments
1 parent 396e364 commit dbac8cc

13 files changed

+369
-6
lines changed

Python/Product/Profiling/Profiling.csproj

+6
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,16 @@
5656
<Compile Include="Profiling\AutomationProfiling.cs" />
5757
<Compile Include="Profiling\AutomationReport.cs" />
5858
<Compile Include="Profiling\AutomationSession.cs" />
59+
<Compile Include="Profiling\CommandArgumentBuilder.cs" />
5960
<Compile Include="Profiling\CompareReportsView.cs" />
6061
<Compile Include="Profiling\CompareReportsWindow.xaml.cs">
6162
<DependentUpon>CompareReportsWindow.xaml</DependentUpon>
6263
</Compile>
6364
<Compile Include="Profiling\IPythonPerformanceReport.cs" />
6465
<Compile Include="Profiling\IPythonProfileSession.cs" />
6566
<Compile Include="Profiling\IPythonProfiling.cs" />
67+
<Compile Include="Profiling\IPythonProfilingCommandArgs.cs" />
68+
<Compile Include="Profiling\IPythonProfilerCommandService.cs" />
6669
<Compile Include="Profiling\ProfilingSessionEditorFactory.cs" />
6770
<Compile Include="Profiling\CustomPythonInterpreterView.cs" />
6871
<Compile Include="Profiling\PythonInterpreterView.cs" />
@@ -80,7 +83,10 @@
8083
<Compile Include="Profiling\ProfilingTargetView.cs" />
8184
<Compile Include="Profiling\SessionNode.cs" />
8285
<Compile Include="Profiling\StandaloneTargetView.cs" />
86+
<Compile Include="Profiling\PythonProfilingCommandArgs.cs" />
8387
<Compile Include="Profiling\TreeViewIconIndex.cs" />
88+
<Compile Include="Profiling\UserInputDialog.cs" />
89+
<Compile Include="Profiling\PythonProfilerCommandService.cs" />
8490
<Compile Include="Properties\AssemblyInfo.cs" />
8591
<Compile Include="ProvideFileFilterAttribute.cs" />
8692
<Compile Include="GlobalSuppressions.cs" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
// Python Tools for Visual Studio
2+
// Copyright(c) Microsoft Corporation
3+
// All rights reserved.
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the License); you may not use
6+
// this file except in compliance with the License. You may obtain a copy of the
7+
// License at http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
10+
// OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY
11+
// IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
12+
// MERCHANTABILITY OR NON-INFRINGEMENT.
13+
//
14+
// See the Apache Version 2.0 License for specific language governing
15+
// permissions and limitations under the License.
16+
17+
18+
19+
namespace Microsoft.PythonTools.Profiling {
20+
using System;
21+
using System.Diagnostics;
22+
using System.IO;
23+
using System.Linq;
24+
using System.Windows;
25+
using Microsoft.PythonTools.Infrastructure;
26+
using Microsoft.PythonTools.Interpreter;
27+
28+
internal class CommandArgumentBuilder {
29+
30+
/// <summary>
31+
/// Constructs a <see cref="PythonProfilingCommandArgs"/> based on the provided profiling target.
32+
/// </summary>
33+
public PythonProfilingCommandArgs BuildCommandArgsFromTarget(ProfilingTarget target) {
34+
if (target == null) {
35+
return null;
36+
}
37+
38+
try {
39+
var pythonProfilingPackage = PythonProfilingPackage.Instance;
40+
var joinableTaskFactory = pythonProfilingPackage.JoinableTaskFactory;
41+
42+
PythonProfilingCommandArgs command = null;
43+
44+
joinableTaskFactory.Run(async () => {
45+
await joinableTaskFactory.SwitchToMainThreadAsync();
46+
47+
var name = target.GetProfilingName(pythonProfilingPackage, out var save);
48+
var explorer = await pythonProfilingPackage.ShowPerformanceExplorerAsync();
49+
var session = explorer.Sessions.AddTarget(target, name, save);
50+
51+
command = SelectBuilder(target, session);
52+
53+
});
54+
55+
return command;
56+
} catch (Exception ex) {
57+
Debug.Fail($"Error building command: {ex.Message}");
58+
throw;
59+
}
60+
}
61+
62+
/// <summary>
63+
/// Select the appropriate builder based on the provided profiling target.
64+
/// </summary>
65+
private PythonProfilingCommandArgs SelectBuilder(ProfilingTarget target, SessionNode session) {
66+
var projectTarget = target.ProjectTarget;
67+
var standaloneTarget = target.StandaloneTarget;
68+
69+
if (projectTarget != null) {
70+
return BuildProjectCommandArgs(projectTarget, session);
71+
} else if (standaloneTarget != null) {
72+
return BuildStandaloneCommandArgs(standaloneTarget, session);
73+
}
74+
return null;
75+
}
76+
77+
private PythonProfilingCommandArgs BuildProjectCommandArgs(ProjectTarget projectTarget, SessionNode session) {
78+
var solution = PythonProfilingPackage.Instance.Solution;
79+
var project = solution.EnumerateLoadedPythonProjects()
80+
.SingleOrDefault(p => p.GetProjectIDGuidProperty() == projectTarget.TargetProject);
81+
82+
if (project == null) {
83+
return null;
84+
}
85+
86+
LaunchConfiguration config = null;
87+
try {
88+
config = project?.GetLaunchConfigurationOrThrow();
89+
} catch (NoInterpretersException ex) {
90+
PythonToolsPackage.OpenNoInterpretersHelpPage(session._serviceProvider, ex.HelpPage);
91+
return null;
92+
} catch (MissingInterpreterException ex) {
93+
MessageBox.Show(ex.Message, Strings.ProductTitle);
94+
return null;
95+
} catch (IOException ex) {
96+
MessageBox.Show(ex.Message, Strings.ProductTitle);
97+
return null;
98+
}
99+
if (config == null) {
100+
MessageBox.Show(Strings.ProjectInterpreterNotFound.FormatUI(project.GetNameProperty()), Strings.ProductTitle);
101+
return null;
102+
}
103+
104+
if (string.IsNullOrEmpty(config.ScriptName)) {
105+
MessageBox.Show(Strings.NoProjectStartupFile, Strings.ProductTitle);
106+
return null;
107+
}
108+
109+
if (string.IsNullOrEmpty(config.WorkingDirectory) || config.WorkingDirectory == ".") {
110+
config.WorkingDirectory = project.ProjectHome;
111+
if (string.IsNullOrEmpty(config.WorkingDirectory)) {
112+
config.WorkingDirectory = Path.GetDirectoryName(config.ScriptName);
113+
}
114+
}
115+
116+
var pythonExePath = config.GetInterpreterPath();
117+
var scriptPath = string.Join(" ", ProcessOutput.QuoteSingleArgument(config.ScriptName), config.ScriptArguments);
118+
var workingDir = config.WorkingDirectory;
119+
var envVars = session._serviceProvider.GetPythonToolsService().GetFullEnvironment(config);
120+
121+
var command = new PythonProfilingCommandArgs {
122+
PythonExePath = pythonExePath,
123+
ScriptPath = scriptPath,
124+
WorkingDir = workingDir,
125+
Args = Array.Empty<string>(),
126+
EnvVars = envVars
127+
};
128+
return command;
129+
}
130+
131+
private PythonProfilingCommandArgs BuildStandaloneCommandArgs(StandaloneTarget standaloneTarget, SessionNode session) {
132+
if (standaloneTarget == null) {
133+
return null;
134+
}
135+
136+
LaunchConfiguration config = null;
137+
138+
if (standaloneTarget.InterpreterPath != null) {
139+
config = new LaunchConfiguration(null);
140+
}
141+
142+
if (standaloneTarget.PythonInterpreter != null) {
143+
var registry = session._serviceProvider.GetComponentModel().GetService<IInterpreterRegistryService>();
144+
var interpreter = registry.FindConfiguration(standaloneTarget.PythonInterpreter.Id);
145+
if (interpreter == null) {
146+
return null;
147+
}
148+
149+
config = new LaunchConfiguration(interpreter);
150+
}
151+
152+
config.InterpreterPath = standaloneTarget.InterpreterPath;
153+
config.ScriptName = standaloneTarget.Script;
154+
config.ScriptArguments = standaloneTarget.Arguments;
155+
config.WorkingDirectory = standaloneTarget.WorkingDirectory;
156+
157+
var argsInput = standaloneTarget.Arguments;
158+
var parsedArgs = string.IsNullOrWhiteSpace(argsInput)
159+
? Array.Empty<string>()
160+
: argsInput.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
161+
162+
var envVars = session._serviceProvider.GetPythonToolsService().GetFullEnvironment(config);
163+
164+
return new PythonProfilingCommandArgs {
165+
PythonExePath = config.GetInterpreterPath(),
166+
WorkingDir = standaloneTarget.WorkingDirectory,
167+
ScriptPath = standaloneTarget.Script,
168+
Args = parsedArgs,
169+
EnvVars = envVars
170+
};
171+
}
172+
}
173+
}
174+
175+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Python Tools for Visual Studio
2+
// Copyright(c) Microsoft Corporation
3+
// All rights reserved.
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the License); you may not use
6+
// this file except in compliance with the License. You may obtain a copy of the
7+
// License at http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
10+
// OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY
11+
// IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
12+
// MERCHANTABILITY OR NON-INFRINGEMENT.
13+
//
14+
// See the Apache Version 2.0 License for specific language governing
15+
// permissions and limitations under the License.
16+
17+
namespace Microsoft.PythonTools.Profiling {
18+
19+
/// <summary>
20+
/// Defines a service interface for collecting user input and converting to Python profiling command arguments.
21+
/// </summary>
22+
public interface IPythonProfilerCommandService {
23+
/// <summary>
24+
/// Collects user input via a dialog and converts it into a <see cref="IPythonProfilingCommandArgs"/>.
25+
/// </summary>
26+
IPythonProfilingCommandArgs GetCommandArgsFromUserInput();
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Python Tools for Visual Studio
2+
// Copyright(c) Microsoft Corporation
3+
// All rights reserved.
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the License); you may not use
6+
// this file except in compliance with the License. You may obtain a copy of the
7+
// License at http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
10+
// OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY
11+
// IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
12+
// MERCHANTABILITY OR NON-INFRINGEMENT.
13+
//
14+
// See the Apache Version 2.0 License for specific language governing
15+
// permissions and limitations under the License.
16+
17+
using System.Collections.Generic;
18+
19+
namespace Microsoft.PythonTools.Profiling
20+
{
21+
/// <summary>
22+
/// Contains the arguments for a Python profiling command.
23+
/// </summary>
24+
public interface IPythonProfilingCommandArgs {
25+
string PythonExePath { get; set; }
26+
string WorkingDir { get; set; }
27+
string ScriptPath { get; set; }
28+
string[] Args { get; set; }
29+
Dictionary<string, string> EnvVars { get; set; }
30+
}
31+
}

Python/Product/Profiling/Profiling/LaunchProfiling.xaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
AutomationProperties.AutomationId="ProfileScript"
6969
GroupName="ProjectOrStandalone"
7070
IsChecked="{Binding IsStandaloneSelected}" />
71-
<ScrollViewer Grid.Row="4" Height="100">
71+
<ScrollViewer Grid.Row="4" Height="200">
7272
<GroupBox Header="{x:Static ui:Strings.LaunchProfiling_ScriptOptions}"
7373
IsEnabled="{Binding IsStandaloneSelected}">
7474
<Grid>

Python/Product/Profiling/Profiling/ProfilingTargetView.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public ProfilingTargetView(IServiceProvider serviceProvider) {
6767
IsProjectSelected = false;
6868
IsStandaloneSelected = true;
6969
}
70-
_startText = Strings.LaunchProfiling_Start;
70+
_startText = Strings.LaunchProfiling_OK;
7171
}
7272

7373
/// <summary>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Python Tools for Visual Studio
2+
// Copyright(c) Microsoft Corporation
3+
// All rights reserved.
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the License); you may not use
6+
// this file except in compliance with the License. You may obtain a copy of the
7+
// License at http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
10+
// OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY
11+
// IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
12+
// MERCHANTABILITY OR NON-INFRINGEMENT.
13+
//
14+
// See the Apache Version 2.0 License for specific language governing
15+
// permissions and limitations under the License.
16+
17+
namespace Microsoft.PythonTools.Profiling {
18+
using System;
19+
using System.ComponentModel.Composition;
20+
using System.Diagnostics;
21+
using System.Windows;
22+
23+
/// <summary>
24+
/// Implements a service to collect user input for profiling and convert to a <see cref="PythonProfilingCommandArgs"/>.
25+
/// </summary>
26+
[Export(typeof(IPythonProfilerCommandService))]
27+
class PythonProfilerCommandService : IPythonProfilerCommandService {
28+
private readonly CommandArgumentBuilder _commandArgumentBuilder;
29+
private readonly UserInputDialog _userInputDialog;
30+
31+
public PythonProfilerCommandService() {
32+
_commandArgumentBuilder = new CommandArgumentBuilder();
33+
_userInputDialog = new UserInputDialog();
34+
}
35+
36+
/// <summary>
37+
/// Collects user input and constructs a <see cref="PythonProfilingCommandArgs"/> object.
38+
/// </summary>
39+
/// <returns>
40+
/// A <see cref="PythonProfilingCommandArgs"/> object based on user input, or <c>null</c> if canceled.
41+
/// </returns>
42+
public IPythonProfilingCommandArgs GetCommandArgsFromUserInput() {
43+
try {
44+
var pythonProfilingPackage = PythonProfilingPackage.Instance;
45+
var targetView = new ProfilingTargetView(pythonProfilingPackage);
46+
47+
if (_userInputDialog.ShowDialog(targetView)) {
48+
var target = targetView.GetTarget();
49+
return _commandArgumentBuilder.BuildCommandArgsFromTarget(target);
50+
}
51+
} catch (Exception ex) {
52+
Debug.Fail($"Error displaying user input dialog: {ex.Message}");
53+
MessageBox.Show($"An unexpected error occurred: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
54+
}
55+
56+
return null;
57+
}
58+
}
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Python Tools for Visual Studio
2+
// Copyright(c) Microsoft Corporation
3+
// All rights reserved.
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the License); you may not use
6+
// this file except in compliance with the License. You may obtain a copy of the
7+
// License at http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
10+
// OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY
11+
// IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
12+
// MERCHANTABILITY OR NON-INFRINGEMENT.
13+
//
14+
// See the Apache Version 2.0 License for specific language governing
15+
// permissions and limitations under the License.
16+
17+
using System.Collections.Generic;
18+
using System.ComponentModel.Composition;
19+
20+
namespace Microsoft.PythonTools.Profiling {
21+
/// <summary>
22+
/// Represents the arguments for a Python profiling command.
23+
/// </summary>
24+
[Export(typeof(IPythonProfilingCommandArgs))]
25+
public class PythonProfilingCommandArgs : IPythonProfilingCommandArgs {
26+
public string PythonExePath { get; set; }
27+
public string WorkingDir { get; set; }
28+
public string ScriptPath { get; set; }
29+
public string[] Args { get; set; }
30+
public Dictionary<string, string> EnvVars { get; set; }
31+
}
32+
}
33+

Python/Product/Profiling/Profiling/StandaloneTargetView.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ public string InterpreterPath {
173173
}
174174

175175
/// <summary>
176-
/// True if InterpreterPath is valid; false if it will be ignored.
176+
/// True if PythonExePath is valid; false if it will be ignored.
177177
/// </summary>
178178
public bool CanSpecifyInterpreterPath {
179179
get {

0 commit comments

Comments
 (0)