Skip to content

Instantly share code, notes, and snippets.

@khenidak
Created October 10, 2015 02:51
Show Gist options
  • Save khenidak/e78a0a3fd6fa071a4308 to your computer and use it in GitHub Desktop.
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
// 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