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

@@ -48,6 +48,7 @@
<PackageReference Update="coverlet.collector" Version="3.1.2" />
<PackageReference Update="coverlet.msbuild" Version="3.1.2" />
<PackageReference Update="TextCopy" Version="6.2.1" />
<PackageReference Update="Microsoft.SqlServer.XEvent.XELite" Version="2023.1.30.3" />
</ItemGroup>
<!-- When updating version of Dependencies in the below section, please also update the version in the following files:

View File

@@ -2,15 +2,19 @@
[![Build Status](https://mssqltools.visualstudio.com/CrossPlatBuildScripts/_apis/build/status/Tools%20Service%20Integration%20tests?branchName=main&label=Integration%20Tests)](https://mssqltools.visualstudio.com/CrossPlatBuildScripts/_build/latest?definitionId=379&branchName=main)
# Microsoft SQL Tools Service
The SQL Tools Service is an application that provides core functionality for various SQL Server tools. These features include the following:
* Connection management
* Language Service support using VS Code protocol
* Query execution and resultset management
# SQL Tools Service API Documentation
Please see the SQL Tools Service API documentation at https://microsoft.github.io/sqltoolssdk/.
# Setup, Building and Testing the codebase
Please see the SQL Tools Service wiki documentation at https://github.com/Microsoft/sqltoolsservice/wiki
# Contribution Guidelines
@@ -104,13 +108,11 @@ so that your commits provide a good history of the changes you are making. To b
### Add Unit Tests for New Code
If you're adding a new feature to the project, please make sure to include adequate [xUnit](http://xunit.github.io/)
tests with your change. In this project, we have chosen write out unit tests in a way that uses the
actual PowerShell environment rather than extensive interface mocking. This allows us to be sure that
our features will work in practice.
If you're adding a new feature to the project, please make sure to include adequate [nUnit](http://nunit.org/)
tests with your change.
We do both component-level and scenario-level testing depending on what code is being tested. We don't
expect contributors to test every possible edge case. Testing mainline scenarios and the most common
We do both component-level and scenario-level testing depending on what code is being tested. We don't
expect contributors to test every possible edge case. Testing mainline scenarios and the most common
failure scenarios is often good enough.
We are very happy to accept unit test contributions for any feature areas that are more error-prone than

View File

@@ -1,5 +1,11 @@
# [Introduction](introduction.md)
# [SQL Tools JSON-RPC Protocol](jsonrpc_protocol.md)
# [Using the JSON-RPC API](using_the_jsonrpc_api.md)
# [Building the SQL Tools API](building_sqltoolsservice.md)
# [Using the .NET API](using_the_dotnet_api.md)
# Table of Contents
## [Introduction](introduction.md)
## [SQL Tools JSON-RPC Protocol](jsonrpc_protocol.md)
## [Using the JSON-RPC API](using_the_jsonrpc_api.md)
## [Building the SQL Tools API](building_sqltoolsservice.md)
## [Using the .NET API](using_the_dotnet_api.md)

View File

@@ -1,4 +1,5 @@
# Using the SQL Tools JSON-RPC API
The SQL Tools JSON-RPC API is the best way to consume the services
functionality in SQL tools. The JSON-RPC API available through stdio
of the SQL Tools Service process.
@@ -9,7 +10,7 @@ of the SQL Tools Service process.
## Download SQL Tools Service binaries
To get started using the SQL Tools Service you'll need to install the service binaries.
Download the SQL Tools Service binaries from the
Download the SQL Tools Service binaries from the
[sqltoolsservice release page](https://github.com/Microsoft/sqltoolsservice/releases).
Daily development builds will end with "-alpha". Release builds will end with " Release".
@@ -26,7 +27,7 @@ for this sample.
```typescript
internal static async Task ExecuteQuery(string query)
{
{
// create a temporary "workspace" file
using (SelfCleaningTempFile queryTempFile = new SelfCleaningTempFile())
// create the client helper which wraps the client driver objects
@@ -74,7 +75,7 @@ internal static async Task ExecuteQuery(string query)
}
Console.Write(Environment.NewLine);
}
}
}
}
// close database connection

View File

@@ -50,6 +50,7 @@
<PackageReference Include="Microsoft.SqlServer.Management.SqlParser" />
<PackageReference Include="System.Configuration.ConfigurationManager" />
<PackageReference Include="System.Text.Encoding.CodePages" />
<PackageReference Include="Microsoft.SqlServer.XEvent.XELite" />
<PackageReference Include="Microsoft.SqlServer.TransactSql.ScriptDom.NRT">
<Aliases>ASAScriptDom</Aliases>
</PackageReference>

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)
{

View File

@@ -53,7 +53,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Connection
query.Execute();
query.ExecutionTask.Wait();
// We should still have 2 DbConnections
// We should see 1 DbConnections
Assert.AreEqual(1, connectionInfo.CountConnections);
// If we disconnect, we should remain in a consistent state to do it over again

View File

@@ -15,71 +15,124 @@ using Microsoft.SqlTools.ServiceLayer.Profiler.Contracts;
using Microsoft.SqlTools.ServiceLayer.Test.Common;
using Moq;
using NUnit.Framework;
using Microsoft.SqlServer.Management.Sdk.Sfc;
using System.Linq;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlServer.Management.XEvent;
namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Profiler
{
public class ProfilerServiceTests
{
/// <summary>
/// Verify that a start profiling request starts a profiling session
/// </summary>
//[Test]
[Test]
public async Task TestHandleStartAndStopProfilingRequests()
{
using (SelfCleaningTempFile queryTempFile = new SelfCleaningTempFile())
{
var connectionResult = await LiveConnectionHelper.InitLiveConnectionInfoAsync("master", queryTempFile.FilePath);
var sqlConnection = ConnectionService.OpenSqlConnection(connectionResult.ConnectionInfo);
SqlStoreConnection connection = new SqlStoreConnection(sqlConnection);
var xeStore = new XEStore(connection);
ProfilerService profilerService = new ProfilerService();
// start a new session
var startParams = new StartProfilingParams();
startParams.OwnerUri = connectionResult.ConnectionInfo.OwnerUri;
startParams.SessionName = "Standard";
string sessionId = null;
var startContext = new Mock<RequestContext<StartProfilingResult>>();
startContext.Setup(rc => rc.SendResult(It.IsAny<StartProfilingResult>()))
.Returns<StartProfilingResult>((result) =>
{
// capture the session id for sending the stop message
return Task.FromResult(0);
});
await profilerService.HandleStartProfilingRequest(startParams, startContext.Object);
startContext.VerifyAll();
// wait a bit for the session monitoring to initialize
Thread.Sleep(TimeSpan.FromHours(1));
// stop the session
var stopParams = new StopProfilingParams()
var sessionName = await StartStandardSession(profilerService, connectionResult.ConnectionInfo.OwnerUri);
xeStore.Sessions.Refresh();
Assert.Multiple(() =>
{
OwnerUri = sessionId
};
Assert.That(xeStore.Sessions.Cast<Session>().Select(s => s.Name), Has.Member(sessionName), "ProfilerService should have created the session");
Assert.That(xeStore.Sessions[sessionName].IsRunning, Is.True, "Session should be running when created by ProfilerService");
});
var stopContext = new Mock<RequestContext<StopProfilingResult>>();
stopContext.Setup(rc => rc.SendResult(It.IsAny<StopProfilingResult>()))
.Returns(Task.FromResult(0));
await profilerService.HandleStopProfilingRequest(stopParams, stopContext.Object);
try
{
var xeSession = xeStore.Sessions[sessionName];
xeSession.Stop();
// start a new session
var startParams = new StartProfilingParams
{
OwnerUri = connectionResult.ConnectionInfo.OwnerUri,
SessionName = sessionName
};
stopContext.VerifyAll();
string sessionId = null;
var startContext = new Mock<RequestContext<StartProfilingResult>>();
startContext.Setup(rc => rc.SendResult(It.IsAny<StartProfilingResult>()))
.Returns<StartProfilingResult>((result) =>
{
// capture the session id for sending the stop message
sessionId = result.UniqueSessionId;
return Task.FromResult(0);
});
await profilerService.HandleStartProfilingRequest(startParams, startContext.Object);
Assert.That(sessionId, Does.Contain(connectionResult.ConnectionInfo.ConnectionDetails.ServerName), "UniqueSessionId");
startContext.VerifyAll();
// wait a bit for the session monitoring to initialize
Thread.Sleep(TimeSpan.FromSeconds(30));
xeSession.Refresh();
Assert.That(xeSession.IsRunning, Is.True, "Session should be running due to HandleStartProfilingRequest");
// stop the session
var stopParams = new StopProfilingParams()
{
OwnerUri = connectionResult.ConnectionInfo.OwnerUri
};
var stopContext = new Mock<RequestContext<StopProfilingResult>>();
stopContext.Setup(rc => rc.SendResult(It.IsAny<StopProfilingResult>()))
.Returns(Task.FromResult(0));
await profilerService.HandleStopProfilingRequest(stopParams, stopContext.Object);
xeSession.Refresh();
Assert.That(xeSession.IsRunning, Is.False, "Session should be stopped due to HandleStopProfilingRequest");
stopContext.VerifyAll();
}
finally
{
try
{
xeStore.Sessions.Refresh();
if (xeStore.Sessions.Contains(sessionName))
{
try
{
xeStore.Sessions[sessionName].Stop();
}
catch
{ }
xeStore.Sessions[sessionName].Drop();
}
}
catch
{ }
}
}
}
/// <summary>
/// Verify the profiler service XEvent session factory
/// </summary>
//[Test]
public void TestCreateXEventSession()
private async Task<string> StartStandardSession(ProfilerService profilerService, string ownerUri)
{
var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo("master");
ProfilerService profilerService = new ProfilerService();
IXEventSession xeSession = profilerService.GetXEventSession("Profiler", liveConnection.ConnectionInfo);
Assert.NotNull(xeSession);
Assert.NotNull(xeSession.GetTargetXml());
const string sessionName = "ADS_Standard_Test";
var template = Newtonsoft.Json.JsonConvert.DeserializeObject<ProfilerSessionTemplate>(standardSessionJson.Substring(1, standardSessionJson.Length - 2));
var createParams = new CreateXEventSessionParams() { OwnerUri = ownerUri, SessionName = sessionName, Template = template };
var requestContext = new Mock<RequestContext<CreateXEventSessionResult>>();
requestContext.Setup(c => c.SendResult(It.IsAny<CreateXEventSessionResult>()))
.Returns<CreateXEventSessionResult>((result) => { return Task.FromResult(0); });
var serviceHostMock = new Mock<IProtocolEndpoint>();
profilerService.ServiceHost = serviceHostMock.Object;
await profilerService.HandleCreateXEventSessionRequest(createParams, requestContext.Object);
return sessionName;
}
const string standardSessionJson = /*lang=json,strict*/ "[{\"name\": \"Standard_OnPrem\", \"defaultView\": \"Standard View\", \"engineTypes\": [\"Standalone\"], \"createStatement\": \"CREATE EVENT SESSION [{sessionName}] ON SERVER ADD EVENT sqlserver.attention(ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.database_id,sqlserver.nt_username,sqlserver.query_hash,sqlserver.server_principal_name,sqlserver.session_id) WHERE ([package0].[equal_boolean]([sqlserver].[is_system],(0)))), ADD EVENT sqlserver.existing_connection(SET collect_options_text=(1) ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.nt_username,sqlserver.server_principal_name,sqlserver.session_id)), ADD EVENT sqlserver.login(SET collect_options_text=(1) ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.nt_username,sqlserver.server_principal_name,sqlserver.session_id)), ADD EVENT sqlserver.logout( ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.nt_username,sqlserver.server_principal_name,sqlserver.session_id)), ADD EVENT sqlserver.rpc_completed( ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.database_id,sqlserver.database_name,sqlserver.nt_username,sqlserver.query_hash,sqlserver.server_principal_name,sqlserver.session_id) WHERE ([package0].[equal_boolean]([sqlserver].[is_system],(0)))), ADD EVENT sqlserver.sql_batch_completed( ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.database_id,sqlserver.database_name,sqlserver.nt_username,sqlserver.query_hash,sqlserver.server_principal_name,sqlserver.session_id) WHERE ([package0].[equal_boolean]([sqlserver].[is_system],(0)))), ADD EVENT sqlserver.sql_batch_starting( ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.database_id,sqlserver.database_name,sqlserver.nt_username,sqlserver.query_hash,sqlserver.server_principal_name,sqlserver.session_id) WHERE ([package0].[equal_boolean]([sqlserver].[is_system],(0)))) ADD TARGET package0.ring_buffer(SET max_events_limit=(1000),max_memory=(51200)) WITH (MAX_MEMORY=8192 KB,EVENT_RETENTION_MODE=ALLOW_SINGLE_EVENT_LOSS,MAX_DISPATCH_LATENCY=5 SECONDS,MAX_EVENT_SIZE=0 KB,MEMORY_PARTITION_MODE=PER_CPU,TRACK_CAUSALITY=ON,STARTUP_STATE=OFF)\"}]";
}
}

View File

@@ -45,4 +45,9 @@
<ItemGroup>
<EmbeddedResource Include="ShowPlan\TestShowPlanRecommendations.xml" />
</ItemGroup>
<ItemGroup>
<None Update="Profiler\TestXel_0.xel">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -16,10 +16,12 @@ using Microsoft.SqlTools.ServiceLayer.Profiler.Contracts;
using Microsoft.SqlTools.ServiceLayer.UnitTests.Utility;
using Moq;
using NUnit.Framework;
using System.IO;
using System.Reflection;
using System.Linq;
namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler
{
[TestFixture]
/// <summary>
/// Unit tests for ProfilerService
/// </summary>
@@ -29,66 +31,39 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler
/// Test starting a profiling session and receiving event callback
/// </summary>
/// <returns></returns>
// TODO: Fix flaky test. See https://github.com/Microsoft/sqltoolsservice/issues/459
//[Test]
public async Task TestStartProfilingRequest()
[Test]
public async Task StartProfilingRequest_creates_pausable_remote_session()
{
string sessionId = null;
bool recievedEvents = false;
var sessionId = new SessionId("testsession_1", 1);
string testUri = "profiler_uri";
var requestContext = new Mock<RequestContext<StartProfilingResult>>();
requestContext.Setup(rc => rc.SendResult(It.IsAny<StartProfilingResult>()))
.Returns<StartProfilingResult>((result) =>
{
// capture the session id for sending the stop message
Assert.Multiple(() =>
{
Assert.That(result.CanPause, Is.True, "Result.CanPause for RingBuffer sessions");
Assert.That(result.UniqueSessionId, Is.EqualTo(sessionId.ToString()), "Result.UniqueSessionId");
});
return Task.FromResult(0);
});
// capture Listener event notifications
var mockListener = new Mock<IProfilerSessionListener>();
mockListener.Setup(p => p.EventsAvailable(It.IsAny<string>(), It.IsAny<List<ProfilerEvent>>(), It.IsAny<bool>())).Callback(() =>
{
recievedEvents = true;
});
var profilerService = new ProfilerService();
profilerService.SessionMonitor.AddSessionListener(mockListener.Object);
profilerService.ConnectionServiceInstance = TestObjects.GetTestConnectionService();
ConnectionInfo connectionInfo = TestObjects.GetTestConnectionInfo();
profilerService.ConnectionServiceInstance.OwnerToConnectionMap.Add(testUri, connectionInfo);
profilerService.XEventSessionFactory = new TestXEventSessionFactory();
var requestParams = new StartProfilingParams();
requestParams.OwnerUri = testUri;
requestParams.SessionName = "Standard";
var requestParams = new StartProfilingParams
{
OwnerUri = testUri,
SessionName = "Standard"
};
// 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;});
while (sessionId == null && !timeout)
{
Thread.Sleep(250);
}
pollingTimer.Stop();
requestContext.VerifyAll();
// Check that the correct XEvent session was started
Assert.AreEqual("1", sessionId);
// check that the proper owner Uri was used
Assert.True(recievedEvents);
await profilerService.HandleStartProfilingRequest(requestParams, requestContext.Object);
requestContext.VerifyAll();
}
/// <summary>
@@ -118,6 +93,9 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler
stopped = true;
});
mockSession.Setup(p => p.GetTargetXml()).Returns("<RingBufferTarget/>");
mockSession.Setup(p => p.Id).Returns(new SessionId("test_1", 1));
var sessionListener = new TestSessionListener();
var profilerService = new ProfilerService();
profilerService.SessionMonitor.AddSessionListener(sessionListener);
@@ -136,8 +114,8 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler
requestContext.VerifyAll();
// check that session was succesfully stopped and stop was called
Assert.True(success);
Assert.True(stopped);
Assert.True(success, nameof(success));
Assert.True(stopped, nameof(stopped));
// should not be able to remove the session, it should already be gone
ProfilerSession ps;
@@ -185,9 +163,9 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler
profilerService.SessionMonitor.StartMonitoringSession(testUri, new TestXEventSession1());
// poll the session
profilerService.SessionMonitor.PollSession(1);
profilerService.SessionMonitor.PollSession(new SessionId("testsession_1", 1));
Thread.Sleep(500);
profilerService.SessionMonitor.PollSession(1);
profilerService.SessionMonitor.PollSession(new SessionId("testsession_1", 1));
// wait for polling to finish, or for timeout
System.Timers.Timer pollingTimer = new System.Timers.Timer();
@@ -211,7 +189,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler
recievedEvents = false;
success = false;
profilerService.SessionMonitor.PollSession(1);
profilerService.SessionMonitor.PollSession(new SessionId("testsession_1", 1));
// confirm that no events were sent to paused Listener
Assert.False(recievedEvents);
@@ -220,7 +198,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler
await profilerService.HandlePauseProfilingRequest(requestParams, requestContext.Object);
Assert.True(success);
profilerService.SessionMonitor.PollSession(1);
profilerService.SessionMonitor.PollSession(new SessionId("testsession_1", 1));
// wait for polling to finish, or for timeout
timeout = false;
@@ -240,7 +218,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler
/// Test notifications for stopped sessions
/// </summary>
[Test]
public async Task TestStoppedSessionNotification()
public void TestStoppedSessionNotification()
{
bool sessionStopped = false;
string testUri = "profiler_uri";
@@ -251,9 +229,9 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler
{
throw new XEventException();
});
mockSession.Setup(p => p.Id).Returns(new SessionId("test_1", 1));
var mockListener = new Mock<IProfilerSessionListener>();
mockListener.Setup(p => p.SessionStopped(It.IsAny<string>(), It.IsAny<int>())).Callback(() =>
mockListener.Setup(p => p.SessionStopped(It.IsAny<string>(), It.IsAny<SessionId>(), It.IsAny<string>())).Callback(() =>
{
sessionStopped = true;
});
@@ -282,5 +260,114 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler
// check that a stopped session notification was sent
Assert.True(sessionStopped);
}
[Test]
public void StartProfilingRequest_defaults_to_remote()
{
var param = new StartProfilingParams();
Assert.That(param.SessionType, Is.EqualTo(ProfilingSessionType.RemoteSession), nameof(param.SessionType));
}
[Test]
public async Task StartProfilingRequest_creates_a_LocalFile_session_on_request()
{
var filePath = @"c:\folder\file.xel";
var param = new StartProfilingParams() { OwnerUri = "someUri", SessionType = ProfilingSessionType.LocalFile, SessionName = filePath};
var mockSession = new Mock<IObservableXEventSession>();
mockSession.Setup(p => p.GetTargetXml()).Callback(() =>
{
throw new XEventException();
});
mockSession.Setup(p => p.Id).Returns(new SessionId("test_1", 1));
var requestContext = new Mock<RequestContext<StartProfilingResult>>();
requestContext.Setup(rc => rc.SendResult(It.IsAny<StartProfilingResult>()))
.Returns<StartProfilingResult>((result) =>
{
return Task.FromResult(0);
});
var sessionFactory = new Mock<IXEventSessionFactory>();
sessionFactory.Setup(s => s.OpenLocalFileSession(filePath))
.Returns (mockSession.Object)
.Verifiable();
var profilerService = new ProfilerService() { XEventSessionFactory = sessionFactory.Object };
await profilerService.HandleStartProfilingRequest(param, requestContext.Object);
sessionFactory.Verify();
requestContext.VerifyAll();
}
[Test]
public async Task ProfilerService_processes_localfile_session()
{
var viewerId = "someUri";
var filePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Profiler", "TestXel_0.xel");
var param = new StartProfilingParams() { OwnerUri = viewerId, SessionType = ProfilingSessionType.LocalFile, SessionName = filePath };
var requestContext = new Mock<RequestContext<StartProfilingResult>>();
requestContext.Setup(rc => rc.SendResult(It.IsAny<StartProfilingResult>()))
.Returns<StartProfilingResult>((result) =>
{
Assert.Multiple(() =>
{
Assert.That(result.CanPause, Is.False, "local file session cannot be paused");
Assert.That(result.UniqueSessionId, Is.EqualTo(filePath), "UniqueSessionId should match file path");
});
return Task.FromResult(0);
});
var profilerService = new ProfilerService();
var listener = new TestSessionListener();
profilerService.SessionMonitor.AddSessionListener(listener);
await profilerService.HandleStartProfilingRequest(param, requestContext.Object);
var retries = 100;
while (retries-- > 0 && !listener.StoppedSessions.Contains(viewerId))
{
Thread.Sleep(100);
}
Assert.Multiple(() =>
{
Assert.That(listener.StoppedSessions, Has.Member(viewerId), "session should have been stopped after reading the file");
Assert.That(listener.AllEvents.Keys, Has.Member(viewerId), "session should have events logged for it");
Assert.That(listener.AllEvents[viewerId]?.Count, Is.EqualTo(149), "all events from the xel should be in the buffer");
});
}
[Test]
public async Task ProfilerService_includes_ErrorMessage_in_session_stop_notification()
{
var param = new StartProfilingParams() { OwnerUri = "someUri", SessionName = "someSession" };
var mockSession = new Mock<IXEventSession>();
mockSession.Setup(p => p.GetTargetXml()).Callback(() =>
{
throw new XEventException("test!");
});
mockSession.Setup(p => p.Id).Returns(new SessionId("test_1", 1));
var requestContext = new Mock<RequestContext<StartProfilingResult>>();
requestContext.Setup(rc => rc.SendResult(It.IsAny<StartProfilingResult>()))
.Returns<StartProfilingResult>((result) =>
{
return Task.FromResult(0);
});
var sessionFactory = new Mock<IXEventSessionFactory>();
sessionFactory.Setup(s => s.GetXEventSession(It.IsAny<string>(), It.IsAny<ConnectionInfo>()))
.Returns(mockSession.Object)
.Verifiable();
var profilerService = new ProfilerService() { XEventSessionFactory = sessionFactory.Object };
profilerService.ConnectionServiceInstance = TestObjects.GetTestConnectionService();
var connectionInfo = TestObjects.GetTestConnectionInfo();
profilerService.ConnectionServiceInstance.OwnerToConnectionMap.Add("someUri", connectionInfo);
var listener = new TestSessionListener();
profilerService.SessionMonitor.AddSessionListener(listener);
await profilerService.HandleStartProfilingRequest(param, requestContext.Object);
var retries = 10;
while (retries-- > 0 && !listener.StoppedSessions.Any())
{
Thread.Sleep(100);
}
Assert.Multiple(() =>
{
Assert.That(listener.ErrorMessages, Is.EqualTo(new[] { "test!" }), "listener.ErrorMessages");
Assert.That(listener.StoppedSessions, Has.Member("someUri"), "listener.StoppedSessions");
});
sessionFactory.Verify();
}
}
}

View File

@@ -25,7 +25,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler
public void TestFilterOldEvents()
{
// create a profiler session and get some test events
var profilerSession = new ProfilerSession();
var profilerSession = new ProfilerSession(new XEventSession());
var allEvents = ProfilerTestObjects.TestProfilerEvents;
var profilerEvents = ProfilerTestObjects.TestProfilerEvents;
@@ -62,7 +62,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler
public void TestFilterProfilerEvents()
{
// create a profiler session and get some test events
var profilerSession = new ProfilerSession();
var profilerSession = new ProfilerSession(new XEventSession());
var profilerEvents = ProfilerTestObjects.TestProfilerEvents;
int expectedEventCount = profilerEvents.Count;
@@ -86,7 +86,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler
public void TestEventsLost()
{
// create a profiler session and get some test events
var profilerSession = new ProfilerSession();
var profilerSession = new ProfilerSession(new XEventSession());
var profilerEvents = ProfilerTestObjects.TestProfilerEvents;
// filter all the results from the first poll
@@ -135,7 +135,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler
DateTime startTime = DateTime.Now;
// create new profiler session
var profilerSession = new ProfilerSession();
var profilerSession = new ProfilerSession(new XEventSession());
// enter the polling block
Assert.True(profilerSession.TryEnterPolling());

View File

@@ -5,6 +5,7 @@
#nullable disable
using System;
using System.Collections.Generic;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Profiler;
@@ -37,21 +38,24 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler
public class TestSessionListener : IProfilerSessionListener
{
public string PreviousSessionId { get; set; }
public readonly Dictionary<string, List<ProfilerEvent>> AllEvents = new Dictionary<string, List<ProfilerEvent>>();
public List<ProfilerEvent> PreviousEvents { get; set; }
public bool Stopped { get; set; }
public readonly List<string> StoppedSessions = new List<string>();
public readonly List<string> ErrorMessages = new List<string>();
public void EventsAvailable(string sessionId, List<ProfilerEvent> events, bool eventsLost)
{
this.PreviousSessionId = sessionId;
this.PreviousEvents = events;
if (!AllEvents.ContainsKey(sessionId))
{
AllEvents[sessionId] = new List<ProfilerEvent>();
}
AllEvents[sessionId].AddRange(events);
}
public void SessionStopped(string viewerId, int sessionId)
public void SessionStopped(string viewerId, SessionId sessionId, string errorMessage)
{
Stopped = true;
StoppedSessions.Add(viewerId);
ErrorMessages.Add(errorMessage);
}
}
@@ -197,7 +201,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler
public int Id { get { return 51; } }
public SessionId Id { get { return new SessionId("testsession_51"); } }
public void Start(){}
@@ -282,7 +286,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler
" </event>" +
"</RingBufferTarget>";
public int Id { get { return 1; } }
public SessionId Id { get { return new SessionId("testsession_1"); } }
public void Start(){}
@@ -373,7 +377,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler
" </event>" +
"</RingBufferTarget>";
public int Id { get { return 2; } }
public SessionId Id { get { return new SessionId("testsession_2"); } }
public void Start(){}
@@ -420,5 +424,10 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler
return new TestXEventSession2();
}
}
public IXEventSession OpenLocalFileSession(string filePath)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,113 @@
//
// 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 System;
using System.Collections.Generic;
using System.IO;
using NUnit.Framework;
using System.Reflection;
using Microsoft.SqlTools.ServiceLayer.Profiler.Contracts;
using Microsoft.SqlTools.ServiceLayer.Profiler;
using System.Threading;
using System.Linq;
using Moq;
namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler
{
public class XeStreamObservableTests
{
/// <summary>
/// this might technically be an integration test but putting it here because it doesn't require any connectivity.
/// </summary>
[Test]
public void XeStreamObservable_reads_entire_xel_file()
{
var observer = InitializeFileObserver();
observer.Observable.Start();
var retries = 100;
while (!observer.Completed && retries-- > 0)
{
Thread.Sleep(100);
}
Assert.That(observer.Completed, Is.True, $"Reading the file didn't complete in 10 seconds. Events read: {observer.ProfilerEvents.Count}");
Assert.Multiple(() =>
{
Assert.That(observer.ProfilerEvents.Count, Is.EqualTo(149), "Number of events read");
Assert.That(observer.ProfilerEvents[0].Name, Is.EqualTo("rpc_completed"), "First event in the file");
Assert.That(observer.ProfilerEvents.Last().Name, Is.EqualTo("sql_batch_completed"), "Last event in the file");
});
}
[Test]
public void XeStreamObservable_calls_OnError_when_the_fetcher_fails()
{
var observer = InitializeFileObserver("thispathdoesnotexist.xel");
observer.Observable.Start();
var retries = 10;
while (!observer.Completed && retries-- > 0)
{
Thread.Sleep(100);
}
Assert.Multiple(() =>
{
Assert.That(observer.Completed, Is.True, $"Reading the missing file didn't complete in 1 second.");
Assert.That(observer.Error?.GetBaseException(), Is.InstanceOf<FileNotFoundException>(), $"Expected Error from missing file. Error:{observer.Error}");
});
}
private XeStreamObserver InitializeFileObserver(string filePath = null)
{
filePath ??= Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Profiler", "TestXel_0.xel");
var profilerService = SetupProfilerService(filePath);
var xeStreamObservable = new XeStreamObservable(() =>
{
return profilerService.initIXEventFetcher(filePath);
});
var observer = new XeStreamObserver() { Observable = xeStreamObservable };
xeStreamObservable.Subscribe(observer);
return observer;
}
private ProfilerService SetupProfilerService (string filePath = null)
{
filePath ??= Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Profiler", "TestXel_0.xel");
var sessionFactory = new Mock<IXEventSessionFactory>();
var profilerService = new ProfilerService() { XEventSessionFactory = sessionFactory.Object };
return profilerService;
}
}
sealed class XeStreamObserver : IObserver<ProfilerEvent>
{
public XeStreamObservable Observable { get; set; }
public readonly List<ProfilerEvent> ProfilerEvents = new List<ProfilerEvent>();
public bool Completed { get; private set; }
public Exception Error { get; private set; }
public void OnCompleted()
{
Completed = true;
}
public void OnError(Exception error)
{
Error = error;
}
public void OnNext(ProfilerEvent value)
{
ProfilerEvents.Add(value);
OnEventAdded?.Invoke(this, EventArgs.Empty);
}
public event EventHandler OnEventAdded;
}
}