Skip to content

Commit a527341

Browse files
authored
Implement .dgml writing capabilities (#2854)
* Implement .dgml generation * Fix .dgml formatting * Update cmd line description * Add individual test, fix some comments * Fix helper class, implement switch * Add tests for file format argument * Remove trailing whitespace * Move logic to helper, fix invalid file format message * Remove compression, add method to finish a file outside of dispose * Fix enum format, other comments * Fix test to check text case and that error is logged * Delete launchSettings.json * fix command line descriptions, run lint
1 parent 9088e6b commit a527341

File tree

16 files changed

+458
-114
lines changed

16 files changed

+458
-114
lines changed

src/ILLink.Shared/DiagnosticId.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public enum DiagnosticId
5555
CouldNotResolveCustomAttributeTypeValue = 1044,
5656
UnexpectedAttributeArgumentType = 1045,
5757
InvalidMetadataOption = 1046,
58+
InvalidDependenciesFileFormat = 1047,
5859

5960
// Linker diagnostic ids.
6061
TypeHasNoFieldsToPreserve = 2001,

src/ILLink.Shared/SharedStrings.resx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
3939
The mimetype is used for serialized objects, and tells the
4040
ResXResourceReader how to depersist the object. This is currently not
41-
extensible. For a given mimetype the value must be set accordingly:
41+
extensible. For a given mimetype the value must be set accordingly
4242
4343
Note - application/x-microsoft.net.object.binary.base64 is the format
4444
that the ResXResourceWriter will generate, however the reader can
@@ -1185,4 +1185,10 @@
11851185
<data name="UnrecognizedInternalAttributeTitle" xml:space="preserve">
11861186
<value>Unrecognized internal attribute '{0}'</value>
11871187
</data>
1188-
</root>
1188+
<data name="InvalidDependenciesFileFormatMessage" xml:space="preserve">
1189+
<value>The only allowed file types are Xml or Dgml.</value>
1190+
</data>
1191+
<data name="InvalidDependenciesFileFormatTitle" xml:space="preserve">
1192+
<value>Unrecognized dependencies file type.</value>
1193+
</data>
1194+
</root>

src/ILLink.Tasks/LinkTask.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,12 @@ public class ILLink : ToolTask
182182
/// </summary>
183183
public bool DumpDependencies { get; set; }
184184

185+
/// <summary>
186+
/// Make illink dump dependencies to the specified file type.
187+
/// Maps to '--dependencies-file-format'.
188+
/// </summary>
189+
public string DependenciesFileFormat { get; set; }
190+
185191
/// <summary>
186192
/// Remove debug symbols from linked assemblies.
187193
/// Maps to '-b' if false.
@@ -476,6 +482,10 @@ protected override string GenerateResponseFileCommands ()
476482
if (DumpDependencies)
477483
args.AppendLine ("--dump-dependencies");
478484

485+
if (DependenciesFileFormat != null) {
486+
args.Append ("--dependencies-file-format ").AppendLine (DependenciesFileFormat);
487+
}
488+
479489
return args.ToString ();
480490
}
481491
}

src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ Copyright (c) .NET Foundation. All rights reserved.
182182
RootDescriptorFiles="@(TrimmerRootDescriptor)"
183183
OutputDirectory="$(IntermediateLinkDir)"
184184
DumpDependencies="$(_TrimmerDumpDependencies)"
185+
DependenciesFileFormat="$(_TrimmerDependenciesFileFormat)"
185186
ExtraArgs="$(_ExtraTrimmerArgs)"
186187
ToolExe="$(_DotNetHostFileName)"
187188
ToolPath="$(_DotNetHostDirectory)"
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using Mono.Cecil;
5+
6+
7+
namespace Mono.Linker
8+
{
9+
/// <summary>
10+
/// Class which implements IDependencyRecorder and writes the dependencies into an DGML file.
11+
/// </summary>
12+
public class DependencyRecorderHelper
13+
{
14+
static bool IsAssemblyBound (TypeDefinition td)
15+
{
16+
do {
17+
if (td.IsNestedPrivate || td.IsNestedAssembly || td.IsNestedFamilyAndAssembly)
18+
return true;
19+
20+
td = td.DeclaringType;
21+
} while (td != null);
22+
23+
return false;
24+
}
25+
26+
public static string TokenString (LinkContext context, object? o)
27+
{
28+
if (o == null)
29+
return "N:null";
30+
31+
if (o is TypeReference t) {
32+
bool addAssembly = true;
33+
var td = context.TryResolve (t);
34+
35+
if (td != null) {
36+
addAssembly = td.IsNotPublic || IsAssemblyBound (td);
37+
t = td;
38+
}
39+
40+
var addition = addAssembly ? $":{t.Module}" : "";
41+
42+
return $"{((IMetadataTokenProvider) o).MetadataToken.TokenType}:{o}{addition}";
43+
}
44+
45+
if (o is IMetadataTokenProvider provider)
46+
return provider.MetadataToken.TokenType + ":" + o;
47+
48+
return "Other:" + o;
49+
}
50+
51+
static bool WillAssemblyBeModified (LinkContext context, AssemblyDefinition assembly)
52+
{
53+
switch (context.Annotations.GetAction (assembly)) {
54+
case AssemblyAction.Link:
55+
case AssemblyAction.AddBypassNGen:
56+
case AssemblyAction.AddBypassNGenUsed:
57+
return true;
58+
default:
59+
return false;
60+
}
61+
}
62+
63+
public static bool ShouldRecord (LinkContext context, object? source, object target)
64+
{
65+
if (source == null || target == null)
66+
return false;
67+
68+
// We use a few hacks to work around MarkStep outputting thousands of edges even
69+
// with the above ShouldRecord checks. Ideally we would format these into a meaningful format
70+
// however I don't think that is worth the effort at the moment.
71+
72+
// Prevent useless logging of attributes like `e="Other:Mono.Cecil.CustomAttribute"`.
73+
if (source is CustomAttribute || target is CustomAttribute)
74+
return false;
75+
76+
// Prevent useless logging of interface implementations like `e="InterfaceImpl:Mono.Cecil.InterfaceImplementation"`.
77+
if (source is InterfaceImplementation || target is InterfaceImplementation)
78+
return false;
79+
80+
if (!ShouldRecord (context, source) && !ShouldRecord (context, target)) {
81+
return false;
82+
}
83+
return true;
84+
}
85+
86+
public static bool ShouldRecord (LinkContext context, object? o)
87+
{
88+
if (!context.EnableReducedTracing)
89+
return true;
90+
91+
if (o is TypeDefinition t)
92+
return WillAssemblyBeModified (context, t.Module.Assembly);
93+
94+
if (o is IMemberDefinition m)
95+
return WillAssemblyBeModified (context, m.DeclaringType.Module.Assembly);
96+
97+
if (o is TypeReference typeRef) {
98+
var resolved = context.TryResolve (typeRef);
99+
100+
// Err on the side of caution if we can't resolve
101+
if (resolved == null)
102+
return true;
103+
104+
return WillAssemblyBeModified (context, resolved.Module.Assembly);
105+
}
106+
107+
if (o is MemberReference mRef) {
108+
var resolved = mRef.Resolve ();
109+
110+
// Err on the side of caution if we can't resolve
111+
if (resolved == null)
112+
return true;
113+
114+
return WillAssemblyBeModified (context, resolved.DeclaringType.Module.Assembly);
115+
}
116+
117+
if (o is ModuleDefinition module)
118+
return WillAssemblyBeModified (context, module.Assembly);
119+
120+
if (o is AssemblyDefinition assembly)
121+
return WillAssemblyBeModified (context, assembly);
122+
123+
if (o is ParameterDefinition parameter) {
124+
if (parameter.Method is MethodDefinition parameterMethodDefinition)
125+
return WillAssemblyBeModified (context, parameterMethodDefinition.DeclaringType.Module.Assembly);
126+
}
127+
128+
return true;
129+
}
130+
}
131+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.IO;
8+
using System.Xml;
9+
10+
namespace Mono.Linker
11+
{
12+
/// <summary>
13+
/// Class which implements IDependencyRecorder and writes the dependencies into an DGML file.
14+
/// </summary>
15+
public class DgmlDependencyRecorder : IDependencyRecorder, IDisposable
16+
{
17+
public const string DefaultDependenciesFileName = "linker-dependencies.dgml";
18+
public Dictionary<string, int> nodeList = new ();
19+
public HashSet<(string dependent, string dependee, string reason)> linkList = new (); // first element is source, second is target (dependent --> dependee), third is reason
20+
21+
private readonly LinkContext context;
22+
private XmlWriter? writer;
23+
private Stream? stream;
24+
25+
public DgmlDependencyRecorder (LinkContext context, string? fileName = null)
26+
{
27+
this.context = context;
28+
29+
XmlWriterSettings settings = new XmlWriterSettings {
30+
Indent = true,
31+
IndentChars = " "
32+
};
33+
34+
if (fileName == null)
35+
fileName = DefaultDependenciesFileName;
36+
37+
if (string.IsNullOrEmpty (Path.GetDirectoryName (fileName)) && !string.IsNullOrEmpty (context.OutputDirectory)) {
38+
fileName = Path.Combine (context.OutputDirectory, fileName);
39+
Directory.CreateDirectory (context.OutputDirectory);
40+
}
41+
42+
var depsFile = File.OpenWrite (fileName);
43+
stream = depsFile;
44+
45+
writer = XmlWriter.Create (stream, settings);
46+
writer.WriteStartDocument ();
47+
writer.WriteStartElement ("DirectedGraph", "http://schemas.microsoft.com/vs/2009/dgml");
48+
}
49+
50+
public void FinishRecording ()
51+
{
52+
Debug.Assert (writer != null);
53+
54+
writer.WriteStartElement ("Nodes");
55+
{
56+
foreach (var pair in nodeList) {
57+
writer.WriteStartElement ("Node");
58+
writer.WriteAttributeString ("Id", pair.Value.ToString ());
59+
writer.WriteAttributeString ("Label", pair.Key);
60+
writer.WriteEndElement ();
61+
}
62+
}
63+
writer.WriteEndElement ();
64+
65+
writer.WriteStartElement ("Links");
66+
{
67+
foreach (var tup in linkList) {
68+
writer.WriteStartElement ("Link");
69+
writer.WriteAttributeString ("Source", nodeList[tup.dependent].ToString ());
70+
writer.WriteAttributeString ("Target", nodeList[tup.dependee].ToString ());
71+
writer.WriteAttributeString ("Reason", tup.reason);
72+
writer.WriteEndElement ();
73+
}
74+
}
75+
writer.WriteEndElement ();
76+
77+
writer.WriteStartElement ("Properties");
78+
{
79+
writer.WriteStartElement ("Property");
80+
writer.WriteAttributeString ("Id", "Label");
81+
writer.WriteAttributeString ("Label", "Label");
82+
writer.WriteAttributeString ("DataType", "String");
83+
writer.WriteEndElement ();
84+
85+
writer.WriteStartElement ("Property");
86+
writer.WriteAttributeString ("Id", "Reason");
87+
writer.WriteAttributeString ("Label", "Reason");
88+
writer.WriteAttributeString ("DataType", "String");
89+
writer.WriteEndElement ();
90+
}
91+
writer.WriteEndElement ();
92+
93+
writer.WriteEndElement ();
94+
writer.WriteEndDocument ();
95+
96+
writer.Flush ();
97+
}
98+
99+
public void Dispose ()
100+
{
101+
if (writer == null)
102+
return;
103+
104+
writer.Dispose ();
105+
stream?.Dispose ();
106+
writer = null;
107+
stream = null;
108+
}
109+
110+
public void RecordDependency (object target, in DependencyInfo reason, bool marked)
111+
{
112+
if (writer == null)
113+
throw new InvalidOperationException ();
114+
115+
if (reason.Kind == DependencyKind.Unspecified)
116+
return;
117+
118+
// For now, just report a dependency from source to target without noting the DependencyKind.
119+
RecordDependency (reason.Source, target, reason.Kind);
120+
}
121+
122+
public void RecordDependency (object? source, object target, object? reason)
123+
{
124+
if (writer == null)
125+
throw new InvalidOperationException ();
126+
127+
if (!DependencyRecorderHelper.ShouldRecord (context, source, target))
128+
return;
129+
130+
string dependent = DependencyRecorderHelper.TokenString (context, source);
131+
string dependee = DependencyRecorderHelper.TokenString (context, target);
132+
133+
// figure out why nodes are sometimes null, are we missing some information in the graph?
134+
if (!nodeList.ContainsKey (dependent)) AddNode (dependent);
135+
if (!nodeList.ContainsKey (dependee)) AddNode (dependee);
136+
if (source != target) {
137+
AddLink (dependent, dependee, reason);
138+
}
139+
}
140+
141+
private int _nodeIndex = 0;
142+
143+
void AddNode (string node)
144+
{
145+
nodeList.Add (node, _nodeIndex);
146+
_nodeIndex++;
147+
}
148+
149+
void AddLink (string source, string target, object? kind)
150+
{
151+
linkList.Add ((source, target, DependencyRecorderHelper.TokenString (context, kind)));
152+
}
153+
154+
public void RecordDependency (object source, object target, bool marked)
155+
{
156+
throw new NotImplementedException ();
157+
}
158+
}
159+
}

0 commit comments

Comments
 (0)