Bug 1720233 [wpt PR 29645] - Reattempt upstreaming of JS Self-Profiling API WPTs, a=testonly
☠☠ backed out by dec0a179f851 ☠ ☠
authorAndrew Comminos <acomminos@fb.com>
Sat, 17 Jul 2021 09:51:22 +0000
changeset 585839 8339cb3e70c8293e98b851d9ff74da6d5b18484d
parent 585838 1ef81c9e982b2eaeb1fcfc55bd336d25b6935beb
child 585840 3ecf9f0828c31d51ab8ff79525036af92883d928
push id38620
push usercsabou@mozilla.com
push dateSun, 18 Jul 2021 09:08:29 +0000
treeherdermozilla-central@cc4e5ea0c986 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstestonly
bugs1720233, 29645, 956688, 3021557, 901174
milestone92.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1720233 [wpt PR 29645] - Reattempt upstreaming of JS Self-Profiling API WPTs, a=testonly Automatic update from web-platform-tests Reattempt upstreaming of JS Self-Profiling API WPTs Wait for a delay of SAMPLE_INTERVAL + 500ms to collect samples, ensuring that we're more robust to flakiness on Mac OS CI bots. Bug: 956688 Change-Id: If39f1d59a09857d5bcb52c5c5d03782de4055b30 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3021557 Reviewed-by: Nicolás Peña Moreno <npm@chromium.org> Commit-Queue: Andrew Comminos <acomminos@fb.com> Cr-Commit-Position: refs/heads/master@{#901174} -- wpt-commits: 9d6d7f8c5356ab9fc4b49656323ce196f1176613 wpt-pr: 29645
testing/web-platform/tests/js-self-profiling/__dir__.headers
testing/web-platform/tests/js-self-profiling/concurrent-profilers.https.html
testing/web-platform/tests/js-self-profiling/external-script.https.html
testing/web-platform/tests/js-self-profiling/function-names.https.html
testing/web-platform/tests/js-self-profiling/iframe-context-filtration.https.html
testing/web-platform/tests/js-self-profiling/inline-script.html
testing/web-platform/tests/js-self-profiling/max-buffer-size.https.html
testing/web-platform/tests/js-self-profiling/resources/__dir__.headers
testing/web-platform/tests/js-self-profiling/resources/child-frame.html
testing/web-platform/tests/js-self-profiling/resources/external-script.js
testing/web-platform/tests/js-self-profiling/resources/external-script.js.headers
testing/web-platform/tests/js-self-profiling/resources/profile-utils.js
testing/web-platform/tests/js-self-profiling/time-domain.window.js
testing/web-platform/tests/js-self-profiling/without-document-policy/disabled.https.html
--- a/testing/web-platform/tests/js-self-profiling/__dir__.headers
+++ b/testing/web-platform/tests/js-self-profiling/__dir__.headers
@@ -1,3 +1,1 @@
-Cross-Origin-Opener-Policy: same-origin
-Cross-Origin-Embedder-Policy: require-corp
 Document-Policy: js-profiling
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/js-self-profiling/concurrent-profilers.https.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <script src="/resources/testharness.js"></script>
+  <script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+  <script>
+    promise_test(async t => {
+      const profiler_a = new Profiler({
+        sampleInterval: 10,
+        maxBufferSize: Number.MAX_SAFE_INTEGER,
+      });
+
+      const profiler_b = new Profiler({
+        sampleInterval: 10,
+        maxBufferSize: Number.MAX_SAFE_INTEGER,
+      });
+
+      const trace_b = await profiler_b.stop();
+      const trace_a = await profiler_a.stop();
+    }, 'concurrent profilers should be supported');
+  </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/js-self-profiling/external-script.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <script src="/resources/testharness.js"></script>
+  <script src="/resources/testharnessreport.js"></script>
+
+  <script src="resources/profile-utils.js"></script>
+
+  <script id="external-script" src="resources/external-script.js"></script>
+</head>
+<body>
+  <script>
+    promise_test(async t => {
+      const trace = await ProfileUtils.profileFunction(function trampoline(sample) {
+        externalScriptFunction(sample);
+      });
+
+      const scriptUrl = document.querySelector('#external-script').src;
+      assert_true(ProfileUtils.containsResource(trace, scriptUrl),
+                  'external resource is included');
+
+      const expectedTrampolineFrame = {
+        name: 'trampoline',
+        resourceId: trace.resources.indexOf(
+          location.href,
+        ),
+      };
+      const expectedExternalFrame = {
+        name: 'externalScriptFunction',
+        resourceId: trace.resources.indexOf(scriptUrl),
+        line: EXTERNAL_SCRIPT_FUNCTION_LINE,
+        column: EXTERNAL_SCRIPT_FUNCTION_COLUMN,
+      };
+
+      assert_true(ProfileUtils.containsFrame(trace, expectedTrampolineFrame),
+                  'trampoline function included');
+
+      assert_true(ProfileUtils.containsFrame(trace, expectedExternalFrame),
+                  'external script function included');
+
+      assert_true(ProfileUtils.containsSubstack(trace, [
+                    externalScriptFunction,
+                    expectedTrampolineFrame,
+                  ]),
+                  'stack exists with external script function');
+
+    }, 'external script function details');
+  </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/js-self-profiling/function-names.https.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <script src="/resources/testharness.js"></script>
+  <script src="/resources/testharnessreport.js"></script>
+
+  <script src="resources/profile-utils.js"></script>
+</head>
+<body>
+  <script>
+    promise_test(async t => {
+      const f = function namedFunctionExpression(sample) {
+        sample();
+      };
+      await ProfileUtils.testFunction(f, {
+        name: 'namedFunctionExpression',
+      });
+    }, 'function expression names are logged correctly');
+
+    promise_test(async t => {
+      const f = function(sample) {
+        sample();
+      };
+      await ProfileUtils.testFunction(f, {
+        name: '',
+      });
+    }, 'anonymous function expression names are logged correctly');
+
+    promise_test(async t => {
+      function namedFunctionDeclaration(sample) {
+        sample();
+      };
+      await ProfileUtils.testFunction(namedFunctionDeclaration, {
+        name: 'namedFunctionDeclaration',
+      });
+    }, 'function declaration names are logged correctly');
+
+    // Methods should use their label as the function/frame name. Source:
+    // https://www.ecma-international.org/ecma-262/#sec-method-definitions-runtime-semantics-propertydefinitionevaluation
+    promise_test(async t => {
+      class SomeClass {
+        method(sample) {
+          sample();
+        }
+      }
+      let instance = new SomeClass();
+
+      await ProfileUtils.testFunction(instance.method.bind(instance), {
+        name: 'method',
+      });
+    }, 'class method names are logged correctly');
+
+    // Getter methods should use `get ${label}` as the function/frame name. Source:
+    // https://www.ecma-international.org/ecma-262/#sec-method-definitions-runtime-semantics-propertydefinitionevaluation
+    promise_test(t => ProfileUtils.testFunction(sample => {
+        class SomeClass {
+          get someValue() {
+            sample();
+          }
+        }
+        let instance = new SomeClass();
+        instance.someValue;
+      }, {
+        name: 'get someValue',
+      }
+    ), 'class getter names are logged correctly');
+
+    // Setter methods should use `set ${label}` as the function/frame name. Source:
+    // https://www.ecma-international.org/ecma-262/#sec-method-definitions-runtime-semantics-propertydefinitionevaluation
+    promise_test(t => ProfileUtils.testFunction(sample => {
+        class SomeClass {
+          set someValue(_) {
+            sample();
+          }
+        }
+        let instance = new SomeClass();
+        instance.someValue = 5;
+      }, {
+        name: 'set someValue',
+      }
+    ), 'class setter names are logged correctly');
+  </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/js-self-profiling/iframe-context-filtration.https.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <script src="/resources/testharness.js"></script>
+  <script src="/resources/testharnessreport.js"></script>
+
+  <script src="resources/profile-utils.js"></script>
+</head>
+<body>
+  <iframe src="resources/child-frame.html"></iframe>
+
+  <script>
+    promise_test(_ => new Promise(res => window.addEventListener('load', res)),
+                 'wait for load event');
+
+    promise_test(async t => {
+      const profiler = new Profiler({
+        sampleInterval: 10,
+        maxBufferSize: Number.MAX_SAFE_INTEGER,
+      });
+
+      const iframe = frames[0];
+      await ProfileUtils.forceSampleFrame(iframe);
+
+      const trace = await profiler.stop();
+
+      assert_false(ProfileUtils.containsFrame(trace, { name: 'sampleFromMessage' }),
+                   'function from child context not included in trace');
+
+      const childUrl = iframe.src;
+      assert_false(ProfileUtils.containsResource(trace, childUrl),
+                  'child resources are not included');
+    }, 'functions from child frame are not included in profile created by parent frame');
+  </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/js-self-profiling/inline-script.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <script src="/resources/testharness.js"></script>
+  <script src="/resources/testharnessreport.js"></script>
+
+  <script src="resources/profile-utils.js"></script>
+</head>
+<body>
+  <script>
+    // Note: moving these function definitions will change the expected
+    //       outcomes below.
+    function nestedInlineScriptFunction(sample) {
+      sample();
+    }
+
+    function inlineScriptFunction(sample) {
+      nestedInlineScriptFunction(sample);
+    }
+  </script>
+
+  <script>
+    promise_test(async t => {
+      const trace = await ProfileUtils.profileFunction(inlineScriptFunction);
+
+      assert_true(ProfileUtils.containsResource(trace, location.href),
+                  'inline script resource is included');
+
+      assert_true(ProfileUtils.containsSubstack(trace, [
+        {
+          name: 'nestedInlineScriptFunction',
+          resourceId: trace.resources.indexOf(location.href),
+          line: 13,
+          column: 40,
+        },
+        {
+          name: 'inlineScriptFunction',
+          resourceId: trace.resources.indexOf(location.href),
+          line: 17,
+          column: 34,
+        },
+      ]));
+    }, 'inline script function details');
+  </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/js-self-profiling/max-buffer-size.https.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <script src="/resources/testharness.js"></script>
+  <script src="/resources/testharnessreport.js"></script>
+
+  <script src="resources/profile-utils.js"></script>
+</head>
+<body>
+  <script>
+    promise_test(async t => {
+      assert_throws_js(TypeError, () => {
+        new Profiler({ sampleInterval: 10 });
+      });
+    }, 'max buffer size must be defined');
+
+    promise_test(async t => {
+      const profiler = new Profiler({
+        sampleInterval: 10,
+        maxBufferSize: 2,
+      });
+
+      // Force 3 samples with a max buffer size of 2.
+      for (let i = 0; i < 3; i++) {
+        ProfileUtils.forceSample();
+      }
+
+      const trace = await profiler.stop();
+      assert_equals(trace.samples.length, 2);
+    }, 'max buffer size is not exceeded');
+
+    promise_test(async t => {
+      const pf = [];
+      pf[0] = new Profiler({ sampleInterval: 10,  maxBufferSize: Number.MAX_SAFE_INTEGER });
+      pf[1] = new Profiler({ sampleInterval: 10,  maxBufferSize: 1 });
+
+      const watcher = new EventWatcher(t, pf[1], ['samplebufferfull']);
+      pf[0].addEventListener("samplebufferfull", ()=>{
+        assert_unreached("samplebufferfull invoked on wrong profiler");
+      });
+      pf[1].addEventListener("samplebufferfull", async ()=>{
+        pf[0].stop();
+        pf[1].stop();
+        assertTrue(true);
+      });
+
+      for (let i = 0; i < 2; i++) {
+        ProfileUtils.forceSample();
+      }
+
+      return watcher.wait_for('samplebufferfull');
+    }, 'executed samplebufferfull function');
+  </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/js-self-profiling/resources/__dir__.headers
@@ -0,0 +1,1 @@
+Document-Policy: js-profiling
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/js-self-profiling/resources/child-frame.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+  <script src="profile-utils.js"></script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/js-self-profiling/resources/external-script.js
@@ -0,0 +1,9 @@
+// NOTE: Modifying the location of functions in this file will cause
+// `external-script.html` to fail! Please update the following constants
+// accordingly.
+const EXTERNAL_SCRIPT_FUNCTION_LINE = 7;
+const EXTERNAL_SCRIPT_FUNCTION_COLUMN = 32;
+
+function externalScriptFunction(sample) {
+  sample();
+}
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/js-self-profiling/resources/external-script.js.headers
@@ -0,0 +1,1 @@
+Access-Control-Allow-Origin: *
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/js-self-profiling/resources/profile-utils.js
@@ -0,0 +1,126 @@
+(function(global) {
+  const TEST_SAMPLE_INTERVAL = 10;
+
+  function forceSample() {
+    // Spin for |TEST_SAMPLE_INTERVAL + 500|ms to ensure that a sample occurs
+    // before this function returns. As periodic sampling is enforced by a
+    // SHOULD clause, it is indeed testable.
+    //
+    // More reliable sampling will be handled in a future testdriver RFC
+    // (https://github.com/web-platform-tests/rfcs/pull/81).
+    for (const deadline = performance.now() + TEST_SAMPLE_INTERVAL + 500; performance.now() < deadline;);
+  }
+
+  // Creates a new profile that captures the execution of when the given
+  // function calls the `sample` function passed to it.
+  async function profileFunction(func) {
+    const profiler = new Profiler({
+      sampleInterval: TEST_SAMPLE_INTERVAL,
+      maxBufferSize: Number.MAX_SAFE_INTEGER,
+    });
+
+    func(() => forceSample());
+
+    const trace = await profiler.stop();
+
+    // Sanity check ensuring that we captured a sample.
+    assert_greater_than(trace.resources.length, 0);
+    assert_greater_than(trace.frames.length, 0);
+    assert_greater_than(trace.stacks.length, 0);
+    assert_greater_than(trace.samples.length, 0);
+
+    return trace;
+  }
+
+  async function testFunction(func, frame) {
+    const trace = await profileFunction(func);
+    assert_true(containsFrame(trace, frame), 'trace contains frame');
+  }
+
+  function substackMatches(trace, stackId, expectedStack) {
+    if (expectedStack.length === 0) {
+      return true;
+    }
+    if (stackId === undefined) {
+      return false;
+    }
+
+    const stackElem = trace.stacks[stackId];
+    const expectedFrame = expectedStack[0];
+
+    if (!frameMatches(trace.frames[stackElem.frameId], expectedFrame)) {
+      return false;
+    }
+    return substackMatches(trace, stackElem.parentId, expectedStack.slice(1));
+  }
+
+  // Returns true if the trace contains a frame matching the given specification.
+  // We define a "match" as follows: a frame A matches an expectation E if (and
+  // only if) for each field of E, A has the same value.
+  function containsFrame(trace, expectedFrame) {
+    return trace.frames.find(frame => {
+      return frameMatches(frame, expectedFrame);
+    }) !== undefined;
+  }
+
+  // Returns true if a trace contains a substack in one of its samples, ordered
+  // leaf to root.
+  function containsSubstack(trace, expectedStack) {
+    return trace.samples.find(sample => {
+      let stackId = sample.stackId;
+      while (stackId !== undefined) {
+        if (substackMatches(trace, stackId, expectedStack)) {
+          return true;
+        }
+        stackId = trace.stacks[stackId].parentId;
+      }
+      return false;
+    }) !== undefined;
+  }
+
+  function containsResource(trace, expectedResource) {
+    return trace.resources.includes(expectedResource);
+  }
+
+  // Compares each set field of `expected` against the given frame `actual`.
+  function frameMatches(actual, expected) {
+    return (expected.name === undefined || expected.name === actual.name) &&
+           (expected.resourceId === undefined || expected.resourceId === actual.resourceId) &&
+           (expected.line === undefined || expected.line === actual.line) &&
+           (expected.column === undefined || expected.column === actual.column);
+  }
+
+  function forceSampleFrame(frame) {
+    const channel = new MessageChannel();
+    const replyPromise = new Promise(res => {
+      channel.port1.onmessage = res;
+    });
+    frame.postMessage('', '*', [channel.port2]);
+    return replyPromise;
+  }
+
+  window.addEventListener('message', message => {
+    // Force sample in response to messages received.
+    (function sampleFromMessage() {
+      ProfileUtils.forceSample();
+      message.ports[0].postMessage('');
+    })();
+  });
+
+  global.ProfileUtils = {
+    // Capturing
+    profileFunction,
+    forceSample,
+
+    // Containment checks
+    containsFrame,
+    containsSubstack,
+    containsResource,
+
+    // Cross-frame sampling
+    forceSampleFrame,
+
+    // Assertions
+    testFunction,
+  };
+})(this);
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/js-self-profiling/time-domain.window.js
@@ -0,0 +1,19 @@
+// META: script=resources/profile-utils.js
+
+promise_test(async () => {
+  const start = performance.now();
+
+  const profiler = new Profiler({
+    sampleInterval: 10,
+    maxBufferSize: Number.MAX_SAFE_INTEGER,
+  });
+  ProfileUtils.forceSample();
+  const trace = await profiler.stop();
+
+  const end = performance.now();
+
+  assert_greater_than(trace.samples.length, 0);
+  for (const sample of trace.samples) {
+    assert_between_inclusive(sample.timestamp, start, end);
+  }
+}, 'sample timestamps use the current high-resolution time');
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/js-self-profiling/without-document-policy/disabled.https.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <script src="/resources/testharness.js"></script>
+  <script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+  <script>
+    test(t => {
+      assert_throws_dom('NotAllowedError', () => {
+        new Profiler({ sampleInterval: 10, maxBufferSize: Number.MAX_SAFE_INTEGER });
+      });
+    }, 'profiling should throw without passing document policy');
+  </script>
+</body>
+</html>