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") },