summaryrefslogtreecommitdiffstats
path: root/testing/talos/talos/pageloader/chrome/tscroll.js
blob: e354a474fbc483741e992dde7c8779140de0b8b8 (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
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/* eslint-env mozilla/frame-script */

// Note: This file is used at both tscrollx and tp5o_scroll. With the former as
//       unprivileged code.
// - Please make sure that any changes apply cleanly to all use cases.

function testScroll(target, stepSize, opt_reportFunc, opt_numSteps) {
  var win;
  if (target == "content") {
    target = content.wrappedJSObject;
    win = content;
  } else {
    win = window;
  }

  var result = {
    names: [],
    values: [],
  };
  // We report multiple results, so we base the name on the path.
  // Everything after '/tp5n/' if exists (for tp5o_scroll), or the file name at
  // the path if non-empty (e.g. with tscrollx), or the last dir otherwise (e.g.
  // 'mydir' for 'http://my.domain/dir1/mydir/').
  var href = win.location.href;
  var testBaseName =
    href.split("/tp5n/")[1] ||
    href.split("/").pop() ||
    href.split("/").splice(-2, 1)[0] ||
    "REALLY_WEIRD_URI";

  // Verbatim copy from talos-powers/content/TalosPowersContent.js
  // If the origin changes, this copy should be updated.
  TalosPowersParent = {
    replyId: 1,

    // dispatch an event to the framescript and register the result/callback event
    exec(commandName, arg, callback, opt_custom_window) {
      let win = opt_custom_window || window;
      let replyEvent = "TalosPowers:ParentExec:ReplyEvent:" + this.replyId++;
      if (callback) {
        win.addEventListener(
          replyEvent,
          function (e) {
            callback(e.detail);
          },
          { once: true }
        );
      }
      win.dispatchEvent(
        new win.CustomEvent("TalosPowers:ParentExec:QueryEvent", {
          bubbles: true,
          detail: {
            command: {
              name: commandName,
              data: arg,
            },
            listeningTo: replyEvent,
          },
        })
      );
    },
  };
  // End of code from talos-powers

  var report;
  /**
   * Sets up the value of 'report' as a function for reporting the test result[s].
   * Chooses between the "usual" tpRecordTime which the pageloader addon injects
   * to pages, or a custom function in case we're a framescript which pageloader
   * added to the tested page, or a debug tpRecordTime from talos-debug.js if
   * running in a plain browser.
   *
   * @returns Promise
   */
  function P_setupReportFn() {
    return new Promise(function (resolve) {
      report = opt_reportFunc || win.tpRecordTime;
      if (report == "PageLoader:RecordTime") {
        report = function (duration, start, name) {
          var msg = { time: duration, startTime: start, testName: name };
          sendAsyncMessage("PageLoader:RecordTime", msg);
        };
        resolve();
        return;
      }

      // Not part of the test and does nothing if we're within talos.
      // Provides an alternative tpRecordTime (with some stats display) if running in a browser.
      if (!report && document.head) {
        var imported = document.createElement("script");
        imported.addEventListener("load", function () {
          report = tpRecordTime;
          resolve();
        });

        imported.src =
          "../../scripts/talos-debug.js?dummy=" + win.performance.now(); // For some browsers to re-read
        document.head.appendChild(imported);
        return;
      }

      resolve();
    });
  }

  function FP_wait(ms) {
    return function () {
      return new Promise(function (resolve) {
        win.setTimeout(resolve, ms);
      });
    };
  }

  function rAF(fn) {
    return win.requestAnimationFrame(fn);
  }

  function P_rAF() {
    return new Promise(function (resolve) {
      rAF(resolve);
    });
  }

  function P_MozAfterPaint() {
    return new Promise(function (resolve) {
      win.addEventListener("MozAfterPaint", () => resolve(), { once: true });
    });
  }

  var isWindow = target.self === target;

  var getPos = isWindow
    ? function () {
        return target.pageYOffset;
      }
    : function () {
        return target.scrollTop;
      };

  var gotoTop = isWindow
    ? function () {
        target.scroll(0, 0);
        ensureScroll();
      }
    : function () {
        target.scrollTop = 0;
        ensureScroll();
      };

  var doScrollTick = isWindow
    ? function () {
        target.scrollBy(0, stepSize);
        ensureScroll();
      }
    : function () {
        target.scrollTop += stepSize;
        ensureScroll();
      };

  var setSmooth = isWindow
    ? function () {
        target.document.scrollingElement.style.scrollBehavior = "smooth";
      }
    : function () {
        target.style.scrollBehavior = "smooth";
      };

  var gotoBottom = isWindow
    ? function () {
        target.scrollTo(0, target.scrollMaxY);
      }
    : function () {
        target.scrollTop = target.scrollHeight;
      };

  function ensureScroll() {
    // Ensure scroll by reading computed values. screenY is for X11.
    if (!this.dummyEnsureScroll) {
      this.dummyEnsureScroll = 1;
    }
    this.dummyEnsureScroll += win.screenY + getPos();
  }

  // For reference, rAF should fire on vsync, but Gecko currently doesn't use vsync.
  // Instead, it uses 1000/layout.frame_rate
  // (with 60 as default value when layout.frame_rate == -1).
  function P_syncScrollTest() {
    return new Promise(function (resolve) {
      // We should be at the top of the page now.
      var start = win.performance.now();
      var lastScrollPos = getPos();
      var lastScrollTime = start;
      var durations = [];

      function tick() {
        var now = win.performance.now();
        var duration = now - lastScrollTime;
        lastScrollTime = now;

        durations.push(duration);
        doScrollTick();

        /* stop scrolling if we can't scroll more, or if we've reached requested number of steps */
        if (
          getPos() == lastScrollPos ||
          (opt_numSteps && durations.length >= opt_numSteps + 2)
        ) {
          let profilerPaused = Promise.resolve();
          if (typeof TalosContentProfiler !== "undefined") {
            profilerPaused = TalosContentProfiler.subtestEnd(
              testBaseName,
              true
            );
          }

          profilerPaused.then(() => {
            // Note: The first (1-5) intervals WILL be longer than the rest.
            // First interval might include initial rendering and be extra slow.
            // Also requestAnimationFrame needs to sync (optimally in 1 frame) after long frames.
            // Suggested: Ignore the first 5 intervals.

            durations.pop(); // Last step was 0.
            durations.pop(); // and the prev one was shorter and with end-of-page logic, ignore both.

            if (win.talosDebug) {
              win.talosDebug.displayData = true;
            } // In a browser: also display all data points.

            // For analysis (otherwise, it's too many data points for talos):
            var sum = 0;
            for (var i = 0; i < durations.length; i++) {
              sum += Number(durations[i]);
            }

            // Report average interval or (failsafe) 0 if no intervls were recorded
            result.values.push(durations.length ? sum / durations.length : 0);
            result.names.push(testBaseName);
            resolve();
          });
          return;
        }

        lastScrollPos = getPos();
        P_MozAfterPaint().then(tick);
      }

      if (typeof TalosContentProfiler !== "undefined") {
        TalosContentProfiler.subtestStart(testBaseName, true).then(() => {
          rAF(tick);
        });
      }
    });
  }

  function P_testAPZScroll() {
    var APZ_MEASURE_MS = 1000;

    function startFrameTimeRecording(cb) {
      TalosPowersParent.exec("startFrameTimeRecording", null, cb, win);
    }

    function stopFrameTimeRecording(handle, cb) {
      TalosPowersParent.exec("stopFrameTimeRecording", handle, cb, win);
    }

    return new Promise(function (resolve) {
      setSmooth();

      var handle = -1;
      startFrameTimeRecording(function (rv) {
        handle = rv;
      });

      // Get the measurements after APZ_MEASURE_MS of scrolling
      win.setTimeout(function () {
        stopFrameTimeRecording(handle, function (intervals) {
          function average(arr) {
            var sum = 0;
            for (var i = 0; i < arr.length; i++) {
              sum += arr[i];
            }
            return arr.length ? sum / arr.length : 0;
          }

          // remove two frames on each side of the recording to get a cleaner result
          result.values.push(average(intervals.slice(2, intervals.length - 2)));
          result.names.push("CSSOM." + testBaseName);

          resolve();
        });
      }, APZ_MEASURE_MS);

      gotoBottom(); // trigger the APZ scroll
    });
  }

  P_setupReportFn()
    .then(FP_wait(260))
    .then(gotoTop)
    .then(P_rAF)
    .then(P_syncScrollTest)
    .then(gotoTop)
    .then(FP_wait(260))
    .then(P_testAPZScroll)
    .then(function () {
      report(result.values.join(","), 0, result.names.join(","));
    });
}

// This code below here is unique to tscroll.js inside of pageloader
try {
  function handleMessageFromChrome(message) {
    var payload = message.data.details;
    testScroll(
      payload.target,
      payload.stepSize,
      "PageLoader:RecordTime",
      payload.opt_numSteps
    );
  }

  addMessageListener("PageLoader:ScrollTest", handleMessageFromChrome);
} catch (e) {}