/// <summary> /// Log the graph in graphviz (dot) format. /// </summary> /// <param name="graph">Graph to log</param> /// <remarks> /// Example of a graphviz graph description /// /// digraph graphname { /// a -> b -> c; /// b -> d; /// } /// /// For more info see https://www.graphviz.org/ /// </remarks> /// <returns>Async task</returns> private async Task LogGraphVizAsync(Channel targetChannel, DependencyFlowGraph graph, bool includeBuildTimes) { StringBuilder subgraphClusterWriter = null; bool writeToSubgraphCluster = targetChannel != null; if (writeToSubgraphCluster) { subgraphClusterWriter = new StringBuilder(); subgraphClusterWriter.AppendLine($" subgraph cluster_{UxHelpers.CalculateGraphVizNodeName(targetChannel.Name)} {{"); subgraphClusterWriter.AppendLine($" label = \"{targetChannel.Name}\""); } using (StreamWriter writer = UxHelpers.GetOutputFileStreamOrConsole(_options.GraphVizOutputFile)) { await writer.WriteLineAsync("digraph repositoryGraph {"); await writer.WriteLineAsync(" node [shape=record]"); foreach (DependencyFlowNode node in graph.Nodes) { StringBuilder nodeBuilder = new StringBuilder(); string style = node.OnLongestBuildPath ? "style=\"diagonals,bold\" color=red" : ""; // First add the node name nodeBuilder.Append($" {UxHelpers.CalculateGraphVizNodeName(node)}"); // Then add the label. label looks like [label="<info here>"] nodeBuilder.Append($"[{style}\nlabel=\""); // Append friendly repo name nodeBuilder.Append(UxHelpers.GetSimpleRepoName(node.Repository)); nodeBuilder.Append(@"\n"); // Append branch name nodeBuilder.Append(node.Branch); if (includeBuildTimes) { // Append best case nodeBuilder.Append(@"\n"); nodeBuilder.Append($"Best Case: {Math.Round(node.BestCasePathTime, 2, MidpointRounding.AwayFromZero)} min"); nodeBuilder.Append(@"\n"); // Append worst case nodeBuilder.Append($"Worst Case: {Math.Round(node.WorstCasePathTime, 2, MidpointRounding.AwayFromZero)} min"); nodeBuilder.Append(@"\n"); // Append build times nodeBuilder.Append($"Official Build Time: {Math.Round(node.OfficialBuildTime, 2, MidpointRounding.AwayFromZero)} min"); nodeBuilder.Append(@"\n"); nodeBuilder.Append($"PR Build Time: {Math.Round(node.PrBuildTime, 2, MidpointRounding.AwayFromZero)} min"); } // Append end of label and end of node. nodeBuilder.Append("\"];"); // If highlighting a specific channel, Add those nodes to a subgraph cluster // if they output to the subgraph cluster. if (writeToSubgraphCluster && node.OutputChannels.Any(c => c == targetChannel.Name)) { subgraphClusterWriter.AppendLine($" {UxHelpers.CalculateGraphVizNodeName(node)}"); } // Write it out. await writer.WriteLineAsync(nodeBuilder.ToString()); } // Now write all the edges foreach (DependencyFlowEdge edge in graph.Edges) { string fromNode = UxHelpers.CalculateGraphVizNodeName(edge.From); string toNode = UxHelpers.CalculateGraphVizNodeName(edge.To); string label = $"{edge.Subscription.Channel.Name} ({edge.Subscription.Policy.UpdateFrequency})"; if (writeToSubgraphCluster && edge.Subscription.Channel.Name == targetChannel.Name) { subgraphClusterWriter.AppendLine($" {fromNode} -> {toNode} [{GetEdgeStyle(edge)}]"); } else { await writer.WriteLineAsync($" {fromNode} -> {toNode} [{GetEdgeStyle(edge)}]"); } } if (writeToSubgraphCluster) { await writer.WriteLineAsync(subgraphClusterWriter.ToString()); await writer.WriteLineAsync(" }"); } // Write a legend await writer.WriteLineAsync(" subgraph cluster1 {"); await writer.WriteLineAsync(" rankdir=RL;"); await writer.WriteLineAsync(" label = \"Legend\""); await writer.WriteLineAsync(" shape = rectangle;"); await writer.WriteLineAsync(" color = black;"); await writer.WriteLineAsync(" a[style = invis];"); await writer.WriteLineAsync(" b[style = invis];"); await writer.WriteLineAsync(" c[style = invis];"); await writer.WriteLineAsync(" d[style = invis];"); await writer.WriteLineAsync(" e[style = invis];"); await writer.WriteLineAsync(" f[style = invis];"); await writer.WriteLineAsync(" g[style = \"diagonals,bold\" color=red];"); await writer.WriteLineAsync(" h[style = \"diagonals,bold\" color=red];"); await writer.WriteLineAsync(" c->d[label = \"Updated Every Build\", style = bold];"); await writer.WriteLineAsync(" a->b[label = \"Updated Every Day\", style = dashed];"); await writer.WriteLineAsync(" e->f[label = \"Disabled/Updated On-demand\", style = dotted];"); await writer.WriteLineAsync(" g->h[label = \"Longest Build Path\", color=\"red:invis:red\"];"); await writer.WriteLineAsync(" }"); await writer.WriteLineAsync(" subgraph cluster2{"); await writer.WriteLineAsync(" rankdir=BT;"); await writer.WriteLineAsync(" style=invis;"); await writer.WriteLineAsync(" note[shape=plaintext label=\"Best Case: Time through the graph assuming no dependency flow\nWorst Case: Time through the graph with dependency flow (PRs)\"];"); await writer.WriteLineAsync(" }"); await writer.WriteLineAsync(" d->note[lhead=cluster2, ltail=cluster1, style=invis];"); await writer.WriteLineAsync("}"); } }
/// <summary> /// Log the graph in graphviz (dot) format. /// </summary> /// <param name="graph">Graph to log</param> /// <remarks> /// Example of a graphviz graph description /// /// digraph graphname { /// a -> b -> c; /// b -> d; /// } /// /// For more info see https://www.graphviz.org/ /// </remarks> /// <returns>Async task</returns> private async Task LogGraphViz(DependencyGraph graph) { using (StreamWriter writer = UxHelpers.GetOutputFileStreamOrConsole(_options.GraphVizOutputFile)) { await writer.WriteLineAsync("digraph repositoryGraph {"); await writer.WriteLineAsync(" node [shape=record]"); foreach (DependencyGraphNode node in graph.Nodes) { StringBuilder nodeBuilder = new StringBuilder(); // First add the node name nodeBuilder.Append($" {UxHelpers.CalculateGraphVizNodeName(node)}"); // Then add the label. label looks like [label="<info here>"] nodeBuilder.Append("[label=\""); // Append friendly repo name nodeBuilder.Append(UxHelpers.GetSimpleRepoName(node.Repository)); nodeBuilder.Append(@"\n"); // Append short commit sha nodeBuilder.Append(node.Commit.Substring(0, node.Commit.Length < 10 ? node.Commit.Length : 10)); // Append a build string (with newline) if available if (node.ContributingBuilds != null && node.ContributingBuilds.Any()) { Build newestBuild = node.ContributingBuilds.OrderByDescending(b => b.DateProduced).First(); nodeBuilder.Append($"\\n{newestBuild.DateProduced.ToString("g")} (UTC)"); } // Append a diff string if the graph contains diff info. GitDiff diffFrom = node.DiffFrom; if (diffFrom != null) { if (!diffFrom.Valid) { nodeBuilder.Append("\\ndiff unknown"); } else if (diffFrom.Ahead != 0 || diffFrom.Behind != 0) { if (node.DiffFrom.Ahead != 0) { nodeBuilder.Append($"\\nahead: {node.DiffFrom.Ahead} commits"); } if (node.DiffFrom.Behind != 0) { nodeBuilder.Append($"\\nbehind: {node.DiffFrom.Behind} commits"); } } else { nodeBuilder.Append("\\nlatest"); } } // Append end of label and end of node. nodeBuilder.Append("\"];"); // Write it out. await writer.WriteLineAsync(nodeBuilder.ToString()); // Now write the edges foreach (DependencyGraphNode childNode in node.Children) { await writer.WriteLineAsync($" {UxHelpers.CalculateGraphVizNodeName(node)} -> {UxHelpers.CalculateGraphVizNodeName(childNode)}"); } } await writer.WriteLineAsync("}"); } }
/// <summary> /// Log the graph in graphviz (dot) format. /// </summary> /// <param name="graph">Graph to log</param> /// <remarks> /// Example of a graphviz graph description /// /// digraph graphname { /// a -> b -> c; /// b -> d; /// } /// /// For more info see https://www.graphviz.org/ /// </remarks> /// <returns>Async task</returns> private async Task LogGraphViz(Channel targetChannel, DependencyFlowGraph graph) { StringBuilder subgraphClusterWriter = null; bool writeToSubgraphCluster = targetChannel != null; if (writeToSubgraphCluster) { subgraphClusterWriter = new StringBuilder(); subgraphClusterWriter.AppendLine($" subgraph cluster_{UxHelpers.CalculateGraphVizNodeName(targetChannel.Name)} {{"); subgraphClusterWriter.AppendLine($" label = \"{targetChannel.Name}\""); } using (StreamWriter writer = UxHelpers.GetOutputFileStreamOrConsole(_options.GraphVizOutputFile)) { await writer.WriteLineAsync("digraph repositoryGraph {"); await writer.WriteLineAsync(" node [shape=record]"); foreach (DependencyFlowNode node in graph.Nodes) { StringBuilder nodeBuilder = new StringBuilder(); // First add the node name nodeBuilder.Append($" {UxHelpers.CalculateGraphVizNodeName(node)}"); // Then add the label. label looks like [label="<info here>"] nodeBuilder.Append("[label=\""); // Append friendly repo name nodeBuilder.Append(UxHelpers.GetSimpleRepoName(node.Repository)); nodeBuilder.Append(@"\n"); // Append branch name nodeBuilder.Append(node.Branch); // Append end of label and end of node. nodeBuilder.Append("\"];"); // If highlighting a specific channel, Add those nodes to a subgraph cluster // if they output to the subgraph cluster. if (writeToSubgraphCluster && node.OutputChannels.Any(c => c == targetChannel.Name)) { subgraphClusterWriter.AppendLine($" {UxHelpers.CalculateGraphVizNodeName(node)}"); } // Write it out. await writer.WriteLineAsync(nodeBuilder.ToString()); } // Now write all the edges foreach (DependencyFlowEdge edge in graph.Edges) { string fromNode = UxHelpers.CalculateGraphVizNodeName(edge.From); string toNode = UxHelpers.CalculateGraphVizNodeName(edge.To); string label = $"{edge.Subscription.Channel.Name} ({edge.Subscription.Policy.UpdateFrequency})"; if (writeToSubgraphCluster && edge.Subscription.Channel.Name == targetChannel.Name) { subgraphClusterWriter.AppendLine($" {fromNode} -> {toNode} [{GetEdgeStyle(edge)}]"); } else { await writer.WriteLineAsync($" {fromNode} -> {toNode} [{GetEdgeStyle(edge)}]"); } } if (writeToSubgraphCluster) { await writer.WriteLineAsync(subgraphClusterWriter.ToString()); await writer.WriteLineAsync(" }"); } // Write a legend await writer.WriteLineAsync(" subgraph cluster1 {"); await writer.WriteLineAsync(" rankdir=RL;"); await writer.WriteLineAsync(" label = \"Legend\""); await writer.WriteLineAsync(" shape = rectangle;"); await writer.WriteLineAsync(" color = black;"); await writer.WriteLineAsync(" a[style = invis];"); await writer.WriteLineAsync(" b[style = invis];"); await writer.WriteLineAsync(" c[style = invis];"); await writer.WriteLineAsync(" d[style = invis];"); await writer.WriteLineAsync(" e[style = invis];"); await writer.WriteLineAsync(" f[style = invis];"); await writer.WriteLineAsync(" c->d[label = \"Updated Every Build\", style = bold];"); await writer.WriteLineAsync(" a->b[label = \"Updated Every Day\", style = dashed];"); await writer.WriteLineAsync(" e->f[label = \"Disabled/Updated On-demand\", style = dotted];"); await writer.WriteLineAsync(" }"); await writer.WriteLineAsync("}"); } }