Skip to content

Commit dae4255

Browse files
vancemsharwell
authored andcommitted
Support SourceLink in Microsoft PDBs
This allows the 'Goto Source' functionality to work on PDBs (Microsoft or Portable) that uses SourceLink to describe how to fetch the source. This is the mechanism used in .NET Core and thus Goto Source 'Just works' on .NET Core V2.0.3 or later.
1 parent 01a41e8 commit dae4255

File tree

5 files changed

+173
-104
lines changed

5 files changed

+173
-104
lines changed

src/Directory.Build.props

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
</PropertyGroup>
2222

2323
<PropertyGroup>
24-
<PerfViewVersion>2.0.1.0</PerfViewVersion>
24+
<PerfViewVersion>2.0.2.0</PerfViewVersion>
2525
</PropertyGroup>
2626

2727
<!-- versions of dependencies that more than one project use -->

src/PerfView/SupportFiles/UsersGuide.htm

+16-2
Original file line numberDiff line numberDiff line change
@@ -8094,6 +8094,19 @@ <h2>
80948094
</ul>
80958095
</li>
80968096
-->
8097+
<li>
8098+
Version 2.0.2 1/12/18
8099+
<ul>
8100+
<li>
8101+
Added support for SourceLink for 'Goto Source' functionality.
8102+
Symlink is a technique of finding source files by placing a mapping from built time file name to URL into the
8103+
symbol file so that the source code can be fetched by url at debug/profiling time.
8104+
.NET Core annotates all its symbol files this way. The result is that 'Goto Source' on .NET Core assemblies
8105+
(that is the framework and ASP.NET) just work in PerfView (it will bring up the relevant source).
8106+
</li>
8107+
8108+
</ul>
8109+
</li>
80978110
<li>
80988111
Version 2.0.1 1/8/18
80998112
<ul>
@@ -8106,8 +8119,9 @@ <h2>
81068119
<li>
81078120
Version 2.0.0 1/5/18
81088121
<ul>
8109-
<li> Officially update the version number to 2.0 in preparation for signing and releasing officially.
8110-
Only the version number update happens here.
8122+
<li>
8123+
Officially update the version number to 2.0 in preparation for signing and releasing officially.
8124+
Only the version number update happens here.
81118125
</li>
81128126

81138127
</ul>

src/TraceEvent/Symbols/NativeSymbolModule.cs

+40-3
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,12 @@ public override string Url
395395
{
396396
get
397397
{
398-
string target, command;
398+
string target = base.Url; // See if it is in sourceLink information.
399+
if (target != null)
400+
return target;
401+
402+
// Use srcsrv information
403+
string command;
399404
GetSourceServerTargetAndCommand(out target, out command);
400405

401406
if (!string.IsNullOrEmpty(target) && Uri.IsWellFormedUriString(target, UriKind.Absolute))
@@ -426,8 +431,13 @@ public override string Url
426431
///
427432
/// If what is at the end is a valid URL it is looked up.
428433
/// </summary>
429-
public override string GetSourceFromSrcServer()
434+
protected override string GetSourceFromSrcServer()
430435
{
436+
// Try getting the source from the source server using SourceLink information.
437+
var ret = base.GetSourceFromSrcServer();
438+
if (ret != null)
439+
return ret;
440+
431441
var cacheDir = _symbolModule.SymbolReader.SourceCacheDirectory;
432442

433443
string target, fetchCmdStr;
@@ -627,7 +637,7 @@ private void GetSourceServerTargetAndCommand(out string target, out string comma
627637

628638
_log.WriteLine("*** Looking up {0} using source server", BuildTimeFilePath);
629639

630-
var srcServerPdb = (_symbolModule as NativeSymbolModule).PdbForSourceServer as NativeSymbolModule;
640+
NativeSymbolModule srcServerPdb = (_symbolModule as NativeSymbolModule).PdbForSourceServer as NativeSymbolModule;
631641
if (srcServerPdb == null)
632642
{
633643
_log.WriteLine("*** Could not find PDB to look up source server information");
@@ -886,6 +896,33 @@ internal string GetSrcSrvStream()
886896
}
887897
}
888898

899+
protected override string GetSourceLinkJson()
900+
{
901+
// To avoid the expensive match below, don't even try to get SourceLink data if you have srcsrv data.
902+
// This at least avoids the cost on many OS PDBs.
903+
// You can remove this when we can look up SourceLink information easily.
904+
uint len = 0;
905+
m_source.getStreamSize("srcsrv", out len);
906+
if (len != 0)
907+
{
908+
m_reader.m_log.WriteLine("Has srcsrv information, skipping looking for SourceLink information for {0}", SymbolFilePath);
909+
return null;
910+
}
911+
912+
// TODO We should be using msdia APIs to fetch this.
913+
// I don't know exactly which ones. In the mean time we grep for something in the PDB that looks like the SourceLink json blob.
914+
string allData = File.ReadAllText(SymbolFilePath);
915+
Match m = Regex.Match(allData, "({\\s*\"documents\"\\s*:\\s*{[ -~]*?}\\s*})", RegexOptions.Singleline);
916+
if (m.Success)
917+
{
918+
string ret = m.Groups[1].Value;
919+
m_reader.m_log.WriteLine("Found SourceLink Infomation for {0}\r\nData: {1}", SymbolFilePath, ret.Replace("\r\n", " "));
920+
return ret;
921+
}
922+
m_reader.m_log.WriteLine("Failed to look up SourceLink Infomation for {0}.", SymbolFilePath);
923+
return null;
924+
}
925+
889926
// returns the path of the PDB that has source server information in it (which for NGEN images is the PDB for the managed image)
890927
internal ManagedSymbolModule PdbForSourceServer
891928
{

src/TraceEvent/Symbols/PortableSymbolModule.cs

+2-67
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ public PortableSymbolModule(SymbolReader reader, Stream stream, string pdbFileNa
1717
_stream = stream;
1818
_provider = MetadataReaderProvider.FromPortablePdbStream(_stream);
1919
_metaData = _provider.GetMetadataReader();
20-
21-
InitializeFileToUrlMap();
2220
}
2321

2422
public override Guid PdbGuid
@@ -58,68 +56,7 @@ public override SourceLocation SourceLocationForManagedCode(uint methodMetadataT
5856

5957
#region private
6058

61-
private string GetUrlForFilePath(string buildTimeFilePath)
62-
{
63-
if (_fileToUrlMap != null)
64-
{
65-
foreach (Tuple<string, string> map in _fileToUrlMap)
66-
{
67-
string path = map.Item1;
68-
string urlReplacement = map.Item2;
69-
70-
if (buildTimeFilePath.StartsWith(path, StringComparison.OrdinalIgnoreCase))
71-
{
72-
string tail = buildTimeFilePath.Substring(path.Length, buildTimeFilePath.Length - path.Length).Replace('\\', '/');
73-
return urlReplacement.Replace("*", tail);
74-
}
75-
}
76-
}
77-
return null;
78-
}
79-
80-
/// <summary>
81-
/// Looks up SourceLink information (if present) and initializes _fileToUrlMap from it
82-
/// </summary>
83-
private void InitializeFileToUrlMap()
84-
{
85-
string sourceLinkJson = GetSourceLinkJson();
86-
if (sourceLinkJson == null)
87-
return;
88-
89-
// TODO this is not right for corner cases (e.g. file paths with " or , } in them)
90-
Match m = Regex.Match(sourceLinkJson, @"documents.?\s*:\s*{(.*?)}", RegexOptions.Singleline);
91-
if (m.Success)
92-
{
93-
string mappings = m.Groups[1].Value;
94-
while (!string.IsNullOrWhiteSpace(mappings))
95-
{
96-
m = Regex.Match(m.Groups[1].Value, "^\\s*\"(.*?)\"\\s*:\\s*\"(.*?)\"\\s*,?(.*)", RegexOptions.Singleline);
97-
if (m.Success)
98-
{
99-
if (_fileToUrlMap == null)
100-
_fileToUrlMap = new List<Tuple<string, string>>();
101-
string pathSpec = m.Groups[1].Value.Replace("\\\\", "\\");
102-
if (pathSpec.EndsWith("*"))
103-
{
104-
pathSpec = pathSpec.Substring(0, pathSpec.Length - 1); // Remove the *
105-
_fileToUrlMap.Add(new Tuple<string, string>(pathSpec, m.Groups[2].Value));
106-
}
107-
else
108-
_log.WriteLine("Warning: {0} does not end in *, skipping this mapping.", pathSpec);
109-
mappings = m.Groups[3].Value;
110-
}
111-
else
112-
{
113-
_log.WriteLine("Error: Could not parse SourceLink Mapping: {0}", mappings);
114-
break;
115-
}
116-
}
117-
}
118-
else
119-
_log.WriteLine("Error: Could not parse SourceLink Json: {0}", sourceLinkJson);
120-
}
121-
122-
private string GetSourceLinkJson()
59+
protected override string GetSourceLinkJson()
12360
{
12461
foreach(CustomDebugInformationHandle customDebugInformationHandle in _metaData.CustomDebugInformation)
12562
{
@@ -162,8 +99,6 @@ internal PortablePdbSourceFile(Document sourceFileDocument, PortableSymbolModule
16299
_log.WriteLine("Opened Portable Pdb Source File: {0}", BuildTimeFilePath);
163100
}
164101

165-
public override string Url { get { return _portablePdb.GetUrlForFilePath(BuildTimeFilePath); } }
166-
167102
#region private
168103
private static Guid HashAlgorithmSha1 = Guid.Parse("ff1816ec-aa5e-4d10-87f7-6f4963833460");
169104
private static Guid HashAlgorithmSha256 = Guid.Parse("8829d00f-11b8-4213-878b-770e8597ac16");
@@ -179,7 +114,7 @@ internal PortablePdbSourceFile(Document sourceFileDocument, PortableSymbolModule
179114
// Needed by other things to look up data
180115
internal MetadataReader _metaData;
181116

182-
List<Tuple<string, string>> _fileToUrlMap; // Used by SourceLink to map build paths to URLs (see GetUrlForFilePath)
117+
183118
MetadataReaderProvider _provider;
184119
Stream _stream;
185120
#endregion

src/TraceEvent/Symbols/SymbolReader.cs

+114-31
Original file line numberDiff line numberDiff line change
@@ -1419,13 +1419,95 @@ public abstract class ManagedSymbolModule
14191419
/// </summary>
14201420
public abstract SourceLocation SourceLocationForManagedCode(uint methodMetadataToken, int ilOffset);
14211421

1422+
/// <summary>
1423+
/// If the symbol file format supports SourceLink JSON this routine should be overriden
1424+
/// to return it.
1425+
/// </summary>
1426+
protected virtual string GetSourceLinkJson() { return null; }
1427+
14221428
#region private
1429+
14231430
protected ManagedSymbolModule(SymbolReader reader, string path) { _pdbPath = path; _reader = reader; }
14241431

14251432
internal TextWriter _log { get { return _reader.m_log; } }
14261433

1434+
/// <summary>
1435+
/// Return a URL for 'buildTimeFilePath' using the source link mapping (that 'GetSourceLinkJson' fetched)
1436+
/// Returns null if there is URL using the SourceLink
1437+
/// </summary>
1438+
/// <param name="buildTimeFilePath"></param>
1439+
/// <returns></returns>
1440+
internal string GetUrlForFilePathUsingSourceLink(string buildTimeFilePath)
1441+
{
1442+
if (!_sourceLinkMappingInited)
1443+
{
1444+
_sourceLinkMappingInited = true;
1445+
string sourceLinkJson = GetSourceLinkJson();
1446+
if (sourceLinkJson != null)
1447+
_sourceLinkMapping = ParseSourceLinkJson(sourceLinkJson);
1448+
}
1449+
1450+
if (_sourceLinkMapping != null)
1451+
{
1452+
foreach (Tuple<string, string> map in _sourceLinkMapping)
1453+
{
1454+
string path = map.Item1;
1455+
string urlReplacement = map.Item2;
1456+
1457+
if (buildTimeFilePath.StartsWith(path, StringComparison.OrdinalIgnoreCase))
1458+
{
1459+
string tail = buildTimeFilePath.Substring(path.Length, buildTimeFilePath.Length - path.Length).Replace('\\', '/');
1460+
return urlReplacement.Replace("*", tail);
1461+
}
1462+
}
1463+
}
1464+
return null;
1465+
}
1466+
1467+
/// <summary>
1468+
/// Parses SourceLink information and returns a list of filepath -> url Prefix tuples.
1469+
/// </summary>
1470+
private List<Tuple<string, string>> ParseSourceLinkJson(string sourceLinkJson)
1471+
{
1472+
List<Tuple<string, string>> ret = null;
1473+
// TODO this is not right for corner cases (e.g. file paths with " or , } in them)
1474+
Match m = Regex.Match(sourceLinkJson, @"documents.?\s*:\s*{(.*?)}", RegexOptions.Singleline);
1475+
if (m.Success)
1476+
{
1477+
string mappings = m.Groups[1].Value;
1478+
while (!string.IsNullOrWhiteSpace(mappings))
1479+
{
1480+
m = Regex.Match(m.Groups[1].Value, "^\\s*\"(.*?)\"\\s*:\\s*\"(.*?)\"\\s*,?(.*)", RegexOptions.Singleline);
1481+
if (m.Success)
1482+
{
1483+
if (ret == null)
1484+
ret = new List<Tuple<string, string>>();
1485+
string pathSpec = m.Groups[1].Value.Replace("\\\\", "\\");
1486+
if (pathSpec.EndsWith("*"))
1487+
{
1488+
pathSpec = pathSpec.Substring(0, pathSpec.Length - 1); // Remove the *
1489+
ret.Add(new Tuple<string, string>(pathSpec, m.Groups[2].Value));
1490+
}
1491+
else
1492+
_log.WriteLine("Warning: {0} does not end in *, skipping this mapping.", pathSpec);
1493+
mappings = m.Groups[3].Value;
1494+
}
1495+
else
1496+
{
1497+
_log.WriteLine("Error: Could not parse SourceLink Mapping: {0}", mappings);
1498+
break;
1499+
}
1500+
}
1501+
}
1502+
else
1503+
_log.WriteLine("Error: Could not parse SourceLink Json: {0}", sourceLinkJson);
1504+
return ret;
1505+
}
1506+
14271507
string _pdbPath;
14281508
SymbolReader _reader;
1509+
List<Tuple<string, string>> _sourceLinkMapping; // Used by SourceLink to map build paths to URLs (see GetUrlForFilePath)
1510+
bool _sourceLinkMappingInited; // Lazy init flag.
14291511
#endregion
14301512
}
14311513

@@ -1489,36 +1571,7 @@ public abstract class SourceFile
14891571
/// can be used to fetch it with HTTP Get), then return that Url. If no such publishing
14901572
/// point exists this property will return null.
14911573
/// </summary>
1492-
public virtual string Url { get { return null; } }
1493-
1494-
/// <summary>
1495-
/// Look up the source from the source server. Returns null if it can't find the source
1496-
/// By default this simply uses the Url to look it up on the web. If 'Url' returns null
1497-
/// so does this.
1498-
/// </summary>
1499-
public virtual string GetSourceFromSrcServer()
1500-
{
1501-
// Search the SourceLink url location
1502-
string url = Url;
1503-
if (url != null)
1504-
{
1505-
HttpClient httpClient = new HttpClient();
1506-
HttpResponseMessage response = httpClient.GetAsync(url).Result;
1507-
1508-
response.EnsureSuccessStatusCode();
1509-
Stream content = response.Content.ReadAsStreamAsync().Result;
1510-
string cachedLocation = GetCachePathForUrl(url);
1511-
if (cachedLocation != null)
1512-
{
1513-
using (FileStream file = File.Create(_filePath))
1514-
content.CopyTo(file);
1515-
return cachedLocation;
1516-
}
1517-
else
1518-
_log.WriteLine("Warning: SourceCache not set, giving up fetching source from the network.");
1519-
}
1520-
return null;
1521-
}
1574+
public virtual string Url { get { return _symbolModule.GetUrlForFilePathUsingSourceLink(BuildTimeFilePath); } }
15221575

15231576
/// <summary>
15241577
/// This may fetch things from the source server, and thus can be very slow, which is why it is not a property.
@@ -1609,10 +1662,40 @@ public virtual string GetSourceFile(bool requireChecksumMatch = false)
16091662

16101663
protected TextWriter _log { get { return _symbolModule._log; } }
16111664

1665+
/// <summary>
1666+
/// Look up the source from the source server. Returns null if it can't find the source
1667+
/// By default this simply uses the Url to look it up on the web. If 'Url' returns null
1668+
/// so does this.
1669+
/// </summary>
1670+
protected virtual string GetSourceFromSrcServer()
1671+
{
1672+
// Search the SourceLink url location
1673+
string url = Url;
1674+
if (url != null)
1675+
{
1676+
HttpClient httpClient = new HttpClient();
1677+
HttpResponseMessage response = httpClient.GetAsync(url).Result;
1678+
1679+
response.EnsureSuccessStatusCode();
1680+
Stream content = response.Content.ReadAsStreamAsync().Result;
1681+
string cachedLocation = GetCachePathForUrl(url);
1682+
if (cachedLocation != null)
1683+
{
1684+
Directory.CreateDirectory(Path.GetDirectoryName(cachedLocation));
1685+
using (FileStream file = File.Create(cachedLocation))
1686+
content.CopyTo(file);
1687+
return cachedLocation;
1688+
}
1689+
else
1690+
_log.WriteLine("Warning: SourceCache not set, giving up fetching source from the network.");
1691+
}
1692+
return null;
1693+
}
1694+
16121695
/// <summary>
16131696
/// Given 'fileName' which is a path to a file (which may not exist), set
16141697
/// _filePath and _checksumMatches appropriately. Namely _filePath should
1615-
/// always be the 'best' candidate for the soruce file path (matching checksum
1698+
/// always be the 'best' candidate for the source file path (matching checksum
16161699
/// wins, otherwise first existing file wins).
16171700
///
16181701
/// Returns true if we have a perfect match (no additional probing needed).

0 commit comments

Comments
 (0)