-
Notifications
You must be signed in to change notification settings - Fork 4.1k
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Ease understandability of incremental generators #67745
Comments
As a teaser, here's a sample diagram produced by a helper we're working on. In this case, the diagram shows RazorSourceGenerator behavior following a change to a single .razor file: |
@sharwell Wow! That is very amazing and promising.
|
The current intent is to include it as a utility in roslyn-sdk's source generator testing library.
|
Thanks @sharwell ! I really can't wait to see this live and try it out! |
Mermaid source for diagram
string ToMermaid(GeneratorRunResult)private string ToMermaid(GeneratorRunResult result)
{
var nodeIdCache = new Dictionary<IncrementalGeneratorRunStep, int>();
var builder = new StringBuilder();
builder.AppendLine("flowchart LR");
foreach (var (name, steps) in result.TrackedOutputSteps)
{
for (var i = 0; i < steps.Length; i++)
{
var step = steps[i];
DefineNode(step, isOutput: true, name, i, builder, nodeIdCache);
}
}
foreach (var (name, steps) in result.TrackedSteps)
{
for (var i = 0; i < steps.Length; i++)
{
var step = steps[i];
DefineNode(step, isOutput: false, name, i, builder, nodeIdCache);
}
}
foreach (var step in GetAllRunSteps(result.TrackedOutputSteps.SelectMany(namedStep => namedStep.Value).Concat(result.TrackedSteps.SelectMany(namedStep => namedStep.Value))))
{
DefineNode(step, isOutput: false, "", 0, builder, nodeIdCache);
}
foreach (var (step, targetId) in nodeIdCache)
{
for (var inputIndex = 0; inputIndex < step.Inputs.Length; inputIndex++)
{
var (source, outputIndex) = step.Inputs[inputIndex];
var sourceId = nodeIdCache[source];
builder.AppendLine(CultureInfo.InvariantCulture, $@" {sourceId}_O_{outputIndex} --> {targetId}_I_{inputIndex}");
}
}
builder.AppendLine(" classDef Context fill:#ccc");
builder.AppendLine(" classDef Skipped fill:#fffef0");
builder.AppendLine(" classDef SourceOutput fill:#cfc");
builder.AppendLine(" classDef Executed fill:#fffec0");
builder.AppendLine(" classDef Inputs stroke-width:0px,fill:none");
builder.AppendLine(" classDef Outputs stroke-width:0px,fill:none");
builder.AppendLine(" classDef Cached stroke-dasharray: 2 2");
builder.AppendLine(" classDef Modified stroke:#f00,fill:#fcc");
builder.AppendLine(" classDef Unchanged stroke:#00f,fill:#ccf");
return builder.ToString();
static IEnumerable<IncrementalGeneratorRunStep> GetAllRunSteps(IEnumerable<IncrementalGeneratorRunStep> initialSteps)
{
var visited = new HashSet<IncrementalGeneratorRunStep>();
var queue = new Queue<IncrementalGeneratorRunStep>(initialSteps);
while (queue.TryDequeue(out var result))
{
if (!visited.Add(result))
{
continue;
}
yield return result;
foreach (var (source, _) in result.Inputs)
{
queue.Enqueue(source);
}
}
}
static void DefineNode(IncrementalGeneratorRunStep step, bool isOutput, string name, int index, StringBuilder builder, Dictionary<IncrementalGeneratorRunStep, int> nodeIdCache)
{
if (nodeIdCache.ContainsKey(step))
{
return;
}
var id = GetNodeId(step, nodeIdCache);
var displayName = string.IsNullOrEmpty(name) ? id.ToString(CultureInfo.InvariantCulture) : $"{name}[{index}]";
bool includeTime = true;
string className;
if (!step.Inputs.Any())
{
className = "Context";
includeTime = false;
}
else if (step.Inputs.All(step => step.Source.Outputs[step.OutputIndex].Reason is IncrementalStepRunReason.Unchanged or IncrementalStepRunReason.Cached))
{
className = "Skipped";
includeTime = false;
}
else if (isOutput && name == "SourceOutput")
{
className = "SourceOutput";
}
else
{
className = "Executed";
}
if (includeTime)
{
displayName = $"{displayName}<br/>{Math.Round(step.ElapsedTime.TotalMilliseconds)}ms";
}
builder.AppendLine(CultureInfo.InvariantCulture, $@" subgraph {id} [""{displayName}""]");
builder.AppendLine(CultureInfo.InvariantCulture, $@" direction LR");
builder.AppendLine(CultureInfo.InvariantCulture, $@" subgraph {id}_I ["" ""]");
builder.AppendLine(CultureInfo.InvariantCulture, $@" direction TB");
if (step.Inputs.Any())
{
builder.Append(" ");
for (var j = 0; j < step.Inputs.Length; j++)
{
if (j > 0)
{
builder.Append(" ~~~ ");
}
builder.Append(CultureInfo.InvariantCulture, $@"{id}_I_{j}[I{j}]:::{step.Inputs[j].Source.Outputs[step.Inputs[j].OutputIndex].Reason}");
}
builder.AppendLine();
}
builder.AppendLine(CultureInfo.InvariantCulture, $@" end");
builder.AppendLine(CultureInfo.InvariantCulture, $@" subgraph {id}_O ["" ""]");
builder.AppendLine(CultureInfo.InvariantCulture, $@" direction TB");
if (step.Outputs.Any())
{
builder.Append(" ");
for (var j = 0; j < step.Outputs.Length; j++)
{
if (j > 0)
{
builder.Append(" ~~~ ");
}
builder.Append(CultureInfo.InvariantCulture, $@"{id}_O_{j}[{GetOutputName(step.Outputs[j].Value, j)}]:::{step.Outputs[j].Reason}");
}
builder.AppendLine();
}
builder.AppendLine(CultureInfo.InvariantCulture, $@" end");
builder.AppendLine(CultureInfo.InvariantCulture, $@" {id}_I:::Inputs ~~~ {id}_O:::Outputs");
builder.AppendLine(CultureInfo.InvariantCulture, $@" end");
builder.AppendLine(CultureInfo.InvariantCulture, $@" {id}:::{className}");
static string GetOutputName(object? value, int index)
{
return value switch
{
AnalyzerConfigOptionsProvider => nameof(AnalyzerConfigOptionsProvider),
(IEnumerable, ImmutableArray<Diagnostic>) => $"O{index}",
Compilation compilation => $"Compilation<br/>'{compilation.AssemblyName}'",
AdditionalText text => $"AdditionalText<br/>'{text.Path}'",
string => "string",
bool b => b.ToString(),
ParseOptions => nameof(ParseOptions),
_ => $"O{index}",
};
}
}
static int GetNodeId(IncrementalGeneratorRunStep step, Dictionary<IncrementalGeneratorRunStep, int> nodeIdCache)
{
if (!nodeIdCache.TryGetValue(step, out var result))
{
result = nodeIdCache.Count;
nodeIdCache.Add(step, result);
}
return result;
}
} |
I'm going to move this to a discussion since it lacked a concrete proposal. @sharwell If you prefer to keep this as a productivity/tooling/IDE issue, feel free to convert it back. |
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
I don't have a concrete proposal, but sharing thoughts if the team can come up with a good solution.
There are three groups of people:
IncrementalStepRunReason
for each step.IIncrementalGenerator
and so it's hard to judge whether a pipeline is good or not. For example, is it always bad to dostep.Combine(context.CompilationProvider)
? For 99.99% of the cases, it is bad. But is it always? probably no. Are we able to tell the 0.01% case? No.The request here is to have some way that helps understand what is happening under the hood while a generator is still in development. For example, get a string representation of the internal tables or see what is stored internally, etc.
As said earlier, I don't have a concrete proposal, and I don't know if this is actionable. But it would be really great if one can easily get an idea of what is happening underneath the hoods and be able to try different approaches and see how they will differ in practice.
This could be new APIs on the compiler side or an IDE visualization feature that can understand the generator pipeline and provide some visual explanation of how it will behave internally.
The text was updated successfully, but these errors were encountered: