Created
October 10, 2015 02:51
-
-
Save khenidak/e78a0a3fd6fa071a4308 to your computer and use it in GitHub Desktop.
Clicker is high performance lock-free in memory histogram (moving calculation) like implementation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// for more details please refer to: http://henidak.com/2015/10/on-train-rides-and-histograms/ | |
/* | |
Typical Use | |
- Create a class (or use Click class) that sub-classes Click (if you need more than *.value) | |
- The implementation favors speed over memory (it will copy the tree everytime you do a calc using the Do method). | |
// this keeps the clicks for 1 min, then remove anything older | |
private Clicker<MyClickType> m_MinuteClicker = new Clicker>MyClickType>(TimeSpan.FromMinutes(1)); | |
// this keeps the clicks for 1 hour, then remove anything older | |
private Clicker<MyClickType> m_HourClicker = new Clicker<MyClickType>(TimeSpan.FromHours(1)); | |
// chain minute clicker into hour clicker | |
m_MinuteClicker.OnTrim = (head) => OnMinuteClickerTrim(head); | |
// later in the code | |
// whenever I need to record an event | |
m_MinuteClicker.Click(new MyClickType(Add or Processed)); | |
// Let us say i want to know the average of added clicks per min for the last hour | |
return m_HourClicker.Do(head => | |
{ | |
int sum = 0; | |
int count = 0; | |
var curr = head; | |
while (curr != null && curr.ClickType == Added) | |
{ | |
sum = (int) curr.Value; | |
count++; | |
curr = (MyClickType)curr.Next; | |
} | |
return count == 0 ? 0 : sum/count ; | |
}); | |
// roll up totals into mins, saved for maximum of 1 hour | |
void OnMinuteClickerTrim(MyClickType head) | |
{ | |
// my click type is either add event or processed event | |
var totalAdd = 0; | |
var totalProcessed = 0; | |
var curr = head; | |
while (curr != null) | |
{ | |
if (curr.ClickType == Added) | |
totalAdd++; | |
else | |
totalProcessed++; | |
} | |
// harvest | |
m_HourClicker.Click(new WorkManagerClick() { ClickType = Add, Value = totalAdd }); | |
m_HourClicker.Click(new WorkManagerClick() { ClickType = Processed, Value = totalProcessed}); | |
} | |
*/ | |
public class Click : ICloneable | |
{ | |
public long When { get; internal set; } = DateTime.UtcNow.Ticks; | |
public Click Next { get; internal set; } | |
public virtual long Value { get; set; } | |
internal Click(Click next) | |
{ | |
Next = next; | |
} | |
public Click() | |
{ | |
} | |
public virtual object Clone() | |
{ | |
return new Click() { When = this.When, Next = this.Next, Value = this.Value }; | |
} | |
} | |
/// <summary> | |
/// an memory histogram implementation, supports | |
/// periodical trim (with Action<Linked List Head> call backs). | |
/// counts and external actions (such as sum, average etc) via Func delegats | |
/// </summary> | |
/// <typeparam name="T">Click Type</typeparam> | |
public class Clicker<T> : IDisposable where T : Click, new() | |
{ | |
private Task m_TrimTask = null; | |
private T m_head = new T(); | |
private CancellationTokenSource m_cts = new CancellationTokenSource(); | |
private Action<T> m_OnTrim = null; | |
private bool m_OnTrimChanged = false; | |
private T CloneInTimeSpan(TimeSpan ts) | |
{ | |
long ticksWhen = DateTime.UtcNow.Ticks - ts.Ticks; | |
var head = new T(); | |
var ret = head; | |
var cur = m_head; | |
while (cur != null && cur.When >= ticksWhen) | |
{ | |
head.Next = (T)cur.Clone(); | |
head = (T)head.Next; | |
cur = (T)cur.Next; | |
} | |
head.Next = null; | |
return (T)ret.Next; | |
} | |
/// <summary> | |
/// Will be called whenever KeepClicksFor elabsed | |
/// </summary> | |
public Action<T> OnTrim | |
{ | |
get { return m_OnTrim; } | |
set | |
{ | |
if (null == value) | |
throw new ArgumentNullException("OnTrim"); | |
m_OnTrim = value; | |
m_OnTrimChanged = true; | |
} | |
} | |
public TimeSpan KeepClicksFor { get; set; } | |
private async Task TrimLoop() | |
{ | |
while (!m_cts.IsCancellationRequested) | |
{ | |
await Task.Delay((int)KeepClicksFor.TotalMilliseconds); | |
Trim(); | |
} | |
} | |
public Clicker(TimeSpan KeepFor) | |
{ | |
m_head.When = 0; // this ensure that head is never counted. | |
KeepClicksFor = KeepFor; | |
m_TrimTask = Task.Run(async () => await TrimLoop()); | |
} | |
public Clicker() : this(TimeSpan.FromMinutes(1)) | |
{ | |
} | |
public void Click(T newNode) | |
{ | |
// set new head. | |
do | |
{ | |
newNode.Next = m_head; | |
} | |
while (newNode.Next != Interlocked.CompareExchange<T>(ref m_head, newNode, (T)newNode.Next)); | |
} | |
public void Click() | |
{ | |
T node = new T(); | |
Click(node); | |
} | |
public M Do<M>(Func<T, M> func) | |
{ | |
return Do(KeepClicksFor, func); | |
} | |
public M Do<M>(TimeSpan ts, Func<T, M> func) | |
{ | |
if (ts > KeepClicksFor) | |
throw new ArgumentException("Can not do for a timespan more than what clicker is keeping track of"); | |
// since we are not sure what will happen in | |
// the func, we are handing out a copy not the original thing | |
return func(CloneInTimeSpan(ts)); | |
} | |
public int Count() | |
{ | |
return Count(KeepClicksFor); | |
} | |
public int Count(TimeSpan ts) | |
{ | |
if (ts > KeepClicksFor) | |
throw new ArgumentException("Can not count for a timespan more than what clicker is keeping track of"); | |
long ticksWhen = DateTime.UtcNow.Ticks - ts.Ticks; | |
int count = 0; | |
Click cur = m_head; | |
while (null != cur && cur.When >= ticksWhen) | |
{ | |
count++; | |
cur = cur.Next; | |
} | |
return count; | |
} | |
private void Trim() | |
{ | |
// trim keeps the head. | |
long ticksWhen = DateTime.UtcNow.Ticks - KeepClicksFor.Ticks; | |
Click cur = m_head; | |
Click next = cur.Next; | |
while (null != next) | |
{ | |
if (next.When <= ticksWhen) | |
{ | |
cur.Next = null; | |
break; | |
} | |
cur = next; | |
next = next.Next; | |
} | |
// call on trim | |
if (m_OnTrimChanged) | |
OnTrim(CloneInTimeSpan(KeepClicksFor)); | |
} | |
#region IDisposable Support | |
private bool disposedValue = false; // To detect redundant calls | |
protected virtual void Dispose(bool disposing) | |
{ | |
if (!disposedValue) | |
{ | |
if (disposing) | |
{ | |
m_cts.Cancel(); | |
} | |
disposedValue = true; | |
} | |
} | |
public void Dispose() | |
{ | |
Dispose(true); | |
} | |
#endregion | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment