Skip to content

Commit 5c9bbdb

Browse files
committed
Merge pull request #516 from vancem/MsPdbSourceLink
Support SourceLink in Microsoft PDBs
2 parents 01a41e8 + dae4255 commit 5c9bbdb

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)