From dbac8ccb66eff8f6cf08bb53acad20674bcbe0fb Mon Sep 17 00:00:00 2001
From: Stella <100439259+StellaHuang95@users.noreply.github.com>
Date: Tue, 28 Jan 2025 13:14:59 -0800
Subject: [PATCH] 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
---
Python/Product/Profiling/Profiling.csproj | 6 +
.../Profiling/CommandArgumentBuilder.cs | 175 ++++++++++++++++++
.../IPythonProfilerCommandService.cs | 28 +++
.../Profiling/IPythonProfilingCommandArgs.cs | 31 ++++
.../Profiling/Profiling/LaunchProfiling.xaml | 2 +-
.../Profiling/ProfilingTargetView.cs | 2 +-
.../Profiling/PythonProfilerCommandService.cs | 59 ++++++
.../Profiling/PythonProfilingCommandArgs.cs | 33 ++++
.../Profiling/StandaloneTargetView.cs | 2 +-
.../Profiling/Profiling/UserInputDialog.cs | 25 +++
.../Profiling/PythonProfilingPackage.cs | 6 +
Python/Product/Profiling/Strings.resx | 2 +-
Python/Tests/ProfilingTests/ProfilingTests.cs | 4 +-
13 files changed, 369 insertions(+), 6 deletions(-)
create mode 100644 Python/Product/Profiling/Profiling/CommandArgumentBuilder.cs
create mode 100644 Python/Product/Profiling/Profiling/IPythonProfilerCommandService.cs
create mode 100644 Python/Product/Profiling/Profiling/IPythonProfilingCommandArgs.cs
create mode 100644 Python/Product/Profiling/Profiling/PythonProfilerCommandService.cs
create mode 100644 Python/Product/Profiling/Profiling/PythonProfilingCommandArgs.cs
create mode 100644 Python/Product/Profiling/Profiling/UserInputDialog.cs
diff --git a/Python/Product/Profiling/Profiling.csproj b/Python/Product/Profiling/Profiling.csproj
index 179a9003f5..6c45688abc 100644
--- a/Python/Product/Profiling/Profiling.csproj
+++ b/Python/Product/Profiling/Profiling.csproj
@@ -56,6 +56,7 @@
+
CompareReportsWindow.xaml
@@ -63,6 +64,8 @@
+
+
@@ -80,7 +83,10 @@
+
+
+
diff --git a/Python/Product/Profiling/Profiling/CommandArgumentBuilder.cs b/Python/Product/Profiling/Profiling/CommandArgumentBuilder.cs
new file mode 100644
index 0000000000..98cb7a8daa
--- /dev/null
+++ b/Python/Product/Profiling/Profiling/CommandArgumentBuilder.cs
@@ -0,0 +1,175 @@
+// Python Tools for Visual Studio
+// Copyright(c) Microsoft Corporation
+// All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the License); you may not use
+// this file except in compliance with the License. You may obtain a copy of the
+// License at http://www.apache.org/licenses/LICENSE-2.0
+//
+// THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
+// OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY
+// IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+// MERCHANTABILITY OR NON-INFRINGEMENT.
+//
+// See the Apache Version 2.0 License for specific language governing
+// permissions and limitations under the License.
+
+
+
+namespace Microsoft.PythonTools.Profiling {
+ using System;
+ using System.Diagnostics;
+ using System.IO;
+ using System.Linq;
+ using System.Windows;
+ using Microsoft.PythonTools.Infrastructure;
+ using Microsoft.PythonTools.Interpreter;
+
+ internal class CommandArgumentBuilder {
+
+ ///
+ /// Constructs a based on the provided profiling target.
+ ///
+ public PythonProfilingCommandArgs BuildCommandArgsFromTarget(ProfilingTarget target) {
+ if (target == null) {
+ return null;
+ }
+
+ try {
+ var pythonProfilingPackage = PythonProfilingPackage.Instance;
+ var joinableTaskFactory = pythonProfilingPackage.JoinableTaskFactory;
+
+ PythonProfilingCommandArgs command = null;
+
+ joinableTaskFactory.Run(async () => {
+ await joinableTaskFactory.SwitchToMainThreadAsync();
+
+ var name = target.GetProfilingName(pythonProfilingPackage, out var save);
+ var explorer = await pythonProfilingPackage.ShowPerformanceExplorerAsync();
+ var session = explorer.Sessions.AddTarget(target, name, save);
+
+ command = SelectBuilder(target, session);
+
+ });
+
+ return command;
+ } catch (Exception ex) {
+ Debug.Fail($"Error building command: {ex.Message}");
+ throw;
+ }
+ }
+
+ ///
+ /// Select the appropriate builder based on the provided profiling target.
+ ///
+ private PythonProfilingCommandArgs SelectBuilder(ProfilingTarget target, SessionNode session) {
+ var projectTarget = target.ProjectTarget;
+ var standaloneTarget = target.StandaloneTarget;
+
+ if (projectTarget != null) {
+ return BuildProjectCommandArgs(projectTarget, session);
+ } else if (standaloneTarget != null) {
+ return BuildStandaloneCommandArgs(standaloneTarget, session);
+ }
+ return null;
+ }
+
+ private PythonProfilingCommandArgs BuildProjectCommandArgs(ProjectTarget projectTarget, SessionNode session) {
+ var solution = PythonProfilingPackage.Instance.Solution;
+ var project = solution.EnumerateLoadedPythonProjects()
+ .SingleOrDefault(p => p.GetProjectIDGuidProperty() == projectTarget.TargetProject);
+
+ if (project == null) {
+ return null;
+ }
+
+ LaunchConfiguration config = null;
+ try {
+ config = project?.GetLaunchConfigurationOrThrow();
+ } catch (NoInterpretersException ex) {
+ PythonToolsPackage.OpenNoInterpretersHelpPage(session._serviceProvider, ex.HelpPage);
+ return null;
+ } catch (MissingInterpreterException ex) {
+ MessageBox.Show(ex.Message, Strings.ProductTitle);
+ return null;
+ } catch (IOException ex) {
+ MessageBox.Show(ex.Message, Strings.ProductTitle);
+ return null;
+ }
+ if (config == null) {
+ MessageBox.Show(Strings.ProjectInterpreterNotFound.FormatUI(project.GetNameProperty()), Strings.ProductTitle);
+ return null;
+ }
+
+ if (string.IsNullOrEmpty(config.ScriptName)) {
+ MessageBox.Show(Strings.NoProjectStartupFile, Strings.ProductTitle);
+ return null;
+ }
+
+ if (string.IsNullOrEmpty(config.WorkingDirectory) || config.WorkingDirectory == ".") {
+ config.WorkingDirectory = project.ProjectHome;
+ if (string.IsNullOrEmpty(config.WorkingDirectory)) {
+ config.WorkingDirectory = Path.GetDirectoryName(config.ScriptName);
+ }
+ }
+
+ var pythonExePath = config.GetInterpreterPath();
+ var scriptPath = string.Join(" ", ProcessOutput.QuoteSingleArgument(config.ScriptName), config.ScriptArguments);
+ var workingDir = config.WorkingDirectory;
+ var envVars = session._serviceProvider.GetPythonToolsService().GetFullEnvironment(config);
+
+ var command = new PythonProfilingCommandArgs {
+ PythonExePath = pythonExePath,
+ ScriptPath = scriptPath,
+ WorkingDir = workingDir,
+ Args = Array.Empty(),
+ EnvVars = envVars
+ };
+ return command;
+ }
+
+ private PythonProfilingCommandArgs BuildStandaloneCommandArgs(StandaloneTarget standaloneTarget, SessionNode session) {
+ if (standaloneTarget == null) {
+ return null;
+ }
+
+ LaunchConfiguration config = null;
+
+ if (standaloneTarget.InterpreterPath != null) {
+ config = new LaunchConfiguration(null);
+ }
+
+ if (standaloneTarget.PythonInterpreter != null) {
+ var registry = session._serviceProvider.GetComponentModel().GetService();
+ var interpreter = registry.FindConfiguration(standaloneTarget.PythonInterpreter.Id);
+ if (interpreter == null) {
+ return null;
+ }
+
+ config = new LaunchConfiguration(interpreter);
+ }
+
+ config.InterpreterPath = standaloneTarget.InterpreterPath;
+ config.ScriptName = standaloneTarget.Script;
+ config.ScriptArguments = standaloneTarget.Arguments;
+ config.WorkingDirectory = standaloneTarget.WorkingDirectory;
+
+ var argsInput = standaloneTarget.Arguments;
+ var parsedArgs = string.IsNullOrWhiteSpace(argsInput)
+ ? Array.Empty()
+ : argsInput.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
+
+ var envVars = session._serviceProvider.GetPythonToolsService().GetFullEnvironment(config);
+
+ return new PythonProfilingCommandArgs {
+ PythonExePath = config.GetInterpreterPath(),
+ WorkingDir = standaloneTarget.WorkingDirectory,
+ ScriptPath = standaloneTarget.Script,
+ Args = parsedArgs,
+ EnvVars = envVars
+ };
+ }
+ }
+}
+
+
diff --git a/Python/Product/Profiling/Profiling/IPythonProfilerCommandService.cs b/Python/Product/Profiling/Profiling/IPythonProfilerCommandService.cs
new file mode 100644
index 0000000000..2ba6c929bb
--- /dev/null
+++ b/Python/Product/Profiling/Profiling/IPythonProfilerCommandService.cs
@@ -0,0 +1,28 @@
+// Python Tools for Visual Studio
+// Copyright(c) Microsoft Corporation
+// All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the License); you may not use
+// this file except in compliance with the License. You may obtain a copy of the
+// License at http://www.apache.org/licenses/LICENSE-2.0
+//
+// THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
+// OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY
+// IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+// MERCHANTABILITY OR NON-INFRINGEMENT.
+//
+// See the Apache Version 2.0 License for specific language governing
+// permissions and limitations under the License.
+
+namespace Microsoft.PythonTools.Profiling {
+
+ ///
+ /// Defines a service interface for collecting user input and converting to Python profiling command arguments.
+ ///
+ public interface IPythonProfilerCommandService {
+ ///
+ /// Collects user input via a dialog and converts it into a .
+ ///
+ IPythonProfilingCommandArgs GetCommandArgsFromUserInput();
+ }
+}
diff --git a/Python/Product/Profiling/Profiling/IPythonProfilingCommandArgs.cs b/Python/Product/Profiling/Profiling/IPythonProfilingCommandArgs.cs
new file mode 100644
index 0000000000..04099fd06e
--- /dev/null
+++ b/Python/Product/Profiling/Profiling/IPythonProfilingCommandArgs.cs
@@ -0,0 +1,31 @@
+// Python Tools for Visual Studio
+// Copyright(c) Microsoft Corporation
+// All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the License); you may not use
+// this file except in compliance with the License. You may obtain a copy of the
+// License at http://www.apache.org/licenses/LICENSE-2.0
+//
+// THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
+// OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY
+// IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+// MERCHANTABILITY OR NON-INFRINGEMENT.
+//
+// See the Apache Version 2.0 License for specific language governing
+// permissions and limitations under the License.
+
+using System.Collections.Generic;
+
+namespace Microsoft.PythonTools.Profiling
+{
+ ///
+ /// Contains the arguments for a Python profiling command.
+ ///
+ public interface IPythonProfilingCommandArgs {
+ string PythonExePath { get; set; }
+ string WorkingDir { get; set; }
+ string ScriptPath { get; set; }
+ string[] Args { get; set; }
+ Dictionary EnvVars { get; set; }
+ }
+}
diff --git a/Python/Product/Profiling/Profiling/LaunchProfiling.xaml b/Python/Product/Profiling/Profiling/LaunchProfiling.xaml
index cca59688e0..98fbac76b6 100644
--- a/Python/Product/Profiling/Profiling/LaunchProfiling.xaml
+++ b/Python/Product/Profiling/Profiling/LaunchProfiling.xaml
@@ -68,7 +68,7 @@
AutomationProperties.AutomationId="ProfileScript"
GroupName="ProjectOrStandalone"
IsChecked="{Binding IsStandaloneSelected}" />
-
+
diff --git a/Python/Product/Profiling/Profiling/ProfilingTargetView.cs b/Python/Product/Profiling/Profiling/ProfilingTargetView.cs
index dfd8275776..4acb2e81e8 100644
--- a/Python/Product/Profiling/Profiling/ProfilingTargetView.cs
+++ b/Python/Product/Profiling/Profiling/ProfilingTargetView.cs
@@ -67,7 +67,7 @@ public ProfilingTargetView(IServiceProvider serviceProvider) {
IsProjectSelected = false;
IsStandaloneSelected = true;
}
- _startText = Strings.LaunchProfiling_Start;
+ _startText = Strings.LaunchProfiling_OK;
}
///
diff --git a/Python/Product/Profiling/Profiling/PythonProfilerCommandService.cs b/Python/Product/Profiling/Profiling/PythonProfilerCommandService.cs
new file mode 100644
index 0000000000..08e910f83b
--- /dev/null
+++ b/Python/Product/Profiling/Profiling/PythonProfilerCommandService.cs
@@ -0,0 +1,59 @@
+// Python Tools for Visual Studio
+// Copyright(c) Microsoft Corporation
+// All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the License); you may not use
+// this file except in compliance with the License. You may obtain a copy of the
+// License at http://www.apache.org/licenses/LICENSE-2.0
+//
+// THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
+// OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY
+// IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+// MERCHANTABILITY OR NON-INFRINGEMENT.
+//
+// See the Apache Version 2.0 License for specific language governing
+// permissions and limitations under the License.
+
+namespace Microsoft.PythonTools.Profiling {
+ using System;
+ using System.ComponentModel.Composition;
+ using System.Diagnostics;
+ using System.Windows;
+
+ ///
+ /// Implements a service to collect user input for profiling and convert to a .
+ ///
+ [Export(typeof(IPythonProfilerCommandService))]
+ class PythonProfilerCommandService : IPythonProfilerCommandService {
+ private readonly CommandArgumentBuilder _commandArgumentBuilder;
+ private readonly UserInputDialog _userInputDialog;
+
+ public PythonProfilerCommandService() {
+ _commandArgumentBuilder = new CommandArgumentBuilder();
+ _userInputDialog = new UserInputDialog();
+ }
+
+ ///
+ /// Collects user input and constructs a object.
+ ///
+ ///
+ /// A object based on user input, or null if canceled.
+ ///
+ public IPythonProfilingCommandArgs GetCommandArgsFromUserInput() {
+ try {
+ var pythonProfilingPackage = PythonProfilingPackage.Instance;
+ var targetView = new ProfilingTargetView(pythonProfilingPackage);
+
+ if (_userInputDialog.ShowDialog(targetView)) {
+ var target = targetView.GetTarget();
+ return _commandArgumentBuilder.BuildCommandArgsFromTarget(target);
+ }
+ } catch (Exception ex) {
+ Debug.Fail($"Error displaying user input dialog: {ex.Message}");
+ MessageBox.Show($"An unexpected error occurred: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/Python/Product/Profiling/Profiling/PythonProfilingCommandArgs.cs b/Python/Product/Profiling/Profiling/PythonProfilingCommandArgs.cs
new file mode 100644
index 0000000000..fab0e8f5e7
--- /dev/null
+++ b/Python/Product/Profiling/Profiling/PythonProfilingCommandArgs.cs
@@ -0,0 +1,33 @@
+// Python Tools for Visual Studio
+// Copyright(c) Microsoft Corporation
+// All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the License); you may not use
+// this file except in compliance with the License. You may obtain a copy of the
+// License at http://www.apache.org/licenses/LICENSE-2.0
+//
+// THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
+// OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY
+// IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+// MERCHANTABILITY OR NON-INFRINGEMENT.
+//
+// See the Apache Version 2.0 License for specific language governing
+// permissions and limitations under the License.
+
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+
+namespace Microsoft.PythonTools.Profiling {
+ ///
+ /// Represents the arguments for a Python profiling command.
+ ///
+ [Export(typeof(IPythonProfilingCommandArgs))]
+ public class PythonProfilingCommandArgs : IPythonProfilingCommandArgs {
+ public string PythonExePath { get; set; }
+ public string WorkingDir { get; set; }
+ public string ScriptPath { get; set; }
+ public string[] Args { get; set; }
+ public Dictionary EnvVars { get; set; }
+ }
+}
+
diff --git a/Python/Product/Profiling/Profiling/StandaloneTargetView.cs b/Python/Product/Profiling/Profiling/StandaloneTargetView.cs
index d58ddc51d3..33fd5d2ff8 100644
--- a/Python/Product/Profiling/Profiling/StandaloneTargetView.cs
+++ b/Python/Product/Profiling/Profiling/StandaloneTargetView.cs
@@ -173,7 +173,7 @@ public string InterpreterPath {
}
///
- /// True if InterpreterPath is valid; false if it will be ignored.
+ /// True if PythonExePath is valid; false if it will be ignored.
///
public bool CanSpecifyInterpreterPath {
get {
diff --git a/Python/Product/Profiling/Profiling/UserInputDialog.cs b/Python/Product/Profiling/Profiling/UserInputDialog.cs
new file mode 100644
index 0000000000..59022d1ebb
--- /dev/null
+++ b/Python/Product/Profiling/Profiling/UserInputDialog.cs
@@ -0,0 +1,25 @@
+// Python Tools for Visual Studio
+// Copyright(c) Microsoft Corporation
+// All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the License); you may not use
+// this file except in compliance with the License. You may obtain a copy of the
+// License at http://www.apache.org/licenses/LICENSE-2.0
+//
+// THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
+// OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY
+// IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+// MERCHANTABILITY OR NON-INFRINGEMENT.
+//
+// See the Apache Version 2.0 License for specific language governing
+// permissions and limitations under the License.
+
+namespace Microsoft.PythonTools.Profiling {
+ internal class UserInputDialog {
+ public bool ShowDialog(ProfilingTargetView targetView) {
+ var pythonProfilingPackage = PythonProfilingPackage.Instance;
+ var dialog = new LaunchProfiling(pythonProfilingPackage, targetView);
+ return dialog.ShowModal() ?? false;
+ }
+ }
+}
diff --git a/Python/Product/Profiling/PythonProfilingPackage.cs b/Python/Product/Profiling/PythonProfilingPackage.cs
index e2a28fd0ba..691da175da 100644
--- a/Python/Product/Profiling/PythonProfilingPackage.cs
+++ b/Python/Product/Profiling/PythonProfilingPackage.cs
@@ -191,6 +191,12 @@ private void StartProfilingWizard(object sender, EventArgs e) {
return;
}
+ // Used for manually testing the user input service.
+ //MessageBox.Show("Test starts");
+ //var tempUserInputService = new PythonProfilerCommandService();
+ //var tempResult = tempUserInputService.GetCommandArgsFromUserInput();
+ //MessageBox.Show("Test ends");
+
var targetView = new ProfilingTargetView(this);
var dialog = new LaunchProfiling(this, targetView);
var res = dialog.ShowModal() ?? false;
diff --git a/Python/Product/Profiling/Strings.resx b/Python/Product/Profiling/Strings.resx
index 63672e99b5..dda463a5fc 100644
--- a/Python/Product/Profiling/Strings.resx
+++ b/Python/Product/Profiling/Strings.resx
@@ -265,7 +265,7 @@ Error:
_Working Directory:
- Command Line _Arguments:
+ Command Line _Arguments (separate by spaces):
_Cancel
diff --git a/Python/Tests/ProfilingTests/ProfilingTests.cs b/Python/Tests/ProfilingTests/ProfilingTests.cs
index 799c0af292..788d4ba46f 100644
--- a/Python/Tests/ProfilingTests/ProfilingTests.cs
+++ b/Python/Tests/ProfilingTests/ProfilingTests.cs
@@ -65,13 +65,13 @@ public async Task ProfileWithEncoding() {
continue;
}
- Trace.TraceInformation(python.InterpreterPath);
+ Trace.TraceInformation(python.PythonExePath);
foreach (var testFile in testFiles) {
Trace.TraceInformation(" {0}", Path.GetFileName(testFile));
using (var p = ProcessOutput.Run(
- python.InterpreterPath,
+ python.PythonExePath,
new[] { proflaun, vspyprof, Path.GetDirectoryName(testFile), testFile },
Environment.CurrentDirectory,
new[] { new KeyValuePair("PYTHONIOENCODING", "utf-8") },