From 2e7bac5659046a48ba1afca59b180d7f40c61ec3 Mon Sep 17 00:00:00 2001 From: Aasim Khan Date: Tue, 16 Nov 2021 22:33:28 -0800 Subject: [PATCH] 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. --- .../Localization/sr.cs | 16 + .../Localization/sr.resx | 11 + .../Localization/sr.strings | 4 + .../Localization/sr.xlf | 14 + .../ExecuteRequests/ResultSetEvents.cs | 16 +- .../QueryExecution/QueryExecutionService.cs | 104 +++-- .../ShowPlan/Contracts/ExecutionPlanGraph.cs | 108 ++++++ .../ShowPlan/ShowPlanGraph/Constants.cs | 15 + .../ShowPlanGraph/DataReaderNodeBuilder.cs | 2 +- .../ShowPlan/ShowPlanGraph/Node.cs | 358 +++++++++++++++++- .../ShowPlan/ShowPlanGraph/PropertyValue.cs | 5 + .../ShowPlan/ShowPlanGraphUtils.cs | 70 ++++ .../ShowPlan/ShowPlanService.cs | 8 +- .../ShowPlan/ShowPlanTests.cs | 15 +- 14 files changed, 680 insertions(+), 66 deletions(-) create mode 100644 src/Microsoft.SqlTools.ServiceLayer/ShowPlan/Contracts/ExecutionPlanGraph.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/ShowPlan/ShowPlanGraph/Constants.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/ShowPlan/ShowPlanGraphUtils.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs index 2e543415..b2241352 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs @@ -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"; diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx index 66505671..d031a41b 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx @@ -4590,6 +4590,17 @@ {0} TB Size in TeraBytes format + + {0:0.#######} ({1}%) + display string for the operator cost property - 0.###### - is the float number format specifier. + Parameters: 0 - cost (double), 1 - percentage (int) + + + {0} of +{1} ({2}%) + . + Parameters: 0 - actual (string), 1 - estimated (string), 2 - percent (decimal) + Initialization is not properly done for table with id '{0}' . diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings index 79ad96ea..b8ce42a0 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings @@ -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: of\n (xx%) +ActualOfEstimated(string actual, string estimated, decimal percent) = {0} of\n{1} ({2}%) ############################################################################ # Table Designer diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf index af360256..ef980a78 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf @@ -5631,6 +5631,20 @@ . Parameters: 0 - path (string), 1 - editType (string) + + {0:0.#######} ({1}%) + {0:0.#######} ({1}%) + display string for the operator cost property - 0.###### - is the float number format specifier. + Parameters: 0 - cost (double), 1 - percentage (int) + + + {0} of +{1} ({2}%) + {0} of +{1} ({2}%) + . + Parameters: 0 - actual (string), 1 - estimated (string), 2 - percent (decimal) + \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ExecuteRequests/ResultSetEvents.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ExecuteRequests/ResultSetEvents.cs index 3e061fe6..9dc74525 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ExecuteRequests/ResultSetEvents.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ExecuteRequests/ResultSetEvents.cs @@ -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 /// public class ResultSetUpdatedEventParams : ResultSetEventParams { + /// + /// Execution plans for statements in the current batch. + /// + public List ExecutionPlans { get; set; } + /// + /// Error message for exception raised while generating execution plan. + /// + 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.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.Create(MethodName); } - public class ResultSetUpdatedEvent + public class ResultSetUpdatedEvent { public static string MethodName { get; } = "query/resultSetUpdated"; diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs index 1a4eed92..a60b045b 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs @@ -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 /// private readonly Lazy> queryExecutionSettings = - new Lazy>(() => new ConcurrentDictionary()); + new Lazy>(() => new ConcurrentDictionary()); /// /// 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 } } - + /// /// Handles a request to set query execution options /// @@ -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); - } + } } /// @@ -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 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; diff --git a/src/Microsoft.SqlTools.ServiceLayer/ShowPlan/Contracts/ExecutionPlanGraph.cs b/src/Microsoft.SqlTools.ServiceLayer/ShowPlan/Contracts/ExecutionPlanGraph.cs new file mode 100644 index 00000000..94b46a89 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/ShowPlan/Contracts/ExecutionPlanGraph.cs @@ -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 +{ + /// + /// Execution plan graph object that is sent over JSON RPC + /// + public class ExecutionPlanGraph + { + /// + /// Root of the execution plan tree + /// + public ExecutionPlanNode Root { get; set; } + /// + /// Underlying query for the execution plan graph + /// + public string Query { get; set; } + } + + public class ExecutionPlanNode + { + /// + /// Type of the node. This determines the icon that is displayed for it + /// + public string Type { get; set; } + /// + /// Cost associated with the node + /// + public double Cost { get; set; } + /// + /// Cost of the node subtree + /// + public double SubTreeCost { get; set; } + /// + /// Relative cost of the node compared to its siblings. + /// + public double RelativeCost { get; set; } + /// + /// Time take by the node operation in milliseconds + /// + public long? ElapsedTimeInMs { get; set; } + /// + /// Node properties to be shown in the tooltip + /// + public List Properties { get; set; } + /// + /// Display name for the node + /// + public string Name { get; set; } + /// + /// Description associated with the node. + /// + public string Description { get; set; } + /// + /// Subtext displayed under the node name + /// + public string[] Subtext { get; set; } + public List Children { get; set; } + public List Edges { get; set; } + } + + public class ExecutionPlanGraphElementProperties + { + /// + /// Name of the property + /// + public string Name { get; set; } + /// + /// Formatted value for the property + /// + public string FormattedValue { get; set; } + /// + /// Flag to show/hide props in tooltip + /// + public bool ShowInTooltip { get; set; } + /// + /// Display order of property + /// + public int DisplayOrder { get; set; } + /// + /// Flag to indicate if the property has a longer value so that it will be shown at the bottom of the tooltip + /// + public bool IsLongString { get; set; } + } + + public class ExecutionPlanEdges + { + /// + /// Count of the rows returned by the subtree of the edge. + /// + public double RowCount { get; set; } + /// + /// Size of the rows returned by the subtree of the edge. + /// + /// + public double RowSize { get; set; } + /// + /// Edge properties to be shown in the tooltip. + /// + /// + public List Properties { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/ShowPlan/ShowPlanGraph/Constants.cs b/src/Microsoft.SqlTools.ServiceLayer/ShowPlan/ShowPlanGraph/Constants.cs new file mode 100644 index 00000000..57f4819a --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/ShowPlan/ShowPlanGraph/Constants.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/ShowPlan/ShowPlanGraph/DataReaderNodeBuilder.cs b/src/Microsoft.SqlTools.ServiceLayer/ShowPlan/ShowPlanGraph/DataReaderNodeBuilder.cs index bb27346f..f6d8631a 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ShowPlan/ShowPlanGraph/DataReaderNodeBuilder.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ShowPlan/ShowPlanGraph/DataReaderNodeBuilder.cs @@ -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 diff --git a/src/Microsoft.SqlTools.ServiceLayer/ShowPlan/ShowPlanGraph/Node.cs b/src/Microsoft.SqlTools.ServiceLayer/ShowPlan/ShowPlanGraph/Node.cs index 87671155..93fdc3a3 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ShowPlan/ShowPlanGraph/Node.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ShowPlan/ShowPlanGraph/Node.cs @@ -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(); - this.childrenEdges = new LinkedList(); + this.children = new List(); + this.childrenEdges = new List(); 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; } + /// + /// Gets Node display name + /// + 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); + } + } + + /// + /// Gets Node description + /// + [DisplayOrder(2), DisplayNameDescription(SR.Keys.OperationDescriptionShort, SR.Keys.OperationDescription)] + public string Description + { + get { return this.Operation.Description; } + } + + /// + /// Gets the value that indicates Node parallelism. + /// + public bool IsParallel + { + get + { + object value = this["Parallel"]; + return value != null ? (bool)value : false; + } + } + + /// + /// Gets the value that indicates whether the Node has warnings. + /// + [Browsable(false)] + public bool HasWarnings + { + get + { + return this["Warnings"] != null; + } + } + + + /// + /// Gets the value that indicates whether the Node has critical warnings. + /// + private bool HasCriticalWarnings + { + get + { + if (this["Warnings"] != null) + { + ExpandableObjectWrapper wrapper = this["Warnings"] as ExpandableObjectWrapper; + if (wrapper["NoJoinPredicate"] != null) + { + return (bool)wrapper["NoJoinPredicate"]; + } + } + + return false; + } + } + + /// + /// Check if this showplan_xml has PDW cost. + /// + private bool HasPDWCost + { + get + { + return this["PDWAccumulativeCost"] != null; + } + } + + /// + /// Gets the cost associated with the Node. + /// + [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)); + } + } + + + + + /// /// Gets the cost associated with the current Node. /// @@ -95,6 +250,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph /// /// Gets the cost associated with the Node subtree. /// + [ShowInToolTip, DisplayOrder(9), DisplayNameDescription(SR.Keys.EstimatedSubtreeCost, SR.Keys.EstimatedSubtreeCostDescription)] public double SubtreeCost { get @@ -116,6 +272,12 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph } } + /// + /// Max Children X Position. + /// + public int MaxChildrenXPosition; + + /// /// Gets the operation information (localized name, description, image, etc) /// @@ -174,11 +336,20 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph /// /// Gets collection of node children. /// - public LinkedList Children + public List Children { get { return this.children; } } + /// + /// Gets collection of node children. + /// + public List Edges + { + get { return this.childrenEdges; } + } + + /// /// Gets current node parent. /// @@ -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; + } + } + /// /// ENU name for Logical Operator /// @@ -334,6 +525,33 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph #region Implementation details + /// + /// 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. + /// + /// Object property in the property bag + 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; + } + /// /// 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; } + /// + /// Gets lines of text displayed under the icon. + /// + /// Array of strings. + 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'); + } + + /// + /// Provide a string for the actual elapsed time if it is available + /// + /// formatted string of execution time + 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; + } + + /// + /// Provide a string for the actual rows vs estimated rows if they are both available in the actual execution plan + /// + /// formatted string of actual rows vs estimated rows; or null if estimateRows or actualRows is null + 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); + } + + /// + /// Inner function to provide a string for the actual rows vs estimated rows if they are both available in the actual execution plan + /// + /// actual rows + /// estimated rows + /// formatted string of actual rows vs estimated rows; or null if any of the arguments is null + 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 children; + private List children; private readonly string objectProperty = NodeBuilderConstants.Object; private readonly string predicateProperty = NodeBuilderConstants.LogicalOp; private Node parent; private Graph graph; private Edge parentEdge; - private LinkedList childrenEdges; + private List 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; } diff --git a/src/Microsoft.SqlTools.ServiceLayer/ShowPlan/ShowPlanGraph/PropertyValue.cs b/src/Microsoft.SqlTools.ServiceLayer/ShowPlan/ShowPlanGraph/PropertyValue.cs index 672b33e2..703791ac 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ShowPlan/ShowPlanGraph/PropertyValue.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ShowPlan/ShowPlanGraph/PropertyValue.cs @@ -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 diff --git a/src/Microsoft.SqlTools.ServiceLayer/ShowPlan/ShowPlanGraphUtils.cs b/src/Microsoft.SqlTools.ServiceLayer/ShowPlan/ShowPlanGraphUtils.cs new file mode 100644 index 00000000..7d7c99d4 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/ShowPlan/ShowPlanGraphUtils.cs @@ -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 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 GetProperties(PropertyDescriptorCollection props) + { + List propsList = new List(); + 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; + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/ShowPlan/ShowPlanService.cs b/src/Microsoft.SqlTools.ServiceLayer/ShowPlan/ShowPlanService.cs index fe945646..66607e79 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ShowPlan/ShowPlanService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ShowPlan/ShowPlanService.cs @@ -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 diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ShowPlan/ShowPlanTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ShowPlan/ShowPlanTests.cs index 3145bb13..7f18b255 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ShowPlan/ShowPlanTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ShowPlan/ShowPlanTests.cs @@ -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"); } } } \ No newline at end of file