Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 7 additions & 29 deletions Build/PreBuild.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@ param (
# The etwtrace release that includes Python 3.12 through 3.14 wheels.
[Parameter()]
[string] $etwtraceVersion = "0.1b9",

# The last etwtrace release that includes Python 3.9 through 3.11 wheels.
[Parameter()]
[string] $etwtraceLegacyVersion = "0.1b8",

# Run in interactive mode for azure feed authentication, defaults to false
[Parameter()]
Expand All @@ -55,21 +51,16 @@ function Install-Package {
param(
[string] $packageName,
[string] $version,
[string] $outdir,
[string] $installDir
[string] $outdir
)

Write-Host "Installing $packageName $version"

if (-not $installDir) {
$installDir = $outdir
}

$argList = "install_pypi_package.py", $packageName, $version, "`"$installDir`""
$argList = "install_pypi_package.py", $packageName, $version, "`"$outdir`""
Start-Process -Wait -NoNewWindow "$outdir\python\tools\python.exe" -ErrorAction Stop -ArgumentList $argList | Write-Host

$installedVersion = ""
$versionPyFile = Join-Path $installDir "$packageName\_version.py"
$versionPyFile = Join-Path $outdir "$packageName\_version.py"
foreach ($line in Get-Content $versionPyFile) {
if ($line.Trim().StartsWith("`"version`"")) {
$installedVersion = $line.split(":")[1].Trim(" `"") # trim spaces and double quotes
Expand Down Expand Up @@ -286,26 +277,13 @@ try {
}

"-----"
# Install a coherent legacy payload for Python 3.9 through 3.11. Newer
# etwtrace releases only publish wheels for currently supported Pythons.
$legacyEtwTraceOutDir = Join-Path $outdir "etwtrace-legacy"
if (Test-Path -Path $legacyEtwTraceOutDir) {
Remove-Item -Recurse -Force -Path $legacyEtwTraceOutDir
}
Install-Package "etwtrace" $etwtraceLegacyVersion $outdir $legacyEtwTraceOutDir | Out-Null

# Install the current payload for Python 3.12 and later.
# Install etwtrace for the supported Python versions.
Install-Package "etwtrace" $etwtraceVersion $outdir | Out-Null

# Delete an unsigned file from etwtrace that shouldn't be there.
$filesToDelete = @(
"$outdir\etwtrace\test\DiagnosticsHub.InstrumentationCollector.dll",
"$legacyEtwTraceOutDir\etwtrace\test\DiagnosticsHub.InstrumentationCollector.dll"
)
foreach ($fileToDelete in $filesToDelete) {
if (Test-Path -Path $fileToDelete) {
Remove-Item -Path $fileToDelete | Out-Null
}
$fileToDelete = "$outdir\etwtrace\test\DiagnosticsHub.InstrumentationCollector.dll"
if (Test-Path -Path $fileToDelete) {
Remove-Item -Path $fileToDelete | Out-Null
}

} finally {
Expand Down
11 changes: 2 additions & 9 deletions Python/Product/Profiling/Profiling.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -200,15 +200,8 @@
<VSIXSubPath>etwtrace\%(RecursiveDir)</VSIXSubPath>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EtwTraceFiles>
<EtwTraceLegacyFiles Include="$(PackagesPath)etwtrace-legacy\etwtrace\**\*" />
<EtwTraceLegacyFiles>
<IncludeInVSIX>true</IncludeInVSIX>
<Link>etwtrace_legacy\etwtrace\%(RecursiveDir)%(Filename)%(Extension)</Link>
<VSIXSubPath>etwtrace_legacy\etwtrace\%(RecursiveDir)</VSIXSubPath>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EtwTraceLegacyFiles>
<Content Include="@(EtwTraceFiles);@(EtwTraceLegacyFiles)" />
<FileWrites Include="@(EtwTraceFiles);@(EtwTraceLegacyFiles)" />
<Content Include="@(EtwTraceFiles)" />
<FileWrites Include="@(EtwTraceFiles)" />
</ItemGroup>
</Target>
<Import Project="..\ProjectAfter.settings" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@

namespace Microsoft.PythonTools.Profiling {
using System;
using System.ComponentModel;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using Microsoft.PythonTools.Infrastructure;
using Microsoft.VisualStudio.Shell;

/// <summary>
Expand All @@ -28,6 +31,10 @@ namespace Microsoft.PythonTools.Profiling {
[Export(typeof(IPythonProfilerCommandService))]
[PartCreationPolicy(CreationPolicy.Shared)]
public sealed class PythonProfilerCommandService : IPythonProfilerCommandService {
private const int MinimumSupportedPythonMinorVersion = 12;
private const int MaximumSupportedPythonMinorVersion = 14;
private static readonly TimeSpan InterpreterVersionTimeout = TimeSpan.FromSeconds(10);

private readonly CommandArgumentBuilder _commandArgumentBuilder;
private readonly IServiceProvider _serviceProvider;
private readonly UserInputDialog _userInputDialog;
Expand All @@ -54,7 +61,38 @@ public async Task<IPythonProfilingCommandArgs> GetCommandArgsFromUserInput() {

if (_userInputDialog.ShowDialog(targetView, _serviceProvider)) {
var target = targetView.GetTarget();
return _commandArgumentBuilder.BuildCommandArgsFromTarget(target, _serviceProvider);
var commandArgs = _commandArgumentBuilder.BuildCommandArgsFromTarget(target, _serviceProvider);
if (commandArgs == null) {
return null;
}

var pythonVersion = await GetPythonVersionAsync(commandArgs.PythonExePath);
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

if (pythonVersion == null) {
MessageBox.Show(
Strings.ProfilingInterpreterVersionUnavailable.FormatUI(commandArgs.PythonExePath),
Strings.ProductTitle,
MessageBoxButton.OK,
MessageBoxImage.Error
);
return null;
}

if (!IsSupportedPythonVersion(pythonVersion)) {
MessageBox.Show(
Strings.ProfilingUnsupportedPythonVersion.FormatUI(
pythonVersion.Major,
pythonVersion.Minor
),
Strings.ProductTitle,
MessageBoxButton.OK,
MessageBoxImage.Warning
);
return null;
}

return commandArgs;
}
} catch (Exception ex) {
Debug.Fail($"Error displaying user input dialog: {ex.Message}");
Expand All @@ -63,5 +101,42 @@ public async Task<IPythonProfilingCommandArgs> GetCommandArgsFromUserInput() {

return null;
}

private static bool IsSupportedPythonVersion(Version version) {
return version.Major == 3 &&
version.Minor >= MinimumSupportedPythonMinorVersion &&
version.Minor <= MaximumSupportedPythonMinorVersion;
}

private static async Task<Version> GetPythonVersionAsync(string interpreterPath) {
try {
using (var output = ProcessOutput.RunHiddenAndCapture(
interpreterPath,
"-c",
"import sys; print('{}.{}'.format(sys.version_info[0], sys.version_info[1]))"
)) {
var exited = await Task.Run(() => output.Wait(InterpreterVersionTimeout));
if (!exited) {
output.Kill();
return null;
}

Version version;
var versionText = output.ExitCode == 0
? output.StandardOutputLines.FirstOrDefault()
: null;
return Version.TryParse(versionText, out version) ? version : null;
}
} catch (ArgumentException ex) {
Debug.WriteLine($"Failed to query Python version: {ex.Message}");
return null;
} catch (Win32Exception ex) {
Debug.WriteLine($"Failed to query Python version: {ex.Message}");
return null;
} catch (InvalidOperationException ex) {
Debug.WriteLine($"Failed to query Python version: {ex.Message}");
return null;
}
}
}
}
18 changes: 18 additions & 0 deletions Python/Product/Profiling/Strings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions Python/Product/Profiling/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,12 @@ Error:
<data name="ProfilingSupportMissingError" xml:space="preserve">
<value>Profiling support seems to be missing or corrupt. Try repairing your Visual Studio installation.</value>
</data>
<data name="ProfilingInterpreterVersionUnavailable" xml:space="preserve">
<value>Visual Studio could not determine the version of the selected Python interpreter '{0}'. Verify that it is a valid Python interpreter and try again.</value>
</data>
<data name="ProfilingUnsupportedPythonVersion" xml:space="preserve">
<value>Python {0}.{1} is not supported for profiling. Visual Studio Python profiling supports Python 3.12 through 3.14. Select a supported interpreter and try again.</value>
</data>
<data name="ProfilingSessionNotConfigured" xml:space="preserve">
<value>Profiling session has not been configured. Configure now and launch?</value>
</data>
Expand Down
47 changes: 3 additions & 44 deletions Python/Product/Profiling/diaghub_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,8 @@
_TARGET_ARGUMENTS = "PTVS_DIAGHUB_TARGET_ARGUMENTS"


def _load_legacy_collector():
import ctypes

collector_root = os.getenv("DIAGHUB_INSTR_COLLECTOR_ROOT")
runtime_name = os.getenv("DIAGHUB_INSTR_RUNTIME_NAME")
if not collector_root or not runtime_name:
raise RuntimeError(
"Python profiling must be launched from the Visual Studio "
"Performance Profiler."
)

if sys.winver.endswith("-32"):
architecture = "x86"
elif sys.winver.endswith("-arm64"):
architecture = "arm64"
else:
architecture = "amd64"

collector_path = os.path.join(collector_root, architecture, runtime_name)
collector = ctypes.WinDLL(collector_path)
collector.ChildAttach.argtypes = []
collector.ChildAttach.restype = ctypes.c_int
if not collector.ChildAttach():
raise RuntimeError("Failed to attach Python to the profiling session.")
return collector


def _is_supported_version(version):
return (3, 9) <= tuple(version[:2]) <= (3, 14)
return (3, 12) <= tuple(version[:2]) <= (3, 14)


def _has_valid_target(arguments):
Expand Down Expand Up @@ -82,7 +55,7 @@ def _run_target(arguments):
def main():
if not _is_supported_version(sys.version_info):
print(
"Visual Studio Python profiling supports Python 3.9 through 3.14.",
"Visual Studio Python profiling supports Python 3.12 through 3.14.",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be a loc string

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its printed message is a fallback diagnostic. The normal user-facing unsupported-version message does come from PTVS before the session starts and uses localized Strings.resx.

file=sys.stderr,
)
return 2
Expand All @@ -96,16 +69,9 @@ def main():
if not is_child:
return _run_profiled_child(arguments)

package_root = os.path.dirname(os.path.abspath(__file__))
is_legacy = sys.version_info[:2] <= (3, 11)
if is_legacy:
package_root = os.path.join(package_root, "etwtrace_legacy")
sys.path.insert(0, package_root)
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

try:
# etwtrace 0.1b8 expects Visual Studio to load and attach the collector.
# Keep this reference alive while the extension uses the collector.
collector = _load_legacy_collector() if is_legacy else None
from etwtrace import DiagnosticsHubTracer
tracer = DiagnosticsHubTracer()
except (ImportError, OSError, RuntimeError, AttributeError) as exc:
Expand All @@ -122,14 +88,7 @@ def main():
arguments[0] = os.path.abspath(arguments[0])
sys.path[0] = os.path.dirname(arguments[0])

if not hasattr(tracer, "_data"):
# etwtrace 0.1b8 only initializes this field for its test collector.
tracer._data = []
tracer.ignore(os.path.abspath(__file__))
# Keep profiling and the compatibility collector alive until interpreter
# shutdown. Worker threads may retain native profile callbacks after the
# top-level script returns.
tracer._collector_keepalive = collector
tracer.enable()
_run_target(arguments)
return 0
Expand Down