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

@@ -8774,6 +8774,16 @@ namespace Microsoft.SqlTools.ServiceLayer
return Keys.GetString(Keys.NameValuePair, name, value);
}
public static string OperatorDisplayCost(double cost, int percentage)
{
return Keys.GetString(Keys.OperatorDisplayCost, cost, percentage);
}
public static string ActualOfEstimated(string actual, string estimated, decimal percent)
{
return Keys.GetString(Keys.ActualOfEstimated, actual, estimated, percent);
}
public static string TableNotInitializedException(string tableId)
{
return Keys.GetString(Keys.TableNotInitializedException, tableId);
@@ -12107,6 +12117,12 @@ namespace Microsoft.SqlTools.ServiceLayer
public const string SizeInTeraBytesFormat = "SizeInTeraBytesFormat";
public const string OperatorDisplayCost = "OperatorDisplayCost";
public const string ActualOfEstimated = "ActualOfEstimated";
public const string TableNotInitializedException = "TableNotInitializedException";

View File

@@ -4590,6 +4590,17 @@
<value>{0} TB</value>
<comment>Size in TeraBytes format</comment>
</data>
<data name="OperatorDisplayCost" xml:space="preserve">
<value>{0:0.#######} ({1}%)</value>
<comment> display string for the operator cost property - 0.###### - is the float number format specifier.
Parameters: 0 - cost (double), 1 - percentage (int) </comment>
</data>
<data name="ActualOfEstimated" xml:space="preserve">
<value>{0} of
{1} ({2}%)</value>
<comment>.
Parameters: 0 - actual (string), 1 - estimated (string), 2 - percent (decimal) </comment>
</data>
<data name="TableNotInitializedException" xml:space="preserve">
<value>Initialization is not properly done for table with id &apos;{0}&apos;</value>
<comment>.

View File

@@ -2213,6 +2213,10 @@ SizeInMegaBytesFormat = {0} MB
SizeInGigaBytesFormat = {0} GB
;Size in TeraBytes format
SizeInTeraBytesFormat = {0} TB
; display string for the operator cost property - 0.###### - is the float number format specifier
OperatorDisplayCost(double cost, int percentage) = {0:0.#######} ({1}%)
#Would like to display actual rows and estimated rows in two lines: <number_actual_rows> of\n <number_estimated_rows> (xx%)
ActualOfEstimated(string actual, string estimated, decimal percent) = {0} of\n{1} ({2}%)
############################################################################
# Table Designer

View File

@@ -5631,6 +5631,20 @@
<note>.
Parameters: 0 - path (string), 1 - editType (string) </note>
</trans-unit>
<trans-unit id="OperatorDisplayCost">
<source>{0:0.#######} ({1}%)</source>
<target state="new">{0:0.#######} ({1}%)</target>
<note> display string for the operator cost property - 0.###### - is the float number format specifier.
Parameters: 0 - cost (double), 1 - percentage (int) </note>
</trans-unit>
<trans-unit id="ActualOfEstimated">
<source>{0} of
{1} ({2}%)</source>
<target state="new">{0} of
{1} ({2}%)</target>
<note>.
Parameters: 0 - actual (string), 1 - estimated (string), 2 - percent (decimal) </note>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -2,7 +2,9 @@
// 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 Microsoft.SqlTools.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.ShowPlan;
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts.ExecuteRequests
{
@@ -35,9 +37,17 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts.ExecuteReques
/// </summary>
public class ResultSetUpdatedEventParams : ResultSetEventParams
{
/// <summary>
/// Execution plans for statements in the current batch.
/// </summary>
public List<ExecutionPlanGraph> ExecutionPlans { get; set; }
/// <summary>
/// Error message for exception raised while generating execution plan.
/// </summary>
public string ExecutionPlanErrorMessage { get; set; }
}
public class ResultSetCompleteEvent
public class ResultSetCompleteEvent
{
public static string MethodName { get; } = "query/resultSetComplete";
@@ -46,7 +56,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts.ExecuteReques
EventType<ResultSetCompleteEventParams>.Create(MethodName);
}
public class ResultSetAvailableEvent
public class ResultSetAvailableEvent
{
public static string MethodName { get; } = "query/resultSetAvailable";
@@ -55,7 +65,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts.ExecuteReques
EventType<ResultSetAvailableEventParams>.Create(MethodName);
}
public class ResultSetUpdatedEvent
public class ResultSetUpdatedEvent
{
public static string MethodName { get; } = "query/resultSetUpdated";

View File

@@ -6,19 +6,21 @@
using System;
using System.IO;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts.ExecuteRequests;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage;
using Microsoft.SqlTools.ServiceLayer.SqlContext;
using Microsoft.SqlTools.ServiceLayer.Workspace;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Hosting;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts.ExecuteRequests;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage;
using Microsoft.SqlTools.ServiceLayer.ShowPlan;
using Microsoft.SqlTools.ServiceLayer.SqlContext;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
using Microsoft.SqlTools.ServiceLayer.Workspace;
using Microsoft.SqlTools.Utility;
using System.Diagnostics;
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
{
@@ -136,7 +138,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
/// Internal storage of active query settings
/// </summary>
private readonly Lazy<ConcurrentDictionary<string, QueryExecutionSettings>> queryExecutionSettings =
new Lazy<ConcurrentDictionary<string, QueryExecutionSettings>>(() => new ConcurrentDictionary<string, QueryExecutionSettings>());
new Lazy<ConcurrentDictionary<string, QueryExecutionSettings>>(() => new ConcurrentDictionary<string, QueryExecutionSettings>());
/// <summary>
/// Settings that will be used to execute queries. Internal for unit testing
@@ -220,12 +222,12 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
WorkTask = Task.Run(async () =>
{
await InterServiceExecuteQuery(
executeParams,
null,
requestContext,
queryCreateSuccessAction,
queryCreateFailureAction,
null,
executeParams,
null,
requestContext,
queryCreateSuccessAction,
queryCreateFailureAction,
null,
null,
isQueryEditor(executeParams.OwnerUri));
});
@@ -267,7 +269,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
Type = ConnectionType.Default
};
Task workTask = Task.Run(async () => {
Task workTask = Task.Run(async () =>
{
await ConnectionService.Connect(connectParams);
ConnectionInfo newConn;
@@ -328,7 +331,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
// remove the active query since we are done with it
ActiveQueries.TryRemove(randomUri, out removedQuery);
ActiveSimpleExecuteRequests.TryRemove(randomUri, out removedTask);
ConnectionService.Disconnect(new DisconnectParams(){
ConnectionService.Disconnect(new DisconnectParams()
{
OwnerUri = randomUri,
Type = null
});
@@ -359,12 +363,14 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
internal Task HandleConnectionUriChangedNotification(ConnectionUriChangedParams changeUriParams,
EventContext eventContext)
{
try {
try
{
string OriginalOwnerUri = changeUriParams.OriginalOwnerUri;
string NewOwnerUri = changeUriParams.NewOwnerUri;
// Attempt to load the query
Query query;
if(!ActiveQueries.TryRemove(OriginalOwnerUri, out query)){
if (!ActiveQueries.TryRemove(OriginalOwnerUri, out query))
{
throw new Exception("Uri: " + OriginalOwnerUri + " is not associated with an active query.");
}
ConnectionService.ReplaceUri(OriginalOwnerUri, NewOwnerUri);
@@ -372,7 +378,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
ActiveQueries.TryAdd(NewOwnerUri, query);
return Task.FromResult(true);
}
catch (Exception ex)
catch (Exception ex)
{
Logger.Write(TraceEventType.Error, "Error encountered " + ex.ToString());
return Task.FromException(ex);
@@ -402,7 +408,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
}
}
/// <summary>
/// Handles a request to set query execution options
/// </summary>
@@ -411,7 +417,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
{
try
{
string uri = queryExecutionOptionsParams.OwnerUri;
string uri = queryExecutionOptionsParams.OwnerUri;
if (ActiveQueryExecutionSettings.ContainsKey(uri))
{
QueryExecutionSettings settings;
@@ -426,7 +432,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
{
// This was unexpected, so send back as error
await requestContext.SendError(e.Message);
}
}
}
/// <summary>
@@ -712,30 +718,30 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
{
QueryExecutionSettings settings;
this.ActiveQueryExecutionSettings.TryRemove(uri, out settings);
}
}
}
catch (Exception ex)
{
Logger.Write(TraceEventType.Error, "Unknown error " + ex.ToString());
}
await Task.FromResult(true);
}
}
#endregion
#region Private Helpers
private Query CreateQuery(
ExecuteRequestParamsBase executeParams,
ConnectionInfo connInfo,
ExecuteRequestParamsBase executeParams,
ConnectionInfo connInfo,
bool applyExecutionSettings)
{
// Attempt to get the connection for the editor
ConnectionInfo connectionInfo;
if (connInfo != null)
if (connInfo != null)
{
connectionInfo = connInfo;
}
}
else if (!ConnectionService.TryFindConnection(executeParams.OwnerUri, out connectionInfo))
{
throw new ArgumentOutOfRangeException(nameof(executeParams.OwnerUri), SR.QueryServiceQueryInvalidOwnerUri);
@@ -752,11 +758,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
oldQuery.Dispose();
ActiveQueries.TryRemove(executeParams.OwnerUri, out oldQuery);
}
// check if there are active query execution settings for the editor, otherwise, use the global settings
QueryExecutionSettings settings;
QueryExecutionSettings settings;
if (this.ActiveQueryExecutionSettings.TryGetValue(executeParams.OwnerUri, out settings))
{
{
// special-case handling for query plan options to maintain compat with query execution API parameters
// the logic is that if either the query execute API parameters or the active query setttings
// request a plan then enable the query option
@@ -772,17 +778,17 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
settings.ExecutionPlanOptions = executionPlanOptions;
}
else
{
{
settings = Settings.QueryExecutionSettings;
settings.ExecutionPlanOptions = executeParams.ExecutionPlanOptions;
}
// If we can't add the query now, it's assumed the query is in progress
Query newQuery = new Query(
GetSqlText(executeParams),
connectionInfo,
settings,
BufferFileFactory,
GetSqlText(executeParams),
connectionInfo,
settings,
BufferFileFactory,
executeParams.GetFullColumnSchema,
applyExecutionSettings);
@@ -893,13 +899,33 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
// Setup the ResultSet updated callback
ResultSet.ResultSetAsyncEventHandler resultUpdatedCallback = async r =>
{
//Generating and sending an execution plan graphs if it is requested.
List<ExecutionPlanGraph> plans = null;
string planErrors = "";
if (r.Summary.Complete && r.Summary.SpecialAction.ExpectYukonXMLShowPlan && r.RowCount == 1 && r.GetRow(0)[0] != null)
{
var xmlString = r.GetRow(0)[0].DisplayValue;
try
{
plans = ShowPlanGraphUtils.CreateShowPlanGraph(xmlString);
}
catch (Exception ex)
{
// In case of error we are sending an empty execution plan graph with the error message.
Logger.Write(TraceEventType.Error, String.Format("Failed to generate show plan graph{0}{1}", Environment.NewLine, ex.Message));
planErrors = ex.Message;
}
}
ResultSetUpdatedEventParams eventParams = new ResultSetUpdatedEventParams
{
ResultSetSummary = r.Summary,
OwnerUri = ownerUri
OwnerUri = ownerUri,
ExecutionPlans = plans,
ExecutionPlanErrorMessage = planErrors
};
Logger.Write(TraceEventType.Information, $"Result:'{r.Summary} on Query:'{ownerUri}' is updated with additional rows");
await eventSender.SendEvent(ResultSetUpdatedEvent.Type, eventParams);
};
query.ResultSetUpdated += resultUpdatedCallback;

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

View File

@@ -3,12 +3,10 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using NUnit.Framework;
using Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph;
using Microsoft.SqlTools.ServiceLayer.ShowPlan;
namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ShowPlan
@@ -16,17 +14,16 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ShowPlan
public class ShowPlanXMLTests
{
[Test]
public async Task ParseXMLFileReturnsValidShowPlanGraph()
public void ParseXMLFileReturnsValidShowPlanGraph()
{
Assembly assembly = Assembly.GetAssembly(typeof(ShowPlanXMLTests));
Stream scriptStream = assembly.GetManifestResourceStream(assembly.GetName().Name + ".ShowPlan.TestExecutionPlan.xml");
StreamReader reader = new StreamReader(scriptStream);
string text = reader.ReadToEnd();
var showPlanGraphs = ShowPlanGraph.ParseShowPlanXML(text, ShowPlanType.Actual);
Assert.AreEqual(1, showPlanGraphs.Length, "Single show plan graph not generated from the test xml file");
var testShowPlanGraph = showPlanGraphs[0];
Assert.NotNull(testShowPlanGraph, "graph should not be null");
Assert.NotNull(testShowPlanGraph.Root, "graph should have a root");
var showPlanGraphs = ShowPlanGraphUtils.CreateShowPlanGraph(text);
Assert.AreEqual(1, showPlanGraphs.Count, "exactly one show plan graph should be returned");
Assert.NotNull(showPlanGraphs[0], "graph should not be null");
Assert.NotNull(showPlanGraphs[0].Root, "graph should have a root");
}
}
}