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

Size
13 KB
Lines
311
Language
csharp
SHA-256
eafc1b6f51bb…
View on GitHub

Sign in with GitHub to download. Each download is recorded with your GitHub username.

1using System;
2using System.Diagnostics;
3using System.Reflection;
4using System.Runtime.InteropServices;
5using System.Threading.Tasks;
6using OpenCKey.Interop;
7
8namespace 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 NET5_0_OR_GREATER
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}