2026-06-05 13:44:39 +09:00

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;
}
}