Add "Open XEL file" support to profiler in sqltoolsservice (#2091)

* Open XEL file changes

* placeholders for openxel

* add observable xe reader

* md format tweaks

* implement localfile as a new session type

* add ErrorMessage to session stopped notice

* fix flaky test

* handle already running session

* fix stopped session event send on file completion

* fix flaky unit test

* Update XElite and dependent versions

* Fix errors after merge and remove failing tests for now

* Fix main merge mess-up.
Address comments.
Add one more relevant test.

* Remove extra namespace.

* Remove unnecessary import

* Fix build error

* Address comments.

* Remove disabiling JSON002 compiler warning

* Address comments and update json handling

* Fix build error

* Fix integration test (emerged due to Main merge mess up)

* Clean up code (no functional changes)

---------

Co-authored-by: Karl Burtram <karlb@microsoft.com>
Co-authored-by: shueybubbles <david.shiflet@microsoft.com>
This commit is contained in:
Sakshi Sharma
2023-06-27 14:25:18 -07:00
committed by GitHub
parent dbcb156816
commit 4334d79d76
27 changed files with 952 additions and 259 deletions

View File

@@ -22,7 +22,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler.Contracts
public class GetXEventSessionsResult
{
/// <summary>
/// Session ID that was started
/// List of XE session names
/// </summary>
public List<string> Sessions { get; set; }
}

View File

@@ -19,6 +19,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler.Contracts
public bool EventsLost { get; set; }
}
/// <summary>
/// Profiler Event available notification mapping entry
/// </summary>
public class ProfilerEventsAvailableNotification
{
public static readonly

View File

@@ -18,6 +18,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler.Contracts
public string TemplateName { get; set; }
}
/// <summary>
/// Profiler Session created notification mapping entry
/// </summary>
public class ProfilerSessionCreatedNotification
{
public static readonly

View File

@@ -13,9 +13,25 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler.Contracts
{
public string OwnerUri { get; set; }
/// <summary>
/// Numeric session id that is only unique on the server where the session resides
/// </summary>
public int SessionId { get; set; }
/// <summary>
/// An key that uniquely identifies a session across all servers
/// </summary>
public string UniqueSessionId { get; set; }
/// <summary>
/// The error that stopped the session, if any.
/// </summary>
public string ErrorMessage { get; set; }
}
/// <summary>
/// Profiler Session stopped notification mapping entry
/// </summary>
public class ProfilerSessionStoppedNotification
{
public static readonly

View File

@@ -17,10 +17,39 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler.Contracts
{
public string OwnerUri { get; set; }
/// <summary>
/// For RemoteSession sessions, the name of the remote session.
/// For LocalFile sessions, the full path of the XEL file to open.
/// </summary>
public string SessionName { get; set; }
/// <summary>
/// Identifies which type of target the session name identifies.
/// </summary>
public ProfilingSessionType SessionType { get; set; } = ProfilingSessionType.RemoteSession;
}
public class StartProfilingResult{}
public enum ProfilingSessionType
{
RemoteSession,
LocalFile
}
/// <summary>
/// Provides information about the session that was started
/// </summary>
public class StartProfilingResult
{
/// <summary>
/// A unique key to identify the session
/// </summary>
public string UniqueSessionId { get; set; }
/// <summary>
/// Whether the profiling session supports the Pause operation.
/// </summary>
public bool CanPause { get; set; }
}
/// <summary>
/// Start Profile request type

View File

@@ -14,6 +14,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
{
void EventsAvailable(string sessionId, List<ProfilerEvent> events, bool eventsLost);
void SessionStopped(string viewerId, int sessionId);
void SessionStopped(string viewerId, SessionId sessionId, string errorMessage);
}
}

View File

@@ -5,6 +5,9 @@
#nullable disable
using System;
using System.Threading;
namespace Microsoft.SqlTools.ServiceLayer.Profiler
{
/// <summary>
@@ -15,7 +18,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
/// <summary>
/// Gets unique XEvent session Id
/// </summary>
int Id { get; }
SessionId Id { get; }
/// <summary>
/// Starts XEvent session
@@ -32,4 +35,47 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
/// </summary>
string GetTargetXml();
}
public interface IObservableXEventSession : IXEventSession
{
IObservable<Contracts.ProfilerEvent> ObservableSessionEvents { get; }
}
/// <summary>
/// Strong type for use as a dictionary key, simply wraps a string.
/// Using a class helps distinguish instances from other strings like viewer id.
/// </summary>
public class SessionId
{
private readonly string sessionId;
// SQL Server starts session counters at around 64k, so it's unlikely that this process-scoped counter would collide with a real session id
// Eventually the profiler extension in ADS will use the string instead of the number.
private static int numericIdCurrent = -1;
/// <summary>
/// Constructs a new sessionId
/// </summary>
/// <param name="sessionId">The true unique identifier string, opaque to the client.</param>
/// <param name="numericId">An optional numeric identifier used to identify the session to older clients that don't consume the string yet</param>
public SessionId(string sessionId, int? numericId = null)
{
this.sessionId = sessionId ?? throw new ArgumentNullException(nameof(sessionId));
NumericId = numericId ?? Interlocked.Increment(ref numericIdCurrent);
}
public int NumericId { get; private set; }
public override int GetHashCode() => sessionId.GetHashCode();
public override bool Equals(object obj)
{
return (obj is SessionId id) && id.sessionId.Equals(sessionId);
}
public override string ToString()
{
return sessionId;
}
}
}

View File

@@ -24,5 +24,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
/// Creates an XEvent session with the given create statement and name
/// </summary>
IXEventSession CreateXEventSession(string createStatement, string sessionName, ConnectionInfo connInfo);
/// <summary>
/// Opens a session whose events are streamed from a local XEL file
/// </summary>
/// <param name="filePath"></param>
/// <returns></returns>
IXEventSession OpenLocalFileSession(string filePath);
}
}

View File

@@ -0,0 +1,170 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
#nullable disable
using Microsoft.SqlServer.XEvent.XELite;
using Microsoft.SqlTools.ServiceLayer.Profiler.Contracts;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.SqlTools.ServiceLayer.Profiler
{
/// <summary>
/// Wrapper XEventSession for IXEventFetcher instances
/// </summary>
class ObservableXEventSession : XEventSession, IObservableXEventSession
{
private readonly XeStreamObservable observableSession;
private readonly SessionId sessionId;
public IObservable<ProfilerEvent> ObservableSessionEvents => observableSession;
public override void Start()
{
Session?.Start();
observableSession.Start();
}
public override void Stop()
{
observableSession.Close();
Session?.Stop();
}
public ObservableXEventSession(Func<IXEventFetcher> xeventFetcher, SessionId sessionId)
{
observableSession = new XeStreamObservable(xeventFetcher);
this.sessionId = sessionId;
}
protected override SessionId GetSessionId()
{
return sessionId;
}
}
/// <summary>
/// Source of ProfilerEvent push notifications. Wraps IXEventFetcher.
/// </summary>
public class XeStreamObservable : IObservable<ProfilerEvent>
{
private readonly object syncObj = new object();
private readonly List<IObserver<ProfilerEvent>> observers = new List<IObserver<ProfilerEvent>>();
private CancellationTokenSource cancellationTokenSource;
private readonly Func<IXEventFetcher> xeventFetcher;
/// <summary>
/// Constructs a new XeStreamObservable that converts xevent data from the fetcher to ProfilerEvent instances
/// </summary>
/// <param name="fetcher"></param>
public XeStreamObservable(Func<IXEventFetcher> fetcher)
{
xeventFetcher = fetcher;
}
/// <summary>
/// Starts processing xevents from the source.
/// </summary>
public void Start()
{
try
{
cancellationTokenSource = new CancellationTokenSource();
var xeventFetcherFuncCallBack = xeventFetcher();
var xeventFetcherTask = xeventFetcherFuncCallBack.ReadEventStream(OnEventRead, cancellationTokenSource.Token);
xeventFetcherTask.ContinueWith(OnStreamClosed);
} catch (Exception ex)
{
Task.FromException<IXEventFetcher>(ex).ContinueWith(OnStreamClosed);
}
}
/// <summary>
/// Stops the xevent fetching task and informs all listeners that the event stream has ended and clears the list of listeners.
/// Start could be called again, but only new subscribers will see the data.
/// </summary>
public void Close()
{
cancellationTokenSource.Cancel();
var currentObservers = CurrentObservers;
currentObservers.ForEach(o => o.OnCompleted());
lock (syncObj)
{
currentObservers.ForEach(o => observers.Remove(o));
}
}
/// <summary>
/// Adds the observer to the listener list
/// </summary>
/// <param name="observer"></param>
/// <returns>An IDisposable for the listener to call when it no longer wishes to receive events</returns>
public IDisposable Subscribe(IObserver<ProfilerEvent> observer)
{
lock (syncObj)
{
if (!observers.Contains(observer))
{
observers.Add(observer);
}
return new Unsubscriber(observers, observer);
}
}
private List<IObserver<ProfilerEvent>> CurrentObservers
{
get
{
lock (syncObj)
{
return new List<IObserver<ProfilerEvent>>(observers);
}
}
}
private void OnStreamClosed(Task fetcherTask)
{
if (fetcherTask.IsFaulted)
{
CurrentObservers.ForEach(o => o.OnError(fetcherTask.Exception));
}
Close();
}
private Task OnEventRead(IXEvent xEvent)
{
ProfilerEvent profileEvent = new ProfilerEvent(xEvent.Name, xEvent.Timestamp.ToString());
foreach (var kvp in xEvent.Fields)
{
profileEvent.Values.Add(kvp.Key, kvp.Value.ToString());
}
CurrentObservers.ForEach(o => o.OnNext(profileEvent));
return Task.FromResult(0);
}
private class Unsubscriber : IDisposable
{
private readonly List<IObserver<ProfilerEvent>> _observers;
private readonly IObserver<ProfilerEvent> _observer;
public Unsubscriber(List<IObserver<ProfilerEvent>> observers, IObserver<ProfilerEvent> observer)
{
_observers = observers;
_observer = observer;
}
public void Dispose()
{
if (_observer != null && _observers.Contains(_observer))
{
_observers.Remove(_observer);
}
}
}
}
}

View File

@@ -0,0 +1,19 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
namespace Microsoft.SqlTools.ServiceLayer.Profiler
{
class ProfilerException : Exception
{
public ProfilerException(string message) : base(message)
{
}
public ProfilerException(string message, Exception innerException) : base(message, innerException)
{
}
}
}

View File

@@ -8,11 +8,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SqlServer.Management.Sdk.Sfc;
using Microsoft.SqlServer.Management.XEvent;
using Microsoft.SqlServer.Management.XEventDbScoped;
using Microsoft.SqlServer.XEvent.XELite;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Hosting;
@@ -120,7 +120,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
out connInfo);
if (connInfo == null)
{
throw new Exception(SR.ProfilerConnectionNotFound);
throw new ProfilerException(SR.ProfilerConnectionNotFound);
}
else if (parameters.SessionName == null)
{
@@ -143,7 +143,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
}
catch { }
// create a new XEvent session and Profiler session
// create a new XEvent session and Profiler session, if it doesn't exist
xeSession ??= this.XEventSessionFactory.CreateXEventSession(parameters.Template.CreateStatement, parameters.SessionName, connInfo);
// start monitoring the profiler session
@@ -161,6 +161,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
/// </summary>
internal async Task HandleStartProfilingRequest(StartProfilingParams parameters, RequestContext<StartProfilingResult> requestContext)
{
if (parameters.SessionType == ProfilingSessionType.LocalFile)
{
await StartLocalFileSession(parameters, requestContext);
return;
}
ConnectionInfo connInfo;
ConnectionServiceInstance.TryFindConnection(
parameters.OwnerUri,
@@ -169,29 +174,48 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
{
// 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();
var result = new StartProfilingResult() { CanPause = true, UniqueSessionId = xeSession.Id.ToString() };
await requestContext.SendResult(result);
}
else
{
throw new Exception(SR.ProfilerConnectionNotFound);
throw new ProfilerException(SR.ProfilerConnectionNotFound);
}
}
private async Task StartLocalFileSession(StartProfilingParams parameters, RequestContext<StartProfilingResult> requestContext)
{
var xeSession = XEventSessionFactory.OpenLocalFileSession(parameters.SessionName);
monitor.StartMonitoringSession(parameters.OwnerUri, xeSession);
xeSession.Start();
var result = new StartProfilingResult() { UniqueSessionId = xeSession.Id.ToString(), CanPause = false };
await requestContext.SendResult(result);
}
public IXEventSession OpenLocalFileSession(string filePath)
{
return new ObservableXEventSession(() => initIXEventFetcher(filePath), new SessionId(filePath));
}
public IXEventFetcher initIXEventFetcher(string filePath)
{
return new XEFileEventStreamer(filePath);
}
/// <summary>
/// Handle request to stop a profiling session
/// </summary>
internal async Task HandleStopProfilingRequest(StopProfilingParams parameters, RequestContext<StopProfilingResult> requestContext)
{
ProfilerSession session;
monitor.StopMonitoringSession(parameters.OwnerUri, out session);
monitor.StopMonitoringSession(parameters.OwnerUri, out ProfilerSession session);
if (session != null)
{
// Occasionally we might see the InvalidOperationException due to a read is
// Occasionally we might see the InvalidOperationException due to a read is
// in progress, add the following retry logic will solve the problem.
int remainingAttempts = 3;
while (true)
@@ -199,6 +223,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
try
{
session.XEventSession.Stop();
session.Dispose();
await requestContext.SendResult(new StopProfilingResult { });
break;
}
@@ -209,13 +234,13 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
{
throw;
}
Thread.Sleep(500);
await Task.Delay(500);
}
}
}
else
{
throw new Exception(SR.SessionNotFound);
throw new ProfilerException(SR.SessionNotFound);
}
}
@@ -241,11 +266,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
out connInfo);
if (connInfo == null)
{
await requestContext.SendError(new Exception(SR.ProfilerConnectionNotFound));
await requestContext.SendError(new ProfilerException(SR.ProfilerConnectionNotFound));
}
else
{
List<string> sessions = GetXEventSessionList(parameters.OwnerUri, connInfo);
List<string> sessions = GetXEventSessionList(connInfo);
result.Sessions = sessions;
await requestContext.SendResult(result);
}
@@ -254,9 +279,10 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
/// <summary>
/// Handle request to disconnect a session
/// </summary>
internal async Task HandleDisconnectSessionRequest(DisconnectSessionParams parameters, RequestContext<DisconnectSessionResult> requestContext)
internal Task HandleDisconnectSessionRequest(DisconnectSessionParams parameters, RequestContext<DisconnectSessionResult> requestContext)
{
monitor.StopMonitoringSession(parameters.OwnerUri, out _);
return Task.CompletedTask;
}
/// <summary>
@@ -265,7 +291,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
/// <returns>
/// A list of the names of all running XEvent sessions
/// </returns>
internal List<string> GetXEventSessionList(string ownerUri, ConnectionInfo connInfo)
internal List<string> GetXEventSessionList(ConnectionInfo connInfo)
{
var sqlConnection = ConnectionService.OpenSqlConnection(connInfo);
SqlStoreConnection connection = new SqlStoreConnection(sqlConnection);
@@ -311,16 +337,19 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
Session session = store.Sessions[sessionName] ?? throw new Exception(SR.SessionNotFound);
// start the session if it isn't already running
if (session != null && !session.IsRunning)
{
session.Start();
}
// create xevent session wrapper
return new XEventSession()
session = session ?? throw new ProfilerException(SR.SessionNotFound);
var xeventSession = new XEventSession()
{
Session = session
};
if (!session.IsRunning)
{
xeventSession.Start();
}
return xeventSession;
}
/// <summary>
@@ -336,7 +365,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
// session shouldn't already exist
if (session != null)
{
throw new Exception(SR.SessionAlreadyExists(sessionName));
throw new ProfilerException(SR.SessionAlreadyExists(sessionName));
}
var statement = createStatement.Replace("{sessionName}", sessionName);
@@ -345,7 +374,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
session = store.Sessions[sessionName];
if (session == null)
{
throw new Exception(SR.SessionNotFound);
throw new ProfilerException(SR.SessionNotFound);
}
if (!session.IsRunning)
{
@@ -378,7 +407,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
/// <summary>
/// Callback when the XEvent session is closed unexpectedly
/// </summary>
public void SessionStopped(string viewerId, int sessionId)
public void SessionStopped(string viewerId, SessionId sessionId, string errorMessage)
{
// notify the client that their session closed
this.ServiceHost.SendEvent(
@@ -386,7 +415,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
new ProfilerSessionStoppedParams()
{
OwnerUri = viewerId,
SessionId = sessionId
SessionId = sessionId.NumericId,
ErrorMessage = errorMessage
});
}

View File

@@ -7,7 +7,10 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Xml;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Profiler.Contracts;
@@ -16,29 +19,43 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
/// <summary>
/// Profiler session class
/// </summary>
public class ProfilerSession
public class ProfilerSession : IDisposable
{
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;
private ProfilerEvent lastSeenEvent = null;
private readonly SessionObserver sessionObserver;
private readonly IXEventSession xEventSession;
private readonly IDisposable observerDisposable;
private bool eventsLost = false;
int lastSeenId = -1;
public bool pollImmediatly = false;
public bool pollImmediately = false;
/// <summary>
/// Connection to use for the session
/// </summary>
public ConnectionInfo ConnectionInfo { get; set; }
/// <summary>
/// Constructs a new ProfilerSession to watch the given IXeventSession's incoming events
/// </summary>
/// <param name="xEventSession"></param>
public ProfilerSession(IXEventSession xEventSession)
{
this.xEventSession = xEventSession;
if (xEventSession is IObservableXEventSession observableSession)
{
observerDisposable = observableSession.ObservableSessionEvents?.Subscribe(sessionObserver = new SessionObserver());
}
}
/// <summary>
/// Underlying XEvent session wrapper
/// </summary>
public IXEventSession XEventSession { get; set; }
public IXEventSession XEventSession => xEventSession;
/// <summary>
/// Try to set the session into polling mode if criteria is meet
@@ -48,11 +65,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
{
lock (this.pollingLock)
{
if (pollImmediatly || (!this.isPolling && DateTime.Now.Subtract(this.lastPollTime) >= pollingDelay))
if (pollImmediately || (!this.isPolling && DateTime.Now.Subtract(this.lastPollTime) >= PollingDelay))
{
this.isPolling = true;
this.lastPollTime = DateTime.Now;
this.pollImmediatly = false;
this.pollImmediately = false;
return true;
}
else
@@ -83,13 +100,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
/// <summary>
/// The delay between session polls
/// </summary>
public TimeSpan PollingDelay
{
get
{
return pollingDelay;
}
}
public TimeSpan PollingDelay { get; } = DefaultPollingDelay;
/// <summary>
/// Could events have been lost in the last poll
@@ -197,5 +208,129 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
events.Clear();
}
}
/// <summary>
/// Indicates if the current session has completed processing and will provide no new events
/// </summary>
public bool Completed
{
get
{
return (sessionObserver != null) ? sessionObserver.Completed : error != null;
}
}
private Exception error;
/// <summary>
/// Provides any fatal error encountered when processing a session
/// </summary>
public Exception Error
{
get
{
return sessionObserver?.Error ?? error;
}
}
/// Returns the current set of events in the session buffer.
/// For RingBuffer sessions, returns the content of the session ring buffer by querying the server.
/// For LiveTarget and LocalFile sessions, returns the events buffered in memory since the last call to GetCurrentEvents.
/// </summary>
/// <returns></returns>
public IEnumerable<ProfilerEvent> GetCurrentEvents()
{
if (XEventSession == null && sessionObserver == null)
{
return Enumerable.Empty<ProfilerEvent>();
}
if (sessionObserver == null)
{
try
{
var targetXml = XEventSession.GetTargetXml();
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.LoadXml(targetXml);
var nodes = xmlDoc.DocumentElement.GetElementsByTagName("event");
var rawEvents = nodes.Cast<XmlNode>().Select(ParseProfilerEvent).ToList();
FilterOldEvents(rawEvents);
return rawEvents;
}
catch (Exception e)
{
error ??= e;
return Enumerable.Empty<ProfilerEvent>();
}
}
return sessionObserver.CurrentEvents;
}
/// <summary>
/// Parse a single event node from XEvent XML
/// </summary>
private static 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;
}
public void Dispose()
{
observerDisposable?.Dispose();
}
}
[DebuggerDisplay("SessionObserver. Current:{writeBuffer.Count} Total:{eventCount}")]
class SessionObserver : IObserver<ProfilerEvent>
{
private List<ProfilerEvent> writeBuffer = new List<ProfilerEvent>();
private Int64 eventCount = 0;
public void OnCompleted()
{
Completed = true;
}
public void OnError(Exception error)
{
Error = error;
}
public void OnNext(ProfilerEvent value)
{
writeBuffer.Add(value);
eventCount++;
}
public bool Completed { get; private set; }
public Exception Error { get; private set; }
public IEnumerable<ProfilerEvent> CurrentEvents
{
get
{
var newBuffer = new List<ProfilerEvent>();
var oldBuffer = Interlocked.Exchange(ref writeBuffer, newBuffer);
return oldBuffer;
}
}
}
}

View File

@@ -7,14 +7,10 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using Microsoft.SqlServer.Management.XEvent;
using Microsoft.SqlTools.ServiceLayer.Profiler.Contracts;
using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.ServiceLayer.Profiler
{
@@ -38,9 +34,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
public string Id { get; set; }
public bool active { get; set; }
public int xeSessionId { get; set; }
public SessionId xeSessionId { get; set; }
public Viewer(string Id, bool active, int xeId)
public Viewer(string Id, bool active, SessionId xeId)
{
this.Id = Id;
this.active = active;
@@ -49,15 +45,15 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
};
// XEvent Session Id's matched to the Profiler Id's watching them
private Dictionary<int, List<string>> sessionViewers = new Dictionary<int, List<string>>();
private readonly Dictionary<SessionId, List<string>> sessionViewers = new Dictionary<SessionId, List<string>>();
// XEvent Session Id's matched to their Profiler Sessions
private Dictionary<int, ProfilerSession> monitoredSessions = new Dictionary<int, ProfilerSession>();
private readonly Dictionary<SessionId, ProfilerSession> monitoredSessions = new Dictionary<SessionId, ProfilerSession>();
// ViewerId -> Viewer objects
private Dictionary<string, Viewer> allViewers = new Dictionary<string, Viewer>();
private readonly Dictionary<string, Viewer> allViewers = new Dictionary<string, Viewer>();
private List<IProfilerSessionListener> listeners = new List<IProfilerSessionListener>();
private readonly List<IProfilerSessionListener> listeners = new List<IProfilerSessionListener>();
/// <summary>
/// Registers a session event Listener to receive a callback when events arrive
@@ -83,8 +79,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
// create new profiling session if needed
if (!this.monitoredSessions.ContainsKey(session.Id))
{
var profilerSession = new ProfilerSession();
profilerSession.XEventSession = session;
var profilerSession = new ProfilerSession(session);
this.monitoredSessions.Add(session.Id, profilerSession);
}
@@ -151,7 +146,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
}
}
private bool RemoveSession(int sessionId, out ProfilerSession session)
private bool RemoveSession(SessionId sessionId, out ProfilerSession session)
{
lock (this.sessionsLock)
{
@@ -181,11 +176,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
}
}
public void PollSession(int sessionId)
public void PollSession(SessionId sessionId)
{
lock (this.sessionsLock)
{
this.monitoredSessions[sessionId].pollImmediatly = true;
this.monitoredSessions[sessionId].pollImmediately = true;
}
lock (this.pollingLock)
{
@@ -228,19 +223,32 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
{
Task.Factory.StartNew(() =>
{
var events = PollSession(session);
bool eventsLost = session.EventsLost;
if (events.Count > 0 || eventsLost)
try
{
// notify all viewers for the polled session
List<string> 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<string> viewerIds = this.sessionViewers[session.XEventSession.Id];
foreach (string viewerId in viewerIds)
{
SendEventsToListeners(viewerId, events, eventsLost);
if (allViewers[viewerId].active)
{
SendEventsToListeners(viewerId, events, eventsLost);
}
}
}
}
}
finally
{
session.IsPolling = false;
}
if (session.Completed)
{
SendStoppedSessionInfoToListeners(session.XEventSession.Id, session.Error?.Message);
RemoveSession(session.XEventSession.Id, out ProfilerSession tempSession);
tempSession.Dispose();
}
});
}
@@ -249,51 +257,20 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
private List<ProfilerEvent> PollSession(ProfilerSession session)
{
var events = new List<ProfilerEvent>();
try
if (session == null || session.XEventSession == null)
{
if (session == null || session.XEventSession == null)
{
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)
{
events.Add(profilerEvent);
}
}
}
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;
return events;
}
session.FilterOldEvents(events);
events.AddRange(session.GetCurrentEvents());
return session.FilterProfilerEvents(events);
}
/// <summary>
/// Notify listeners about closed sessions
/// </summary>
private void SendStoppedSessionInfoToListeners(int sessionId)
private void SendStoppedSessionInfoToListeners(SessionId sessionId, string errorMessage)
{
lock (listenersLock)
{
@@ -301,7 +278,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
{
foreach(string viewerId in sessionViewers[sessionId])
{
listener.SessionStopped(viewerId, sessionId);
listener.SessionStopped(viewerId, sessionId, errorMessage);
}
}
}
@@ -321,30 +298,5 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
}
}
/// <summary>
/// Parse a single event node from XEvent XML
/// </summary>
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;
}
}
}

View File

@@ -17,22 +17,27 @@ namespace Microsoft.SqlTools.ServiceLayer.Profiler
{
public Session Session { get; set; }
public int Id
private SessionId sessionId;
public SessionId Id
{
get { return Session.ID; }
get { return sessionId ??= GetSessionId(); }
}
public void Start()
protected virtual SessionId GetSessionId()
{
return new SessionId($"{Session.Parent.Name}_{Session.ID}", Session?.ID);
}
public virtual void Start()
{
this.Session.Start();
this.Session.Start();
}
public void Stop()
public virtual void Stop()
{
this.Session.Stop();
}
public string GetTargetXml()
public virtual string GetTargetXml()
{
if (this.Session == null)
{