Skip to content

Commit

Permalink
Added a dispatch queue to address an issue where batching wasn't as e…
Browse files Browse the repository at this point in the history
…ffective as I'd like.
  • Loading branch information
nnaaa-vr committed May 3, 2021
1 parent a5d4d79 commit 6fb875a
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 67 deletions.
25 changes: 25 additions & 0 deletions XSOverlay VRChat Parser/Models/NotificationDispatchModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using XSNotifications;
using XSOverlay_VRChat_Parser.Helpers;

namespace XSOverlay_VRChat_Parser.Models
{
class NotificationDispatchModel
{
public int DurationMilliseconds { get; set; }
public EventType Type { get; set; }
public XSNotification Message { get; set; }
public bool WasGrouped { get; set; }
public List<NotificationDispatchModel> GroupedNotifications { get; set; }

public NotificationDispatchModel()
{
WasGrouped = false;
GroupedNotifications = new List<NotificationDispatchModel>();
}
}
}
159 changes: 92 additions & 67 deletions XSOverlay VRChat Parser/Program.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using Avalonia;
using Avalonia.ReactiveUI;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using XSNotifications;
using XSNotifications.Enum;
using XSNotifications.Helpers;
Expand Down Expand Up @@ -40,6 +42,12 @@ class Program

static XSNotifier Notifier { get; set; }

static ConcurrentQueue<NotificationDispatchModel> DispatchQueue = new ConcurrentQueue<NotificationDispatchModel>();
static Task DispatchTask;
static volatile bool IsExiting = false;
static int DispatchResolutionMilliseconds = 50;
static volatile int DispatchRemainingDelay = 0;

static void Main(string[] args)
{
DateTime now = DateTime.Now;
Expand Down Expand Up @@ -118,6 +126,8 @@ static void Main(string[] args)

Log(LogEventType.Info, $"XSNotifier initialized.");

DispatchTask = Task.Run(() => { SendNotificationTask(); return Task.CompletedTask; });

try
{
Notifier.SendNotification(new XSNotification()
Expand Down Expand Up @@ -146,6 +156,11 @@ static void Exit()
{
Log(LogEventType.Info, "Cleaning up before termination.");

IsExiting = true;

if(DispatchTask != null)
DispatchTask.Wait(DispatchResolutionMilliseconds * 2);

if (Notifier != null)
{
Notifier.Dispose();
Expand Down Expand Up @@ -184,7 +199,7 @@ static void Log(LogEventType type, string message, bool uiLogOnly = false)
{
MainWindow.EventLogAppend($"[{timeStamp}] <{typeStamp}> {message}\r\n");

if(!uiLogOnly)
if (!uiLogOnly)
File.AppendAllText($@"{ConfigurationModel.ExpandedUserFolderPath}\Logs\{LogFileName}", $"[{dateStamp} {timeStamp}] [{typeStamp}] {message}\r\n");
}
}
Expand All @@ -194,17 +209,82 @@ static void Log(Exception ex)
Log(LogEventType.Error, $"{ex.Message}\r\n{ex.InnerException}\r\n{ex.StackTrace}");
}

static void SendNotification(XSNotification notification)
static void SendNotificationTask()
{
try
{
Notifier.SendNotification(notification);
}
catch (Exception ex)
while (!IsExiting)
{
Log(LogEventType.Error, "An exception occurred while sending a routine event notification.");
Log(ex);
Exit();
if (DispatchRemainingDelay > 0 && DispatchRemainingDelay > DispatchResolutionMilliseconds)
DispatchRemainingDelay -= DispatchResolutionMilliseconds;
else if (DispatchRemainingDelay > 0)
DispatchRemainingDelay = 0;

if (DispatchRemainingDelay == 0 && DispatchQueue.Any())
{
NotificationDispatchModel nextNotification;
bool success = false;

while (true)
{
success = DispatchQueue.TryDequeue(out nextNotification);

if (success && nextNotification.WasGrouped)
continue;
break;
}

if (success)
{
DispatchRemainingDelay += nextNotification.DurationMilliseconds;

// Combine joins and leaves, marked as grouped
if ((nextNotification.Type == EventType.PlayerJoin ||
nextNotification.Type == EventType.PlayerLeft) &&
DispatchQueue.Count > 0)
{
// This is only thread-safe because this task is the only place where dequeues can happen.
for (int i = 0; i < DispatchQueue.Count; i++)
{
NotificationDispatchModel thisModel = DispatchQueue.ElementAt(i);

if (!thisModel.WasGrouped
&& (nextNotification.Type == EventType.PlayerJoin
&& thisModel.Type == EventType.PlayerJoin)
|| (nextNotification.Type == EventType.PlayerLeft
&& thisModel.Type == EventType.PlayerLeft))
{
nextNotification.GroupedNotifications.Add(thisModel);
thisModel.WasGrouped = true;
}
}
}

if (nextNotification.Type == EventType.PlayerJoin && nextNotification.GroupedNotifications.Count > 0)
{
nextNotification.Message.Content = $"{nextNotification.Message.Title}, {string.Join(", ", nextNotification.GroupedNotifications.Select(x => x.Message.Title))}";
nextNotification.Message.Title = $"Group Join: {nextNotification.GroupedNotifications.Count + 1} users.";
}

if (nextNotification.Type == EventType.PlayerLeft && nextNotification.GroupedNotifications.Count > 0)
{
nextNotification.Message.Content = $"{nextNotification.Message.Title}, {string.Join(", ", nextNotification.GroupedNotifications.Select(x => x.Message.Title))}";
nextNotification.Message.Title = $"Group Leave: {nextNotification.GroupedNotifications.Count + 1} users.";
}
try
{
Notifier.SendNotification(nextNotification.Message);
}
catch (Exception ex)
{
Log(LogEventType.Error, "An exception occurred while sending a routine event notification.");
Log(ex);
Exit();
}
}
else
Task.Delay(DispatchResolutionMilliseconds).GetAwaiter().GetResult();
}
else
Task.Delay(DispatchResolutionMilliseconds).GetAwaiter().GetResult();
}
}

Expand Down Expand Up @@ -422,61 +502,6 @@ static void ParseTick(string content)

if (ToSend.Count > 0)
{
// Batch joins/leaves to avoid message spam during simultaneous joins/leaves that fall outside of the world change case

List<Tuple<EventType, XSNotification>> BatchJoins = new List<Tuple<EventType, XSNotification>>();
List<Tuple<EventType, XSNotification>> BatchLeaves = new List<Tuple<EventType, XSNotification>>();

int numJoins = 0, numLeaves = 0, batchJoinIndex = 0, batchLeaveIndex = 0;
List<int> IndicesToRemove = new List<int>();
for (int i = 0; i < ToSend.Count; i++)
{
if (i > 0 && numJoins == 0 && batchJoinIndex == 0 && ToSend[i].Item1 == EventType.PlayerJoin)
{
++numJoins;
batchJoinIndex = i;
}
else if (i > 0 && numLeaves == 0 && batchLeaveIndex == 0 && ToSend[i].Item1 == EventType.PlayerLeft)
{
++numLeaves;
batchLeaveIndex = i;
}
else if (numJoins > 0 && ToSend[i].Item1 == EventType.PlayerJoin)
{
++numJoins;
BatchJoins.Add(ToSend[i]);
IndicesToRemove.Add(i);
}
else if (numLeaves > 0 && ToSend[i].Item1 == EventType.PlayerLeft)
{
++numLeaves;
BatchLeaves.Add(ToSend[i]);
IndicesToRemove.Add(i);
}
}

if (numJoins > 1)
{
XSNotification thisNotification = ToSend[batchJoinIndex].Item2;
thisNotification.Content = $"{thisNotification.Title}, {string.Join(", ", BatchJoins.Select(x => x.Item2.Title))}";
thisNotification.Title = $"Group Join: {BatchJoins.Count + 1} users.";
}

if (numLeaves > 1)
{
XSNotification thisNotification = ToSend[batchLeaveIndex].Item2;
thisNotification.Content = $"{thisNotification.Title}, {string.Join(", ", BatchLeaves.Select(x => x.Item2.Title))}";
thisNotification.Title = $"Group Leave: {BatchLeaves.Count + 1} users.";
}

if (IndicesToRemove.Any())
{
int[] orderedRemovals = IndicesToRemove.OrderByDescending(x => x).ToArray();

for (int i = 0; i < orderedRemovals.Length; i++)
ToSend.RemoveAt(orderedRemovals[i]);
}

foreach (Tuple<EventType, XSNotification> notification in ToSend)
{
if (
Expand All @@ -485,12 +510,12 @@ static void ParseTick(string content)
|| (Configuration.DisplayWorldChanged && notification.Item1 == EventType.WorldChange)
|| (Configuration.DisplayPortalDropped && notification.Item1 == EventType.PortalDropped)
)
SendNotification(notification.Item2);
DispatchQueue.Enqueue(new NotificationDispatchModel() { Type = notification.Item1, Message = notification.Item2, DurationMilliseconds = (int)(notification.Item2.Timeout * 1000.0f) });
else if (Configuration.DisplayMaximumKeywordsExceeded && notification.Item1 == EventType.KeywordsExceeded
&& DateTime.Now > LastMaximumKeywordsNotification.AddSeconds(Configuration.MaximumKeywordsExceededCooldownSeconds))
{
LastMaximumKeywordsNotification = DateTime.Now;
SendNotification(notification.Item2);
DispatchQueue.Enqueue(new NotificationDispatchModel() { Type = notification.Item1, Message = notification.Item2, DurationMilliseconds = (int)(notification.Item2.Timeout * 1000.0f) });
}
}
}
Expand Down
1 change: 1 addition & 0 deletions XSOverlay VRChat Parser/XSOverlay VRChat Parser.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<TargetFramework>net5.0</TargetFramework>
<RootNamespace>XSOverlay_VRChat_Parser</RootNamespace>
<Platforms>AnyCPU;x64</Platforms>
<Version>0.22</Version>
</PropertyGroup>

<ItemGroup>
Expand Down

0 comments on commit 6fb875a

Please sign in to comment.