196 lines
8.9 KiB
C#
196 lines
8.9 KiB
C#
using Aurora.Api;
|
|
using System;
|
|
using System.Diagnostics;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace AuroraDeviceCommon.Utils
|
|
{
|
|
// ref. https://tekeye.uk/visual_studio/c-sharp-async-await-microseconds-timer
|
|
// 스탑워치 Freqeuncy: the number of ticks per second
|
|
public class MicroTimer
|
|
{
|
|
protected Logger Log = new Logger("MicroTimer");
|
|
|
|
// The stopwatch uses the system's high performance counter
|
|
Stopwatch sw = new Stopwatch();
|
|
//The delegate used to call an external function
|
|
public delegate void ExternalCode();
|
|
public ExternalCode OnTimeout;
|
|
//Store the timeout period in microseconds : 100 [Hz] => 0.01 s => 10 ms => 10 * 10^3 us
|
|
private long updateRateUS = 10 * 1000;
|
|
//and the equivalent CPU ticks (Freq * (x / 1 000 000)) [ticks]
|
|
private long cpuTicksPerCycle = Stopwatch.Frequency / 100;
|
|
//Accessors for the microseconds property
|
|
//and conversion to the CPU ticks equivalent
|
|
public long UpdateRateUS {
|
|
get {
|
|
return updateRateUS;
|
|
}
|
|
set {
|
|
updateRateUS = value;
|
|
// (Stopwatch.Frequency / 10^6) == [ticks/us]
|
|
cpuTicksPerCycle = value * Stopwatch.Frequency / 1000000; // 한 주기에 필요한 [ticks]
|
|
}
|
|
}
|
|
//Progress reporter
|
|
public IProgress<int> Updater { get; set; }
|
|
// Default feedback rate to a 10th of a second : 10 [Hz] => 0.1 [s] => 100 ms
|
|
private int feedbackPerMS = 100;
|
|
//in the equivalent CPU ticks (Freq * (x / 1000)) [ticks]
|
|
private long cpuTicksPerfeedbackCycle = Stopwatch.Frequency / 10;
|
|
//Accessors for the feedbackMilliseconds property
|
|
//and conversion to the CPU ticks equivalent
|
|
public int FeedbackPerMS {
|
|
get {
|
|
return feedbackPerMS;
|
|
}
|
|
set {
|
|
feedbackPerMS = value;
|
|
// (Stopwatch.Frequency / 10^3) = [ticks/ms]
|
|
cpuTicksPerfeedbackCycle = value * Stopwatch.Frequency / 1000;
|
|
}
|
|
}
|
|
// 10 [ms] : the equivalent CPU ticks (1,000,000/10,000=100)
|
|
private long cpuTicksPer10MS = Stopwatch.Frequency / 100;
|
|
// Freq: [tick/s], 1 ms = 0.001 s = 1/1000 s
|
|
private long cpuTicksPer1MS = Stopwatch.Frequency / 1000;
|
|
|
|
public bool IsUseFeedbackCallback = false;
|
|
private long ticksPerUS = Stopwatch.Frequency / 1000000;
|
|
|
|
// 스탑워치의 ElaspedTicks 는 long 타입임
|
|
// overflow 방어 코드를 넣자... overflow 발생에 대략 20여년이 걸려서 크리티컬하지는 않을 것 같다만..
|
|
// 고속 타이머를 가정하므로, 1분의 마진을 주면 충분하지 않을까 !!
|
|
private long protectorElaspedTicksOverflow = long.MaxValue - (Stopwatch.Frequency * 60);
|
|
|
|
// 모니터리용 업데이트 주기
|
|
public double ActualUpdateRateMS { get; private set; }
|
|
// 총 구동 시간 모니터링
|
|
public TimeSpan TotalRunningTime { get => sw.Elapsed; }
|
|
|
|
|
|
//Support cancelling to stop the timer
|
|
CancellationTokenSource stopTimer;
|
|
//Stop the timer
|
|
public void Stop()
|
|
{
|
|
if (stopTimer != null) {
|
|
stopTimer.Cancel();
|
|
}
|
|
}
|
|
//Start the MicroTimer (async for new thread)
|
|
public async void Start()
|
|
{
|
|
//Start if delegate set
|
|
if (stopTimer == null && OnTimeout != null) {
|
|
//Need to be able to cancel the MicroTimer
|
|
stopTimer = new CancellationTokenSource();
|
|
// CPU bound task, hence Task.Run
|
|
await Task.Run(() => Timing(Updater, stopTimer.Token));
|
|
// Stop() 이 호출되어야 아래 코드가 실행됨...
|
|
// When Timing function returns, finished with CancellationTokenSource
|
|
stopTimer.Dispose();
|
|
stopTimer = null;
|
|
}
|
|
}
|
|
//Run the timer
|
|
void Timing(IProgress<int> progress, CancellationToken cancelTimimg)
|
|
{
|
|
Log.I("Timer is started");
|
|
//Calculates the timing difference
|
|
//long interval;
|
|
// Counter for progress timing
|
|
int progressCounter = 0;
|
|
|
|
ActualUpdateRateMS = 0.0;
|
|
|
|
// 스레드 우선순위를 높일 필요가 있을까???
|
|
Log.I($"Update thread priority: {Thread.CurrentThread.Priority}");
|
|
// 스레드 우선순위 변경
|
|
Thread.CurrentThread.Priority = ThreadPriority.Highest;
|
|
Log.I($"Update thread priority: {Thread.CurrentThread.Priority}");
|
|
|
|
// 현재 설정값 디스플레이
|
|
Log.I($"timer interval: {UpdateRateUS * 1e-3} [ms]");
|
|
Log.I($"feedback interval: {FeedbackPerMS} [ms]");
|
|
Log.I($"timer freq.: {Stopwatch.Frequency} [Hz]");
|
|
|
|
// Start the stopwatch
|
|
sw.Start();
|
|
// Loop until stopped
|
|
var remainedInterval = sw.ElapsedTicks + cpuTicksPerCycle; // microSecondsInCPUTTicks : 한 주기의 ticks
|
|
var tickInterval = remainedInterval;
|
|
var curTicks = sw.ElapsedTicks;
|
|
var prevTicks = tickInterval;
|
|
while (sw.IsRunning) {
|
|
// Run the external code
|
|
OnTimeout();
|
|
// see if a progress report is required
|
|
if (IsUseFeedbackCallback && ++progressCounter >= cpuTicksPerfeedbackCycle / cpuTicksPerCycle) {
|
|
progress?.Report(progressCounter);
|
|
progressCounter = 0;
|
|
}
|
|
|
|
// 로직을 살짝 수정해보자...
|
|
// 항상 정주기 비슷하게 동작하도록...
|
|
|
|
// 현재 사이클에서 사용된 시간 계산
|
|
curTicks = sw.ElapsedTicks;
|
|
var elapsed1CycleTicks = curTicks - prevTicks; // 현재 사이클에서 소요된 시간 [tick]
|
|
// update
|
|
//prevTicks = curTicks;
|
|
// 실제 업데이트 주기를 계산 : 연산코드가 중간에 삽입된거라, 주기 계산에 오차 요소...
|
|
// 여기는 아래 while 루프때문에 변동이 큼...
|
|
//ActualUpdateRateMS = ((double)elapsed1CycleTicks / Stopwatch.Frequency)*1000.0;
|
|
// 사이클에서 남은 시간 계산 [tick]
|
|
remainedInterval = cpuTicksPerCycle - elapsed1CycleTicks; // 주기를 맞추기 위해, 대기해야 할 남은 시간 [ticks]
|
|
// 업데이트 주기를 맞추기 위해 대기해야 할 시간 계산 [tick]
|
|
tickInterval = remainedInterval + curTicks;
|
|
// 아래 if 문은 모두 주석 처리 예정
|
|
/*
|
|
if (remainedInterval < 0) { // 주기 재설정 혹은 점검 필요.
|
|
//Log.W($"over time: {remainedInterval * 1.0e3 / microSecondsInCPUTicks} [ms]");
|
|
Debug.WriteLine($"Over time: {(double)remainedInterval * 0.001 / ticksPerUS} [ms]: {cpuTicksPerCycle}, {elapsed1CycleTicks}");
|
|
|
|
// 어차피 정해진 주기를 over 했다면, 바로 다음 사이클을 시작하면 되니, 별도의 조치는 불필요
|
|
//tickInterval = sw.ElapsedTicks;
|
|
}
|
|
else { // For Test
|
|
Debug.WriteLine($"remained time: {(double)remainedInterval * 0.001 / ticksPerUS} [ms]");
|
|
}
|
|
*/
|
|
|
|
// 아래 while 루프 소요시간 얼마인지 검사...
|
|
//var latchBefore = sw.ElapsedTicks;
|
|
// Loop until interval has passed
|
|
while (tickInterval - sw.ElapsedTicks > 0) {
|
|
// Chance for kernel to yield the tight CPU loop
|
|
Thread.Sleep(0);
|
|
}
|
|
//Debug.WriteLine($"Elasped time: {(double)(sw.ElapsedTicks - latchBefore) * 0.001 / ticksPerUS} [ms]");
|
|
|
|
// actual 주기는 여기서 측정하는게 더 정확/일관성이 있을 것 같음..
|
|
ActualUpdateRateMS = ((double)(sw.ElapsedTicks - prevTicks)) * 0.001 / ticksPerUS;
|
|
// update
|
|
prevTicks = curTicks;
|
|
|
|
// ElaspedTcks 의 overflow 방어 코드.... overflow 에 약 20여년 걸릴 것 같지만, 일단 넣자.
|
|
if (sw.ElapsedTicks > protectorElaspedTicksOverflow) {
|
|
// 필요한 것들을 리셋
|
|
prevTicks = curTicks = 0;
|
|
sw.Restart();
|
|
}
|
|
|
|
if (cancelTimimg.IsCancellationRequested) {
|
|
sw.Stop();
|
|
}
|
|
}
|
|
Log.I("Timer is terminated");
|
|
}
|
|
// Stopwatch IsRunning used for a MicroTimer IsRunning
|
|
// 이걸로 실행 중 or 종료 여부를 알 수 있음
|
|
public bool IsRunning => sw.IsRunning;
|
|
}
|
|
}
|