浏览代码

Modifying UpdateSlow to accommodate differing periods, including acting as a regular Update for subscribers that aren't necessarily MonoBehaviours. I'm also stripping the staggering behavior, since it was unclear and not particularly necessary.

/main/staging/rate_limit_data
nathaniel.buck@unity3d.com 3 年前
当前提交
3045dabc
共有 6 个文件被更改,包括 136 次插入119 次删除
  1. 128
      Assets/Scripts/Infrastructure/UpdateSlow.cs
  2. 2
      Assets/Scripts/Lobby/LobbyAsyncRequests.cs
  3. 2
      Assets/Scripts/Lobby/LobbyContentHeartbeat.cs
  4. 11
      Assets/Scripts/Lobby/LobbyListHeartbeat.cs
  5. 4
      Assets/Scripts/Relay/RelayUtpClient.cs
  6. 108
      Assets/Scripts/Tests/PlayMode/UpdateSlowTests.cs

128
Assets/Scripts/Infrastructure/UpdateSlow.cs


/// </summary>
public class UpdateSlow : MonoBehaviour, IUpdateSlow
{
[SerializeField]
[Tooltip("Update interval. Note that lobby Get requests must occur at least 1 second apart, so this period should likely be greater than that.")]
private float m_updatePeriod = 1.5f;
private class Subscriber
{
public UpdateMethod updateMethod;
public readonly float period;
public float periodCurrent;
public Subscriber(UpdateMethod updateMethod, float period)
{
this.updateMethod = updateMethod;
this.period = period;
this.periodCurrent = 0;
}
}
[SerializeField]
[Tooltip("If a subscriber to slow update takes longer than this to execute, it can be automatically unsubscribed.")]
private float m_durationToleranceMs = 10;

private List<UpdateMethod> m_subscribers = new List<UpdateMethod>();
private float m_updateTimer = 0;
private int m_nextActiveSubIndex = 0; // For staggering subscribers, to prevent spikes of lots of things triggering at once.
private List<Subscriber> m_subscribers = new List<Subscriber>();
public void Awake()
{

{
// We should clean up references in case they would prevent garbage collection.
m_subscribers.Clear();
m_subscribers.Clear(); // We should clean up references in case they would prevent garbage collection.
/// <summary>Don't assume that onUpdate will be called in any particular order compared to other subscribers.</summary>
public void Subscribe(UpdateMethod onUpdate)
/// <summary>
/// Subscribe in order to have onUpdate called approximately every period seconds (or every frame, if period <= 0).
/// Don't assume that onUpdate will be called in any particular order compared to other subscribers.
/// </summary>
public void Subscribe(UpdateMethod onUpdate, float period)
if (!m_subscribers.Contains(onUpdate))
m_subscribers.Add(onUpdate);
if (onUpdate == null)
return;
foreach (Subscriber currSub in m_subscribers)
if (currSub.updateMethod.Equals(onUpdate))
return;
m_subscribers.Add(new Subscriber(onUpdate, period));
int index = m_subscribers.IndexOf(onUpdate);
if (index >= 0)
{
m_subscribers.Remove(onUpdate);
if (index < m_nextActiveSubIndex)
m_nextActiveSubIndex--;
}
for (int sub = m_subscribers.Count - 1; sub >= 0; sub--)
if (m_subscribers[sub].updateMethod.Equals(onUpdate))
m_subscribers.RemoveAt(sub);
if (m_subscribers.Count == 0)
return;
m_updateTimer += Time.deltaTime;
float effectivePeriod = m_updatePeriod / m_subscribers.Count;
while (m_updateTimer > effectivePeriod)
{
m_updateTimer -= effectivePeriod;
OnUpdate(m_updatePeriod); // Using m_updatePeriod will be incorrect on the first update for a new subscriber, due to the staggering. However, we don't expect UpdateSlow subscribers to require precision, and this is less verbose than tracking per-subscriber.
}
OnUpdate(Time.deltaTime);
/// <summary>
/// Each frame, advance all subscribers. Any that have hit their period should then act, though if they take too long they could be removed.
/// </summary>
Stopwatch stopwatch = new Stopwatch();
m_nextActiveSubIndex = System.Math.Max(0, System.Math.Min(m_subscribers.Count - 1, m_nextActiveSubIndex)); // Just a backup.
UpdateMethod onUpdate = m_subscribers[m_nextActiveSubIndex];
if (onUpdate == null || onUpdate.Target == null) // In case something forgets to Unsubscribe when it dies.
{ Remove(m_nextActiveSubIndex, $"Did not Unsubscribe from UpdateSlow: {onUpdate.Target} : {onUpdate.Method}");
return;
}
if (onUpdate.Method.ToString().Contains("<")) // Detect an anonymous or lambda or local method that cannot be Unsubscribed, by checking for a character that can't exist in a declared method name.
{ Remove(m_nextActiveSubIndex, $"Removed anonymous from UpdateSlow: {onUpdate.Target} : {onUpdate.Method}");
return;
}
stopwatch.Restart();
onUpdate?.Invoke(dt);
stopwatch.Stop();
if (stopwatch.ElapsedMilliseconds > m_durationToleranceMs)
for (int s = m_subscribers.Count - 1; s >= 0; s--) // Iterate in reverse in case we need to remove something.
if (!m_doNotRemoveIfTooLong)
Remove(m_nextActiveSubIndex, $"UpdateSlow subscriber took too long, removing: {onUpdate.Target} : {onUpdate.Method}");
else
var sub = m_subscribers[s];
sub.periodCurrent += Time.deltaTime;
if (sub.periodCurrent > sub.period)
Debug.LogWarning($"UpdateSlow subscriber took too long: {onUpdate.Target} : {onUpdate.Method}");
Increment();
Stopwatch stopwatch = new Stopwatch();
UpdateMethod onUpdate = sub.updateMethod;
if (onUpdate == null) // In case something forgets to Unsubscribe when it dies.
{ Remove(s, $"Did not Unsubscribe from UpdateSlow: {onUpdate.Target} : {onUpdate.Method}");
continue;
}
if (onUpdate.Target == null) // Detect a local function that cannot be Unsubscribed since it could go out of scope.
{ Remove(s, $"Removed local function from UpdateSlow: {onUpdate.Target} : {onUpdate.Method}");
continue;
}
if (onUpdate.Method.ToString().Contains("<")) // Detect an anonymous function that cannot be Unsubscribed, by checking for a character that can't exist in a declared method name.
{ Remove(s, $"Removed anonymous from UpdateSlow: {onUpdate.Target} : {onUpdate.Method}");
continue;
}
stopwatch.Restart();
onUpdate?.Invoke(sub.periodCurrent);
stopwatch.Stop();
sub.periodCurrent = 0;
if (stopwatch.ElapsedMilliseconds > m_durationToleranceMs)
{
if (!m_doNotRemoveIfTooLong)
Remove(s, $"UpdateSlow subscriber took too long, removing: {onUpdate.Target} : {onUpdate.Method}");
else
Debug.LogWarning($"UpdateSlow subscriber took too long: {onUpdate.Target} : {onUpdate.Method}");
}
else
Increment();
m_nextActiveSubIndex--;
Increment();
}
void Increment()
{
m_nextActiveSubIndex++;
if (m_nextActiveSubIndex >= m_subscribers.Count)
m_nextActiveSubIndex = 0;
}
}

public interface IUpdateSlow : IProvidable<IUpdateSlow>
{
void OnUpdate(float dt);
void Subscribe(UpdateMethod onUpdate);
void Subscribe(UpdateMethod onUpdate, float period);
void Unsubscribe(UpdateMethod onUpdate);
}

public class UpdateSlowNoop : IUpdateSlow
{
public void OnUpdate(float dt) { }
public void Subscribe(UpdateMethod onUpdate) { }
public void Subscribe(UpdateMethod onUpdate, float period) { }
public void Unsubscribe(UpdateMethod onUpdate) { }
public void OnReProvided(IUpdateSlow prev) { }
}

2
Assets/Scripts/Lobby/LobbyAsyncRequests.cs


public LobbyAsyncRequests()
{
Locator.Get.UpdateSlow.Subscribe(UpdateLobby); // Shouldn't need to unsubscribe since this instance won't be replaced.
Locator.Get.UpdateSlow.Subscribe(UpdateLobby, 1.5f); // Shouldn't need to unsubscribe since this instance won't be replaced.
}
private static bool IsSuccessful(Response response)

2
Assets/Scripts/Lobby/LobbyContentHeartbeat.cs


{
m_localLobby = lobby;
m_localUser = localUser;
Locator.Get.UpdateSlow.Subscribe(OnUpdate);
Locator.Get.UpdateSlow.Subscribe(OnUpdate, 1.5f);
m_localLobby.onChanged += OnLocalLobbyChanged;
m_shouldPushData = true; // Ensure the initial presence of a new player is pushed to the lobby; otherwise, when a non-host joins, the LocalLobby never receives their data until they push something new.
}

11
Assets/Scripts/Lobby/LobbyListHeartbeat.cs


public class LobbyListHeartbeat : MonoBehaviour
{
private const float k_refreshRate = 5;
private float m_refreshTimer = 0;
Locator.Get.UpdateSlow.Subscribe(OnUpdate);
Locator.Get.UpdateSlow.Subscribe(OnUpdate, k_refreshRate);
m_refreshTimer = 0;
m_refreshTimer += dt;
if (m_refreshTimer > k_refreshRate)
{
Locator.Get.Messenger.OnReceiveMessage(MessageType.QueryLobbies, null);
m_refreshTimer = 0;
}
Locator.Get.Messenger.OnReceiveMessage(MessageType.QueryLobbies, null);
}
}
}

4
Assets/Scripts/Relay/RelayUtpClient.cs


protected LocalLobby m_localLobby;
protected NetworkDriver m_networkDriver;
protected List<NetworkConnection> m_connections; // For clients, this has just one member, but for hosts it will have more.
private const float k_heartbeatPeriod = 5;
public virtual void Initialize(NetworkDriver networkDriver, List<NetworkConnection> connections, LobbyUser localUser, LocalLobby localLobby)
{

m_networkDriver = networkDriver;
m_connections = connections;
Locator.Get.UpdateSlow.Subscribe(UpdateSlow);
Locator.Get.UpdateSlow.Subscribe(UpdateSlow, k_heartbeatPeriod);
}
protected virtual void Uninitialize()
{

108
Assets/Scripts/Tests/PlayMode/UpdateSlowTests.cs


{
private GameObject m_updateSlowObj;
private List<Subscriber> m_activeSubscribers = new List<Subscriber>(); // For cleaning up, in case an Assert prevents a Subscriber from taking care of itself.
private const float k_period = 1.5f;
/// <summary>Trivial Subscriber to do some action every UpdateSlow.</summary>
private class Subscriber : IDisposable

public Subscriber(Action thingToDo)
public Subscriber(Action thingToDo, float period)
Locator.Get.UpdateSlow.Subscribe(OnUpdate);
Locator.Get.UpdateSlow.Subscribe(OnUpdate, period);
m_thingToDo = thingToDo;
}

}
[UnityTest]
public IEnumerator BasicBehavior()
public IEnumerator BasicBehavior_MultipleSubs()
Subscriber sub = new Subscriber(() => { updateCount++; });
float period = 1.5f;
Subscriber sub = new Subscriber(() => { updateCount++; }, period);
yield return new WaitForSeconds(k_period - 0.1f);
yield return new WaitForSeconds(period - 0.1f);
Assert.AreEqual(1.5f, sub.prevDt, "Slow update should have received the full time delta.");
Assert.AreNotEqual(period, sub.prevDt, "Slow update should have received the actual amount of time that passed, not necessarily its period.");
Assert.True(sub.prevDt - period < 0.05f && sub.prevDt - period > 0, "The time delta received by slow update should match the actual time since their previous update.");
yield return new WaitForSeconds(k_period);
yield return new WaitForSeconds(period);
Assert.AreEqual(1.5f, sub.prevDt, "Slow update should have received the full time delta again.");
Assert.AreNotEqual(period, sub.prevDt, "Slow update should have received the full time delta, not just its period, again.");
Assert.True(sub.prevDt - period < 0.05f && sub.prevDt - period > 0, "The time delta received by slow update should match the actual time since their previous update, again.");
Subscriber sub2 = new Subscriber(() => { updateCount += 7; });
float period2 = period - 0.2f;
Subscriber sub2 = new Subscriber(() => { updateCount += 7; }, period2);
yield return new WaitForSeconds(k_period);
yield return new WaitForSeconds(period);
Assert.AreEqual(1.5f, sub.prevDt, "Slow update should have received the full time delta with two subscribers.");
Assert.AreEqual(1.5f, sub2.prevDt, "Slow update should have received the full time delta on the second subscriber as well.");
Assert.True(sub.prevDt - period < 0.05f && sub.prevDt - period > 0, "Slow update on the first subscriber should have received the full time delta with two subscribers.");
Assert.True(sub2.prevDt - period2 < 0.05f && sub2.prevDt - period2 > 0, "Slow update on the second subscriber should receive the actual time, even if its period is shorter.");
yield return new WaitForSeconds(k_period);
yield return new WaitForSeconds(period);
yield return new WaitForSeconds(k_period);
yield return new WaitForSeconds(period);
public IEnumerator BasicBehavior_UpdateEveryFrame()
{
int updateCount = 0;
Subscriber sub = new Subscriber(() => { updateCount++; }, 0);
m_activeSubscribers.Add(sub);
yield return null;
Assert.AreEqual(1, updateCount, "Update loop should update per-frame if a subscriber opts for that (#1).");
yield return null;
Assert.AreEqual(2, updateCount, "Update loop should update per-frame if a subscriber opts for that (#2).");
Assert.AreEqual(sub.prevDt, Time.deltaTime, "Subscriber should receive the correct update time since their previous update.");
sub.Dispose();
yield return new WaitForSeconds(0.5f);
Assert.AreEqual(2, updateCount, "Should have unsubscribed the subscriber.");
}
[UnityTest]
Locator.Get.UpdateSlow.Subscribe((dt) => { updateCount++; });
float period = 0.5f;
Locator.Get.UpdateSlow.Subscribe((dt) => { updateCount++; }, period);
yield return new WaitForSeconds(k_period + 0.1f);
yield return new WaitForSeconds(period + 0.1f);
Locator.Get.UpdateSlow.Subscribe(ThisIsALocalFunction, period);
LogAssert.Expect(LogType.Error, new Regex(".*Removed local function.*"));
yield return new WaitForSeconds(period + 0.1f);
Assert.AreEqual(0, updateCount, "Local functions should not be permitted, since they can't be Unsubscribed.");
void ThisIsALocalFunction(float dt) { }
public IEnumerator StaggerClients()
public IEnumerator SubscribeNoDuplicates()
int updateCountA = 0, updateCountB = 0;
Subscriber subA = new Subscriber(() => { updateCountA++; });
Subscriber subB = new Subscriber(() => { updateCountB++; });
m_activeSubscribers.Add(subA);
m_activeSubscribers.Add(subB);
float periodHalf = k_period / 2;
yield return new WaitForSeconds(periodHalf - 0.1f);
Assert.AreEqual(0, updateCountA, "Base case (count A)");
Assert.AreEqual(0, updateCountB, "Base case (count B)");
yield return new WaitForSeconds(0.2f);
Assert.AreEqual(1, updateCountA, "Updates are now on half the normal period. First update is first.");
Assert.AreEqual(0, updateCountB, "Updates are now on half the normal period. Second update is second.");
dummyOnUpdateCalls = 0;
Locator.Get.UpdateSlow.Subscribe(DummyOnUpdate, 1);
Locator.Get.UpdateSlow.Subscribe(DummyOnUpdate, 0.1f);
yield return new WaitForSeconds(periodHalf);
Assert.AreEqual(1, updateCountA, "First update is still offset.");
Assert.AreEqual(1, updateCountB, "Second update should hit now.");
yield return new WaitForSeconds(0.9f);
Assert.AreEqual(0, dummyOnUpdateCalls, "The second Subscribe call should not have gone through.");
subB.Dispose();
yield return new WaitForSeconds(periodHalf);
Assert.AreEqual(1, updateCountA, "First update should no longer be offset.");
Assert.AreEqual(1, updateCountB, "Second update is unsubscribed.");
yield return new WaitForSeconds(0.2f);
Assert.AreEqual(1, dummyOnUpdateCalls, "The first Subscribe call should have gone through.");
yield return new WaitForSeconds(periodHalf);
Assert.AreEqual(2, updateCountA, "First update should hit with normal timing.");
Assert.AreEqual(1, updateCountB, "Second update is still unsubscribed.");
Locator.Get.UpdateSlow.Unsubscribe(DummyOnUpdate);
yield return new WaitForSeconds(1);
Assert.AreEqual(1, dummyOnUpdateCalls, "Unsubscribe should work as expected.");
private int dummyOnUpdateCalls = 0;
private void DummyOnUpdate(float dt) { dummyOnUpdateCalls++; }
[UnityTest]
public IEnumerator WhatIfASubscriberIsVerySlow()

float period = 1.5f;
});
}, period);
yield return new WaitForSeconds(k_period + 0.1f);
yield return new WaitForSeconds(period + 0.1f);
yield return new WaitForSeconds(k_period);
yield return new WaitForSeconds(period);
Assert.AreEqual(1, updateCount, "Should have removed the offending subscriber.");
}
}
正在加载...
取消
保存