Sending show plan graph to ADS on Result Set updated event (#1300)

* Sending showplan graph over json rpc in Result updated event
Translating showplan graph into simple objects to be sent over JSON RPC

* Revert "Sending showplan graph over json rpc in Result updated event"

This reverts commit 2d63a625fd200d057bf6093e233f05dea440347c.

* Added string for localization

* Sending showplan graph over json rpc in Result updated event
Translating showplan graph into simple objects to be sent over JSON RPC

* Refactoring class

* Removing test warning

* Removing unused imports
Adding copyright

* Removing unused prop

* removing formatted string out .strings file

* Formatting files
Adding Errors in show plan graph

* Adding a separate event for execution plan

* Now sending mulitple graphs when a batch has more than one query.
This commit is contained in:
Aasim Khan
2021-11-16 22:33:28 -08:00
committed by GitHub
parent 482afd8427
commit 2e7bac5659
14 changed files with 680 additions and 66 deletions

View File

@@ -0,0 +1,108 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Collections.Generic;
namespace Microsoft.SqlTools.ServiceLayer.ShowPlan
{
/// <summary>
/// Execution plan graph object that is sent over JSON RPC
/// </summary>
public class ExecutionPlanGraph
{
/// <summary>
/// Root of the execution plan tree
/// </summary>
public ExecutionPlanNode Root { get; set; }
/// <summary>
/// Underlying query for the execution plan graph
/// </summary>
public string Query { get; set; }
}
public class ExecutionPlanNode
{
/// <summary>
/// Type of the node. This determines the icon that is displayed for it
/// </summary>
public string Type { get; set; }
/// <summary>
/// Cost associated with the node
/// </summary>
public double Cost { get; set; }
/// <summary>
/// Cost of the node subtree
/// </summary>
public double SubTreeCost { get; set; }
/// <summary>
/// Relative cost of the node compared to its siblings.
/// </summary>
public double RelativeCost { get; set; }
/// <summary>
/// Time take by the node operation in milliseconds
/// </summary>
public long? ElapsedTimeInMs { get; set; }
/// <summary>
/// Node properties to be shown in the tooltip
/// </summary>
public List<ExecutionPlanGraphElementProperties> Properties { get; set; }
/// <summary>
/// Display name for the node
/// </summary>
public string Name { get; set; }
/// <summary>
/// Description associated with the node.
/// </summary>
public string Description { get; set; }
/// <summary>
/// Subtext displayed under the node name
/// </summary>
public string[] Subtext { get; set; }
public List<ExecutionPlanNode> Children { get; set; }
public List<ExecutionPlanEdges> Edges { get; set; }
}
public class ExecutionPlanGraphElementProperties
{
/// <summary>
/// Name of the property
/// </summary>
public string Name { get; set; }
/// <summary>
/// Formatted value for the property
/// </summary>
public string FormattedValue { get; set; }
/// <summary>
/// Flag to show/hide props in tooltip
/// </summary>
public bool ShowInTooltip { get; set; }
/// <summary>
/// Display order of property
/// </summary>
public int DisplayOrder { get; set; }
/// <summary>
/// Flag to indicate if the property has a longer value so that it will be shown at the bottom of the tooltip
/// </summary>
public bool IsLongString { get; set; }
}
public class ExecutionPlanEdges
{
/// <summary>
/// Count of the rows returned by the subtree of the edge.
/// </summary>
public double RowCount { get; set; }
/// <summary>
/// Size of the rows returned by the subtree of the edge.
/// </summary>
/// <value></value>
public double RowSize { get; set; }
/// <summary>
/// Edge properties to be shown in the tooltip.
/// </summary>
/// <value></value>
public List<ExecutionPlanGraphElementProperties> Properties { get; set; }
}
}

View File

@@ -0,0 +1,15 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph
{
public class Constants
{
public static string Parenthesis(string text)
{
return string.Format("({0})", text);
}
}
}

View File

@@ -90,7 +90,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph
if (parentNode != null)
{
parentNode.Children.AddLast(node);
parentNode.Children.Add(node);
}
// Add node to the hashtable

View File

@@ -6,6 +6,8 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph
{
@@ -29,12 +31,12 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph
{
this.ID = id;
this.properties = new PropertyDescriptorCollection(new PropertyDescriptor[0]);
this.children = new LinkedList<Node>();
this.childrenEdges = new LinkedList<Edge>();
this.children = new List<Node>();
this.childrenEdges = new List<Edge>();
this.LogicalOpUnlocName = null;
this.PhysicalOpUnlocName = null;
this.root = context.Graph.Root;
if(this.root == null)
if (this.root == null)
{
this.root = this;
}
@@ -55,6 +57,159 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph
get; set;
}
/// <summary>
/// Gets Node display name
/// </summary>
public virtual string DisplayName
{
get
{
if (this.Operation == Operation.Unknown)
{
return String.Empty;
}
// The display name can consist of two lines
// The first line is the Physical name and the physical kind in parenthesis
// The second line should contains either Object value or LogicalOp name.
// The second line should not show the same content as the first line.
string firstLine = this["PhysicalOp"] as string;
if (firstLine == null)
{
if (this.Operation == null)
{
return String.Empty;
}
firstLine = this.Operation.DisplayName;
}
// Check if the PhysicalOp is specialized to a specific kind
string firstLineAppend = this["PhysicalOperationKind"] as string;
if (firstLineAppend != null)
{
firstLine = String.Format(CultureInfo.CurrentCulture, "{0} {1}", firstLine, Constants.Parenthesis(firstLineAppend));
}
string secondLine;
object objectValue = this["Object"];
if (objectValue != null)
{
secondLine = GetObjectNameForDisplay(objectValue);
}
else
{
secondLine = this["LogicalOp"] as string;
if (secondLine != null)
{
if (secondLine != firstLine)
{
// Enclose logical name in parenthesis.
secondLine = Constants.Parenthesis(secondLine);
}
else
{
// Don't show the second line if its value is the same as on the first line.
secondLine = null;
}
}
}
return secondLine == null || secondLine.Length == 0
? firstLine
: String.Format(CultureInfo.CurrentCulture, "{0}\n{1}", firstLine, secondLine);
}
}
/// <summary>
/// Gets Node description
/// </summary>
[DisplayOrder(2), DisplayNameDescription(SR.Keys.OperationDescriptionShort, SR.Keys.OperationDescription)]
public string Description
{
get { return this.Operation.Description; }
}
/// <summary>
/// Gets the value that indicates Node parallelism.
/// </summary>
public bool IsParallel
{
get
{
object value = this["Parallel"];
return value != null ? (bool)value : false;
}
}
/// <summary>
/// Gets the value that indicates whether the Node has warnings.
/// </summary>
[Browsable(false)]
public bool HasWarnings
{
get
{
return this["Warnings"] != null;
}
}
/// <summary>
/// Gets the value that indicates whether the Node has critical warnings.
/// </summary>
private bool HasCriticalWarnings
{
get
{
if (this["Warnings"] != null)
{
ExpandableObjectWrapper wrapper = this["Warnings"] as ExpandableObjectWrapper;
if (wrapper["NoJoinPredicate"] != null)
{
return (bool)wrapper["NoJoinPredicate"];
}
}
return false;
}
}
/// <summary>
/// Check if this showplan_xml has PDW cost.
/// </summary>
private bool HasPDWCost
{
get
{
return this["PDWAccumulativeCost"] != null;
}
}
/// <summary>
/// Gets the cost associated with the Node.
/// </summary>
[ShowInToolTip, DisplayOrder(8), DisplayNameDescription(SR.Keys.EstimatedOperatorCost, SR.Keys.EstimatedOperatorCostDescription)]
public string DisplayCost
{
get
{
double cost = this.RelativeCost * 100;
if (this.HasPDWCost && cost <= 0)
{
return string.Empty;
}
return SR.OperatorDisplayCost(this.Cost, (int)Math.Round(cost));
}
}
/// <summary>
/// Gets the cost associated with the current Node.
/// </summary>
@@ -95,6 +250,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph
/// <summary>
/// Gets the cost associated with the Node subtree.
/// </summary>
[ShowInToolTip, DisplayOrder(9), DisplayNameDescription(SR.Keys.EstimatedSubtreeCost, SR.Keys.EstimatedSubtreeCostDescription)]
public double SubtreeCost
{
get
@@ -116,6 +272,12 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph
}
}
/// <summary>
/// Max Children X Position.
/// </summary>
public int MaxChildrenXPosition;
/// <summary>
/// Gets the operation information (localized name, description, image, etc)
/// </summary>
@@ -174,11 +336,20 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph
/// <summary>
/// Gets collection of node children.
/// </summary>
public LinkedList<Node> Children
public List<Node> Children
{
get { return this.children; }
}
/// <summary>
/// Gets collection of node children.
/// </summary>
public List<Edge> Edges
{
get { return this.childrenEdges; }
}
/// <summary>
/// Gets current node parent.
/// </summary>
@@ -320,6 +491,26 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph
return true;
}
public long? ElapsedTimeInMs
{
get
{
long? time = null;
var actualStatsWrapper = this["ActualTimeStatistics"] as ExpandableObjectWrapper;
if (actualStatsWrapper != null)
{
var counters = actualStatsWrapper["ActualElapsedms"] as RunTimeCounters;
if (counters != null)
{
var elapsedTime = counters.MaxCounter;
long ticks = (long)elapsedTime * TimeSpan.TicksPerMillisecond;
time = new DateTime(ticks).Millisecond;
}
}
return time;
}
}
/// <summary>
/// ENU name for Logical Operator
/// </summary>
@@ -334,6 +525,33 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph
#region Implementation details
/// <summary>
/// Gets short object name for display.
/// Since database and schema is not important and displaying table first is much useful,
/// we are displaying object name in [Table].[Index] [Alias] format.
/// </summary>
/// <param name="objectProperty">Object property in the property bag</param>
private string GetObjectNameForDisplay(object objectProperty)
{
string objectNameForDisplay = string.Empty;
Debug.Assert(objectProperty != null);
if (objectProperty != null)
{
objectNameForDisplay = objectProperty.ToString();
ExpandableObjectWrapper objectWrapper = objectProperty as ExpandableObjectWrapper;
Debug.Assert(objectWrapper != null);
if (objectWrapper != null)
{
objectNameForDisplay = ObjectWrapperTypeConverter.MergeString(".", objectWrapper["Table"], objectWrapper["Index"]);
objectNameForDisplay = ObjectWrapperTypeConverter.MergeString(" ", objectNameForDisplay, objectWrapper["Alias"]);
}
}
return objectNameForDisplay;
}
/// <summary>
/// used to compare multiple string type PropertyValue in Object properties,
/// for ex: Server, Database, Schema, Table, Index, etc...
@@ -358,6 +576,130 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph
return true;
}
/// <summary>
/// Gets lines of text displayed under the icon.
/// </summary>
/// <returns>Array of strings.</returns>
public string[] GetDisplayLinesOfText()
{
string newDisplayNameLines = this.DisplayName;
// cost
double cost = this.RelativeCost * 100;
if (!this.HasPDWCost || cost > 0)
{
string costText = SR.CostFormat((int)Math.Round(cost));
newDisplayNameLines += '\n' + costText;
}
// elapsed time in miliseconds
string elapsedTime = GetElapsedTimeDisplayString();
if (!String.IsNullOrEmpty(elapsedTime))
{
newDisplayNameLines += '\n' + elapsedTime;
}
// actual/estimated rows
string rowStatistics = GetRowStatisticsDisplayString();
if (!String.IsNullOrEmpty(rowStatistics))
{
newDisplayNameLines += '\n' + rowStatistics;
}
return newDisplayNameLines.Split('\n');
}
/// <summary>
/// Provide a string for the actual elapsed time if it is available
/// </summary>
/// <returns>formatted string of execution time</returns>
public string GetElapsedTimeDisplayString()
{
string formattedTime = null;
var actualStatsWrapper = this["ActualTimeStatistics"] as ExpandableObjectWrapper;
if (actualStatsWrapper != null)
{
var counters = actualStatsWrapper["ActualElapsedms"] as RunTimeCounters;
if (counters != null)
{
var elapsedTime = counters.MaxCounter;
long ticks = (long)elapsedTime * TimeSpan.TicksPerMillisecond;
var time = new DateTime(ticks);
if (ticks < 1000L * TimeSpan.TicksPerMillisecond * 60) // 60 seconds
{
formattedTime = time.ToString("s.fff") + "s";
}
else
{
// calculate the hours
long hours = ticks / (1000L * TimeSpan.TicksPerMillisecond * 60 * 60); //1 hour
formattedTime = hours.ToString() + time.ToString(":mm:ss");
}
}
}
return formattedTime;
}
/// <summary>
/// Provide a string for the actual rows vs estimated rows if they are both available in the actual execution plan
/// </summary>
/// <returns>formatted string of actual rows vs estimated rows; or null if estimateRows or actualRows is null</returns>
private string GetRowStatisticsDisplayString()
{
var actualRowsCounters = this[NodeBuilderConstants.ActualRows] as RunTimeCounters;
ulong? actualRows = actualRowsCounters != null ? actualRowsCounters.TotalCounters : (ulong?)null;
var estimateRows = this[NodeBuilderConstants.EstimateRows] as double?;
var estimateExecutions = this[NodeBuilderConstants.EstimateExecutions] as double?;
if (estimateRows != null)
{
if (estimateExecutions != null)
{
estimateRows = estimateRows * estimateExecutions;
}
// we display estimate rows as integer so need round function
estimateRows = Math.Round(estimateRows.Value);
}
return GetRowStatisticsDisplayString(actualRows, estimateRows);
}
/// <summary>
/// Inner function to provide a string for the actual rows vs estimated rows if they are both available in the actual execution plan
/// </summary>
/// <param name="actualRows">actual rows</param>
/// <param name="estimateRows">estimated rows</param>
/// <returns>formatted string of actual rows vs estimated rows; or null if any of the arguments is null</returns>
private string GetRowStatisticsDisplayString(ulong? actualRows, double? estimateRows)
{
if (!actualRows.HasValue || !estimateRows.HasValue)
{
return null;
}
// estimateRows should always to be positive, I just change it to 1 just in case since we need to calculate the percentage
estimateRows = estimateRows > 0 ? estimateRows : 1;
// get the difference in percentage
var actualString = actualRows.Value.ToString();
var estimateString = estimateRows.Value.ToString();
int percent = 100;
if (estimateRows > 0)
{
percent = (int)(100 * ((double)actualRows / estimateRows));
}
actualString = actualString.PadLeft(estimateString.Length);
estimateString = estimateString.PadLeft(actualString.Length);
return SR.ActualOfEstimated(actualString, estimateString, percent);
}
#endregion
#region Private variables
@@ -367,13 +709,13 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph
private double subtreeCost;
private Operation operation;
private PropertyDescriptorCollection properties;
private LinkedList<Node> children;
private List<Node> children;
private readonly string objectProperty = NodeBuilderConstants.Object;
private readonly string predicateProperty = NodeBuilderConstants.LogicalOp;
private Node parent;
private Graph graph;
private Edge parentEdge;
private LinkedList<Edge> childrenEdges;
private List<Edge> childrenEdges;
private string nodeType;
private Node root;
@@ -388,9 +730,9 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph
public void AddChild(Node child)
{
Edge edge = new Edge(this, child);
this.childrenEdges.AddLast(edge);
this.childrenEdges.Add(edge);
child.parentEdge = edge;
this.children.AddLast(child);
this.children.Add(child);
child.parent = this;
}

View File

@@ -35,6 +35,11 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph
set { this.propertyValue = value; }
}
public string DisplayValue
{
get => this.Converter.ConvertToString(null, null, this.Value);
}
public int DisplayOrder
{
get

View File

@@ -0,0 +1,70 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph;
namespace Microsoft.SqlTools.ServiceLayer.ShowPlan
{
public class ShowPlanGraphUtils
{
public static List<ExecutionPlanGraph> CreateShowPlanGraph(string xml)
{
ShowPlanGraph.ShowPlanGraph[] graphs = ShowPlanGraph.ShowPlanGraph.ParseShowPlanXML(xml, ShowPlanGraph.ShowPlanType.Unknown);
return graphs.Select(g => new ExecutionPlanGraph
{
Root = ConvertShowPlanTreeToExecutionPlanTree(g.Root),
Query = g.Statement
}).ToList();
}
private static ExecutionPlanNode ConvertShowPlanTreeToExecutionPlanTree(Node currentNode)
{
return new ExecutionPlanNode
{
Type = currentNode.Operation.Image,
Cost = currentNode.Cost,
SubTreeCost = currentNode.SubtreeCost,
Description = currentNode.Description,
Subtext = currentNode.GetDisplayLinesOfText(),
RelativeCost = currentNode.RelativeCost,
Properties = GetProperties(currentNode.Properties),
Children = currentNode.Children.Select(x => ConvertShowPlanTreeToExecutionPlanTree(x)).ToList(),
Edges = currentNode.Edges.Select(x => ConvertShowPlanEdgeToExecutionPlanEdge(x)).ToList(),
Name = currentNode.DisplayName,
ElapsedTimeInMs = currentNode.ElapsedTimeInMs
};
}
private static ExecutionPlanEdges ConvertShowPlanEdgeToExecutionPlanEdge(Edge edge)
{
return new ExecutionPlanEdges
{
RowCount = edge.RowCount,
RowSize = edge.RowSize,
Properties = GetProperties(edge.Properties)
};
}
private static List<ExecutionPlanGraphElementProperties> GetProperties(PropertyDescriptorCollection props)
{
List<ExecutionPlanGraphElementProperties> propsList = new List<ExecutionPlanGraphElementProperties>();
foreach (PropertyValue prop in props)
{
propsList.Add(new ExecutionPlanGraphElementProperties()
{
Name = prop.DisplayName,
FormattedValue = prop.DisplayValue,
ShowInTooltip = prop.IsBrowsable,
DisplayOrder = prop.DisplayOrder,
IsLongString = prop.IsLongString
});
}
return propsList;
}
}
}

View File

@@ -5,14 +5,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.SqlServer.DataCollection.Common;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
using Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph;
using Microsoft.SqlTools.ServiceLayer.Hosting;
namespace Microsoft.SqlTools.ServiceLayer.ShowPlan