From 196364e81ee16a70768ecd566b3f82ab772b0a2e Mon Sep 17 00:00:00 2001 From: Alex Ma Date: Fri, 17 Dec 2021 12:33:57 -0800 Subject: [PATCH] Revert "Revert "Update to XElite (#1287)" (#1334)" (#1337) This reverts commit 0d7a3b4168659e0e747be498cb8d6c0b45d2a3fa. --- Packages.props | 1 + .../Localization/sr.cs | 16 ++ .../Localization/sr.resx | 10 + .../Localization/sr.strings | 3 +- .../Localization/sr.xlf | 12 + .../Microsoft.SqlTools.ServiceLayer.csproj | 1 + .../Profiler/IProfilerSessionMonitor.cs | 2 +- .../Profiler/IXEventSession.cs | 12 + .../Profiler/ProfilerService.cs | 107 +++++---- .../Profiler/ProfilerSession.cs | 66 +----- .../Profiler/ProfilerSessionMonitor.cs | 206 +++++++----------- .../Profiler/XEventSession.cs | 5 +- .../Profiler/ProfilerServiceTests.cs | 83 ++----- .../Profiler/ProfilerSessionTests.cs | 36 --- .../Profiler/ProfilerTestObjects.cs | 13 ++ 15 files changed, 245 insertions(+), 328 deletions(-) diff --git a/Packages.props b/Packages.props index 9a9bf056..a5d44818 100644 --- a/Packages.props +++ b/Packages.props @@ -30,6 +30,7 @@ + diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs index 5cb28ee4..118da69d 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs @@ -8756,6 +8756,16 @@ namespace Microsoft.SqlTools.ServiceLayer return Keys.GetString(Keys.SessionAlreadyExists, sessionName); } + public static string SessionMissingDetails(int id) + { + return Keys.GetString(Keys.SessionMissingDetails, id); + } + + public static string StartProfilingFailed(String error) + { + return Keys.GetString(Keys.StartProfilingFailed, error); + } + public static string UnknownSizeUnit(string unit) { return Keys.GetString(Keys.UnknownSizeUnit, unit); @@ -9944,6 +9954,12 @@ namespace Microsoft.SqlTools.ServiceLayer public const string SessionAlreadyExists = "SessionAlreadyExists"; + public const string SessionMissingDetails = "SessionMissingDetails"; + + + public const string StartProfilingFailed = "StartProfilingFailed"; + + public const string CategoryLocal = "CategoryLocal"; diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx index a2e5b6db..ef3b7856 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx @@ -1571,6 +1571,16 @@ An XEvent session named {0} already exists . Parameters: 0 - sessionName (String) + + + Unable to start streaming session {0} due to missing session details. + . + Parameters: 0 - id (int) + + + Failed to start profiler: {0} + . + Parameters: 0 - error (String) [Uncategorized (Local)] diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings index 72c0a886..b7cb98e9 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings @@ -759,7 +759,8 @@ PauseSessionFailed(String error) = Failed to pause session: {0} StopSessionFailed(String error) = Failed to stop session: {0} SessionNotFound = Cannot find requested XEvent session SessionAlreadyExists(String sessionName) = An XEvent session named {0} already exists - +SessionMissingDetails(int id) = Unable to start streaming session {0} due to missing session details. +StartProfilingFailed(String error) = Failed to start profiler: {0} ;job categories CategoryLocal = [Uncategorized (Local)] diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf index 794925cb..75611a0d 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf @@ -5706,6 +5706,18 @@ Failed to start profiler: {0} Failed to start profiler: {0} . + Parameters: 0 - error (String) + + + The underlying type "{0}" for sql variant column "{1}" could not be resolved. + The underlying type "{0}" for sql variant column "{1}" could not be resolved. + . + Parameters: 0 - id (int) + + + Failed to start profiler: {0} + Failed to start profiler: {0} + . Parameters: 0 - error (String) diff --git a/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj b/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj index 95d1f2a5..50ddb5a6 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj +++ b/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj @@ -26,6 +26,7 @@ + diff --git a/src/Microsoft.SqlTools.ServiceLayer/Profiler/IProfilerSessionMonitor.cs b/src/Microsoft.SqlTools.ServiceLayer/Profiler/IProfilerSessionMonitor.cs index 4fd5a6d3..2b6aa292 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Profiler/IProfilerSessionMonitor.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Profiler/IProfilerSessionMonitor.cs @@ -22,7 +22,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler /// /// Starts monitoring a profiler session /// - bool StartMonitoringSession(string viewerId, IXEventSession session); + void StartMonitoringSession(string viewerId, IXEventSession session); /// /// Stops monitoring a profiler session diff --git a/src/Microsoft.SqlTools.ServiceLayer/Profiler/IXEventSession.cs b/src/Microsoft.SqlTools.ServiceLayer/Profiler/IXEventSession.cs index 4e91be3a..61a262fa 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Profiler/IXEventSession.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Profiler/IXEventSession.cs @@ -2,6 +2,8 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; +using Microsoft.SqlServer.Management.XEvent; namespace Microsoft.SqlTools.ServiceLayer.Profiler { @@ -15,6 +17,16 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler /// int Id { get; } + /// + /// Connection details associated with the session id. + /// + ConnectionDetails ConnectionDetails { get; set; } + + /// + /// Session associated with the session id. + /// + Session Session { get; set; } + /// /// Starts XEvent session /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/Profiler/ProfilerService.cs b/src/Microsoft.SqlTools.ServiceLayer/Profiler/ProfilerService.cs index dc659fbc..59e7f89a 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Profiler/ProfilerService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Profiler/ProfilerService.cs @@ -18,6 +18,7 @@ using Microsoft.SqlServer.Management.Sdk.Sfc; using Microsoft.SqlServer.Management.Smo; using Microsoft.SqlServer.Management.XEvent; using Microsoft.SqlServer.Management.XEventDbScoped; +using Microsoft.SqlServer.XEvent.XELite; using Microsoft.SqlTools.Hosting.Protocol; using Microsoft.SqlTools.Hosting.Protocol.Contracts; using Microsoft.SqlTools.ServiceLayer.Connection; @@ -120,6 +121,42 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler this.SessionMonitor.AddSessionListener(this); } + /// + /// Handle request to start a profiling session + /// + internal async Task HandleStartProfilingRequest(StartProfilingParams parameters, RequestContext requestContext) + { + await Task.Run(async () => + { + try + { + Logger.Write(TraceEventType.Verbose, "HandleStartProfilingRequest started"); + ConnectionInfo connInfo; + ConnectionServiceInstance.TryFindConnection( + parameters.OwnerUri, + out connInfo); + if (connInfo != null) + { + // create a new XEvent session and Profiler session + var xeSession = this.XEventSessionFactory.GetXEventSession(parameters.SessionName, connInfo); + monitor.StartMonitoringSession(parameters.OwnerUri, xeSession); + var result = new StartProfilingResult(); + await requestContext.SendResult(result); + } + else + { + Logger.Write(TraceEventType.Error, "Connection Info could not be found for " + parameters.OwnerUri); + throw new Exception(SR.ProfilerConnectionNotFound); + } + } + catch (Exception e) + { + Logger.Write(TraceEventType.Error, "HandleStartProfilingRequest failed for uri " + parameters.OwnerUri); + await requestContext.SendError(new Exception (SR.StartProfilingFailed(e.Message), e)); + } + }); + } + /// /// Handle request to start a profiling session /// @@ -129,20 +166,24 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler { try { + Logger.Write(TraceEventType.Verbose, "HandleCreateXEventSessionRequest started"); ConnectionInfo connInfo; ConnectionServiceInstance.TryFindConnection( parameters.OwnerUri, out connInfo); if (connInfo == null) { + Logger.Write(TraceEventType.Error, "Connection Info could not be found for " + parameters.OwnerUri); throw new Exception(SR.ProfilerConnectionNotFound); } else if (parameters.SessionName == null) { + Logger.Write(TraceEventType.Error, "Session Name could not be found for " + parameters.OwnerUri); throw new ArgumentNullException("SessionName"); } else if (parameters.Template == null) { + Logger.Write(TraceEventType.Error, "Template could not be found for " + parameters.OwnerUri); throw new ArgumentNullException("Template"); } else @@ -156,10 +197,13 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler { xeSession = this.XEventSessionFactory.GetXEventSession(parameters.SessionName, connInfo); } - catch { } + catch { + Logger.Write(TraceEventType.Verbose, "Session with name '" + parameters.SessionName + "' was not found"); + } if (xeSession == null) { + Logger.Write(TraceEventType.Verbose, "Creating new XEventSession with SessionName " + parameters.SessionName); // create a new XEvent session and Profiler session xeSession = this.XEventSessionFactory.CreateXEventSession(parameters.Template.CreateStatement, parameters.SessionName, connInfo); } @@ -175,42 +219,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler } catch (Exception e) { - await requestContext.SendError(new Exception(SR.CreateSessionFailed(e.Message))); - } - }); - } - - /// - /// Handle request to start a profiling session - /// - internal async Task HandleStartProfilingRequest(StartProfilingParams parameters, RequestContext requestContext) - { - await Task.Run(async () => - { - try - { - ConnectionInfo connInfo; - ConnectionServiceInstance.TryFindConnection( - parameters.OwnerUri, - out connInfo); - if (connInfo != null) - { - // create a new XEvent session and Profiler session - var xeSession = this.XEventSessionFactory.GetXEventSession(parameters.SessionName, connInfo); - // start monitoring the profiler session - monitor.StartMonitoringSession(parameters.OwnerUri, xeSession); - - var result = new StartProfilingResult(); - await requestContext.SendResult(result); - } - else - { - throw new Exception(SR.ProfilerConnectionNotFound); - } - } - catch (Exception e) - { - await requestContext.SendError(new Exception(SR.StartSessionFailed(e.Message))); + Logger.Write(TraceEventType.Error, "HandleCreateXEventSessionRequest failed for uri " + parameters.OwnerUri); + await requestContext.SendError(new Exception (SR.CreateSessionFailed(e.Message), e)); } }); } @@ -224,6 +234,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler { try { + Logger.Write(TraceEventType.Verbose, "HandleStopProfilingRequest started"); ProfilerSession session; monitor.StopMonitoringSession(parameters.OwnerUri, out session); @@ -240,11 +251,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler await requestContext.SendResult(new StopProfilingResult { }); break; } - catch (InvalidOperationException) + catch (InvalidOperationException e) { remainingAttempts--; if (remainingAttempts == 0) { + Logger.Write(TraceEventType.Error, "Stop profiler session '" + session.XEventSession.Session.Name + "' failed after three retries, last exception was: " + e.Message); throw; } Thread.Sleep(500); @@ -258,6 +270,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler } catch (Exception e) { + Logger.Write(TraceEventType.Error, "HandleStopProfilingRequest failed for uri " + parameters.OwnerUri); await requestContext.SendError(new Exception(SR.StopSessionFailed(e.Message))); } }); @@ -272,12 +285,14 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler { try { + Logger.Write(TraceEventType.Verbose, "HandlePauseProfilingRequest started"); monitor.PauseViewer(parameters.OwnerUri); await requestContext.SendResult(new PauseProfilingResult { }); } catch (Exception e) { + Logger.Write(TraceEventType.Error, "HandlePauseProfilingRequest failed for uri " + parameters.OwnerUri); await requestContext.SendError(new Exception(SR.PauseSessionFailed(e.Message))); } }); @@ -292,6 +307,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler { try { + Logger.Write(TraceEventType.Verbose, "HandleGetXEventSessionsRequest started"); var result = new GetXEventSessionsResult(); ConnectionInfo connInfo; ConnectionServiceInstance.TryFindConnection( @@ -299,6 +315,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler out connInfo); if (connInfo == null) { + Logger.Write(TraceEventType.Error, "Connection Info could not be found for " + parameters.OwnerUri); await requestContext.SendError(new Exception(SR.ProfilerConnectionNotFound)); } else @@ -310,6 +327,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler } catch (Exception e) { + Logger.Write(TraceEventType.Error, "HandleGetXEventSessionsRequest failed for uri " + parameters.OwnerUri); await requestContext.SendError(e); } }); @@ -324,11 +342,13 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler { try { + Logger.Write(TraceEventType.Verbose, "HandleDisconnectSessionRequest started"); ProfilerSession session; monitor.StopMonitoringSession(parameters.OwnerUri, out session); } catch (Exception e) { + Logger.Write(TraceEventType.Error, "HandleDisconnectSessionRequest failed for uri " + parameters.OwnerUri); await requestContext.SendError(e); } }); @@ -374,6 +394,15 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler return store; } + /// + /// Nulls out properties in ConnectionDetails that aren't compatible with XElite. + /// + private static void RemoveIncompatibleConnectionProperties(ConnectionDetails connDetails){ + connDetails.ConnectRetryCount = null; + connDetails.ConnectRetryInterval = null; + connDetails.MultiSubnetFailover = null; + } + /// /// Gets an XEvent session with the given name per the IXEventSessionFactory contract /// Also starts the session if it isn't currently running @@ -383,6 +412,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler var sqlConnection = ConnectionService.OpenSqlConnection(connInfo); SqlStoreConnection connection = new SqlStoreConnection(sqlConnection); BaseXEStore store = CreateXEventStore(connInfo, connection); + RemoveIncompatibleConnectionProperties(connInfo.ConnectionDetails); Session session = store.Sessions[sessionName]; // start the session if it isn't already running @@ -399,6 +429,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler // create xevent session wrapper return new XEventSession() { + ConnectionDetails = connInfo.ConnectionDetails, Session = session }; } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Profiler/ProfilerSession.cs b/src/Microsoft.SqlTools.ServiceLayer/Profiler/ProfilerSession.cs index 5835cd3b..bce45249 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Profiler/ProfilerSession.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Profiler/ProfilerSession.cs @@ -17,18 +17,13 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler /// public class ProfilerSession { - private static readonly TimeSpan DefaultPollingDelay = TimeSpan.FromSeconds(1); - private object pollingLock = new object(); - private bool isPolling = false; - private DateTime lastPollTime = DateTime.Now.Subtract(DefaultPollingDelay); - private TimeSpan pollingDelay = DefaultPollingDelay; + public bool IsStreaming { get; set; } + private ProfilerEvent lastSeenEvent = null; private bool eventsLost = false; int lastSeenId = -1; - public bool pollImmediatly = false; - /// /// Connection to use for the session /// @@ -39,57 +34,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler /// public IXEventSession XEventSession { get; set; } - /// - /// Try to set the session into polling mode if criteria is meet - /// - /// True if session set to polling mode, False otherwise - public bool TryEnterPolling() - { - lock (this.pollingLock) - { - if (pollImmediatly || (!this.isPolling && DateTime.Now.Subtract(this.lastPollTime) >= pollingDelay)) - { - this.isPolling = true; - this.lastPollTime = DateTime.Now; - this.pollImmediatly = false; - return true; - } - else - { - return false; - } - } - } - - /// - /// Is the session currently being polled - /// - public bool IsPolling - { - get - { - return this.isPolling; - } - set - { - lock (this.pollingLock) - { - this.isPolling = value; - } - } - } - - /// - /// The delay between session polls - /// - public TimeSpan PollingDelay - { - get - { - return pollingDelay; - } - } - /// /// Could events have been lost in the last poll /// @@ -106,7 +50,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler /// private bool IsProfilerEvent(ProfilerEvent currentEvent) { - if (string.IsNullOrWhiteSpace(currentEvent.Name) || currentEvent.Values == null) + if (string.IsNullOrWhiteSpace(currentEvent.Name) || currentEvent.Values == null) { return false; } @@ -145,7 +89,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler public void FilterOldEvents(List events) { this.eventsLost = false; - + if (lastSeenId != -1) { // find the last event we've previously seen @@ -169,7 +113,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler { events.RemoveRange(0, idx + 1); } - else if(earliestSeenEventId > (lastSeenId + 1)) + else if (earliestSeenEventId > (lastSeenId + 1)) { // if there's a gap between the expected next event sequence // and the furthest back event seen, we know we've lost events diff --git a/src/Microsoft.SqlTools.ServiceLayer/Profiler/ProfilerSessionMonitor.cs b/src/Microsoft.SqlTools.ServiceLayer/Profiler/ProfilerSessionMonitor.cs index 49a411b0..3a3f7315 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Profiler/ProfilerSessionMonitor.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Profiler/ProfilerSessionMonitor.cs @@ -4,19 +4,13 @@ // using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using Microsoft.Data.SqlClient; -using System.Diagnostics; using System.Linq; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using System.Xml; -using Microsoft.SqlServer.Management.Sdk.Sfc; -using Microsoft.SqlServer.Management.XEvent; -using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; +using Microsoft.SqlServer.XEvent.XELite; +using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Profiler.Contracts; -using Microsoft.SqlTools.Utility; namespace Microsoft.SqlTools.ServiceLayer.Profiler { @@ -25,14 +19,10 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler /// public class ProfilerSessionMonitor : IProfilerSessionMonitor { - private const int PollingLoopDelay = 1000; - private object sessionsLock = new object(); private object listenersLock = new object(); - private object pollingLock = new object(); - private Task processorThread = null; private struct Viewer @@ -56,6 +46,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler // XEvent Session Id's matched to their Profiler Sessions private Dictionary monitoredSessions = new Dictionary(); + // XEvent Session Id's matched to their stream cancellation tokens + private Dictionary monitoredCancellationTokenSources = new Dictionary(); + // ViewerId -> Viewer objects private Dictionary allViewers = new Dictionary(); @@ -72,10 +65,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler } } - /// - /// Start monitoring the provided session - /// - public bool StartMonitoringSession(string viewerId, IXEventSession session) + public void StartMonitoringSession(string viewerId, IXEventSession session) { lock (this.sessionsLock) { @@ -115,12 +105,10 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler } else { - viewers = new List{ viewerId }; + viewers = new List { viewerId }; sessionViewers.Add(session.Id, viewers); } } - - return true; } /// @@ -160,8 +148,16 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler { lock (this.sessionsLock) { + //cancel running XEventStream for session. + CancellationTokenSource targetToken; + if (monitoredCancellationTokenSources.Remove(sessionId, out targetToken)) + { + targetToken.Cancel(); + } if (this.monitoredSessions.Remove(sessionId, out session)) { + session.IsStreaming = false; + //remove all viewers for this session List viewerIds; if (sessionViewers.Remove(sessionId, out viewerIds)) @@ -186,113 +182,105 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler } } - public void PollSession(int sessionId) - { - lock (this.sessionsLock) - { - this.monitoredSessions[sessionId].pollImmediatly = true; - } - lock (this.pollingLock) - { - Monitor.Pulse(pollingLock); - } - } - /// - /// The core queue processing method + /// The core queue processing method, cycles through monitored sessions and creates a stream for them if not already. /// - /// private void ProcessSessions() { while (true) { - lock (this.pollingLock) + lock (this.sessionsLock) { - lock (this.sessionsLock) + foreach (var id in this.monitoredSessions.Keys) { - foreach (var session in this.monitoredSessions.Values) + ProfilerSession session; + this.monitoredSessions.TryGetValue(id, out session); + if (!session.IsStreaming) { List viewers = this.sessionViewers[session.XEventSession.Id]; - if (viewers.Any(v => allViewers[v].active)) - { - ProcessSession(session); + if (viewers.Any(v => allViewers[v].active)){ + StartStream(id, session); } } } - Monitor.Wait(this.pollingLock, PollingLoopDelay); } } } /// - /// Process a session for new XEvents if it meets the polling criteria + /// Helper function used to process the XEvent feed from a session's stream. /// - private void ProcessSession(ProfilerSession session) + private async Task HandleXEvent(IXEvent xEvent, ProfilerSession session) { - if (session.TryEnterPolling()) + ProfilerEvent profileEvent = new ProfilerEvent(xEvent.Name, xEvent.Timestamp.ToString()); + foreach (var kvp in xEvent.Fields) { - Task.Factory.StartNew(() => + profileEvent.Values.Add(kvp.Key, kvp.Value.ToString()); + } + foreach (var kvp in xEvent.Actions) + { + profileEvent.Values.Add(kvp.Key, kvp.Value.ToString()); + } + var eventList = new List(); + eventList.Add(profileEvent); + var eventsLost = session.EventsLost; + + if (eventList.Count > 0 || eventsLost) + { + session.FilterOldEvents(eventList); + eventList = session.FilterProfilerEvents(eventList); + // notify all viewers of the event. + List viewerIds = this.sessionViewers[session.XEventSession.Id]; + + foreach (string viewerId in viewerIds) { - var events = PollSession(session); - bool eventsLost = session.EventsLost; - if (events.Count > 0 || eventsLost) + if (allViewers[viewerId].active) { - // notify all viewers for the polled session - List viewerIds = this.sessionViewers[session.XEventSession.Id]; - foreach (string viewerId in viewerIds) - { - if (allViewers[viewerId].active) - { - SendEventsToListeners(viewerId, events, eventsLost); - } - } + SendEventsToListeners(viewerId, eventList, eventsLost); } - }); + } } } - private List PollSession(ProfilerSession session) + /// + /// Function that creates a brand new stream from a session, this is called from ProcessSessions when a session doesn't have a stream running currently. + /// + private void StartStream(int id, ProfilerSession session) { - var events = new List(); - try - { - if (session == null || session.XEventSession == null) + if(session.XEventSession != null && session.XEventSession.Session != null && session.XEventSession.ConnectionDetails != null){ + CancellationTokenSource threadCancellationToken = new CancellationTokenSource(); + var connectionString = ConnectionService.BuildConnectionString(session.XEventSession.ConnectionDetails); + var eventStreamer = new XELiveEventStreamer(connectionString, session.XEventSession.Session.Name); + // Start streaming task here, will run until cancellation or error with the feed. + var task = eventStreamer.ReadEventStream(xEvent => HandleXEvent(xEvent, session), threadCancellationToken.Token); + + task.ContinueWith(t => { - return events; - } - - var targetXml = session.XEventSession.GetTargetXml(); - - XmlDocument xmlDoc = new XmlDocument(); - xmlDoc.LoadXml(targetXml); - - var nodes = xmlDoc.DocumentElement.GetElementsByTagName("event"); - foreach (XmlNode node in nodes) - { - var profilerEvent = ParseProfilerEvent(node); - if (profilerEvent != null) + //If cancellation token is missing, that means stream was stopped by the client, do not notify in this case. + CancellationTokenSource targetToken; + if (monitoredCancellationTokenSources.TryGetValue(id, out targetToken)) { - events.Add(profilerEvent); + StopSession(session.XEventSession.Id); } - } - } - catch (XEventException) - { - SendStoppedSessionInfoToListeners(session.XEventSession.Id); - ProfilerSession tempSession; - RemoveSession(session.XEventSession.Id, out tempSession); - } - catch (Exception ex) - { - Logger.Write(TraceEventType.Warning, "Failed to poll session. error: " + ex.Message); - } - finally - { - session.IsPolling = false; - } + }, TaskContinuationOptions.OnlyOnFaulted); - session.FilterOldEvents(events); - return session.FilterProfilerEvents(events); + this.monitoredCancellationTokenSources.Add(id, threadCancellationToken); + session.IsStreaming = true; + } + else { + ProfilerSession tempSession; + RemoveSession(id, out tempSession); + throw new Exception(SR.SessionMissingDetails(id)); + } + } + + /// + /// Helper function for notifying listeners and stopping session in case the session is stopped on the server. This is public for tests. + /// + public void StopSession(int Id){ + SendStoppedSessionInfoToListeners(Id); + ProfilerSession tempSession; + RemoveSession(Id, out tempSession); } /// @@ -304,7 +292,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler { foreach (var listener in this.listeners) { - foreach(string viewerId in sessionViewers[sessionId]) + foreach (string viewerId in sessionViewers[sessionId]) { listener.SessionStopped(viewerId, sessionId); } @@ -325,31 +313,5 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler } } } - - /// - /// Parse a single event node from XEvent XML - /// - private ProfilerEvent ParseProfilerEvent(XmlNode node) - { - var name = node.Attributes["name"]; - var timestamp = node.Attributes["timestamp"]; - - var profilerEvent = new ProfilerEvent(name.InnerText, timestamp.InnerText); - - foreach (XmlNode childNode in node.ChildNodes) - { - var childName = childNode.Attributes["name"]; - XmlNode typeNode = childNode.SelectSingleNode("type"); - var typeName = typeNode.Attributes["name"]; - XmlNode valueNode = childNode.SelectSingleNode("value"); - - if (!profilerEvent.Values.ContainsKey(childName.InnerText)) - { - profilerEvent.Values.Add(childName.InnerText, valueNode.InnerText); - } - } - - return profilerEvent; - } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Profiler/XEventSession.cs b/src/Microsoft.SqlTools.ServiceLayer/Profiler/XEventSession.cs index b1a3903c..8c2ba38c 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Profiler/XEventSession.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Profiler/XEventSession.cs @@ -5,8 +5,7 @@ using System.Linq; using Microsoft.SqlServer.Management.XEvent; -using Microsoft.SqlTools.ServiceLayer.Connection; -using Microsoft.SqlTools.ServiceLayer.Profiler.Contracts; +using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; namespace Microsoft.SqlTools.ServiceLayer.Profiler { @@ -17,6 +16,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler { public Session Session { get; set; } + public ConnectionDetails ConnectionDetails { get; set; } + public int Id { get { return Session.ID; } diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Profiler/ProfilerServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Profiler/ProfilerServiceTests.cs index 7efe8216..fffb1f9e 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Profiler/ProfilerServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Profiler/ProfilerServiceTests.cs @@ -65,17 +65,12 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler // start profiling session await profilerService.HandleStartProfilingRequest(requestParams, requestContext.Object); - profilerService.SessionMonitor.PollSession(1); - // simulate a short polling delay - Thread.Sleep(200); - profilerService.SessionMonitor.PollSession(1); - // wait for polling to finish, or for timeout System.Timers.Timer pollingTimer = new System.Timers.Timer(); pollingTimer.Interval = 10000; pollingTimer.Start(); bool timeout = false; - pollingTimer.Elapsed += new System.Timers.ElapsedEventHandler((s_, e_) => {timeout = true;}); + pollingTimer.Elapsed += new System.Timers.ElapsedEventHandler((s_, e_) => { timeout = true; }); while (sessionId == null && !timeout) { Thread.Sleep(250); @@ -125,6 +120,8 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler ConnectionInfo connectionInfo = TestObjects.GetTestConnectionInfo(); profilerService.ConnectionServiceInstance.OwnerToConnectionMap.Add(testUri, connectionInfo); profilerService.XEventSessionFactory = new TestXEventSessionFactory(); + mockSession.SetupProperty(p => p.ConnectionDetails, connectionInfo.ConnectionDetails); + mockSession.SetupProperty(p => p.Session, new Session(null, testUri)); var requestParams = new StopProfilingParams(); requestParams.OwnerUri = testUri; @@ -149,12 +146,12 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler /// /// [Test] + // TODO: Add more in-depth testing for pause effects on events received. public async Task TestPauseProfilingRequest() { bool success = false; string testUri = "test_session"; - bool recievedEvents = false; - + // capture pausing results var requestContext = new Mock>(); requestContext.Setup(rc => rc.SendResult(It.IsAny())) @@ -164,75 +161,34 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler return Task.FromResult(0); }); - // capture Listener event notifications - var mockListener = new Mock(); - mockListener.Setup(p => p.EventsAvailable(It.IsAny(), It.IsAny>(), It.IsAny())).Callback(() => - { - recievedEvents = true; - }); - // setup profiler service + var mockListener = new TestSessionListener(); var profilerService = new ProfilerService(); - profilerService.SessionMonitor.AddSessionListener(mockListener.Object); + profilerService.SessionMonitor.AddSessionListener(mockListener); profilerService.ConnectionServiceInstance = TestObjects.GetTestConnectionService(); ConnectionInfo connectionInfo = TestObjects.GetTestConnectionInfo(); profilerService.ConnectionServiceInstance.OwnerToConnectionMap.Add(testUri, connectionInfo); + var testSession = new TestXEventSession1(); + testSession.ConnectionDetails = connectionInfo.ConnectionDetails; + testSession.Session = new Session(null, testUri); + var requestParams = new PauseProfilingParams(); requestParams.OwnerUri = testUri; // begin monitoring session - profilerService.SessionMonitor.StartMonitoringSession(testUri, new TestXEventSession1()); - - // poll the session - profilerService.SessionMonitor.PollSession(1); - Thread.Sleep(500); - profilerService.SessionMonitor.PollSession(1); - - // wait for polling to finish, or for timeout - System.Timers.Timer pollingTimer = new System.Timers.Timer(); - pollingTimer.Interval = 10000; - pollingTimer.Start(); - bool timeout = false; - pollingTimer.Elapsed += new System.Timers.ElapsedEventHandler((s_, e_) => {timeout = true;}); - while (!recievedEvents && !timeout) - { - Thread.Sleep(250); - } - pollingTimer.Stop(); - - // confirm that polling works - Assert.True(recievedEvents); + profilerService.SessionMonitor.StartMonitoringSession(testUri, testSession); // pause viewer await profilerService.HandlePauseProfilingRequest(requestParams, requestContext.Object); Assert.True(success); - recievedEvents = false; success = false; - profilerService.SessionMonitor.PollSession(1); - - // confirm that no events were sent to paused Listener - Assert.False(recievedEvents); - // unpause viewer await profilerService.HandlePauseProfilingRequest(requestParams, requestContext.Object); Assert.True(success); - profilerService.SessionMonitor.PollSession(1); - - // wait for polling to finish, or for timeout - timeout = false; - pollingTimer.Start(); - while (!recievedEvents && !timeout) - { - Thread.Sleep(250); - } - - // check that events got sent to Listener - Assert.True(recievedEvents); - requestContext.VerifyAll(); } @@ -263,21 +219,14 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler profilerService.ConnectionServiceInstance = TestObjects.GetTestConnectionService(); ConnectionInfo connectionInfo = TestObjects.GetTestConnectionInfo(); profilerService.ConnectionServiceInstance.OwnerToConnectionMap.Add(testUri, connectionInfo); + mockSession.SetupProperty(p => p.ConnectionDetails, connectionInfo.ConnectionDetails); + mockSession.SetupProperty(p => p.Session, new Session(null, testUri)); // start monitoring test session profilerService.SessionMonitor.StartMonitoringSession(testUri, mockSession.Object); - // wait for polling to finish, or for timeout - System.Timers.Timer pollingTimer = new System.Timers.Timer(); - pollingTimer.Interval = 10000; - pollingTimer.Start(); - bool timeout = false; - pollingTimer.Elapsed += new System.Timers.ElapsedEventHandler((s_, e_) => {timeout = true;}); - while (sessionStopped == false && !timeout) - { - Thread.Sleep(250); - } - pollingTimer.Stop(); + // Call stop session to simulate when a server has stopped a session on its side. + profilerService.SessionMonitor.StopSession(0); // check that a stopped session notification was sent Assert.True(sessionStopped); diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Profiler/ProfilerSessionTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Profiler/ProfilerSessionTests.cs index 85a00e83..63d96268 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Profiler/ProfilerSessionTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Profiler/ProfilerSessionTests.cs @@ -123,41 +123,5 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler profilerSession.FilterOldEvents(profilerEvents); Assert.False(profilerSession.EventsLost); } - - /// - /// Test the TryEnterPolling method - /// - [Test] - public void TestTryEnterPolling() - { - DateTime startTime = DateTime.Now; - - // create new profiler session - var profilerSession = new ProfilerSession(); - - // enter the polling block - Assert.True(profilerSession.TryEnterPolling()); - Assert.True(profilerSession.IsPolling); - - // verify we can't enter again - Assert.False(profilerSession.TryEnterPolling()); - - // set polling to false to exit polling block - profilerSession.IsPolling = false; - - bool outsideDelay = DateTime.Now.Subtract(startTime) >= profilerSession.PollingDelay; - - // verify we can only enter again if we're outside polling delay interval - Assert.AreEqual(profilerSession.TryEnterPolling(), outsideDelay); - - // reset IsPolling in case the delay has elasped on slow machine or while debugging - profilerSession.IsPolling = false; - - // wait for the polling delay to elapse - Thread.Sleep(profilerSession.PollingDelay); - - // verify we can enter the polling block again - Assert.True(profilerSession.TryEnterPolling()); - } } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Profiler/ProfilerTestObjects.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Profiler/ProfilerTestObjects.cs index 94dcd171..198f6f77 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Profiler/ProfilerTestObjects.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Profiler/ProfilerTestObjects.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Microsoft.SqlServer.Management.XEvent; using Microsoft.SqlTools.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; using Microsoft.SqlTools.ServiceLayer.Profiler; using Microsoft.SqlTools.ServiceLayer.Profiler.Contracts; using Microsoft.SqlTools.ServiceLayer.UnitTests.Utility; @@ -206,6 +207,10 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler public int Id { get { return 51; } } + public ConnectionDetails ConnectionDetails { get; set; } + + public Session Session { get; set; } + public void Start(){} public void Stop(){} @@ -291,6 +296,10 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler public int Id { get { return 1; } } + public ConnectionDetails ConnectionDetails { get; set; } + + public Session Session { get; set; } + public void Start(){} public void Stop(){} @@ -382,6 +391,10 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler public int Id { get { return 2; } } + public ConnectionDetails ConnectionDetails { get; set; } + + public Session Session { get; set; } + public void Start(){} public void Stop(){}