/// <summary> /// Stores a <see cref="Datum"/> within the <see cref="LocalDataStore"/>. Will not throw an <see cref="Exception"/>. /// </summary> /// <param name="datum">Datum.</param> /// <param name="cancellationToken">Cancellation token.</param> public void StoreDatum(Datum datum, CancellationToken?cancellationToken = null) { // track/limit the raw rate if (_rawRateCalculator.Add(datum) == DataRateCalculator.SamplingAction.Drop) { return; } // track the storage rate _storageRateCalculator.Add(datum); // set properties that we were unable to set within the datum constructor. datum is allowed to // be null, indicating the the probe attempted to obtain data but it didn't find any (in the // case of polling probes). if (datum != null) { datum.ProtocolId = Protocol.Id; datum.ParticipantId = Protocol.ParticipantId; } // track the most recent datum regardless of whether the datum is null or whether we're storing data Datum previousDatum = _mostRecentDatum; _mostRecentDatum = datum; _mostRecentStoreTimestamp = DateTimeOffset.UtcNow; // fire events to notify observers of the stored data and associated UI values MostRecentDatumChanged?.Invoke(this, new Tuple <Datum, Datum>(previousDatum, _mostRecentDatum)); // don't update the UI too often, as doing so at really high rates causes UI deadlocks. if (_uiUpdateRateCalculator.Add(datum) == DataRateCalculator.SamplingAction.Keep) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SubCaption))); } // store non-null data if (_storeData && datum != null) { #region update chart data ChartDataPoint chartDataPoint = null; try { chartDataPoint = GetChartDataPointFromDatum(datum); } catch (NotImplementedException) { } if (chartDataPoint != null) { lock (_chartData) { _chartData.Add(chartDataPoint); while (_chartData.Count > 0 && _chartData.Count > _maxChartDataCount) { _chartData.RemoveAt(0); } } } #endregion // write datum to local data store. catch any exceptions, as the caller (e.g., a listening // probe) could very well be unprotected on the UI thread. throwing an exception here can crash the app. try { _protocol.LocalDataStore.WriteDatum(datum, cancellationToken.GetValueOrDefault()); } catch (Exception ex) { SensusServiceHelper.Get().Logger.Log("Failed to write datum: " + ex, LoggingLevel.Normal, GetType()); } } }
public sealed override Task <bool> StoreDatumAsync(Datum datum, CancellationToken?cancellationToken = default(CancellationToken?)) { bool store = true; float maxDataStoresPerSecond = _maxDataStoresPerSecond.GetValueOrDefault(-1); // 0 (or negligible) data per second: don't store. if max data per second is not set, the following inequality will be false. if (Math.Abs(maxDataStoresPerSecond) < DATA_RATE_EPSILON) { store = false; } // non-negligible (or default -1) data per second: check data rate else if (maxDataStoresPerSecond > 0) { _incomingDataRateCalculator.Add(datum); // recalculate the sampling modulus after accumulating a full sample size in the data rate calculator if ((_incomingDataRateCalculator.TotalAdded % _incomingDataRateCalculator.SampleSize) == 0) { double incomingDataPerSecond = _incomingDataRateCalculator.DataPerSecond; double extraDataPerSecond = incomingDataPerSecond - maxDataStoresPerSecond; // if we're not over the limit then store all samples if (extraDataPerSecond <= 0) { _samplingModulus = 1; _samplingModulusMatchAction = SamplingModulusMatchAction.Store; } // otherwise calculate a modulus that will get as close as possible to the desired rate given the empirical rate else { double samplingModulusMatchRate = extraDataPerSecond / incomingDataPerSecond; _samplingModulusMatchAction = SamplingModulusMatchAction.Drop; if (samplingModulusMatchRate > 0.5) { samplingModulusMatchRate = 1 - samplingModulusMatchRate; _samplingModulusMatchAction = SamplingModulusMatchAction.Store; } if (_samplingModulusMatchAction == SamplingModulusMatchAction.Store) { // round the (store) modulus down to oversample -- more is better, right? _samplingModulus = (int)Math.Floor(1 / samplingModulusMatchRate); } else { // round the (drop) modulus up to oversample -- more is better, right? _samplingModulus = (int)Math.Ceiling(1 / samplingModulusMatchRate); } } } bool isModulusMatch = (_incomingDataRateCalculator.TotalAdded % _samplingModulus) == 0; if ((_samplingModulusMatchAction == SamplingModulusMatchAction.Store && !isModulusMatch) || (_samplingModulusMatchAction == SamplingModulusMatchAction.Drop && isModulusMatch)) { store = false; } } if (store) { return(base.StoreDatumAsync(datum, cancellationToken)); } else { return(Task.FromResult(false)); } }
/// <summary> /// Stores a <see cref="Datum"/> within the <see cref="LocalDataStore"/>. Will not throw an <see cref="Exception"/>. /// </summary> /// <param name="datum">Datum.</param> /// <param name="cancellationToken">Cancellation token.</param> public async Task StoreDatumAsync(Datum datum, CancellationToken?cancellationToken = null) { // it's possible for the current method to be called when the protocol is not running. the obvious case is when // the protocol is paused, but there are other race-like conditions. we try to prevent this (e.g., by forcing // the user to start the protocol before taking a survey saved from a previous run of the app), but there are // probably corner cases we haven't accounted for. at the very least, there are race conditions (e.g., taking a // survey when a protocol is about to stop) that could cause data to be stored without a running protocol. if (_protocol.State != ProtocolState.Running) { return; } // track/limit the raw rate of non-null data. all null data will pass this test, and this is // fine given such data are generated by polling probes when no data were retrieved. such // return values from polling probes are used to indicate that the poll was completed, which // will be reflected in the _mostRecentStoreTimestamp below. if (datum != null) { // impose a limit on the raw data rate if (_rawRateCalculator.Add(datum) == DataRateCalculator.SamplingAction.Drop) { return; } // set properties that we were unable to set within the datum constructor. datum.ProtocolId = Protocol.Id; datum.ParticipantId = Protocol.ParticipantId; // tag the data if we're in tagging mode, indicated with a non-null event id on the protocol. avoid // any race conditions related to starting/stopping a tagging by getting the required values and // then checking both for validity. we need to guarantee that any tagged datum has both an id and tags. string taggedEventId = Protocol.TaggedEventId; List <string> taggedEventTags = Protocol.TaggedEventTags.ToList(); if (!string.IsNullOrWhiteSpace(taggedEventId) && taggedEventTags.Count > 0) { datum.TaggedEventId = taggedEventId; datum.TaggedEventTags = taggedEventTags; } // if the protocol is configured with a sensing agent, if (Protocol.Agent != null) { datum.SensingAgentStateDescription = Protocol.Agent.StateDescription; } } // store non-null data if (_storeData && datum != null) { #region update chart data ChartDataPoint chartDataPoint = null; try { chartDataPoint = GetChartDataPointFromDatum(datum); } catch (NotImplementedException) { } if (chartDataPoint != null) { lock (_chartData) { _chartData.Add(chartDataPoint); while (_chartData.Count > 0 && _chartData.Count > _maxChartDataCount) { _chartData.RemoveAt(0); } } } #endregion // write datum to local data store. catch any exceptions, as the caller (e.g., a listening // probe) could very well be unprotected on the UI thread. throwing an exception here can crash the app. try { _protocol.LocalDataStore.WriteDatum(datum, cancellationToken.GetValueOrDefault()); // track the storage rate _storageRateCalculator.Add(datum); } catch (Exception ex) { SensusServiceHelper.Get().Logger.Log("Failed to write datum: " + ex, LoggingLevel.Normal, GetType()); } } // update the timestamp of the most recent store. this is used to calculate storage latency, so we // do not restrict its values to those obtained when non-null data are stored (see above). some // probes call this method with null data to signal that they have run their collection to completion. _mostRecentStoreTimestamp = DateTimeOffset.UtcNow; // don't update the UI too often, as doing so at really high rates causes UI deadlocks. always let // null data update the UI, as these are only generated by polling probes at low rates. if (datum == null || _uiUpdateRateCalculator.Add(datum) == DataRateCalculator.SamplingAction.Keep) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SubCaption))); } // track the most recent datum regardless of whether the datum is null or whether we're storing data Datum previousDatum = _mostRecentDatum; _mostRecentDatum = datum; // notify observers of the stored data and associated UI values await(MostRecentDatumChanged?.Invoke(previousDatum, _mostRecentDatum) ?? Task.CompletedTask); // let the script probe's agent observe the data, as long as the probe is enabled and there is an agent. Protocol.TryGetProbe(typeof(ScriptProbe), out Probe scriptProbe); if (scriptProbe?.Enabled ?? false) { // agents might be third-party and badly behaving...catch their exceptions. try { await((scriptProbe as ScriptProbe).Agent?.ObserveAsync(datum) ?? Task.CompletedTask); } catch (Exception ex) { SensusServiceHelper.Get().Logger.Log("Exception while script probe agent was observing datum: " + ex.Message, LoggingLevel.Normal, GetType()); } } // let the protocol's sensing agent observe the data, and schedule any returned control // completion check. agents might be third-party and badly behaving...catch their exceptions. try { await Protocol.ScheduleAgentControlCompletionCheckAsync(await (Protocol.Agent?.ObserveAsync(datum, cancellationToken.GetValueOrDefault()) ?? Task.FromResult <ControlCompletionCheck>(null))); } catch (Exception ex) { SensusServiceHelper.Get().Logger.Log("Exception while sensing agent was observing datum: " + ex.Message, LoggingLevel.Normal, GetType()); } }