private InstantDataTarget InternalUnregisterInstantDataTarget(Guid registrationToken) { lock (this.instantStreamReaders) { for (int index = this.instantStreamReaders.Count - 1; index >= 0; index--) { // Unregister the target from the instant stream reader InstantDataTarget target = this.instantStreamReaders[index].UnregisterInstantDataTarget(registrationToken); if (target != null) { // If the instant stream reader now has no data providers, remove it from the collection if (!this.instantStreamReaders[index].HasAdaptingDataProviders) { this.instantStreamReaders.RemoveAt(index); } // If there's no instant stream readers, remove the index view if (this.instantStreamReaders.Count <= 0) { this.instantIndexView = null; } return(target); } } return(null); } }
/// <summary> /// Reads instant data from the stream at the given cursor time and pushes it to all registered adapting data providers. /// </summary> /// <param name="reader">The simple reader that will read the data.</param> /// <param name="cursorTime">The cursor time at which to read the data.</param> /// <param name="indexCache">The stream reader's index cache.</param> public void ReadInstantData(ISimpleReader reader, DateTime cursorTime, ObservableKeyedCache <DateTime, IndexEntry> indexCache) { // Get the index of the data, given the cursor time int index = IndexHelper.GetIndexForTime(cursorTime, indexCache?.Count ?? 0, (idx) => indexCache[idx].OriginatingTime, this.CursorEpsilon); T data = default; IndexEntry indexEntry = default; if (index >= 0) { // Get the index entry indexEntry = indexCache[index]; // Read the data data = reader.Read <T>(indexEntry); } // Notify each adapting data provider of the new data foreach (IAdaptingInstantDataProvider <T> adaptingInstantDataProvider in this.dataProviders.ToList()) { adaptingInstantDataProvider.PushData(data, indexEntry); } // Release the reference to the local copy of the data if it's shared if (this.isSharedType && data != null) { (data as IDisposable).Dispose(); } }
/// <summary> /// Reads instant data from the stream at the given cursor time and pushes it to all registered adapting data providers. /// </summary> /// <param name="reader">The simple reader that will read the data.</param> /// <param name="cursorTime">The cursor time at which to read the data.</param> /// <param name="indexCache">The stream reader's index cache.</param> public void ReadInstantData(ISimpleReader reader, DateTime cursorTime, ObservableKeyedCache <DateTime, IndexEntry> indexCache) { // Get the index of the data, given the cursor time int index = IndexHelper.GetIndexForTime(cursorTime, indexCache?.Count ?? 0, (idx) => indexCache[idx].OriginatingTime, this.CursorEpsilon); T data = default; IndexEntry indexEntry = default; if (index >= 0) { // Get the index entry indexEntry = indexCache[index]; // Read the data data = reader.Read <T>(indexEntry); } // Notify all registered adapting data providers of the new data. If the data is Shared<T> then perform a deep clone // (which resolves to an AddRef() for this type) for each provider we call. The providers are responsible for releasing // their reference to the data once they're done with it. if (this.isSharedType && data != null) { Parallel.ForEach(this.dataProviders.ToList(), provider => provider.PushData(data.DeepClone <T>(), indexEntry)); // Release the reference to the local copy of the data (data as IDisposable).Dispose(); } else { Parallel.ForEach(this.dataProviders.ToList(), provider => provider.PushData(data, indexEntry)); } }
public void Initialize() { this.basis = new SortedList <double, double>(); var eventCount = 0; var itemCount = 0; NotifyCollectionChangedEventHandler testCollectionChanged = (s, e) => { this.CollectionChangedHandler(e, ref eventCount, ref itemCount); }; this.test = new ObservableKeyedCache <double, double>((d) => d); this.test.DetailedCollectionChanged += testCollectionChanged; double[] values = { 15, 65, -1, 2, 44, 100, 123, -456, 0, 10 }; foreach (var value in values) { this.basis.Add(value, value); this.test.Add(value); } Assert.AreEqual(eventCount, values.Length); Assert.AreEqual(itemCount, values.Length); Assert.AreEqual(this.basis.Count, values.Length); UnitTestHelper.AssertAreEqual(this.basis.Values, this.test, values.Length); this.test.DetailedCollectionChanged -= testCollectionChanged; }
/// <inheritdoc/> public void UnregisterStreamValueSubscriber <TTarget>(Guid registrationToken) { lock (this.publishers) { foreach (var epsilonTimeIntervalPublishers in this.publishers.Values) { foreach (var publisher in epsilonTimeIntervalPublishers) { if (publisher.HasSubscriber(registrationToken)) { publisher.UnregisterSubscriber(registrationToken); } } epsilonTimeIntervalPublishers.RemoveAll(publisher => !publisher.HasSubscribers); } foreach (var epsilonTimeInterval in this.publishers.Keys.ToArray()) { if (!this.publishers[epsilonTimeInterval].Any()) { this.publishers.Remove(epsilonTimeInterval); } } } // If no publishers remain, remove the index view if (!this.publishers.Any()) { this.indexView = null; this.indexViewRange = new NavigatorRange(DateTime.MinValue, DateTime.MinValue); } }
/// <summary> /// Initializes a new instance of the <see cref="StreamCache{T}"/> class. /// </summary> /// <param name="streamName">the name of the stream to read.</param> /// <param name="streamAdapter">the stream adapter to convert data from the stream into the type required by clients of this stream reader.</param> public StreamCache(string streamName, IStreamAdapter streamAdapter /*, object[] streamAdapterParameters*/) { if (string.IsNullOrWhiteSpace(streamName)) { throw new ArgumentNullException(nameof(streamName)); } this.StreamName = streamName; this.StreamAdapter = streamAdapter; this.pool = PoolManager.Instance.GetPool <T>(); this.readRequestsInternal = new List <ReadRequest>(); this.readRequests = new ReadOnlyCollection <ReadRequest>(this.readRequestsInternal); this.bufferLock = new object(); this.dataBuffer = new List <Message <T> >(1000); this.indexBuffer = new List <StreamCacheEntry>(1000); var itemComparer = Comparer <Message <T> > .Create((m1, m2) => m1.OriginatingTime.CompareTo(m2.OriginatingTime)); var indexComarer = Comparer <StreamCacheEntry> .Create((i1, i2) => i1.OriginatingTime.CompareTo(i2.OriginatingTime)); this.data = new ObservableKeyedCache <DateTime, Message <T> >(null, itemComparer, m => m.OriginatingTime); this.index = new ObservableKeyedCache <DateTime, StreamCacheEntry>(null, indexComarer, ie => ie.OriginatingTime); this.instantIndexView = null; this.instantStreamReaders = new List <EpsilonInstantStreamReader <T> >(); if (this.needsDisposing) { this.data.CollectionChanged += this.OnCollectionChanged; } }
/// <summary> /// Initializes a new instance of the <see cref="StreamReader{T}"/> class. /// </summary> /// <param name="streamBinding">Stream binding used to indentify stream.</param> /// <param name="useIndex">Indicates stream reader should use index for access.</param> public StreamReader(StreamBinding streamBinding, bool useIndex) { this.streamBinding = streamBinding; this.useIndex = useIndex; this.pool = PoolManager.Instance.GetPool <T>(); this.readRequestsInternal = new List <Tuple <DateTime, DateTime, uint, Func <DateTime, DateTime> > >(); this.readRequests = new ReadOnlyCollection <Tuple <DateTime, DateTime, uint, Func <DateTime, DateTime> > >(this.readRequestsInternal); this.bufferLock = new object(); this.dataBuffer = new List <Message <T> >(1000); this.indexBuffer = new List <IndexEntry>(1000); var itemComparer = Comparer <Message <T> > .Create((m1, m2) => m1.OriginatingTime.CompareTo(m2.OriginatingTime)); var indexComarer = Comparer <IndexEntry> .Create((i1, i2) => i1.OriginatingTime.CompareTo(i2.OriginatingTime)); this.data = new ObservableKeyedCache <DateTime, Message <T> >(null, itemComparer, m => m.OriginatingTime); this.index = new ObservableKeyedCache <DateTime, IndexEntry>(null, indexComarer, ie => ie.OriginatingTime); if (this.needsDisposing) { this.data.CollectionChanged += this.OnCollectionChanged; } }
public void EmptyTest() { var basisEmpty = new SortedList <double, double>(); var testEmpty = new ObservableKeyedCache <double, double>((d) => d); Assert.AreEqual(testEmpty.Count, 0); Assert.AreEqual(basisEmpty.Count, testEmpty.Count); }
/// <summary> /// Creates a view of the messages identified by the matching parameters and asynchronously fills it in. /// View mode can be one of three values: /// Fixed - fixed range based on start and end times /// TailCount - sliding dynamic range that includes the tail of the underlying data based on quantity /// TailRange - sliding dynamic range that includes the tail of the underlying data based on function. /// </summary> /// <typeparam name="T">The type of the message to read.</typeparam> /// <param name="streamBinding">The stream binding indicating which stream to read from.</param> /// <param name="viewMode">Mode the view will be created in.</param> /// <param name="startTime">Start time of messages to read.</param> /// <param name="endTime">End time of messages to read.</param> /// <param name="tailCount">Number of messages to included in tail.</param> /// <param name="tailRange">Function to determine range included in tail.</param> /// <returns>Observable view of data.</returns> internal ObservableKeyedCache <DateTime, Message <T> > .ObservableKeyedView ReadStream <T>( StreamBinding streamBinding, ObservableKeyedCache <DateTime, Message <T> > .ObservableKeyedView.ViewMode viewMode, DateTime startTime, DateTime endTime, uint tailCount, Func <DateTime, DateTime> tailRange) { return(this.GetOrCreateStreamReader <T>(streamBinding.StreamName, streamBinding.StreamAdapter).ReadStream <T>(viewMode, startTime, endTime, tailCount, tailRange)); }
/// <inheritdoc /> public void Dispose() { lock (this.bufferLock) { this.summaryCache?.Clear(); this.summaryCache = null; this.summaryDataBuffer?.Clear(); this.summaryDataBuffer = null; } }
/// <summary> /// Creates a view of the messages identified by the matching parameters and asynchronously fills it in. /// View mode can be one of three values: /// Fixed - fixed range based on start and end times /// TailCount - sliding dynamic range that includes the tail of the underlying data based on quantity /// TailRange - sliding dynamic range that includes the tail of the underlying data based on function /// </summary> /// <typeparam name="T">The type of the message to read.</typeparam> /// <param name="streamBinding">The stream binding inidicating which stream to read from.</param> /// <param name="viewMode">Mode the view will be created in</param> /// <param name="startTime">Start time of messages to read.</param> /// <param name="endTime">End time of messages to read.</param> /// <param name="tailCount">Number of messages to included in tail.</param> /// <param name="tailRange">Function to determine range included in tail.</param> /// <returns>Observable view of data.</returns> public ObservableKeyedCache <DateTime, Message <T> > .ObservableKeyedView ReadStream <T>( StreamBinding streamBinding, ObservableKeyedCache <DateTime, Message <T> > .ObservableKeyedView.ViewMode viewMode, DateTime startTime, DateTime endTime, uint tailCount, Func <DateTime, DateTime> tailRange) { return(this.GetStreamReader <T>(streamBinding, false).ReadStream <T>(viewMode, startTime, endTime, tailCount, tailRange)); }
/// <summary> /// Initializes a new instance of the <see cref="StreamIntervalProvider{T}"/> class. /// </summary> /// <param name="streamSource">The stream source.</param> public StreamIntervalProvider(StreamSource streamSource) : base(streamSource) { this.StreamAdapter = streamSource.StreamAdapter; var itemComparer = Comparer <Message <T> > .Create((m1, m2) => m1.OriginatingTime.CompareTo(m2.OriginatingTime)); this.data = new ObservableKeyedCache <DateTime, Message <T> >(null, itemComparer, m => m.OriginatingTime); this.data.CollectionChanged += this.OnCollectionChanged; this.dataBuffer = new List <Message <T> >(1000); }
/// <summary> /// Initializes a new instance of the <see cref="StreamValueProvider{T}"/> class. /// </summary> /// <param name="streamSource">The stream source.</param> public StreamValueProvider(StreamSource streamSource) : base(streamSource) { this.indexView = null; this.publishers = new Dictionary <RelativeTimeInterval, List <IStreamValuePublisher <TSource> > >(); var indexComparer = Comparer <MessageIndex <TSource> > .Create((i1, i2) => i1.OriginatingTime.CompareTo(i2.OriginatingTime)); this.index = new ObservableKeyedCache <DateTime, MessageIndex <TSource> >(null, indexComparer, ie => ie.OriginatingTime); this.indexBuffer = new List <MessageIndex <TSource> >(1000); }
/// <inheritdoc /> public void UnregisterInstantDataTarget(Guid registrationToken) { this.InternalUnregisterInstantDataTarget(registrationToken); // If no instant visualization objects are now using // this stream reader, remove the instant index view if (this.instantStreamReaders.Count <= 0) { this.instantIndexView = null; this.currentIndexViewRange = new NavigatorRange(DateTime.MinValue, DateTime.MinValue); } }
/// <inheritdoc /> public ObservableKeyedCache <DateTime, Message <TItem> > .ObservableKeyedView ReadStream <TItem>( ObservableKeyedCache <DateTime, Message <TItem> > .ObservableKeyedView.ViewMode viewMode, DateTime startTime, DateTime endTime, uint tailCount, Func <DateTime, DateTime> tailRange) { lock (this.readRequestsInternal) { this.readRequestsInternal.AddRange(this.ComputeReadRequests(startTime, endTime, false)); } return((this.data as ObservableKeyedCache <DateTime, Message <TItem> >).GetView(viewMode, startTime, endTime, tailCount, tailRange)); }
/// <inheritdoc/> public void OnInstantViewRangeChanged(TimeInterval viewRange) { // Check if the navigator view range exceeds the current range of the data index if (viewRange.Left < this.currentIndexViewRange.StartTime || viewRange.Right > this.currentIndexViewRange.EndTime) { // Set a new data index range thats extends to the left and right of the navigator view by the navigator view // duration so that we're not constantly needing to initiate an index read every time the navigator moves. TimeSpan viewDuration = viewRange.Span; this.currentIndexViewRange.SetRange( viewRange.Left > DateTime.MinValue + viewDuration ? viewRange.Left - viewDuration : DateTime.MinValue, viewRange.Right < DateTime.MaxValue - viewDuration ? viewRange.Right + viewDuration : DateTime.MaxValue); this.instantIndexView = this.ReadIndex(this.currentIndexViewRange.StartTime, this.currentIndexViewRange.EndTime); } }
public void TryGetValueTest() { double value = 0; Assert.IsTrue(this.test.TryGetValue(100, out value)); Assert.AreEqual(value, 100); Assert.IsTrue(this.test.TryGetValue(15, out value)); Assert.AreEqual(value, 15); Assert.IsTrue(this.test.TryGetValue(10, out value)); Assert.AreEqual(value, 10); Assert.IsFalse(this.test.TryGetValue(101, out value)); Assert.AreEqual(value, 0); // Empty test var empty = new ObservableKeyedCache<double, double>((d) => d); Assert.IsFalse(empty.TryGetValue(0, out value)); }
/// <summary> /// Initializes a new instance of the <see cref="StreamSummary{TSrc, TDest}"/> class. /// </summary> /// <param name="streamBinding">Stream binding indicating which stream to summarize.</param> /// <param name="interval">The time interval over which summary <see cref="IntervalData"/> values are calculated.</param> /// <param name="maxCacheSize">The maximum amount of data to cache before purging older summarized data.</param> public StreamSummary(StreamBinding streamBinding, TimeSpan interval, uint maxCacheSize) { this.streamBinding = streamBinding; this.interval = interval; this.maxCacheSize = maxCacheSize; this.summaryDataBuffer = new List <List <IntervalData <TDest> > >(); this.keySelector = s => Summarizer <TSrc, TDest> .GetIntervalStartTime(s.OriginatingTime, interval); this.itemComparer = Comparer <IntervalData <TDest> > .Create((r1, r2) => this.keySelector(r1).CompareTo(this.keySelector(r2))); this.summaryCache = new ObservableKeyedCache <DateTime, IntervalData <TDest> >(null, this.itemComparer, this.keySelector); this.activeStreamViews = new Dictionary <Tuple <DateTime, DateTime, uint, Func <DateTime, DateTime> >, ObservableKeyedCache <DateTime, Message <TSrc> > .ObservableKeyedView>(); this.cachedSummaryViews = new Dictionary <Tuple <DateTime, DateTime, uint, Func <DateTime, DateTime> >, ObservableKeyedCache <DateTime, IntervalData <TDest> > .ObservableKeyedView>(); // Cache the summarizer (cast to the correct type) to call its methods later on without dynamic binding this.summarizer = this.StreamBinding.Summarizer as ISummarizer <TSrc, TDest>; }
/// <summary> /// Reads instant data from the stream at the given cursor time and pushes it to all registered adapting data providers. /// </summary> /// <param name="streamReader">The stream reader that will read the data.</param> /// <param name="cursorTime">The cursor time at which to read the data.</param> /// <param name="streamCache">The stream reader's cache.</param> public void ReadInstantData(IStreamReader streamReader, DateTime cursorTime, ObservableKeyedCache <DateTime, StreamCacheEntry> streamCache) { // Get the index of the data, given the cursor time int index = IndexHelper.GetIndexForTime(cursorTime, streamCache?.Count ?? 0, (idx) => streamCache[idx].OriginatingTime, this.CursorEpsilon); T data = default; StreamCacheEntry cacheEntry = default; if (index >= 0) { // Get the index entry cacheEntry = streamCache[index]; // Read the data data = cacheEntry.Read <T>(streamReader); } else { // cache miss, attempt to seek directly while the cache is presumably being populated streamReader.Seek(cursorTime + this.CursorEpsilon, true); streamReader.OpenStream <T>(this.streamName, (m, e) => { cacheEntry = new StreamCacheEntry(null, e.CreationTime, e.OriginatingTime); data = m; }); streamReader.MoveNext(out var envelope); } // Notify each adapting data provider of the new data foreach (IAdaptingInstantDataProvider <T> adaptingInstantDataProvider in this.dataProviders.ToList()) { adaptingInstantDataProvider.PushData(data, cacheEntry); } // Release the reference to the local copy of the data if it's shared if (this.isSharedType && data != null) { (data as IDisposable).Dispose(); } }
/// <inheritdoc /> public ObservableKeyedCache <DateTime, IntervalData <TItem> > .ObservableKeyedView ReadSummary <TItem>( ObservableKeyedCache <DateTime, IntervalData <TItem> > .ObservableKeyedView.ViewMode viewMode, DateTime startTime, DateTime endTime, uint tailCount, Func <DateTime, DateTime> tailRange) { if (viewMode == ObservableKeyedCache <DateTime, IntervalData <TItem> > .ObservableKeyedView.ViewMode.TailRange) { // Just read directly from the stream with the same tail range in live mode this.ReadStream(tailRange); } else if (viewMode == ObservableKeyedCache <DateTime, IntervalData <TItem> > .ObservableKeyedView.ViewMode.TailCount) { // We should read enough of the stream to generate the last tailCount intervals. So take the product of our // summarization interval and tailCount, and use that interval as the tail range to read from the stream. TimeSpan tailInterval = TimeSpan.FromTicks(this.Interval.Ticks * tailCount); this.ReadStream(last => last - tailInterval); } else if (viewMode == ObservableKeyedCache <DateTime, IntervalData <TItem> > .ObservableKeyedView.ViewMode.Fixed) { // Ranges for which we have not yet computed summary data. foreach (var range in this.ComputeRangeRequests(startTime, endTime)) { this.ReadStream(range.Item1, range.Item2); } } else { throw new NotSupportedException($"Summarization not yet supported in {viewMode} view mode."); } // Get or create the summary view from the cache return(this.GetCachedSummaryView( (ObservableKeyedCache <DateTime, IntervalData <TDest> > .ObservableKeyedView.ViewMode)viewMode, startTime, endTime, tailCount, tailRange) as ObservableKeyedCache <DateTime, IntervalData <TItem> > .ObservableKeyedView); }
/// <summary> /// Gets a view over the specified time range of the cached summary data. /// </summary> /// <typeparam name="T">The summary data type.</typeparam> /// <param name="streamSource">The stream source indicating which stream to read from.</param> /// <param name="viewMode">The view mode, which may be either fixed or live data.</param> /// <param name="startTime">The start time of the view range.</param> /// <param name="endTime">The end time of the view range.</param> /// <param name="interval">The time interval each summary value should cover.</param> /// <param name="tailCount">Not yet supported and should be set to zero.</param> /// <param name="tailRange">Tail duration function. Computes the view range start time given an end time. Applies to live view mode only.</param> /// <returns>A view over the cached summary data that covers the specified time range.</returns> public ObservableKeyedCache <DateTime, IntervalData <T> > .ObservableKeyedView ReadSummary <T>( StreamSource streamSource, ObservableKeyedCache <DateTime, IntervalData <T> > .ObservableKeyedView.ViewMode viewMode, DateTime startTime, DateTime endTime, TimeSpan interval, uint tailCount, Func <DateTime, DateTime> tailRange) { if (startTime > DateTime.MinValue) { // Extend the start time to include the preceding data point to facilitate continuous plots. startTime = this.FindPreviousDataPoint <T>(startTime, interval); } if (endTime < DateTime.MaxValue) { // Extend the start time to include the next data point to facilitate continuous plots. endTime = this.FindNextDataPoint <T>(endTime, interval); } return(this.GetOrCreateSummaryCache(streamSource, interval).ReadSummary <T>(viewMode, startTime, endTime, tailCount, tailRange)); }
/// <inheritdoc /> public void Dispose() { lock (this.bufferLock) { if (this.needsDisposing) { this.data.CollectionChanged -= this.OnCollectionChanged; foreach (var message in this.data) { var item = message.Data; (item as IDisposable).Dispose(); } foreach (var message in this.dataBuffer) { var item = message.Data; (item as IDisposable).Dispose(); } } this.data.Clear(); this.data = null; this.dataBuffer.Clear(); this.dataBuffer = null; this.index.Clear(); this.index = null; this.indexBuffer.Clear(); this.indexBuffer = null; this.pool?.Dispose(); this.pool = null; this.streamAdapter?.Dispose(); } }
/// <summary> /// Purges the cache of summary views which do not include the specified protected range, but /// only if the total number of underlying data items across all the views exceeds the limit. /// </summary> /// <param name="protectedViewKey">The key associated with the protected view.</param> /// <param name="protectedView">The protected view.</param> private void PurgeSummaryViews( Tuple <DateTime, DateTime, uint, Func <DateTime, DateTime> > protectedViewKey, ObservableKeyedCache <DateTime, IntervalData <TDest> > .ObservableKeyedView protectedView) { // Check if we need to purge if (this.cachedSummaryViews.Values.Sum(v => v.Count) <= this.maxCacheSize) { return; } // List of cached views, ordered by (startTime, endTime) var startTime = protectedViewKey.Item1; var endTime = protectedViewKey.Item2; var cachedViews = this.cachedSummaryViews.OrderBy(v => v.Key.Item1).ThenBy(v => v.Key.Item2).ToList(); foreach (var existingView in cachedViews) { // Skip overlapping views and preserve them if ((startTime < existingView.Key.Item2) && (endTime > existingView.Key.Item1)) { continue; } // Remove all disjoint views that do not touch the current view range this.cachedSummaryViews.Remove(existingView.Key); } // Check whether the cached views still exceed the limit. The above removal may not have // removed enough. If so, then simply clear the entire cache, leaving just the current // protected view. if (this.cachedSummaryViews.Values.Sum(v => v.Count) > this.maxCacheSize) { this.cachedSummaryViews.Clear(); this.cachedSummaryViews.Add(protectedViewKey, protectedView); } }
/// <summary> /// Initializes a new instance of the <see cref="StreamUpdateWithView{T}"/> class. /// </summary> /// <param name="streamUpdate">An existing stream update to convert to a stream update with view.</param> /// <param name="view">The view of the data cache spanning the duration of the update.</param> public StreamUpdateWithView(StreamUpdate <T> streamUpdate, ObservableKeyedCache <DateTime, Message <T> > .ObservableKeyedView view) : base(streamUpdate.UpdateType, streamUpdate.Message) { this.View = view; }
/// <summary> /// Returns a view over the summary data and ensure that the view is preserved in the cache. /// </summary> /// <param name="viewMode">The view mode.</param> /// <param name="startTime">Start stime of the view.</param> /// <param name="endTime">End time of the view.</param> /// <param name="tailCount">Number of items to include in view.</param> /// <param name="tailRange">Tail duration function.</param> /// <returns>The requested summary view.</returns> private ObservableKeyedCache <DateTime, IntervalData <TDest> > .ObservableKeyedView GetCachedSummaryView( ObservableKeyedCache <DateTime, IntervalData <TDest> > .ObservableKeyedView.ViewMode viewMode, DateTime startTime, DateTime endTime, uint tailCount, Func <DateTime, DateTime> tailRange) { ObservableKeyedCache <DateTime, IntervalData <TDest> > .ObservableKeyedView newView; var newViewKey = Tuple.Create(startTime, endTime, tailCount, tailRange); if (!this.cachedSummaryViews.TryGetValue(newViewKey, out newView)) { // Create the requested view over the cached summary data. newView = this.summaryCache.GetView(viewMode, startTime, endTime, tailCount, tailRange); // Retain cached data by maintaining a table of summary views for which we want the data to be retained. // This is currently done for fixed mode views only, as the views are constantly being updated in live // mode, so it probably makes sense to just defer to the underlying cache to manage data retention. if (viewMode == ObservableKeyedCache <DateTime, IntervalData <TDest> > .ObservableKeyedView.ViewMode.Fixed) { // List of cached views, ordered by (startTime, endTime) var cachedViews = this.cachedSummaryViews.OrderBy(v => v.Key.Item1).ThenBy(v => v.Key.Item2).ToList(); foreach (var existingView in cachedViews) { // Terminate when we have passed the end time of the new view if (existingView.Key.Item1 > endTime) { break; } // Skip views that are disjoint from the new view if (existingView.Key.Item2 < startTime) { continue; } // Extend start time to include existing overlapping view if (startTime > existingView.Key.Item1) { startTime = existingView.Key.Item1; } // Extend end time to include existing overlapping view if (endTime < existingView.Key.Item2) { endTime = existingView.Key.Item2; } // Remove existing overlapping view which will be subsumed by the new range (startTime, endTime) this.cachedSummaryViews.Remove(existingView.Key); } // Get a new view covering the expanded time range var adjustedView = this.summaryCache.GetView(viewMode, startTime, endTime, tailCount, tailRange); // Add it to the table of cached views to preserve the underlying data var adjustedViewKey = Tuple.Create(startTime, endTime, tailCount, tailRange); this.cachedSummaryViews.Add(adjustedViewKey, adjustedView); // Prune the table of cached views to keep them within the cache limit this.PurgeSummaryViews(newViewKey, newView); } } // Remove references to stream views that we no longer need this.PurgeStreamViews(); return(newView); }