Skip to content
Open
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
10 changes: 10 additions & 0 deletions src/Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@
<None Include="$(MicrosoftDebuggingDataModelDbgModelApiXtnPath)\**\*" CopyToOutputDirectory="PreserveNewest" Visible="False" Link="%(RecursiveDir)%(FileName)%(Extension)"/>
</ItemGroup>

<!-- Drop symsrv.dll next to msdia140.dll so DIA's loadDataForExe can resolve PDBs from symbol servers.
msdia140 looks for symsrv.dll in its own directory; without this, srv*...*https://... segments are silently ignored. -->
<ItemGroup Condition="'$(IncludeDbgXAssets)'=='true' and '$(MicrosoftDebuggingPlatformSymSrvContent)' != ''">
<Content Include="$(MicrosoftDebuggingPlatformSymSrvContent)amd64\symsrv.dll" Condition="Exists('$(MicrosoftDebuggingPlatformSymSrvContent)amd64\symsrv.dll')">
<Link>symsrv.dll</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Visible>False</Visible>
</Content>
</ItemGroup>

<!-- Embed the git hash in the assembly informational version -->
<Target Name="InitializeSourceControlInformation" BeforeTargets="AddSourceRevisionToInformationalVersion">
<Exec
Expand Down
3 changes: 3 additions & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
<PackageVersion Include="Castle.Windsor" Version="6.0.0" />
<PackageVersion Include="ClosedXML" Version="0.105.0" />
<PackageVersion Include="DiffPlex" Version="1.7.2" />
<PackageVersion Include="DiffPlex.Wpf" Version="1.4.0" />
<PackageVersion Include="DotNet.ReproducibleBuilds" Version="2.0.5" />
<PackageVersion Include="DotNet.ReproducibleBuilds.Isolated" Version="2.0.5" />
<PackageVersion Include="Microsoft.Debugging.DataModel.DbgModelApiXtn" Version="20260619.1305.0" />
<PackageVersion Include="Microsoft.Debugging.Platform.DbgEng" Version="20260319.1511.0" />
<PackageVersion Include="Microsoft.Debugging.Platform.DbgX" Version="20260622.1.0" />
<PackageVersion Include="Microsoft.Debugging.Platform.SymSrv" Version="20260319.1511.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.9" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.77" />
Expand Down
31 changes: 29 additions & 2 deletions src/SizeBench.AnalysisEngine/DIAInterop/DIAAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ internal sealed class DIAAdapter : IDIAAdapter, IDisposable
private uint _sectionAlignment;
private readonly int _affinitizedThreadId;

/// <summary>
/// The path DIA actually loaded the PDB from. For explicit-PDB sessions this echoes the path we passed
/// to <c>loadDataFromPdb</c>. For symbol-server sessions this is the cached/downloaded location DIA
/// resolved via <c>loadDataForExe</c>.
/// </summary>
internal string? LoadedPdbPath { get; private set; }

private static readonly string[] debugFastlinkSwitchNames = ["/debug:fastlink"];

[ThreadStatic]
Expand Down Expand Up @@ -124,6 +131,16 @@ private PEFile PEFile
#region Construction, opening, all the startup-y things

internal DIAAdapter(Session session, string pdbPath)
: this(session, source => ((IDiaDataSourceEx2)source).loadDataFromPdbEx(pdbPath, fPdbPrefetching: 1), pdbPath)
{
}

internal DIAAdapter(Session session, string binaryPath, string symbolSearchPath)
: this(session, source => source.loadDataForExe(binaryPath, symbolSearchPath, pCallback: null), binaryPath)
{
}

private DIAAdapter(Session session, Action<IDiaDataSource> loadFromSource, string sourceDescriptionForErrors)
{
this._session = session;
this._cache = session.DataCache;
Expand All @@ -135,7 +152,7 @@ internal DIAAdapter(Session session, string pdbPath)
{
this._diaDataSource = CoClassLoaderRegFree.CreateInstance<IDiaDataSourceEx2>(_diaLibraryModule, Dia140Clsid);

this._diaDataSource.loadDataFromPdbEx(pdbPath, fPdbPrefetching: 1);
loadFromSource(this._diaDataSource);

this._diaDataSource.openSession(out var diaSession);
this._diaSession = (IDiaSessionEx)diaSession;
Expand All @@ -149,7 +166,7 @@ internal DIAAdapter(Session session, string pdbPath)
{
if (((uint)comException.HResult) == Convert.ToUInt32(diaHRESULTValues.GetValue(i), CultureInfo.InvariantCulture.NumberFormat))
{
throw new PDBNotSuitableForAnalysisException($"Unable to open PDB from '{session.PdbPath}'" + Environment.NewLine +
throw new PDBNotSuitableForAnalysisException($"Unable to open PDB from '{sourceDescriptionForErrors}'" + Environment.NewLine +
$"DIA returned this error: {Enum.GetName((DIAHRESULTs)comException.HResult)}", comException);

}
Expand All @@ -158,6 +175,16 @@ internal DIAAdapter(Session session, string pdbPath)
}

this._globalScope = this._diaSession.globalScope;

try
{
this.LoadedPdbPath = this._globalScope.symbolsFileName;
}
catch (COMException)
{
// Leave LoadedPdbPath null if DIA can't report it for this PDB type
}

var machineType = this._globalScope.machineType;

// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms680313(v=vs.85).aspx
Expand Down
32 changes: 26 additions & 6 deletions src/SizeBench.AnalysisEngine/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ public sealed class Session : ISession
{
private readonly string _originalPDBPathMayBeRemote;
private GuaranteedLocalFile? _guaranteedLocalPDBFile;
public string PdbPath => this._guaranteedLocalPDBFile?.OriginalPath ?? "No pdb opened yet";
public string PdbPath => this._guaranteedLocalPDBFile?.OriginalPath
?? this._diaAdapter?.LoadedPdbPath
?? (String.IsNullOrEmpty(this._originalPDBPathMayBeRemote) ? "No pdb opened yet" : this._originalPDBPathMayBeRemote);

private readonly string _originalBinaryPathMayBeRemote;
public string BinaryPath => this._peFile?.GuaranteedLocalCopyOfBinary.OriginalPath ?? "No binary opened yet";
Expand Down Expand Up @@ -141,8 +143,14 @@ internal Session(string binaryPath, string pdbPath, SessionOptions options, ILog
this.SessionOptions = options;
this.DataCache = new SessionDataCache(options.SymbolSourcesSupported);

Debug.Assert(File.Exists(pdbPath));
this._originalPDBPathMayBeRemote = pdbPath;
var hasExplicitPdbPath = !String.IsNullOrEmpty(pdbPath);
var hasSymbolServer = !String.IsNullOrWhiteSpace(options.SymbolServerSearchPath);
if (!hasExplicitPdbPath && !hasSymbolServer)
{
throw new ArgumentException("Either a PDB path or a SymbolServerSearchPath in SessionOptions must be supplied.", nameof(pdbPath));
}
Debug.Assert(!hasExplicitPdbPath || File.Exists(pdbPath));
this._originalPDBPathMayBeRemote = pdbPath ?? String.Empty;
Debug.Assert(File.Exists(binaryPath));
this._originalBinaryPathMayBeRemote = binaryPath;

Expand All @@ -160,10 +168,22 @@ private void InitializeDIAThread()
using var initializeDiaThreadLog = this._logger.StartTaskLog($"Setting up initial data needed to open the session (symbol sources: {this.SessionOptions.SymbolSourcesSupported})");
this._diaManagedThreadId = Environment.CurrentManagedThreadId;

this.ProgressReporter?.Report(new SessionTaskProgress("Copying PDB file locally if necessary.", 0, null));
this._guaranteedLocalPDBFile = new GuaranteedLocalFile(this._originalPDBPathMayBeRemote, initializeDiaThreadLog);
var hasExplicitPdb = !String.IsNullOrEmpty(this._originalPDBPathMayBeRemote);
if (hasExplicitPdb)
{
this.ProgressReporter?.Report(new SessionTaskProgress("Copying PDB file locally if necessary.", 0, null));
this._guaranteedLocalPDBFile = new GuaranteedLocalFile(this._originalPDBPathMayBeRemote, initializeDiaThreadLog);

this._diaAdapter = new DIAAdapter(this, this._guaranteedLocalPDBFile.GuaranteedLocalPath);
}
else
{
var searchPath = this.SessionOptions.SymbolServerSearchPath!;
initializeDiaThreadLog.Log($"No explicit PDB supplied - using symbol server search path: {searchPath}");
this.ProgressReporter?.Report(new SessionTaskProgress("Downloading PDB from symbol server if necessary.", 0, null));

this._diaAdapter = new DIAAdapter(this, this._guaranteedLocalPDBFile.GuaranteedLocalPath);
this._diaAdapter = new DIAAdapter(this, this._originalBinaryPathMayBeRemote, searchPath);
}
this._taskParameters = new SessionTaskParameters(this, this._diaAdapter, this.DataCache);

this._peFile = new PEFile(this._originalBinaryPathMayBeRemote, this.SessionOptions.SymbolSourcesSupported, initializeDiaThreadLog);
Expand Down
14 changes: 14 additions & 0 deletions src/SizeBench.AnalysisEngine/SessionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,18 @@
public sealed record class SessionOptions
{
public SymbolSourcesSupported SymbolSourcesSupported { get; init; } = SymbolSourcesSupported.All;

/// <summary>
/// Optional symsrv-style symbol search path used to locate (and download) PDBs when an explicit
/// PDB path is not supplied. The string is passed verbatim to DIA's loadDataForExe, so it follows
/// the standard format used by dbghelp / symsrv. Examples:
/// <list type="bullet">
/// <item><description><c>srv*https://msdl.microsoft.com/download/symbols</c></description></item>
/// <item><description><c>srv*C:\Symbols*https://msdl.microsoft.com/download/symbols</c></description></item>
/// <item><description><c>C:\local\symbols;srv*C:\cache*https://internal.symbol.server</c></description></item>
/// </list>
/// When this is non-empty and the PDB path passed to <see cref="Session.Create(string, string, SessionOptions, SizeBench.Logging.ILogger)"/>
/// is empty, the symbol server will be used to locate the PDB for the binary.
/// </summary>
public string? SymbolServerSearchPath { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="Castle.Windsor" />
<PackageReference Include="Microsoft.Debugging.DataModel.DbgModelApiXtn" />
<PackageReference Include="Microsoft.Debugging.Platform.DbgEng" />
<PackageReference Include="Microsoft.Debugging.Platform.DbgX" />
<PackageReference Include="System.ComponentModel.Composition" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using SizeBench.LocalBuild;
using System.IO;
using SizeBench.LocalBuild;
using SizeBench.PathLocators;

namespace SizeBench.GUI.Windows.Tests;
Expand Down Expand Up @@ -27,4 +28,39 @@ public void OKButtonRemainsDisabledWithJustBinaryPathSet()
Assert.IsFalse(vm.OKEnabled);
Assert.IsEmpty(propertiesChanged);
}

[TestMethod]
public void OKButtonEnabledWhenBinaryExistsAndSymbolServerConfigured()
{
// Pick an existing binary that we know does not have a matching PDB next to it on disk.
// notepad.exe is always present in System32 on test machines and won't have a sibling .pdb.
var existingBinaryPath = Path.Combine(Environment.SystemDirectory, "notepad.exe");
if (!File.Exists(existingBinaryPath))
{
Assert.Inconclusive($"Cannot run test - expected sentinel binary at {existingBinaryPath}");
}

var sso = new SelectSessionOptionsControlViewModel();
var vm = new OpenSingleBinaryWindowViewModel(new SelectSingleBinaryAndPDBControlViewModel(new IBinaryLocator[] { new LocalBuildPathLocator() }),
sso);

// Just the binary, no PDB, no symbol server -> still disabled
vm.SelectSingleBinaryAndPDBControlViewModel.BinaryPath = existingBinaryPath;
// Defensive: blow away any inferred PDB so the symbol-server branch is the only way to enable OK
vm.SelectSingleBinaryAndPDBControlViewModel.PDBPath = String.Empty;
Assert.IsFalse(vm.OKEnabled);

// Enabling the symbol server checkbox without any paths -> still disabled
sso.UseSymbolServer = true;
sso.SymbolServerPathsText = String.Empty;
Assert.IsFalse(vm.OKEnabled);

// Once a path is provided, OK becomes enabled even without a local PDB
sso.SymbolServerPathsText = "srv*https://msdl.microsoft.com/download/symbols";
Assert.IsTrue(vm.OKEnabled);

// Disabling the symbol server toggle should drop the OK enablement again
sso.UseSymbolServer = false;
Assert.IsFalse(vm.OKEnabled);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,36 @@ public void ChangingIndividualSymbolTypesUpdatesSymbolSourcesSupported()
Assert.HasCount(4, propertiesChanged);
Assert.AreSequenceEqual(new[] { "", "", "", "" }, propertiesChanged, Microsoft.VisualStudio.TestTools.UnitTesting.SequenceOrder.InAnyOrder);
}

[TestMethod]
public void SymbolServerToggleFlowsThroughToSessionOptions()
{
var vm = new SelectSessionOptionsControlViewModel();

// Default - no symbol server
Assert.IsFalse(vm.UseSymbolServer);
Assert.IsNull(vm.SessionOptions.SymbolServerSearchPath);
Assert.IsFalse(vm.HasAnySymbolServerPaths);

// Setting the text only does not enable usage; SessionOptions still has null search path
vm.SymbolServerPathsText = "srv*https://contoso/symbols";
Assert.IsTrue(vm.HasAnySymbolServerPaths);
Assert.IsNull(vm.SessionOptions.SymbolServerSearchPath);

// Enabling the checkbox surfaces the search path to SessionOptions
vm.UseSymbolServer = true;
Assert.AreEqual("srv*https://contoso/symbols", vm.SessionOptions.SymbolServerSearchPath);

// Multiple lines are joined with ';' for symsrv
vm.SymbolServerPathsText = "C:\\local\r\nsrv*C:\\cache*https://msdl";
Assert.AreEqual("C:\\local;srv*C:\\cache*https://msdl", vm.SessionOptions.SymbolServerSearchPath);

// Blank lines are ignored
vm.SymbolServerPathsText = "\r\n\r\nC:\\local\r\n\r\n";
Assert.AreEqual("C:\\local", vm.SessionOptions.SymbolServerSearchPath);

// Disabling the toggle removes the search path even if text remains
vm.UseSymbolServer = false;
Assert.IsNull(vm.SessionOptions.SymbolServerSearchPath);
}
}
115 changes: 115 additions & 0 deletions src/SizeBench.GUI/Settings/AppSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SizeBench.GUI.Settings;

internal sealed class AppSettings : IAppSettings
{
private const string DefaultPublicMsftSymbolServer = "srv*https://msdl.microsoft.com/download/symbols";

private readonly string _filePath;
private readonly PersistedShape _data;

public AppSettings() : this(DefaultSettingsFilePath()) { }

internal AppSettings(string filePath)
{
this._filePath = filePath;
this._data = Load(filePath);
}

public bool UseSymbolServer
{
get => this._data.UseSymbolServer;
set
{
if (this._data.UseSymbolServer != value)
{
this._data.UseSymbolServer = value;
Save();
}
}
}

public IList<string> SymbolServerPaths => this._data.SymbolServerPaths;

public void SetSymbolServerPaths(IEnumerable<string> paths)
{
ArgumentNullException.ThrowIfNull(paths);

this._data.SymbolServerPaths = paths.Where(p => !String.IsNullOrWhiteSpace(p))
.Select(p => p.Trim())
.ToList();
Save();
}

public string BuildSymbolSearchPath()
=> String.Join(";", this._data.SymbolServerPaths.Where(p => !String.IsNullOrWhiteSpace(p)));

private void Save()
{
try
{
var dir = Path.GetDirectoryName(this._filePath);
if (!String.IsNullOrEmpty(dir))
{
Directory.CreateDirectory(dir);
}
var json = JsonSerializer.Serialize(this._data, SerializerOptions);
File.WriteAllText(this._filePath, json);
}
#pragma warning disable CA1031 // Do not catch general exception types - settings persistence failures should not crash the app
catch
{
// Best-effort persistence: if we can't save settings we still want the app to keep working.
}
#pragma warning restore CA1031
}

internal static string DefaultSettingsFilePath()
=> Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"SizeBench",
"settings.json");

private static PersistedShape Load(string filePath)
{
try
{
if (File.Exists(filePath))
{
var json = File.ReadAllText(filePath);
var loaded = JsonSerializer.Deserialize<PersistedShape>(json, SerializerOptions);
if (loaded != null)
{
loaded.SymbolServerPaths ??= new List<string>();
return loaded;
}
}
}
#pragma warning disable CA1031 // Do not catch general exception types - corrupt settings should fall back to defaults
catch
{
// Fall through to defaults on any read/parse failure
}
#pragma warning restore CA1031

return new PersistedShape
{
UseSymbolServer = false,
SymbolServerPaths = new List<string> { DefaultPublicMsftSymbolServer },
};
}

private static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};

internal sealed class PersistedShape
{
public bool UseSymbolServer { get; set; }
public List<string> SymbolServerPaths { get; set; } = new();
}
}
10 changes: 10 additions & 0 deletions src/SizeBench.GUI/Settings/IAppSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace SizeBench.GUI.Settings;

public interface IAppSettings
{
bool UseSymbolServer { get; set; }

IList<string> SymbolServerPaths { get; }

void SetSymbolServerPaths(IEnumerable<string> paths);
}
3 changes: 3 additions & 0 deletions src/SizeBench.GUI/SizeBench.GUI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@
<PackageReference Include="Castle.Windsor" />
<PackageReference Include="DiffPlex" />
<PackageReference Include="Microsoft.Debugging.DataModel.DbgModelApiXtn" />
<PackageReference Include="Microsoft.Debugging.Platform.DbgEng" />
<PackageReference Include="Microsoft.Debugging.Platform.DbgX" />
<PackageReference Include="Microsoft.Debugging.Platform.SymSrv" />
<PackageReference Include="DiffPlex.Wpf" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" />
<PackageReference Include="System.ComponentModel.Composition" />
</ItemGroup>
Expand Down
Loading