Skip to content

Commit

Permalink
Add PythonProfilderCommandService to support new profiler (#8150)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
StellaHuang95 authored Jan 28, 2025
1 parent 396e364 commit dbac8cc
Show file tree
Hide file tree
Showing 13 changed files with 369 additions and 6 deletions.
6 changes: 6 additions & 0 deletions Python/Product/Profiling/Profiling.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,16 @@
<Compile Include="Profiling\AutomationProfiling.cs" />
<Compile Include="Profiling\AutomationReport.cs" />
<Compile Include="Profiling\AutomationSession.cs" />
<Compile Include="Profiling\CommandArgumentBuilder.cs" />
<Compile Include="Profiling\CompareReportsView.cs" />
<Compile Include="Profiling\CompareReportsWindow.xaml.cs">
<DependentUpon>CompareReportsWindow.xaml</DependentUpon>
</Compile>
<Compile Include="Profiling\IPythonPerformanceReport.cs" />
<Compile Include="Profiling\IPythonProfileSession.cs" />
<Compile Include="Profiling\IPythonProfiling.cs" />
<Compile Include="Profiling\IPythonProfilingCommandArgs.cs" />
<Compile Include="Profiling\IPythonProfilerCommandService.cs" />
<Compile Include="Profiling\ProfilingSessionEditorFactory.cs" />
<Compile Include="Profiling\CustomPythonInterpreterView.cs" />
<Compile Include="Profiling\PythonInterpreterView.cs" />
Expand All @@ -80,7 +83,10 @@
<Compile Include="Profiling\ProfilingTargetView.cs" />
<Compile Include="Profiling\SessionNode.cs" />
<Compile Include="Profiling\StandaloneTargetView.cs" />
<Compile Include="Profiling\PythonProfilingCommandArgs.cs" />
<Compile Include="Profiling\TreeViewIconIndex.cs" />
<Compile Include="Profiling\UserInputDialog.cs" />
<Compile Include="Profiling\PythonProfilerCommandService.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ProvideFileFilterAttribute.cs" />
<Compile Include="GlobalSuppressions.cs" />
Expand Down
175 changes: 175 additions & 0 deletions Python/Product/Profiling/Profiling/CommandArgumentBuilder.cs
Original file line number Diff line number Diff line change
@@ -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 {

/// <summary>
/// Constructs a <see cref="PythonProfilingCommandArgs"/> based on the provided profiling target.
/// </summary>
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;
}
}

/// <summary>
/// Select the appropriate builder based on the provided profiling target.
/// </summary>
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<string>(),
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<IInterpreterRegistryService>();
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<string>()
: 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
};
}
}
}


Original file line number Diff line number Diff line change
@@ -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 {

/// <summary>
/// Defines a service interface for collecting user input and converting to Python profiling command arguments.
/// </summary>
public interface IPythonProfilerCommandService {
/// <summary>
/// Collects user input via a dialog and converts it into a <see cref="IPythonProfilingCommandArgs"/>.
/// </summary>
IPythonProfilingCommandArgs GetCommandArgsFromUserInput();
}
}
31 changes: 31 additions & 0 deletions Python/Product/Profiling/Profiling/IPythonProfilingCommandArgs.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Contains the arguments for a Python profiling command.
/// </summary>
public interface IPythonProfilingCommandArgs {
string PythonExePath { get; set; }
string WorkingDir { get; set; }
string ScriptPath { get; set; }
string[] Args { get; set; }
Dictionary<string, string> EnvVars { get; set; }
}
}
2 changes: 1 addition & 1 deletion Python/Product/Profiling/Profiling/LaunchProfiling.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
AutomationProperties.AutomationId="ProfileScript"
GroupName="ProjectOrStandalone"
IsChecked="{Binding IsStandaloneSelected}" />
<ScrollViewer Grid.Row="4" Height="100">
<ScrollViewer Grid.Row="4" Height="200">
<GroupBox Header="{x:Static ui:Strings.LaunchProfiling_ScriptOptions}"
IsEnabled="{Binding IsStandaloneSelected}">
<Grid>
Expand Down
2 changes: 1 addition & 1 deletion Python/Product/Profiling/Profiling/ProfilingTargetView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public ProfilingTargetView(IServiceProvider serviceProvider) {
IsProjectSelected = false;
IsStandaloneSelected = true;
}
_startText = Strings.LaunchProfiling_Start;
_startText = Strings.LaunchProfiling_OK;
}

/// <summary>
Expand Down
59 changes: 59 additions & 0 deletions Python/Product/Profiling/Profiling/PythonProfilerCommandService.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Implements a service to collect user input for profiling and convert to a <see cref="PythonProfilingCommandArgs"/>.
/// </summary>
[Export(typeof(IPythonProfilerCommandService))]
class PythonProfilerCommandService : IPythonProfilerCommandService {
private readonly CommandArgumentBuilder _commandArgumentBuilder;
private readonly UserInputDialog _userInputDialog;

public PythonProfilerCommandService() {
_commandArgumentBuilder = new CommandArgumentBuilder();
_userInputDialog = new UserInputDialog();
}

/// <summary>
/// Collects user input and constructs a <see cref="PythonProfilingCommandArgs"/> object.
/// </summary>
/// <returns>
/// A <see cref="PythonProfilingCommandArgs"/> object based on user input, or <c>null</c> if canceled.
/// </returns>
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;
}
}
}
33 changes: 33 additions & 0 deletions Python/Product/Profiling/Profiling/PythonProfilingCommandArgs.cs
Original file line number Diff line number Diff line change
@@ -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 {
/// <summary>
/// Represents the arguments for a Python profiling command.
/// </summary>
[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<string, string> EnvVars { get; set; }
}
}

2 changes: 1 addition & 1 deletion Python/Product/Profiling/Profiling/StandaloneTargetView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ public string InterpreterPath {
}

/// <summary>
/// True if InterpreterPath is valid; false if it will be ignored.
/// True if PythonExePath is valid; false if it will be ignored.
/// </summary>
public bool CanSpecifyInterpreterPath {
get {
Expand Down
Loading

0 comments on commit dbac8cc

Please sign in to comment.