summaryrefslogtreecommitdiffstats
path: root/toolkit/components/glean/tests/xpcshell/test_GIFFT.js
blob: 015b4d4e38e68beef153dbbdef29458ce571b37b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

const { AppConstants } = ChromeUtils.importESModule(
  "resource://gre/modules/AppConstants.sys.mjs"
);
const { setTimeout } = ChromeUtils.importESModule(
  "resource://gre/modules/Timer.sys.mjs"
);
const { TelemetryTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/TelemetryTestUtils.sys.mjs"
);

const Telemetry = Services.telemetry;

function sleep(ms) {
  /* eslint-disable mozilla/no-arbitrary-setTimeout */
  return new Promise(resolve => setTimeout(resolve, ms));
}

function scalarValue(aScalarName) {
  let snapshot = Telemetry.getSnapshotForScalars();
  return "parent" in snapshot ? snapshot.parent[aScalarName] : undefined;
}

function keyedScalarValue(aScalarName) {
  let snapshot = Telemetry.getSnapshotForKeyedScalars();
  return "parent" in snapshot ? snapshot.parent[aScalarName] : undefined;
}

add_setup(function test_setup() {
  // FOG needs a profile directory to put its data in.
  do_get_profile();

  Services.prefs.setBoolPref(
    "toolkit.telemetry.testing.overrideProductsCheck",
    true
  );

  // We need to initialize it once, otherwise operations will be stuck in the pre-init queue.
  // On Android FOG is set up through head.js.
  if (AppConstants.platform != "android") {
    Services.fog.initializeFOG();
  }
});

add_task(function test_gifft_counter() {
  Glean.testOnlyIpc.aCounter.add(20);
  Assert.equal(20, Glean.testOnlyIpc.aCounter.testGetValue());
  Assert.equal(20, scalarValue("telemetry.test.mirror_for_counter"));
});

add_task(function test_gifft_boolean() {
  Glean.testOnlyIpc.aBool.set(false);
  Assert.equal(false, Glean.testOnlyIpc.aBool.testGetValue());
  Assert.equal(false, scalarValue("telemetry.test.boolean_kind"));
});

add_task(function test_gifft_datetime() {
  const dateStr = "2021-03-22T16:06:00";
  const value = new Date(dateStr);
  Glean.testOnlyIpc.aDate.set(value.getTime() * 1000);

  let received = Glean.testOnlyIpc.aDate.testGetValue();
  Assert.equal(value.getTime(), received.getTime());
  Assert.ok(scalarValue("telemetry.test.mirror_for_date").startsWith(dateStr));
});

add_task(function test_gifft_string() {
  const value = "a string!";
  Glean.testOnlyIpc.aString.set(value);

  Assert.equal(value, Glean.testOnlyIpc.aString.testGetValue());
  Assert.equal(value, scalarValue("telemetry.test.multiple_stores_string"));
});

add_task(function test_gifft_memory_dist() {
  Glean.testOnlyIpc.aMemoryDist.accumulate(7);
  Glean.testOnlyIpc.aMemoryDist.accumulate(17);

  let data = Glean.testOnlyIpc.aMemoryDist.testGetValue();
  // `data.sum` is in bytes, but the metric is in KB.
  Assert.equal(24 * 1024, data.sum, "Sum's correct");
  for (let [bucket, count] of Object.entries(data.values)) {
    Assert.ok(
      count == 0 || (count == 1 && (bucket == 6888 || bucket == 17109)),
      `Only two buckets have a sample ${bucket} ${count}`
    );
  }

  data = Telemetry.getHistogramById("TELEMETRY_TEST_LINEAR").snapshot();
  Telemetry.getHistogramById("TELEMETRY_TEST_LINEAR").clear();
  Assert.equal(24, data.sum, "Histogram's in `memory_unit` units");
  Assert.equal(2, data.values["1"], "Both samples in a low bucket");

  // MemoryDistribution's Accumulate method to takes
  // a platform specific type (size_t).
  // Glean's, however, is i64, and, glean_memory_dist is uint64_t
  // What happens when we give accumulate dubious values?
  // This may occur on some uncommon platforms.
  // Note: there are issues in JS with numbers above 2**53
  Glean.testOnlyIpc.aMemoryDist.accumulate(36893488147419103232);
  let dubiousValue = Object.entries(
    Glean.testOnlyIpc.aMemoryDist.testGetValue().values
  )[0][1];
  Assert.equal(
    dubiousValue,
    1,
    "Greater than 64-Byte number did not accumulate correctly"
  );

  // Values lower than the out-of-range value are not clamped
  // resulting in an exception being thrown from the glean side
  // when the value exceeds the glean maximum allowed value
  Glean.testOnlyIpc.aMemoryDist.accumulate(Math.pow(2, 31));
  Assert.throws(
    () => Glean.testOnlyIpc.aMemoryDist.testGetValue(),
    /DataError/,
    "Did not accumulate correctly"
  );
});

add_task(function test_gifft_custom_dist() {
  Glean.testOnlyIpc.aCustomDist.accumulateSamples([7, 268435458]);

  let data = Glean.testOnlyIpc.aCustomDist.testGetValue();
  Assert.equal(7 + 268435458, data.sum, "Sum's correct");
  for (let [bucket, count] of Object.entries(data.values)) {
    Assert.ok(
      count == 0 || (count == 1 && (bucket == 1 || bucket == 268435456)),
      `Only two buckets have a sample ${bucket} ${count}`
    );
  }

  data = Telemetry.getHistogramById(
    "TELEMETRY_TEST_MIRROR_FOR_CUSTOM"
  ).snapshot();
  Telemetry.getHistogramById("TELEMETRY_TEST_MIRROR_FOR_CUSTOM").clear();
  Assert.equal(7 + 268435458, data.sum, "Sum in histogram is correct");
  Assert.equal(1, data.values["1"], "One sample in the low bucket");
  // Yes, the bucket is off-by-one compared to Glean.
  Assert.equal(1, data.values["268435457"], "One sample in the next bucket");
});

add_task(async function test_gifft_timing_dist() {
  let t1 = Glean.testOnlyIpc.aTimingDist.start();
  // Interleave some other metric's samples. bug 1768636.
  let ot1 = Glean.testOnly.whatTimeIsIt.start();
  let t2 = Glean.testOnlyIpc.aTimingDist.start();
  let ot2 = Glean.testOnly.whatTimeIsIt.start();
  Glean.testOnly.whatTimeIsIt.cancel(ot1);
  Glean.testOnly.whatTimeIsIt.cancel(ot2);

  await sleep(5);

  let t3 = Glean.testOnlyIpc.aTimingDist.start();
  Glean.testOnlyIpc.aTimingDist.cancel(t1);

  await sleep(5);

  Glean.testOnlyIpc.aTimingDist.stopAndAccumulate(t2); // 10ms
  Glean.testOnlyIpc.aTimingDist.stopAndAccumulate(t3); // 5ms

  let data = Glean.testOnlyIpc.aTimingDist.testGetValue();
  const NANOS_IN_MILLIS = 1e6;
  // bug 1701949 - Sleep gets close, but sometimes doesn't wait long enough.
  const EPSILON = 40000;

  // Variance in timing makes getting the sum impossible to know.
  // 10 and 5 input value can be trunacted to 4. + 9. >= 13. from cast
  Assert.greater(data.sum, 13 * NANOS_IN_MILLIS - EPSILON);

  // No guarantees from timers means no guarantees on buckets.
  // But we can guarantee it's only two samples.
  Assert.equal(
    2,
    Object.entries(data.values).reduce(
      (acc, [bucket, count]) => acc + count,
      0
    ),
    "Only two buckets with samples"
  );

  data = Telemetry.getHistogramById("TELEMETRY_TEST_EXPONENTIAL").snapshot();
  // Suffers from same cast truncation issue of 9.... and 4.... values
  Assert.greaterOrEqual(data.sum, 13, "Histogram's in milliseconds");
  Assert.equal(
    2,
    Object.entries(data.values).reduce(
      (acc, [bucket, count]) => acc + count,
      0
    ),
    "Only two samples"
  );
});

add_task(function test_gifft_string_list_works() {
  const value = "a string!";
  const value2 = "another string!";
  const value3 = "yet another string.";

  // `set` doesn't work in the mirror, so use `add`
  Glean.testOnlyIpc.aStringList.add(value);
  Glean.testOnlyIpc.aStringList.add(value2);
  Glean.testOnlyIpc.aStringList.add(value3);

  let val = Glean.testOnlyIpc.aStringList.testGetValue();
  // Note: This is incredibly fragile and will break if we ever rearrange items
  // in the string list.
  Assert.deepEqual([value, value2, value3], val);

  val = keyedScalarValue("telemetry.test.keyed_boolean_kind");
  // This too may be fragile.
  Assert.deepEqual(
    {
      [value]: true,
      [value2]: true,
      [value3]: true,
    },
    val
  );
});

add_task(function test_gifft_events() {
  Telemetry.setEventRecordingEnabled("telemetry.test", true);

  Glean.testOnlyIpc.noExtraEvent.record();
  var events = Glean.testOnlyIpc.noExtraEvent.testGetValue();
  Assert.equal(1, events.length);
  Assert.equal("test_only.ipc", events[0].category);
  Assert.equal("no_extra_event", events[0].name);

  let extra = { extra1: "can set extras", extra2: "passing more data" };
  Glean.testOnlyIpc.anEvent.record(extra);
  events = Glean.testOnlyIpc.anEvent.testGetValue();
  Assert.equal(1, events.length);
  Assert.equal("test_only.ipc", events[0].category);
  Assert.equal("an_event", events[0].name);
  Assert.deepEqual(extra, events[0].extra);

  TelemetryTestUtils.assertEvents(
    [
      ["telemetry.test", "not_expired_optout", "object1", undefined, undefined],
      ["telemetry.test", "mirror_with_extra", "object1", null, extra],
    ],
    { category: "telemetry.test" }
  );
});

add_task(function test_gifft_uuid() {
  const kTestUuid = "decafdec-afde-cafd-ecaf-decafdecafde";
  Glean.testOnlyIpc.aUuid.set(kTestUuid);
  Assert.equal(kTestUuid, Glean.testOnlyIpc.aUuid.testGetValue());
  Assert.equal(kTestUuid, scalarValue("telemetry.test.string_kind"));
});

add_task(function test_gifft_labeled_counter() {
  Assert.equal(
    undefined,
    Glean.testOnlyIpc.aLabeledCounter.a_label.testGetValue(),
    "New labels with no values should return undefined"
  );
  Glean.testOnlyIpc.aLabeledCounter.a_label.add(1);
  Glean.testOnlyIpc.aLabeledCounter.another_label.add(2);
  Glean.testOnlyIpc.aLabeledCounter.a_label.add(3);
  Assert.equal(4, Glean.testOnlyIpc.aLabeledCounter.a_label.testGetValue());
  Assert.equal(
    2,
    Glean.testOnlyIpc.aLabeledCounter.another_label.testGetValue()
  );
  // What about invalid/__other__?
  Assert.equal(
    undefined,
    Glean.testOnlyIpc.aLabeledCounter.__other__.testGetValue()
  );
  Glean.testOnlyIpc.aLabeledCounter["1".repeat(72)].add(3);
  Assert.throws(
    () => Glean.testOnlyIpc.aLabeledCounter.__other__.testGetValue(),
    /DataError/,
    "Can't get the value when you're error'd"
  );

  let value = keyedScalarValue(
    "telemetry.test.another_mirror_for_labeled_counter"
  );
  Assert.deepEqual(
    {
      a_label: 4,
      another_label: 2,
      ["1".repeat(72)]: 3,
    },
    value
  );
});

add_task(async function test_gifft_timespan() {
  // We start, briefly sleep and then stop.
  // That guarantees some time to measure.
  Glean.testOnly.mirrorTime.start();
  await sleep(10);
  Glean.testOnly.mirrorTime.stop();

  const NANOS_IN_MILLIS = 1e6;
  // bug 1701949 - Sleep gets close, but sometimes doesn't wait long enough.
  const EPSILON = 40000;
  Assert.greater(
    Glean.testOnly.mirrorTime.testGetValue(),
    10 * NANOS_IN_MILLIS - EPSILON
  );
  // Mirrored to milliseconds.
  Assert.greaterOrEqual(scalarValue("telemetry.test.mirror_for_timespan"), 9);
});

add_task(async function test_gifft_timespan_raw() {
  Glean.testOnly.mirrorTimeNanos.setRaw(15 /*ns*/);

  Assert.equal(15, Glean.testOnly.mirrorTimeNanos.testGetValue());
  // setRaw, unlike start/stop, mirrors the raw value directly.
  Assert.equal(scalarValue("telemetry.test.mirror_for_timespan_nanos"), 15);
});

add_task(async function test_gifft_labeled_boolean() {
  Assert.equal(
    undefined,
    Glean.testOnly.mirrorsForLabeledBools.a_label.testGetValue(),
    "New labels with no values should return undefined"
  );
  Glean.testOnly.mirrorsForLabeledBools.a_label.set(true);
  Glean.testOnly.mirrorsForLabeledBools.another_label.set(false);
  Assert.equal(
    true,
    Glean.testOnly.mirrorsForLabeledBools.a_label.testGetValue()
  );
  Assert.equal(
    false,
    Glean.testOnly.mirrorsForLabeledBools.another_label.testGetValue()
  );
  // What about invalid/__other__?
  Assert.equal(
    undefined,
    Glean.testOnly.mirrorsForLabeledBools.__other__.testGetValue()
  );
  Glean.testOnly.mirrorsForLabeledBools["1".repeat(72)].set(true);
  Assert.throws(
    () => Glean.testOnly.mirrorsForLabeledBools.__other__.testGetValue(),
    /DataError/,
    "Should throw because of a recording error."
  );

  // In Telemetry there is no invalid label
  let value = keyedScalarValue("telemetry.test.mirror_for_labeled_bool");
  Assert.deepEqual(
    {
      a_label: true,
      another_label: false,
      ["1".repeat(72)]: true,
    },
    value
  );
});

add_task(function test_gifft_boolean() {
  Glean.testOnly.meaningOfLife.set(42);
  Assert.equal(42, Glean.testOnly.meaningOfLife.testGetValue());
  Assert.equal(42, scalarValue("telemetry.test.mirror_for_quantity"));
});

add_task(function test_gifft_rate() {
  Glean.testOnlyIpc.irate.addToNumerator(22);
  Glean.testOnlyIpc.irate.addToDenominator(7);
  Assert.deepEqual(
    { numerator: 22, denominator: 7 },
    Glean.testOnlyIpc.irate.testGetValue()
  );
  Assert.deepEqual(
    { numerator: 22, denominator: 7 },
    keyedScalarValue("telemetry.test.mirror_for_rate")
  );
});

add_task(function test_gifft_numeric_limits() {
  // Glean and Telemetry don't share the same storage sizes or signedness.
  // Check the edges.

  // 0) Reset everything
  Services.fog.testResetFOG();
  Services.telemetry.getSnapshotForHistograms("main", true /* aClearStore */);
  Services.telemetry.getSnapshotForScalars("main", true /* aClearStore */);
  Services.telemetry.getSnapshotForKeyedScalars("main", true /* aClearStore */);

  // 1) Counter: i32 (saturates), mirrored to uint Scalar: u32 (overflows)
  // 1.1) Negative parameters refused.
  Glean.testOnlyIpc.aCounter.add(-20);
  // Unfortunately we can't check what the error was, due to API design.
  // (chutten blames chutten for his shortsightedness)
  Assert.throws(
    () => Glean.testOnlyIpc.aCounter.testGetValue(),
    /DataError/,
    "Can't get the value when you're error'd"
  );
  Assert.equal(undefined, scalarValue("telemetry.test.mirror_for_counter"));
  // Clear the error state
  Services.fog.testResetFOG();

  // 1.2) Values that sum larger than u32::max saturate (counter) and overflow (Scalar)
  // Sums to 2^32 + 1
  Glean.testOnlyIpc.aCounter.add(Math.pow(2, 31) - 1);
  Glean.testOnlyIpc.aCounter.add(1);
  Glean.testOnlyIpc.aCounter.add(Math.pow(2, 31) - 1);
  Glean.testOnlyIpc.aCounter.add(2);
  // Glean doesn't actually throw on saturation (bug 1751469),
  // so we can just check the saturation value.
  Assert.equal(Math.pow(2, 31) - 1, Glean.testOnlyIpc.aCounter.testGetValue());
  // Telemetry will have wrapped around to 1
  Assert.equal(1, scalarValue("telemetry.test.mirror_for_counter"));

  // 2) Quantity: i64 (saturates), mirrored to uint Scalar: u32 (overflows)
  // 2.1) Negative parameters refused.
  Glean.testOnly.meaningOfLife.set(-42);
  // Glean will error on this.
  Assert.throws(
    () => Glean.testOnly.meaningOfLife.testGetValue(),
    /DataError/,
    "Can't get the value when you're error'd"
  );
  // GIFFT doesn't tell Telemetry about the weird value at all.
  Assert.equal(undefined, scalarValue("telemetry.test.mirror_for_quantity"));
  // Clear the error state
  Services.fog.testResetFOG();

  // 2.2) A parameter larger than u32::max is passed to Glean unchanged,
  //      but is clamped to u32::max before being passed to Telemetry.
  Glean.testOnly.meaningOfLife.set(Math.pow(2, 32));
  Assert.equal(Math.pow(2, 32), Glean.testOnly.meaningOfLife.testGetValue());
  Assert.equal(
    Math.pow(2, 32) - 1,
    scalarValue("telemetry.test.mirror_for_quantity")
  );

  // 3) Rate: two i32 (saturates), mirrored to keyed uint Scalar: u32s (overflow)
  // 3.1) Negative parameters refused.
  Glean.testOnlyIpc.irate.addToNumerator(-22);
  Glean.testOnlyIpc.irate.addToDenominator(7);
  Assert.throws(
    () => Glean.testOnlyIpc.irate.testGetValue(),
    /DataError/,
    "Can't get the value when you're error'd"
  );
  Assert.deepEqual(
    { denominator: 7 },
    keyedScalarValue("telemetry.test.mirror_for_rate")
  );
  // Clear the error state
  Services.fog.testResetFOG();
  // Clear the partial Telemetry value
  Services.telemetry.getSnapshotForKeyedScalars("main", true /* aClearStore */);

  // Now the denominator:
  Glean.testOnlyIpc.irate.addToNumerator(22);
  Glean.testOnlyIpc.irate.addToDenominator(-7);
  Assert.throws(
    () => Glean.testOnlyIpc.irate.testGetValue(),
    /DataError/,
    "Can't get the value when you're error'd"
  );
  Assert.deepEqual(
    { numerator: 22 },
    keyedScalarValue("telemetry.test.mirror_for_rate")
  );

  // 4) Timespan
  // ( Can't overflow time without finding a way to get TimeStamp to think
  // we're 2^32 milliseconds later without waiting a month )

  // 5) TimingDistribution
  // ( Can't overflow time with start() and stopAndAccumulate() without
  // waiting for ages. But we _do_ have a test-only raw API...)
  // The max sample for timing_distribution is 600000000000.
  // The type for timing_distribution samples is i64.
  // This means when we explore the edges of GIFFT's limits, we're well past
  // Glean's limits. All we can get out of Glean is errors.
  // (Which is good for data, difficult for tests.)
  // But GIFFT should properly saturate in Telemetry at i32::max,
  // so we shall test that.
  Glean.testOnlyIpc.aTimingDist.testAccumulateRawMillis(Math.pow(2, 31) + 1);
  Glean.testOnlyIpc.aTimingDist.testAccumulateRawMillis(Math.pow(2, 32) + 1);
  Assert.throws(
    () => Glean.testOnlyIpc.aTimingDist.testGetValue(),
    /DataError/,
    "Can't get the value when you're error'd"
  );
  let snapshot = Telemetry.getHistogramById(
    "TELEMETRY_TEST_EXPONENTIAL"
  ).snapshot();
  Assert.equal(
    snapshot.values["2147483646"],
    2,
    "samples > i32::max should end up in the top bucket"
  );
});

add_task(function test_gifft_url() {
  const value = "https://www.example.com";
  Glean.testOnlyIpc.aUrl.set(value);

  Assert.equal(value, Glean.testOnlyIpc.aUrl.testGetValue());
  Assert.equal(value, scalarValue("telemetry.test.mirror_for_url"));
});

add_task(function test_gifft_url_cropped() {
  const value = `https://example.com${"/test".repeat(47)}`;
  Glean.testOnlyIpc.aUrl.set(value);

  Assert.equal(value, Glean.testOnlyIpc.aUrl.testGetValue());
  // We expect the mirrored URL to be truncated at the maximum
  // length supported by string scalars.
  Assert.equal(
    value.substring(0, 50),
    scalarValue("telemetry.test.mirror_for_url")
  );
});