Generic data recorder
The purpose of the generic data recorder is to record sample-based data of small to moderate sample size from multiple sources (usually sensors) with moderate sampling rates (about 1 kHz). It is assumed that sensor samples are obtained independently, i.e. without hardware synchronization during acquisition, and with possibly different sampling rates. The library further assumes that the involved sensors do not produce time stamps; hence the high-precision timer of the host is used for time stamping. After recording a common timeline with all sensors synchronized onto a common sampling rate using linear interpolation is computed.
You can run the example presented in this document by cloning the Git repository and executing FsiAnyCPU.exe docs/content/recorder-usage.fsx
.
Sensor interface
The data recorder can obtain samples from any object that implements the ISensor<'T>
interface where 'T
is the data type of the sample and can be an arbitrary type (for example float []
).
Note that your sensor does not have to correspond to a hardware sensor.
In the context of this document a sensor can be anything, that periodically produces data samples.
This sensor interface is defined as follows
1: 2: 3: 4: |
|
To make your sensor compatible with the data recorder, implement this interface.
The DataType
property must return typeof<'T>
.
SampleAcquired
must be an event that your sensor raises each time a new sample is acquired.
The argument of the event must be the sample itself.
The Interpolate
method is used for computing a common timeline of multiple sensors after data acquisition is finished.
Your implementation of this method must perform the equivalent of linear interpolation for your sample data type between the two samples a
and b
using the interpolation factor fac
according to the formula: (1 - fac) * a + fac * b
.
Example sensor measuring performance counters
In this section we will write a simple sensor that queries a Windows performance counter (for example CPU utilization or available memory) at a fixed sampling interval.
Performance counter measurements have the data type float32
.
For the sake of decency we define a type alias.
1:
|
|
Then we write code for the sensor type and implement the ISensor<'T> interface.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: |
|
The samplingTimer
triggers the execution of the acquireSample
function every time the samplingInterval
has passed.
This function obtains a sample from the performance counter using NextValue
method and triggers the SampleAcquired
event of the SampleRecorder.ISensor<_>
interface.
Interpolation is simple in this case, the only thing we need to care about is the conversion of the interpolation factor fac
to our sample data type float32
.
Before using the recorder, let us test our sensor by instantiating it with a sampling interval of 100 ms and the performance counter for CPU load.
Then we attach a function that prints the current measurement every time the ISensor<_>.SampleAcquired
event is triggered.
1: 2: 3: 4: 5: 6: |
|
After waiting for two seconds we detach our event handler to stop the continuous output.
This will print something similar to
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: |
|
Let us instantiate a second sensor measuring available memory at an interval of 70 ms.
1:
|
|
Using the recorder
The sample recorder is provided by the SampleRecorder.Recorder<'T>
type where 'T
must be a (user-defined) record type that stores the data of all recorded sensors and has an additional field for the sample time of type float
.
The sample time is stored in seconds since the start of the recording.
Continuing with our example we define the sample record LoadSample
that captures CPU load and available memory.
1: 2: 3: 4: 5: |
|
Also note, that no additional fields may be present in the sample type.
The constructor of the recorder takes a list of sensors to record from as its only argument.
The sensors must be listed in the same order as they appear in the sample type 'T
.
We can now instantiate the recorder.
1: 2: |
|
During instantiation the recorder automatically subscribes to the sample events of your sensors.
We can control recording by calling the recorder.Start
and recorder.Stop
methods.
Let us record for two seconds.
1: 2: 3: |
|
Recording statistics can be outputted by calling the recorder.PrintStatistics
methods.
1:
|
|
This will print individual sensor statistics. For example, here we obtain:
1: 2: 3: 4: 5: 6: |
|
Getting the recorded samples
Use the recorder.GetSamples
method to obtain the recorded data samples on a common timeline.
The function takes an argument that can either be
Some interval
. In this case the specified interval is used on the common timeline.None
. In this case the average sampling interval of the fastest sensor is used.
Linear interpolation is performed to calculate the sensor data on the common timeline.
1:
|
|
The function returns a sequence of the sample type; that is LoadSample
in our case.
We convert the sequence to an array for efficient storage.
We can now print the recorded data.
1: 2: 3: |
|
The output will be similar to
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: |
|
Clearing the recorder's memory
The sample recorder can be reused without having to create a new instance by calling the recorder.Clear
method.
1:
|
|
Usage with BRML drivers
The BioTac and XY table drivers in this library already implement the sensor interface. Thus they can be directly used with the sample recorder. It is also recommended that you implement the recorder interface for your custom-built drivers.
Conclusion
The generic data recorder provides a simple, efficient way of obtaining data from multiple sensors and synchronizing them on a common time line. After recording, samples are returned in a user-defined sample record that combines the data from all sensors.
Full name: Microsoft.FSharp.Control.IEvent<_>
val float : value:'T -> float (requires member op_Explicit)
Full name: Microsoft.FSharp.Core.Operators.float
--------------------
type float = System.Double
Full name: Microsoft.FSharp.Core.float
--------------------
type float<'Measure> = float
Full name: Microsoft.FSharp.Core.float<_>
Full name: Recorder-usage.PerfSample
val float32 : value:'T -> float32 (requires member op_Explicit)
Full name: Microsoft.FSharp.Core.Operators.float32
--------------------
type float32 = System.Single
Full name: Microsoft.FSharp.Core.float32
--------------------
type float32<'Measure> = float32
Full name: Microsoft.FSharp.Core.float32<_>
type PerfSensor =
interface IDisposable
interface obj
new : categoryName:string * counterName:string * instanceName:string * samplingInterval:float -> PerfSensor
override Interpolate : fac:float -> a:float32 -> b:float32 -> float32
override DataType : Type
override SampleAcquired : IEvent<PerfSample>
Full name: Recorder-usage.PerfSensor
--------------------
new : categoryName:string * counterName:string * instanceName:string * samplingInterval:float -> PerfSensor
val string : value:'T -> string
Full name: Microsoft.FSharp.Core.Operators.string
--------------------
type string = System.String
Full name: Microsoft.FSharp.Core.string
type PerformanceCounter =
inherit Component
new : unit -> PerformanceCounter + 5 overloads
member BeginInit : unit -> unit
member CategoryName : string with get, set
member Close : unit -> unit
member CounterHelp : string
member CounterName : string with get, set
member CounterType : PerformanceCounterType
member Decrement : unit -> int64
member EndInit : unit -> unit
member Increment : unit -> int64
...
Full name: System.Diagnostics.PerformanceCounter
--------------------
PerformanceCounter() : unit
PerformanceCounter(categoryName: string, counterName: string) : unit
PerformanceCounter(categoryName: string, counterName: string, instanceName: string) : unit
PerformanceCounter(categoryName: string, counterName: string, readOnly: bool) : unit
PerformanceCounter(categoryName: string, counterName: string, instanceName: string, machineName: string) : unit
PerformanceCounter(categoryName: string, counterName: string, instanceName: string, readOnly: bool) : unit
module Event
from Microsoft.FSharp.Control
--------------------
type Event<'T> =
new : unit -> Event<'T>
member Trigger : arg:'T -> unit
member Publish : IEvent<'T>
Full name: Microsoft.FSharp.Control.Event<_>
--------------------
type Event<'Delegate,'Args (requires delegate and 'Delegate :> Delegate)> =
new : unit -> Event<'Delegate,'Args>
member Trigger : sender:obj * args:'Args -> unit
member Publish : IEvent<'Delegate,'Args>
Full name: Microsoft.FSharp.Control.Event<_,_>
--------------------
new : unit -> Event<'T>
--------------------
new : unit -> Event<'Delegate,'Args>
type Timer =
inherit Component
new : unit -> Timer + 1 overload
member AutoReset : bool with get, set
member BeginInit : unit -> unit
member Close : unit -> unit
member Enabled : bool with get, set
member EndInit : unit -> unit
member Interval : float with get, set
member Site : ISite with get, set
member Start : unit -> unit
member Stop : unit -> unit
...
Full name: System.Timers.Timer
--------------------
Timer() : unit
Timer(interval: float) : unit
Full name: Recorder-usage.PerfSensor.DataType
Full name: Microsoft.FSharp.Core.Operators.typeof
Full name: Recorder-usage.PerfSensor.SampleAcquired
Full name: Recorder-usage.PerfSensor.Interpolate
member Dispose : unit -> unit
Full name: System.IDisposable
Full name: Recorder-usage.PerfSensor.Dispose
Full name: Recorder-usage.cpuSensor
Full name: Recorder-usage.evtHandle
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
type Async
static member AsBeginEnd : computation:('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit)
static member AwaitEvent : event:IEvent<'Del,'T> * ?cancelAction:(unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate)
static member AwaitIAsyncResult : iar:IAsyncResult * ?millisecondsTimeout:int -> Async<bool>
static member AwaitTask : task:Task -> Async<unit>
static member AwaitTask : task:Task<'T> -> Async<'T>
static member AwaitWaitHandle : waitHandle:WaitHandle * ?millisecondsTimeout:int -> Async<bool>
static member CancelDefaultToken : unit -> unit
static member Catch : computation:Async<'T> -> Async<Choice<'T,exn>>
static member FromBeginEnd : beginAction:(AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg:'Arg1 * beginAction:('Arg1 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * beginAction:('Arg1 * 'Arg2 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * arg3:'Arg3 * beginAction:('Arg1 * 'Arg2 * 'Arg3 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromContinuations : callback:(('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T>
static member Ignore : computation:Async<'T> -> Async<unit>
static member OnCancel : interruption:(unit -> unit) -> Async<IDisposable>
static member Parallel : computations:seq<Async<'T>> -> Async<'T []>
static member RunSynchronously : computation:Async<'T> * ?timeout:int * ?cancellationToken:CancellationToken -> 'T
static member Sleep : millisecondsDueTime:int -> Async<unit>
static member Start : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions * ?cancellationToken:CancellationToken -> Task<'T>
static member StartChild : computation:Async<'T> * ?millisecondsTimeout:int -> Async<Async<'T>>
static member StartChildAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions -> Async<Task<'T>>
static member StartImmediate : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartWithContinuations : computation:Async<'T> * continuation:('T -> unit) * exceptionContinuation:(exn -> unit) * cancellationContinuation:(OperationCanceledException -> unit) * ?cancellationToken:CancellationToken -> unit
static member SwitchToContext : syncContext:SynchronizationContext -> Async<unit>
static member SwitchToNewThread : unit -> Async<unit>
static member SwitchToThreadPool : unit -> Async<unit>
static member TryCancelled : computation:Async<'T> * compensation:(OperationCanceledException -> unit) -> Async<'T>
static member CancellationToken : Async<CancellationToken>
static member DefaultCancellationToken : CancellationToken
Full name: Microsoft.FSharp.Control.Async
--------------------
type Async<'T>
Full name: Microsoft.FSharp.Control.Async<_>
Full name: Recorder-usage.memSensor
{Time: float;
Cpu: PerfSample;
Mem: PerfSample;}
Full name: Recorder-usage.LoadSample
Full name: Recorder-usage.recorder
Full name: Recorder-usage.smpls
from Microsoft.FSharp.Collections
Full name: Microsoft.FSharp.Collections.Array.ofSeq