// // 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.Diagnostics; using System.Linq; using System.Threading; using System.Xml; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Profiler.Contracts; namespace Microsoft.SqlTools.ServiceLayer.Profiler { /// /// Profiler session class /// 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 ProfilerEvent lastSeenEvent = null; private readonly SessionObserver sessionObserver; private readonly IXEventSession xEventSession; private readonly IDisposable observerDisposable; private bool eventsLost = false; int lastSeenId = -1; public bool pollImmediately = false; /// /// Connection to use for the session /// public ConnectionInfo ConnectionInfo { get; set; } /// /// Constructs a new ProfilerSession to watch the given IXeventSession's incoming events /// /// public ProfilerSession(IXEventSession xEventSession) { this.xEventSession = xEventSession; if (xEventSession is IObservableXEventSession observableSession) { observerDisposable = observableSession.ObservableSessionEvents?.Subscribe(sessionObserver = new SessionObserver()); } } /// /// Underlying XEvent session wrapper /// public IXEventSession XEventSession => xEventSession; /// /// Try to set the session into polling mode if criteria is meet /// /// True if session set to polling mode, False otherwise public bool TryEnterPolling() { lock (this.pollingLock) { if (pollImmediately || (!this.isPolling && DateTime.Now.Subtract(this.lastPollTime) >= PollingDelay)) { this.isPolling = true; this.lastPollTime = DateTime.Now; this.pollImmediately = false; return true; } else { return false; } } } /// /// Is the session currently being polled /// public bool IsPolling { get { return this.isPolling; } set { lock (this.pollingLock) { this.isPolling = value; } } } /// /// The delay between session polls /// public TimeSpan PollingDelay { get; } = DefaultPollingDelay; /// /// Could events have been lost in the last poll /// public bool EventsLost { get { return this.eventsLost; } } /// /// Determine if an event was caused by the XEvent polling queries /// private bool IsProfilerEvent(ProfilerEvent currentEvent) { if (string.IsNullOrWhiteSpace(currentEvent.Name) || currentEvent.Values == null) { return false; } if ((currentEvent.Name.Equals("sql_batch_completed") || currentEvent.Name.Equals("sql_batch_starting")) && currentEvent.Values.TryGetValue("batch_text", out string value)) { return value.Contains("SELECT target_data FROM sys.dm_xe_session_targets") || currentEvent.Values["batch_text"].Contains("SELECT target_data FROM sys.dm_xe_database_session_targets"); } return false; } /// /// Removed profiler polling events from event list /// public List FilterProfilerEvents(List events) { int idx = events.Count; while (--idx >= 0) { if (IsProfilerEvent(events[idx])) { events.RemoveAt(idx); } } return events; } /// /// Filter the event list to not include previously seen events, /// and to exclude events that happened before the profiling session began. /// public void FilterOldEvents(List events) { this.eventsLost = false; if (lastSeenId != -1) { // find the last event we've previously seen bool foundLastEvent = false; int idx = events.Count; int earliestSeenEventId = int.Parse(events.LastOrDefault().Values["event_sequence"]); while (--idx >= 0) { // update the furthest back event we've found so far earliestSeenEventId = Math.Min(earliestSeenEventId, int.Parse(events[idx].Values["event_sequence"])); if (events[idx].Equals(lastSeenEvent)) { foundLastEvent = true; break; } } // remove all the events we've seen before if (foundLastEvent) { events.RemoveRange(0, idx + 1); } else if(earliestSeenEventId > (lastSeenId + 1)) { // if there's a gap between the expected next event sequence // and the furthest back event seen, we know we've lost events this.eventsLost = true; } // save the last event so we know where to clean-up the list from next time if (events.Count > 0) { lastSeenEvent = events.LastOrDefault(); lastSeenId = int.Parse(lastSeenEvent.Values["event_sequence"]); } } else // first poll at start of session, all data is old { // save the last event as the beginning of the profiling session if (events.Count > 0) { lastSeenEvent = events.LastOrDefault(); lastSeenId = int.Parse(lastSeenEvent.Values["event_sequence"]); } // ignore all events before the session began events.Clear(); } } /// /// Indicates if the current session has completed processing and will provide no new events /// public bool Completed { get { return (sessionObserver != null) ? sessionObserver.Completed : error != null; } } private Exception error; /// /// Provides any fatal error encountered when processing a session /// 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. /// /// public IEnumerable GetCurrentEvents() { if (XEventSession == null && sessionObserver == null) { return Enumerable.Empty(); } 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().Select(ParseProfilerEvent).ToList(); FilterOldEvents(rawEvents); return rawEvents; } catch (Exception e) { error ??= e; return Enumerable.Empty(); } } return sessionObserver.CurrentEvents; } /// /// Parse a single event node from XEvent XML /// 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 { private List writeBuffer = new List(); 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 CurrentEvents { get { var newBuffer = new List(); var oldBuffer = Interlocked.Exchange(ref writeBuffer, newBuffer); return oldBuffer; } } } }