/* * Copyright (c) 2025 LG Electronics Inc. * SPDX-License-Identifier: LicenseRef-LGE-Proprietary */ 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 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 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; } }