GhostTick 1.0.4

dotnet add package GhostTick --version 1.0.4
                    
NuGet\Install-Package GhostTick -Version 1.0.4
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="GhostTick" Version="1.0.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="GhostTick" Version="1.0.4" />
                    
Directory.Packages.props
<PackageReference Include="GhostTick" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add GhostTick --version 1.0.4
                    
#r "nuget: GhostTick, 1.0.4"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package GhostTick@1.0.4
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=GhostTick&version=1.0.4
                    
Install as a Cake Addin
#tool nuget:?package=GhostTick&version=1.0.4
                    
Install as a Cake Tool

GhostTick

CI NuGet Version NuGet Downloads License: MIT

A high-precision timer toolkit for .NET that delivers events through System.Threading.Channels.

Features

  • Sub-millisecond precision — hybrid sleep + busy-spin strategy; Reduce OS timer granularity from ~15 ms to ~1 ms on Windows
  • Channel-based API — consumers receive TimerEvent values via ChannelReader<TimerEvent>, keeping the timer thread fully decoupled from consumer latency
  • Drift correctionGhostTicker computes every target as start + seq × interval, so accumulated error stays bounded over time
  • Multi-targetingnetstandard2.0, net8.0, net10.0

Installation

dotnet add package GhostTick

Quick start

Repeating ticker

using var ticker = new GhostTicker(TimeSpan.FromMilliseconds(16.67)); // ~60 fps
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

await foreach (var evt in ticker.Reader.ReadAllAsync(cts.Token))
{
    // evt.Sequence — monotonic tick counter (gaps indicate dropped ticks)
    // evt.Drift    — how late this tick fired vs its scheduled time
    Render(evt.Sequence);
}

API reference

TimerEvent

Member Type Description
ScheduledAt DateTimeOffset UTC time the event was expected to fire
FiredAt DateTimeOffset UTC time the event actually fired (Stopwatch-derived, sub-ms accurate)
Sequence ulong Increments per tick
Drift TimeSpan FiredAt − ScheduledAt — positive means late

GhostTicker

new GhostTicker(TimeSpan interval, GhostTickerOptions? options = null)
Member Description
Reader ChannelReader<TimerEvent> — completes on stop
Stop() Signal the tick loop to exit and complete the channel
Dispose() Same as Stop()

Options

GhostTickerOptions

Property Default Description
SpinThreshold 1.5 ms Switch from sleep to busy-spin this far before each target
ChannelCapacity 1 Bounded channel capacity
FullMode DropOldest What to do when the channel is full (slow consumer)
ThreadPriority AboveNormal Priority of the dedicated ticker thread
ThreadName auto Name visible in debuggers / profilers

Slow consumers and back-pressure

The timer thread calls TryWrite and never blocks. When the channel is full:

  • DropOldest (default) — oldest pending tick is discarded; consumer always gets the freshest event. Gaps in Sequence reveal how many ticks were dropped.
  • DropNewest — new tick is discarded; consumer drains at its own pace.
  • Wait — has no effect on the timer thread; TryWrite is always used so ticks are still dropped when the channel is full.

Fan-out to multiple consumers

ChannelReader<T> delivers each item to exactly one reader. For broadcast semantics, dispatch manually:

using var ticker = new GhostTicker(TimeSpan.FromMilliseconds(100));

var ch1 = Channel.CreateBounded<TimerEvent>(4);
var ch2 = Channel.CreateBounded<TimerEvent>(4);

// Dispatcher
_ = Task.Run(async () =>
{
    await foreach (var evt in ticker.Reader.ReadAllAsync())
    {
        ch1.Writer.TryWrite(evt);
        ch2.Writer.TryWrite(evt);
    }
    ch1.Writer.Complete();
    ch2.Writer.Complete();
});

// Consumer A
_ = Task.Run(async () =>
{
    await foreach (var evt in ch1.Reader.ReadAllAsync())
        Console.WriteLine($"A #{evt.Sequence}");
});

Benchmarks

Environment: Windows 11, 13th Gen Intel Core i9-13980HX, .NET 10.0.6, BenchmarkDotNet v0.15.8

Fire accuracy — error vs scheduled time (µs, lower is better)

100 samples per cell.

Method Delay Min Mean StdDev P95 P99
GhostTicker 1 ms 0.0 0.3 0.2 0.7 0.9
Task.Delay 1 ms 12,549 14,623 565 15,238 16,326
Threading.Timer 1 ms 13,260 14,545 470 15,362 15,784
Timers.Timer 1 ms 12,078 14,674 560 15,372 16,535
GhostTicker 5 ms 0.0 5,487 4,806 12,641 13,216
Task.Delay 5 ms 4,492 10,559 983 11,205 16,578
Threading.Timer 5 ms 9,627 10,665 457 11,166 11,929
Timers.Timer 5 ms 9,783 10,539 440 11,177 11,478
GhostTicker 10 ms 0.0 5,434 4,158 11,927 13,139
Task.Delay 10 ms 4,507 5,603 505 6,145 6,996
Threading.Timer 10 ms 4,781 5,684 480 6,253 7,190
Timers.Timer 10 ms 2,701 5,552 641 6,359 8,460

At 1 ms delay, GhostTicker mean error is 0.3 µs vs ~14,600 µs for all others — a ~49,000× improvement.

Tick cadence — 100 consecutive ticks, total wall time (ms, lower is better)

GhostTicker uses a dedicated AboveNormal-priority thread; others depend on the thread pool.

Method Interval Mean Ratio Allocated
GhostTicker 1 ms 100.4 ms 1.00 16.27 KB
PeriodicTimer 1 ms 1,552.1 ms 15.46× 2.32 KB
Threading.Timer 1 ms 1,556.6 ms 15.50× 2.80 KB
GhostTicker 10 ms 1,007.7 ms 1.00 14.40 KB
PeriodicTimer 10 ms 1,557.2 ms 1.55× 2.32 KB
Threading.Timer 10 ms 1,557.5 ms 1.55× 2.80 KB
GhostTicker 50 ms 5,004.3 ms 1.00 16.72 KB
PeriodicTimer 50 ms 5,008.6 ms 1.00× 2.32 KB
Threading.Timer 50 ms 5,008.5 ms 1.00× 14.85 KB

Run benchmarks locally:

dotnet run --project benchmarks/GhostTick.Benchmarks -c Release

Examples

See examples/GhostTick.Examples for runnable examples covering all features.

dotnet run --project examples/GhostTick.Examples

License

MIT


GhostTick(中文)

CI NuGet Version NuGet Downloads License: MIT

适用于 .NET 的高精度计时工具库,通过 System.Threading.Channels 传递事件。

功能特性

  • 亚毫秒级精度 — 采用休眠 + 忙等待混合策略;将 Windows 的 OS 定时器粒度从约 15 ms 降至约 1 ms
  • 基于 Channel 的 API — 消费者通过 ChannelReader<TimerEvent> 接收 TimerEvent,计时线程与消费者延迟完全解耦
  • 漂移修正GhostTicker 将每个目标时刻计算为 start + seq × interval,无论运行多久,累积误差始终有界
  • 多目标框架netstandard2.0net8.0net10.0

安装

dotnet add package GhostTick

快速上手

周期性 Ticker

using var ticker = new GhostTicker(TimeSpan.FromMilliseconds(16.67)); // ~60 fps
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

await foreach (var evt in ticker.Reader.ReadAllAsync(cts.Token))
{
    // evt.Sequence — 单调递增的 tick 计数器(序号出现间隙表示有 tick 被丢弃)
    // evt.Drift    — 该 tick 相对于计划触发时刻的延迟
    Render(evt.Sequence);
}

API 参考

TimerEvent

成员 类型 说明
ScheduledAt DateTimeOffset 事件预期触发的 UTC 时间
FiredAt DateTimeOffset 事件实际触发的 UTC 时间(基于 Stopwatch,亚毫秒精度)
Sequence ulong 每次 tick 递增
Drift TimeSpan FiredAt − ScheduledAt,正值表示延迟触发

GhostTicker

new GhostTicker(TimeSpan interval, GhostTickerOptions? options = null)
成员 说明
Reader ChannelReader<TimerEvent> — 停止后完成
Stop() 通知 tick 循环退出并完成 channel
Dispose() Stop()

选项

GhostTickerOptions

属性 默认值 说明
SpinThreshold 1.5 ms 距目标时刻此值以内切换为忙等待
ChannelCapacity 1 有界 channel 容量
FullMode DropOldest channel 满时的处理策略(慢消费者场景)
ThreadPriority AboveNormal 专用 ticker 线程的优先级
ThreadName 自动 在调试器 / 分析器中可见的线程名称

慢消费者与背压

计时线程调用 TryWrite 且永不阻塞。channel 满时:

  • DropOldest(默认)— 丢弃最旧的待处理 tick,消费者始终获得最新事件。Sequence 出现间隙即可得知丢弃了多少 tick。
  • DropNewest — 丢弃新到达的 tick,消费者按自身速度消费。
  • Wait — 对计时线程无效;始终使用 TryWrite,channel 满时 tick 仍会被丢弃。

广播给多个消费者

ChannelReader<T> 每个事件只投递给一个读取者。如需广播语义,可手动分发:

using var ticker = new GhostTicker(TimeSpan.FromMilliseconds(100));

var ch1 = Channel.CreateBounded<TimerEvent>(4);
var ch2 = Channel.CreateBounded<TimerEvent>(4);

// 分发器
_ = Task.Run(async () =>
{
    await foreach (var evt in ticker.Reader.ReadAllAsync())
    {
        ch1.Writer.TryWrite(evt);
        ch2.Writer.TryWrite(evt);
    }
    ch1.Writer.Complete();
    ch2.Writer.Complete();
});

// 消费者 A
_ = Task.Run(async () =>
{
    await foreach (var evt in ch1.Reader.ReadAllAsync())
        Console.WriteLine($"A #{evt.Sequence}");
});

基准测试

环境:Windows 11,13th Gen Intel Core i9-13980HX,.NET 10.0.6,BenchmarkDotNet v0.15.8

触发精度 — 相对计划时刻的误差(µs,越低越好)

每格 100 个样本。

方法 延迟 最小值 均值 标准差 P95 P99
GhostTicker 1 ms 0.0 0.3 0.2 0.7 0.9
Task.Delay 1 ms 12,549 14,623 565 15,238 16,326
Threading.Timer 1 ms 13,260 14,545 470 15,362 15,784
Timers.Timer 1 ms 12,078 14,674 560 15,372 16,535
GhostTicker 5 ms 0.0 5,487 4,806 12,641 13,216
Task.Delay 5 ms 4,492 10,559 983 11,205 16,578
Threading.Timer 5 ms 9,627 10,665 457 11,166 11,929
Timers.Timer 5 ms 9,783 10,539 440 11,177 11,478
GhostTicker 10 ms 0.0 5,434 4,158 11,927 13,139
Task.Delay 10 ms 4,507 5,603 505 6,145 6,996
Threading.Timer 10 ms 4,781 5,684 480 6,253 7,190
Timers.Timer 10 ms 2,701 5,552 641 6,359 8,460

在 1 ms 延迟下,GhostTicker 均值误差为 0.3 µs,其他方案约为 14,600 µs — 提升约 49,000×

Tick 节奏 — 100 次连续 tick 的总耗时(ms,越低越好)

GhostTicker 使用独立的 AboveNormal 优先级线程;其他方案依赖线程池。

方法 间隔 均值 比率 内存分配
GhostTicker 1 ms 100.4 ms 1.00 16.27 KB
PeriodicTimer 1 ms 1,552.1 ms 15.46× 2.32 KB
Threading.Timer 1 ms 1,556.6 ms 15.50× 2.80 KB
GhostTicker 10 ms 1,007.7 ms 1.00 14.40 KB
PeriodicTimer 10 ms 1,557.2 ms 1.55× 2.32 KB
Threading.Timer 10 ms 1,557.5 ms 1.55× 2.80 KB
GhostTicker 50 ms 5,004.3 ms 1.00 16.72 KB
PeriodicTimer 50 ms 5,008.6 ms 1.00× 2.32 KB
Threading.Timer 50 ms 5,008.5 ms 1.00× 14.85 KB

本地运行基准测试:

dotnet run --project benchmarks/GhostTick.Benchmarks -c Release

示例

可运行示例请参见 examples/GhostTick.Examples,涵盖所有功能。

dotnet run --project examples/GhostTick.Examples

许可证

MIT

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.4 3 5/26/2026
1.0.2 101 4/20/2026
1.0.1 97 4/16/2026
1.0.0 99 4/16/2026