Skip to content
Snippets Groups Projects
Commit dee4ad60 authored by Tim Übelhör's avatar Tim Übelhör
Browse files

Moved the OxyPlot dataconversion to its own thread.

Quite a few performance tweaks.
parent d45c1a05
No related branches found
No related tags found
1 merge request!5Central value repository
Showing
with 836 additions and 369 deletions
......@@ -44,6 +44,7 @@
<OutputPath>bin\x64\Release\</OutputPath>
<PlatformTarget>x64</PlatformTarget>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
......@@ -59,6 +60,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Classes\Channel.cs" />
<Compile Include="Classes\CircularBuffer.cs" />
<Compile Include="Classes\EnumerationChannel.cs" />
<Compile Include="Classes\LinearChannelLink.cs" />
<Compile Include="Classes\BufferedSamplesTable.cs" />
......
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ModeliChart.Basics
......
using System;
using System.Collections.Generic;
using System.Collections;
namespace CircularBuffer
{
/// <inheritdoc/>
/// <summary>
/// Circular buffer.
///
/// When writing to a full buffer:
/// PushBack -> removes this[0] / Front()
/// PushFront -> removes this[Size-1] / Back()
///
/// this implementation is inspired by
/// http://www.boost.org/doc/libs/1_53_0/libs/circular_buffer/doc/circular_buffer.html
/// because I liked their interface.
/// </summary>
public class CircularBuffer<T> : IEnumerable<T>
{
private readonly T[] _buffer;
/// <summary>
/// The _start. Index of the first element in buffer.
/// </summary>
private int _start;
/// <summary>
/// The _end. Index after the last element in the buffer.
/// </summary>
private int _end;
/// <summary>
/// The _size. Buffer size.
/// </summary>
private int _size;
public CircularBuffer(int capacity)
: this(capacity, new T[] { })
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CircularBuffer{T}"/> class.
///
/// </summary>
/// <param name='capacity'>
/// Buffer capacity. Must be positive.
/// </param>
/// <param name='items'>
/// Items to fill buffer with. Items length must be less than capacity.
/// Suggestion: use Skip(x).Take(y).ToArray() to build this argument from
/// any enumerable.
/// </param>
public CircularBuffer(int capacity, T[] items)
{
if (capacity < 1)
{
throw new ArgumentException(
"Circular buffer cannot have negative or zero capacity.", nameof(capacity));
}
if (items == null)
{
throw new ArgumentNullException(nameof(items));
}
if (items.Length > capacity)
{
throw new ArgumentException(
"Too many items to fit circular buffer", nameof(items));
}
_buffer = new T[capacity];
Array.Copy(items, _buffer, items.Length);
_size = items.Length;
_start = 0;
_end = _size == capacity ? 0 : _size;
}
/// <summary>
/// Maximum capacity of the buffer. Elements pushed into the buffer after
/// maximum capacity is reached (IsFull = true), will remove an element.
/// </summary>
public int Capacity { get { return _buffer.Length; } }
public bool IsFull
{
get
{
return Size == Capacity;
}
}
public bool IsEmpty
{
get
{
return Size == 0;
}
}
/// <summary>
/// Current buffer size (the number of elements that the buffer has).
/// </summary>
public int Size { get { return _size; } }
/// <summary>
/// Element at the front of the buffer - this[0].
/// </summary>
/// <returns>The value of the element of type T at the front of the buffer.</returns>
public T Front()
{
ThrowIfEmpty();
return _buffer[_start];
}
/// <summary>
/// Element at the back of the buffer - this[Size - 1].
/// </summary>
/// <returns>The value of the element of type T at the back of the buffer.</returns>
public T Back()
{
ThrowIfEmpty();
return _buffer[(_end != 0 ? _end : Capacity) - 1];
}
public T this[int index]
{
get
{
if (IsEmpty)
{
throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer is empty", index));
}
if (index >= _size)
{
throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer size is {1}", index, _size));
}
int actualIndex = InternalIndex(index);
return _buffer[actualIndex];
}
set
{
if (IsEmpty)
{
throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer is empty", index));
}
if (index >= _size)
{
throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer size is {1}", index, _size));
}
int actualIndex = InternalIndex(index);
_buffer[actualIndex] = value;
}
}
/// <summary>
/// Pushes a new element to the back of the buffer. Back()/this[Size-1]
/// will now return this element.
///
/// When the buffer is full, the element at Front()/this[0] will be
/// popped to allow for this new element to fit.
/// </summary>
/// <param name="item">Item to push to the back of the buffer</param>
public void PushBack(T item)
{
if (IsFull)
{
_buffer[_end] = item;
Increment(ref _end);
_start = _end;
}
else
{
_buffer[_end] = item;
Increment(ref _end);
++_size;
}
}
/// <summary>
/// Pushes a new element to the front of the buffer. Front()/this[0]
/// will now return this element.
///
/// When the buffer is full, the element at Back()/this[Size-1] will be
/// popped to allow for this new element to fit.
/// </summary>
/// <param name="item">Item to push to the front of the buffer</param>
public void PushFront(T item)
{
if (IsFull)
{
Decrement(ref _start);
_end = _start;
_buffer[_start] = item;
}
else
{
Decrement(ref _start);
_buffer[_start] = item;
++_size;
}
}
/// <summary>
/// Removes the element at the back of the buffer. Decreasing the
/// Buffer size by 1.
/// </summary>
public void PopBack()
{
ThrowIfEmpty("Cannot take elements from an empty buffer.");
Decrement(ref _end);
_buffer[_end] = default;
--_size;
}
/// <summary>
/// Removes the element at the front of the buffer. Decreasing the
/// Buffer size by 1.
/// </summary>
public void PopFront()
{
ThrowIfEmpty("Cannot take elements from an empty buffer.");
_buffer[_start] = default;
Increment(ref _start);
--_size;
}
/// <summary>
/// Copies the buffer contents to an array, according to the logical
/// contents of the buffer (i.e. independent of the internal
/// order/contents)
/// </summary>
/// <returns>A new array with a copy of the buffer contents.</returns>
public T[] ToArray()
{
T[] newArray = new T[Size];
int newArrayOffset = 0;
var segments = new ArraySegment<T>[2] { ArrayOne(), ArrayTwo() };
foreach (ArraySegment<T> segment in segments)
{
Array.Copy(segment.Array, segment.Offset, newArray, newArrayOffset, segment.Count);
newArrayOffset += segment.Count;
}
return newArray;
}
#region IEnumerable<T> implementation
public IEnumerator<T> GetEnumerator()
{
var segments = new ArraySegment<T>[2] { ArrayOne(), ArrayTwo() };
foreach (ArraySegment<T> segment in segments)
{
for (int i = 0; i < segment.Count; i++)
{
yield return segment.Array[segment.Offset + i];
}
}
}
#endregion
#region IEnumerable implementation
IEnumerator IEnumerable.GetEnumerator()
{
return (IEnumerator)GetEnumerator();
}
#endregion
private void ThrowIfEmpty(string message = "Cannot access an empty buffer.")
{
if (IsEmpty)
{
throw new InvalidOperationException(message);
}
}
/// <summary>
/// Increments the provided index variable by one, wrapping
/// around if necessary.
/// </summary>
/// <param name="index"></param>
private void Increment(ref int index)
{
if (++index == Capacity)
{
index = 0;
}
}
/// <summary>
/// Decrements the provided index variable by one, wrapping
/// around if necessary.
/// </summary>
/// <param name="index"></param>
private void Decrement(ref int index)
{
if (index == 0)
{
index = Capacity;
}
index--;
}
/// <summary>
/// Converts the index in the argument to an index in <code>_buffer</code>
/// </summary>
/// <returns>
/// The transformed index.
/// </returns>
/// <param name='index'>
/// External index.
/// </param>
private int InternalIndex(int index)
{
return _start + (index < (Capacity - _start) ? index : index - Capacity);
}
// doing ArrayOne and ArrayTwo methods returning ArraySegment<T> as seen here:
// http://www.boost.org/doc/libs/1_37_0/libs/circular_buffer/doc/circular_buffer.html#classboost_1_1circular__buffer_1957cccdcb0c4ef7d80a34a990065818d
// http://www.boost.org/doc/libs/1_37_0/libs/circular_buffer/doc/circular_buffer.html#classboost_1_1circular__buffer_1f5081a54afbc2dfc1a7fb20329df7d5b
// should help a lot with the code.
#region Array items easy access.
// The array is composed by at most two non-contiguous segments,
// the next two methods allow easy access to those.
private ArraySegment<T> ArrayOne()
{
if (_start < _end)
{
return new ArraySegment<T>(_buffer, _start, _end - _start);
}
else
{
return new ArraySegment<T>(_buffer, _start, _buffer.Length - _start);
}
}
private ArraySegment<T> ArrayTwo()
{
if (_start < _end)
{
return new ArraySegment<T>(_buffer, _end, 0);
}
else
{
return new ArraySegment<T>(_buffer, 0, _end);
}
}
#endregion
}
}
using System.Collections.Generic;
using System.Linq;
using CircularBuffer;
namespace ModeliChart.Basics
{
// Using queues to store the data as its implementation is actually a circular buffer.
// This avoids high frequent garbage collections.
// Using the Beer-Ware (hooray!) licensed circular buffer implementation by Joao Portela
// to prevent the gc from collecting lots of samples.
public class CyclicSamplesTable
{
private readonly Queue<double> timeQueue;
private readonly IDictionary<uint, (Queue<double> Queue, double LastValue)> data;
private readonly CircularBuffer<double> timeQueue;
private readonly IDictionary<uint, (CircularBuffer<double> Queue, double LastValue)> data;
private readonly object dataLock = new object();
// Limit the memory usage
private readonly int capacity;
......@@ -16,10 +17,10 @@ namespace ModeliChart.Basics
public CyclicSamplesTable(IEnumerable<uint> valueRefs, int capacity = 60000)
{
this.capacity = capacity;
timeQueue = new Queue<double>(capacity);
timeQueue = new CircularBuffer<double>(capacity);
data = valueRefs
.ToDictionary(vr => vr,
vr => (new Queue<double>(capacity), 0.0));
vr => (new CircularBuffer<double>(capacity), 0.0));
}
public void AddSamples(double time, IEnumerable<uint> valueRefs, IEnumerable<double> values)
......@@ -27,22 +28,12 @@ namespace ModeliChart.Basics
var zipped = valueRefs.Zip(values, (vr, value) => (vr, value));
lock (dataLock)
{
// Limit size to prevent reallocation
while (timeQueue.Count >= capacity)
{
timeQueue.Dequeue();
}
timeQueue.Enqueue(time);
timeQueue.PushBack(time);
foreach (var (vr, value) in zipped)
{
var (Queue, LastValue) = data[vr];
// Limit size to prevent reallocation
while (Queue.Count >= capacity)
{
Queue.Dequeue();
}
Queue.Enqueue(value);
LastValue = value;
var (queue, lastValue) = data[vr];
queue.PushBack(value);
lastValue = value;
}
}
}
......@@ -51,13 +42,19 @@ namespace ModeliChart.Basics
{
lock (dataLock)
{
timeQueue.Clear();
while (!timeQueue.IsEmpty)
{
timeQueue.PopFront();
}
// Use key because we cannot modify the iterated variables... I know good Hackerboi :D
foreach (var key in data.Keys)
{
var (Queue, LastValue) = data[key];
Queue.Clear();
LastValue = 0;
var (queue, lastValue) = data[key];
while (!queue.IsEmpty)
{
queue.PopFront();
}
lastValue = 0;
}
}
}
......@@ -73,11 +70,20 @@ namespace ModeliChart.Basics
public IEnumerable<(double Time, double Value)> GetSamples(uint valueRef)
{
lock (dataLock)
{
if (timeQueue.IsEmpty)
{
// Empty queue fails to execute ToArray
return Enumerable.Empty<(double Time, double Value)>();
}
else
{
// It is important to copy the data, because the linq query will be executed delayed
return timeQueue.ToArray()
.Zip(data[valueRef].Queue.ToArray(),
(time, value) => (time, value));
return timeQueue
.Zip(data[valueRef].Queue,
(time, value) => (time, value))
.ToArray();
}
}
}
......
......@@ -3,6 +3,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System.Linq;
using System.Data;
......@@ -81,20 +82,27 @@ namespace ModeliChart.Basics
{
writeCache = new BlockingCollection<double[]>();
}
writeTask = Task.Run(() =>
// Create longrunning! task
writeTask = new Task(() =>
{
try
{
while (!writeCache.IsCompleted)
{
// Use less resources by waiting for new values
if (writeCache.TryTake(out var valueBuffer, 100))
{
var valueBuffer = writeCache.Take();
// Convert to byte array and write it
byte[] byteBuffer = new byte[valueBuffer.Length * sizeof(double)];
Buffer.BlockCopy(valueBuffer, 0, byteBuffer, 0, byteBuffer.Length);
stream.Write(byteBuffer, 0, byteBuffer.Length);
}
}
});
catch (InvalidOperationException)
{
// The collection has been completed while taking.
}
}, TaskCreationOptions.LongRunning);
writeTask.Start();
}
private async Task StopWriteTaskAsync()
......
......@@ -12,13 +12,13 @@ namespace ModeliChart.Basics
/// </summary>
public interface ISimulation : IDisposable
{
Task<double> GetCurrentTime();
double CurrentTime { get; }
/// <summary>
/// Get the simulations step size in seconds.
/// </summary>
/// <returns></returns>
Task<double> GetStepSize();
double GetStepSize();
/// <summary>
/// Set the simulations step size in seconds.
......
......@@ -46,6 +46,7 @@
<OutputPath>bin\x64\Release\</OutputPath>
<PlatformTarget>x64</PlatformTarget>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<None Include="App.config" />
......
......@@ -16,7 +16,7 @@ namespace ModeliChart.Files
/// <summary>
/// Where the proto file is stored.
/// </summary>
private string path;
private readonly string path;
/// <summary>
/// The simulation will be stored in a proto file (path).
......@@ -37,7 +37,7 @@ namespace ModeliChart.Files
return simulation;
}
public async Task SaveSimulationAsync(ISimulation simulation)
public Task SaveSimulationAsync(ISimulation simulation)
{
// Load the proto and then update it
var simulationSettings = LoadSimulationSettings();
......@@ -47,12 +47,13 @@ namespace ModeliChart.Files
simulationSettings.FmuInstances.Clear();
simulationSettings.FmuInstances.AddRange(simulation.ModelInstances.Select(ProtoConverter.ToProtoFmuInstance));
simulationSettings.RemoteSettings = ProtoConverter.ToProtoRemoteSettings(simulation);
simulationSettings.StepSize = await simulation.GetStepSize().ConfigureAwait(false);
simulationSettings.StepSize = simulation.GetStepSize();
// Save changes
using (var fs = File.OpenWrite(path))
{
simulationSettings.WriteTo(fs);
}
return Task.CompletedTask;
}
......
......@@ -95,7 +95,7 @@ namespace ModeliChart.LocalMode
var (ValueRefs, Values) = instance.GetCurrentValues();
NewValuesAvailable?.Invoke(this, (instance.Name, currentTime, ValueRefs, Values));
}
currentTime += stepSize;
Interlocked.Exchange(ref currentTime, currentTime + stepSize);
// Execute pending tasks
while (taskQueue.ExecuteOne())
{
......@@ -183,12 +183,16 @@ namespace ModeliChart.LocalMode
modelInstance.Dispose();
}
public Task<double> GetStepSize() => taskQueue.Add(() => stepSize);
public double GetStepSize() => stepSize;
public Task SetStepSize(double stepSize) => taskQueue.Add(() =>
public Task SetStepSize(double stepSize)
{
if (stepSize > 0) this.stepSize = stepSize;
});
if (stepSize > 0)
{
Interlocked.Exchange(ref this.stepSize, stepSize);
}
return Task.CompletedTask;
}
/// <summary>
/// Modifying the elements directly is not threadsafe!
......@@ -212,7 +216,6 @@ namespace ModeliChart.LocalMode
public Task SetValue(IChannel channel, double value) =>
taskQueue.Add(() => fmuInstances[channel.ModelInstanceName].SetValue(channel, value));
public Task<double> GetCurrentTime() =>
taskQueue.Add(() => currentTime);
public double CurrentTime => currentTime;
}
}
......@@ -38,7 +38,7 @@ namespace ModeliChart.LocalMode
/// <returns>True if a task has been executed otherwise false.</returns>
public bool ExecuteOne()
{
// Only run task if the loop is not running
// Only run task if the loop is not running, do not block!
if (_taskQueue.TryTake(out Task task) && _loopTask.IsCompleted)
{
task.RunSynchronously();
......
......@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
namespace ModeliChart.RemoteMode
{
......@@ -17,7 +18,6 @@ namespace ModeliChart.RemoteMode
private readonly List<LinearChannelLink> channelLinks = new List<LinearChannelLink>();
private double stepSize = 0.01;
private double currentTime = 0;
private readonly object currentTimeLock = new object();
public event EventHandler<(string ModelInstanceName, double Time, IEnumerable<uint> ValueRefs, IEnumerable<double> Values)> NewValuesAvailable;
......@@ -30,7 +30,7 @@ namespace ModeliChart.RemoteMode
public RemoteSimulation(string targetUri)
{
rpcClient = new ModeliGrpcClient(targetUri);
rpcClient.LogArrived += _rpc_LogArrived;
rpcClient.LogArrived += Rpc_LogArrived;
rpcClient.NewValuesArrived += RpcClient_NewValuesArrived;
}
......@@ -46,16 +46,12 @@ namespace ModeliChart.RemoteMode
values.AddRange(e.IntegerValues.Select(value => Convert.ToDouble(value)));
values.AddRange(e.RealValues);
values.AddRange(e.BoolValues.Select(value => Convert.ToDouble(value)));
// Invoke changes
lock (currentTimeLock)
{
currentTime = e.Timestamp;
}
Interlocked.Exchange(ref currentTime, e.Timestamp);
NewValuesAvailable?.Invoke(this, (e.InstanceName, e.Timestamp, valueRefs, values));
}
private void _rpc_LogArrived(object sender, RemoteLogEventArgs e)
private void Rpc_LogArrived(object sender, RemoteLogEventArgs e)
{
ConsoleLogger.WriteLine(e.InstanceName, "status: " + e.Status
+ ", category: " + e.Category + ", message: " + e.Message);
......@@ -65,7 +61,7 @@ namespace ModeliChart.RemoteMode
public string TargetUri => rpcClient.TargetUri;
public Task<double> GetStepSize() => Task.FromResult(stepSize);
public double GetStepSize() =>stepSize;
public async Task SetStepSize(double stepSize)
{
......@@ -110,11 +106,8 @@ namespace ModeliChart.RemoteMode
{
ConsoleLogger.WriteLine("RemoteSimulation", "Sttop failed.");
}
lock (currentTimeLock)
{
currentTime = 0;
}
}
public async Task ConnectAsync()
{
......@@ -193,12 +186,6 @@ namespace ModeliChart.RemoteMode
public Task SetValue(IChannel channel, double value) =>
modelInstances[channel.ModelInstanceName].SetValueAsync(channel.ValueRef, value);
public Task<double> GetCurrentTime()
{
lock (currentTimeLock)
{
return Task.FromResult(currentTime);
}
}
public double CurrentTime => currentTime;
}
}
\ No newline at end of file
......@@ -49,15 +49,26 @@ namespace ModeliChart.UI
}
/// <summary>
/// Updates the instruments time to the given time.
/// Updates the instruments data to the given time.
/// It does not refresh the view, call InvalidateArea!
/// </summary>
/// <param name="value"></param>
public void SetCurrentTime(double value)
public void UpdateDataToTime(double time)
{
foreach(var instrument in instruments)
{
instrument.SetCurrentTime(value);
instrument.RefreshInstrument();
instrument.UpdateDataToTime(time);
}
}
/// <summary>
/// Refreshes the data view.
/// </summary>
public void InvalidateArea(double currentTime)
{
foreach(var instrument in instruments)
{
instrument.InvalidateInstrument(currentTime);
}
}
......
......@@ -9,6 +9,7 @@ using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using WeifenLuo.WinFormsUI.Docking;
......@@ -20,8 +21,6 @@ namespace ModeliChart.UI
/// </summary>
public partial class MainWindow : RibbonForm
{
// The UI Refresh timer
private Timer timerUi = new Timer();
private Workspace workspace = new Workspace(Workspace.DefaultPath);
private ISimulation simulation;
private IDataRepository dataRepository;
......@@ -29,6 +28,11 @@ namespace ModeliChart.UI
// Display areas
private List<ModelInstanceArea> _dataSourceAreas = new List<ModelInstanceArea>();
private List<InstrumentArea> instrumentAreas = new List<InstrumentArea>();
// UI worker task
private const int REFRESH_RATE = 30; // milliseconds
private readonly System.Windows.Forms.Timer refreshTimer = new System.Windows.Forms.Timer();
private readonly Task oxyConversionTask;
private readonly CancellationTokenSource oxyConversionCanceller = new CancellationTokenSource();
// Create a new Mainwindow with all dependencies injected
......@@ -40,10 +44,14 @@ namespace ModeliChart.UI
this.simulation = simulation;
this.dataRepository = dataRepository;
dataStorer = new SimulationDataStorer(simulation, dataRepository);
// Init timer
timerUi.Interval = 20; // In [ms]
timerUi.Tick += TimerUI_Tick;
timerUi.Start();
// Init refresh task
refreshTimer.Interval = REFRESH_RATE;
refreshTimer.Tick += RefreshTimer_Tick;
refreshTimer.Start();
oxyConversionTask = new Task(
() => OxyConversionLoop(REFRESH_RATE, oxyConversionCanceller.Token),
TaskCreationOptions.LongRunning);
oxyConversionTask.Start();
// Make it good looking
dockPanel.Theme = new VS2015LightTheme();
// Show the console
......@@ -54,6 +62,42 @@ namespace ModeliChart.UI
StatusLogger.Status = "Starting";
}
private void RefreshTimer_Tick(object sender, EventArgs e)
{
var currentTime = simulation.CurrentTime;
// Invoke refresh on UI thread
foreach (var area in instrumentAreas)
{
area.InvalidateArea(currentTime);
}
}
/// <summary>
/// Keeps looping and refreshes the UI approximately every interval ms.
/// </summary>
/// <param name="interval"></param>
private void OxyConversionLoop(int interval, CancellationToken token)
{
long nextUpdate = 0;
System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();
while (!token.IsCancellationRequested)
{
var elapsedMs = stopwatch.ElapsedMilliseconds;
if (elapsedMs > nextUpdate)
{
// Update the data in the Task which calls this method
var currentTime = simulation.CurrentTime;
foreach (var area in instrumentAreas)
{
area.UpdateDataToTime(currentTime);
}
nextUpdate += interval;
}
Thread.Sleep(1);
}
}
/// <summary>
/// Replace the current simulation with a new one.
/// </summary>
......@@ -133,15 +177,6 @@ namespace ModeliChart.UI
}
#endregion
private async void TimerUI_Tick(object sender, EventArgs e)
{
var currentTime = await simulation.GetCurrentTime();
foreach (var item in instrumentAreas)
{
item.SetCurrentTime(currentTime);
}
}
/// <summary>
/// When the mainWindow is done loading
/// Create the InstrumentArea
......@@ -162,6 +197,8 @@ namespace ModeliChart.UI
{
await SaveWorkspaceAsync();
await simulation.Stop();
oxyConversionCanceller.Cancel();
await oxyConversionTask;
// Free all resources from the simulation
simulation.Dispose();
}
......@@ -246,7 +283,7 @@ namespace ModeliChart.UI
{
try
{
await simulation.Play();
await simulation.Play().ConfigureAwait(false);
}
catch (Exception ex)
{
......@@ -259,7 +296,7 @@ namespace ModeliChart.UI
{
try
{
await simulation.PlayFast(res);
await simulation.PlayFast(res).ConfigureAwait(false);
}
catch (Exception ex)
{
......@@ -273,7 +310,7 @@ namespace ModeliChart.UI
{
try
{
await simulation.Pause();
await simulation.Pause().ConfigureAwait(false);
}
catch (Exception ex)
{
......@@ -329,7 +366,7 @@ namespace ModeliChart.UI
// Shows or hides the DataSource Areas
public void CheckBoxShowChannels_CheckChanged(object sender, EventArgs e)
{
if (this.ribChkBoxShowAvailableChannels.Checked == true)
if (ribChkBoxShowAvailableChannels.Checked == true)
{
for (int i = 0; i < _dataSourceAreas.Count; i++)
{
......@@ -397,7 +434,7 @@ namespace ModeliChart.UI
if (int.TryParse(ribTxtSimInterval.TextBoxText, out int Hz) && Hz > 0)
{
// Update the Settings, on next Play the Rate will be updated
await simulation.SetStepSize(1.0 / Hz);
await simulation.SetStepSize(1.0 / Hz).ConfigureAwait(false);
}
}
private void RibTxtUIInterval_TextBoxTextChanged(object sender, EventArgs e)
......@@ -406,7 +443,7 @@ namespace ModeliChart.UI
if (int.TryParse(ribTxtUIInterval.TextBoxText, out int Hz) && Hz > 0)
{
// Set interval in milliseconds
timerUi.Interval = (int)((1.0 / Hz) * 1000);
// TODO timerUi.Interval = (int)((1.0 / Hz) * 1000);
// Save to persistence
workspace.UiConfig.RefreshRate_Hz = Hz;
}
......@@ -602,7 +639,7 @@ namespace ModeliChart.UI
}
StatusLogger.Progress = 90;
// Load time values
ribTxtSimInterval.TextBoxText = (1.0 / await simulation.GetStepSize()).ToString();
ribTxtSimInterval.TextBoxText = (1.0 / simulation.GetStepSize()).ToString();
ribTxtUIInterval.TextBoxText = workspace.UiConfig.RefreshRate_Hz.ToString();
}
catch (Exception ex)
......@@ -722,7 +759,7 @@ namespace ModeliChart.UI
// Show the dialog
ExportDialog.ShowDialog(dataRepository,
simulation.ModelInstances.SelectMany(mi => mi.Channels),
await simulation.GetStepSize());
simulation.GetStepSize());
}
private async void RibOrbItImport_Click(object sender, EventArgs e)
......
......@@ -15,7 +15,6 @@ namespace ModeliChart.UserControls
protected IChannel channel;
protected readonly ISimulation simulation;
protected readonly IDataRepository dataRepository;
protected double currentTime;
public virtual double DisplayedInterval { get; set; }
......@@ -188,15 +187,10 @@ namespace ModeliChart.UserControls
#endregion
public void SetCurrentTime(double value)
{
currentTime = value;
}
/// <summary>
/// Refresh the instruments view.
/// Refresh the data view.
/// </summary>
public virtual void RefreshInstrument()
public virtual void InvalidateInstrument(double currentTime)
{
throw new NotImplementedException();
}
......
using ModeliChart.Basics;
using OxyPlot;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
namespace ModeliChart.UserControls
{
/// <summary>
/// The access is threadsafe.
/// </summary>
internal class OxyPlotDataConverter
{
private readonly IDataRepository dataRepository;
private IEnumerable<(DataPoint[] values, DataPoint annotation)> data;
private readonly object dataLock = new object();
public OxyPlotDataConverter(IDataRepository dataRepository)
{
this.dataRepository = dataRepository;
}
/// <summary>
/// Executes the conversion of the query.
/// </summary>
private DataPoint[] Convert(IEnumerable<(double X, double Y)> values) =>
values
.Select(p => new DataPoint(p.X, p.Y))
.ToArray();
/// <summary>
/// Qeries the data from the repository and cuts it to the given interval.
/// </summary>
private IEnumerable<(double Time, double Value)> PollData(IChannel channel, double minTime, double maxTime) =>
from point in dataRepository.GetValues(channel)
where point.Time >= minTime && point.Time <= maxTime
select point;
private ((double Time, double Value) SweepPoint, IEnumerable<(double Time, double Value)> Values) PollDataSweep(
IChannel channel, double minTime, double maxTime, double sweepTime)
{
var channelData = PollData(channel, minTime, maxTime);
var beforeSweepTime = from point in channelData
where point.Time < sweepTime
select (point.Time + maxTime - minTime, point.Value);
var afterSweepTime = from point in channelData
where point.Time >= sweepTime
select point;
return (afterSweepTime.LastOrDefault(), afterSweepTime.Concat(beforeSweepTime));
}
/// <summary>
/// Queries the data for two channels and uses the value of channelX as x-Coordinate
/// and the value of channelY as y-Coordinate of the points.
/// </summary>
private ((double X, double Y) Annotation, IEnumerable<(double X, double Y)> Values) PollDataTwoChannel(
IChannel channelX, IChannel channelY, double minTime, double maxTime)
{
var values = from pointX in PollData(channelX, minTime, maxTime)
join pointY in PollData(channelY, minTime, maxTime) on pointX.Time equals pointY.Time
select (pointX.Value, pointY.Value);
return (values.LastOrDefault(), values);
}
/// <summary>
/// Convert the data of multiple channels.
/// </summary>
/// <param name="channels"></param>
/// <param name="minTime"></param>
/// <param name="maxTime"></param>
public void PollDataMultiChannel(IEnumerable<IChannel> channels, double minTime, double maxTime)
{
lock (dataLock)
{
data = from channel in channels
select (Convert(PollData(channel, minTime, maxTime)), DataPoint.Undefined);
}
}
public void PollDataMultiChannelSweep(IEnumerable<IChannel> channels, double minTime, double maxTime, double sweepTime)
{
lock (dataLock)
{
data = from channel in channels
let polled = PollDataSweep(channel, minTime, maxTime, sweepTime)
select (Convert(polled.Values), new DataPoint(polled.SweepPoint.Time, polled.SweepPoint.Value));
}
}
/// <summary>
/// Convert the data of multiple channel pairs.
/// Uuses the value of channelX as x-Coordinate
/// and the value of channelY as y-Coordinate of the points.
/// </summary>
/// <param name="tuples"></param>
/// <param name="minTime"></param>
/// <param name="maxTime"></param>
public void PollDataTwoChannel(IEnumerable<(IChannel channelX, IChannel channelY)> tuples, double minTime, double maxTime)
{
lock (dataLock)
{
data = from tuple in tuples
let polled = PollDataTwoChannel(tuple.channelX, tuple.channelY, minTime, maxTime)
select (Convert(polled.Values), new DataPoint(polled.Annotation.X, polled.Annotation.Y));
}
}
/// <summary>
/// This call blocks until new converted data is available.
/// </summary>
/// <returns></returns>
public IEnumerable<(DataPoint[] Values, DataPoint Annotation)> GetData()
{
// Execute the query by calling .ToArray()
// Otherwise it might be evaluated somewhere else while it changes.
lock (dataLock)
{
if (data == null)
{
return Enumerable.Empty<(DataPoint[] Values, DataPoint Annotation)>();
}
else
{
return data.ToArray();
}
}
}
}
}
......@@ -59,17 +59,14 @@ namespace ModeliChart.UserControls
private void MenuItem10s_Click(object sender, System.EventArgs e)
{
DisplayedInterval = 10;
RefreshInstrument();
}
private void MenuItem20s_Click(object sender, System.EventArgs e)
{
DisplayedInterval = 20;
RefreshInstrument();
}
private void MenuItem30s_Click(object sender, System.EventArgs e)
{
DisplayedInterval = 30;
RefreshInstrument();
}
private void MenuItemCustomTime_Click(object sender, EventArgs e)
{
......@@ -79,7 +76,6 @@ namespace ModeliChart.UserControls
out var interval))
{
DisplayedInterval = interval;
RefreshInstrument();
}
}
/// <summary>
......
This diff is collapsed.
......@@ -60,6 +60,7 @@
<OutputPath>bin\x64\Release\</OutputPath>
<PlatformTarget>x64</PlatformTarget>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<Compile Include="AvailableChannelEntry.cs">
......@@ -100,6 +101,7 @@
<DependentUpon>OxyOverlay.cs</DependentUpon>
</Compile>
<Compile Include="InstrumentTypes.cs" />
<Compile Include="OxyPlotDataConverter.cs" />
<Compile Include="UniversalOxyInstrument.cs">
<SubType>UserControl</SubType>
</Compile>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment