mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-02-05 17:24:59 -05:00
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:
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user