Viewing is open to everyone. Downloading and starring need a GitHub sign-in.
Sign in with GitHub
CKeyTelemetry.cs
v0.1.0-build.85
Latest
Commit ede38f5
Sign in with GitHub to download. Each download is recorded with your GitHub username.
| 1 | using System; |
| 2 | using System.Diagnostics; |
| 3 | using System.Reflection; |
| 4 | using System.Runtime.InteropServices; |
| 5 | using System.Threading.Tasks; |
| 6 | using OpenCKey.Interop; |
| 7 | |
| 8 | namespace OpenCKey |
| 9 | { |
| 10 | /// <summary> |
| 11 | /// Library-wide settings. Opting out of telemetry is one line: |
| 12 | /// <code>CKey.Telemetry = 0;</code> |
| 13 | /// </summary> |
| 14 | public static class CKey |
| 15 | { |
| 16 | /// <summary>0: no counting, nothing sent.</summary> |
| 17 | public const int TelemetryOff = 0; |
| 18 | /// <summary>1: local counting and anonymous aggregate batches (the default).</summary> |
| 19 | public const int TelemetryOn = 1; |
| 20 | /// <summary>2: local counting, never sent.</summary> |
| 21 | public const int TelemetryLocal = 2; |
| 22 | |
| 23 | /// <summary> |
| 24 | /// Telemetry mode: 0 off, 1 on (default), 2 local only. Any other value |
| 25 | /// throws <see cref="ArgumentOutOfRangeException"/> and leaves the mode |
| 26 | /// unchanged. See docs/adoption-and-telemetry.md. |
| 27 | /// </summary> |
| 28 | public static int Telemetry |
| 29 | { |
| 30 | get => CKeyTelemetry.Mode; |
| 31 | set => CKeyTelemetry.Mode = value; |
| 32 | } |
| 33 | |
| 34 | /// <summary>The SDK (NuGet package) version, e.g. "0.1.0-build.63".</summary> |
| 35 | public static string Version => CKeyTelemetry.SdkVersion; |
| 36 | |
| 37 | /// <summary>The native core's software version (<c>ckey_get_software_version</c>).</summary> |
| 38 | public static unsafe string CoreVersion |
| 39 | { |
| 40 | get |
| 41 | { |
| 42 | Native.EnsureAbi(); |
| 43 | return Native.FromUtf8Z(NativeMethods.ckey_get_software_version()) ?? string.Empty; |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | /// <summary>The native core's ABI version.</summary> |
| 48 | public static int AbiVersion |
| 49 | { |
| 50 | get |
| 51 | { |
| 52 | Native.EnsureAbi(); |
| 53 | return (int)NativeMethods.ckey_get_abi_version(); |
| 54 | } |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | /// <summary>Aggregate histogram (count, sum, ten buckets).</summary> |
| 59 | public sealed class CKeyTelemetryHistogram |
| 60 | { |
| 61 | internal CKeyTelemetryHistogram(ulong count, ulong sum, ulong[] buckets) |
| 62 | { |
| 63 | Count = count; |
| 64 | Sum = sum; |
| 65 | Buckets = buckets; |
| 66 | } |
| 67 | |
| 68 | /// <summary>Observations.</summary> |
| 69 | public ulong Count { get; } |
| 70 | /// <summary>Sum of observations (ms).</summary> |
| 71 | public ulong Sum { get; } |
| 72 | /// <summary>Ten buckets with inclusive upper edges (see docs/adoption-and-telemetry.md).</summary> |
| 73 | public ulong[] Buckets { get; } |
| 74 | } |
| 75 | |
| 76 | /// <summary>Local telemetry totals (<c>ckey_telemetry_counters</c>).</summary> |
| 77 | public sealed class CKeyTelemetryCounters |
| 78 | { |
| 79 | // By value: the copy is a local, so its address can be taken directly. |
| 80 | internal unsafe CKeyTelemetryCounters(NativeTelemetryCounters c) |
| 81 | { |
| 82 | SessionsStarted = c.SessionsStarted; |
| 83 | SessionsSucceeded = c.SessionsSucceeded; |
| 84 | SessionsFailed = c.SessionsFailed; |
| 85 | Frames = c.Frames; |
| 86 | NativeTelemetryCounters* p = &c; |
| 87 | Decoded = new ulong[NativeConstants.TelemetryProfiles]; |
| 88 | for (int i = 0; i < Decoded.Length; i++) Decoded[i] = p->Decoded[i]; |
| 89 | TimeToDecodeMs = Histogram(&p->TimeToDecodeMs); |
| 90 | FrameMs = Histogram(&p->FrameMs); |
| 91 | } |
| 92 | |
| 93 | private static unsafe CKeyTelemetryHistogram Histogram(NativeTelemetryHistogram* h) |
| 94 | { |
| 95 | var buckets = new ulong[NativeConstants.TelemetryBuckets]; |
| 96 | for (int i = 0; i < buckets.Length; i++) buckets[i] = h->Buckets[i]; |
| 97 | return new CKeyTelemetryHistogram(h->Count, h->Sum, buckets); |
| 98 | } |
| 99 | |
| 100 | /// <summary>Scan sessions started.</summary> |
| 101 | public ulong SessionsStarted { get; } |
| 102 | /// <summary>Sessions that decoded a symbol.</summary> |
| 103 | public ulong SessionsSucceeded { get; } |
| 104 | /// <summary>Sessions that ended or timed out without a decode.</summary> |
| 105 | public ulong SessionsFailed { get; } |
| 106 | /// <summary>Frames processed.</summary> |
| 107 | public ulong Frames { get; } |
| 108 | /// <summary>Decodes by profile: p0, p0_1 (print), p1, p2, p3.</summary> |
| 109 | public ulong[] Decoded { get; } |
| 110 | /// <summary>Time from session start to first decode.</summary> |
| 111 | public CKeyTelemetryHistogram TimeToDecodeMs { get; } |
| 112 | /// <summary>Per-frame processing time.</summary> |
| 113 | public CKeyTelemetryHistogram FrameMs { get; } |
| 114 | } |
| 115 | |
| 116 | /// <summary> |
| 117 | /// Anonymous, aggregate scan telemetry (specification §9, |
| 118 | /// docs/adoption-and-telemetry.md). Counts sessions, frames, decodes per |
| 119 | /// profile and coarse durations; never payloads, URLs, identifiers or |
| 120 | /// location. Recording happens only through <see cref="CKeyScanSession"/>; |
| 121 | /// <see cref="CKeyDecoder"/> and rendering record nothing. In mode 1 a |
| 122 | /// background timer posts at most one batch per hour, off the scan thread. |
| 123 | /// </summary> |
| 124 | public static class CKeyTelemetry |
| 125 | { |
| 126 | private static readonly object Gate = new object(); |
| 127 | private static int _available = -1; // -1 unknown, 0 no, 1 yes |
| 128 | private static bool _clientSet; |
| 129 | |
| 130 | /// <summary>The endpoint batches are posted to (CKEY_TELEMETRY_ENDPOINT).</summary> |
| 131 | public const string DefaultEndpoint = "https://openckey.org/api/telemetry/v1/batch"; |
| 132 | |
| 133 | /// <summary> |
| 134 | /// True when the native library includes the telemetry module. A build |
| 135 | /// with CKEY_DOTNET_TELEMETRY=OFF omits it; then the mode reads 0 and |
| 136 | /// nothing is counted or sent. |
| 137 | /// </summary> |
| 138 | public static bool IsAvailable |
| 139 | { |
| 140 | get |
| 141 | { |
| 142 | if (_available < 0) |
| 143 | { |
| 144 | lock (Gate) |
| 145 | { |
| 146 | if (_available < 0) |
| 147 | { |
| 148 | Native.EnsureAbi(); |
| 149 | try |
| 150 | { |
| 151 | NativeTelemetry.ckey_telemetry_get_mode(); |
| 152 | _available = 1; |
| 153 | } |
| 154 | catch (EntryPointNotFoundException) |
| 155 | { |
| 156 | _available = 0; |
| 157 | } |
| 158 | } |
| 159 | } |
| 160 | } |
| 161 | return _available == 1; |
| 162 | } |
| 163 | } |
| 164 | |
| 165 | /// <summary>Same as <see cref="CKey.Telemetry"/>.</summary> |
| 166 | public static int Mode |
| 167 | { |
| 168 | get => IsAvailable ? NativeTelemetry.ckey_telemetry_get_mode() : CKey.TelemetryOff; |
| 169 | set |
| 170 | { |
| 171 | if (value < CKey.TelemetryOff || value > CKey.TelemetryLocal) |
| 172 | throw new ArgumentOutOfRangeException(nameof(value), value, |
| 173 | "CKey.Telemetry must be 0 (off), 1 (on) or 2 (local only)."); |
| 174 | if (!IsAvailable) |
| 175 | { |
| 176 | if (value == CKey.TelemetryOff) return; |
| 177 | throw new InvalidOperationException("This native ckey library was built without the telemetry module."); |
| 178 | } |
| 179 | EnsureClient(); |
| 180 | int status = NativeTelemetry.ckey_telemetry_set_mode(value); |
| 181 | if (status != 0) |
| 182 | throw new ArgumentOutOfRangeException(nameof(value), value, $"ckey_telemetry_set_mode refused {value}."); |
| 183 | } |
| 184 | } |
| 185 | |
| 186 | /// <summary>Local totals since counting started (all zero in mode 0).</summary> |
| 187 | public static unsafe CKeyTelemetryCounters Snapshot() |
| 188 | { |
| 189 | NativeTelemetryCounters c = default; |
| 190 | if (IsAvailable) Native.Check(NativeTelemetry.ckey_telemetry_snapshot(&c), "ckey_telemetry_snapshot"); |
| 191 | return new CKeyTelemetryCounters(c); |
| 192 | } |
| 193 | |
| 194 | /// <summary> |
| 195 | /// Optional persistence: the local totals and unsent delta as compact |
| 196 | /// JSON. Store it and pass it to <see cref="ImportState"/> at the next start. |
| 197 | /// </summary> |
| 198 | public static unsafe string ExportState() |
| 199 | { |
| 200 | if (!IsAvailable) throw new InvalidOperationException("This native ckey library was built without the telemetry module."); |
| 201 | nuint length = 0; |
| 202 | int status = NativeTelemetry.ckey_telemetry_export_state(null, 0, &length); |
| 203 | if (status != (int)CKeyStatus.InsufficientBuffer) Native.Check(status, "ckey_telemetry_export_state"); |
| 204 | var buffer = new byte[Native.ToInt32Length(length + 1, "Telemetry state")]; |
| 205 | fixed (byte* b = buffer) |
| 206 | { |
| 207 | Native.Check(NativeTelemetry.ckey_telemetry_export_state(b, (nuint)buffer.Length, &length), "ckey_telemetry_export_state"); |
| 208 | return Native.FromUtf8Z(b, buffer.Length) ?? string.Empty; |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | /// <summary> |
| 213 | /// Adds a state saved by <see cref="ExportState"/> to the current counts. |
| 214 | /// Call once at startup. Throws <see cref="CKeyException"/> for malformed |
| 215 | /// state or in mode 0. Never changes the mode or re-enables sending. |
| 216 | /// </summary> |
| 217 | public static unsafe void ImportState(string state) |
| 218 | { |
| 219 | if (state == null) throw new ArgumentNullException(nameof(state)); |
| 220 | if (!IsAvailable) throw new InvalidOperationException("This native ckey library was built without the telemetry module."); |
| 221 | byte[] json = Native.ToUtf8Z(state)!; |
| 222 | fixed (byte* j = json) Native.Check(NativeTelemetry.ckey_telemetry_import_state(j), "ckey_telemetry_import_state"); |
| 223 | if (Mode == CKey.TelemetryOn) TelemetrySender.EnsureStarted(); |
| 224 | } |
| 225 | |
| 226 | /// <summary> |
| 227 | /// Tries a send now, for example when the app moves to the background |
| 228 | /// or is about to exit. Runs on the thread pool and never blocks the |
| 229 | /// caller; the hourly cadence still applies, so this usually does |
| 230 | /// nothing. Completes with true when a batch was accepted. |
| 231 | /// </summary> |
| 232 | public static Task<bool> FlushAsync() => Task.Run(() => TelemetrySender.TrySendAsync()); |
| 233 | |
| 234 | internal static string SdkVersion |
| 235 | { |
| 236 | get |
| 237 | { |
| 238 | foreach (var attribute in typeof(CKeyTelemetry).Assembly.GetCustomAttributes<AssemblyMetadataAttribute>()) |
| 239 | { |
| 240 | if (attribute.Key == "CKeyVersion" && !string.IsNullOrEmpty(attribute.Value)) return attribute.Value!; |
| 241 | } |
| 242 | return "0.0.0"; |
| 243 | } |
| 244 | } |
| 245 | |
| 246 | internal static string Platform |
| 247 | { |
| 248 | get |
| 249 | { |
| 250 | #if |
| 251 | if (OperatingSystem.IsAndroid()) return "android"; |
| 252 | if (OperatingSystem.IsIOS() || OperatingSystem.IsMacCatalyst()) return "ios"; |
| 253 | if (OperatingSystem.IsBrowser()) return "web"; |
| 254 | #endif |
| 255 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return "windows"; |
| 256 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return "macos"; |
| 257 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return "linux"; |
| 258 | return "other"; |
| 259 | } |
| 260 | } |
| 261 | |
| 262 | /// <summary>Reports sdk "dotnet", this package's version and the platform (once).</summary> |
| 263 | internal static void EnsureClient() |
| 264 | { |
| 265 | if (_clientSet || !IsAvailable) return; |
| 266 | lock (Gate) |
| 267 | { |
| 268 | if (_clientSet) return; |
| 269 | ApplyClient(); |
| 270 | _clientSet = true; |
| 271 | } |
| 272 | } |
| 273 | |
| 274 | private static unsafe void ApplyClient() |
| 275 | { |
| 276 | byte[] sdk = Native.ToUtf8Z("dotnet")!; |
| 277 | byte[] platform = Native.ToUtf8Z(Platform)!; |
| 278 | byte[] version = Native.ToUtf8Z(SdkVersion)!; |
| 279 | fixed (byte* s = sdk) |
| 280 | fixed (byte* p = platform) |
| 281 | fixed (byte* v = version) |
| 282 | { |
| 283 | if (NativeTelemetry.ckey_telemetry_set_client(s, v, p) == 0) return; |
| 284 | // A version outside the schema pattern: report the core's version instead. |
| 285 | byte* core = NativeMethods.ckey_get_software_version(); |
| 286 | if (NativeTelemetry.ckey_telemetry_set_client(s, core, p) == 0) return; |
| 287 | Debug.WriteLine("CKey: ckey_telemetry_set_client refused the client identity."); |
| 288 | } |
| 289 | } |
| 290 | |
| 291 | /// <summary>TEST ONLY: restores the core's telemetry defaults, then re-applies the client identity.</summary> |
| 292 | internal static void ResetForTesting() |
| 293 | { |
| 294 | if (!IsAvailable) return; |
| 295 | lock (Gate) |
| 296 | { |
| 297 | NativeTelemetry.ckey_telemetry_reset_for_testing(); |
| 298 | ApplyClient(); |
| 299 | _clientSet = true; |
| 300 | } |
| 301 | } |
| 302 | } |
| 303 | |
| 304 | /// <summary>A monotonic millisecond clock shared by every telemetry call.</summary> |
| 305 | internal static class MonotonicClock |
| 306 | { |
| 307 | private static readonly Stopwatch Watch = Stopwatch.StartNew(); |
| 308 | |
| 309 | internal static ulong NowMs => (ulong)Watch.ElapsedMilliseconds; |
| 310 | } |
| 311 | } |