Bug 1567390 - Add a stackwalking test to the profiler; r=canaltinova
authorGreg Tatum <gtatum@mozilla.com>
Fri, 02 Aug 2019 14:06:26 +0000
changeset 485962 6ab1565e59002751c4bd5b64be99a14f69ce8eee
parent 485961 5313c2e63f0946974d5b53c96d8f4f5f3dcb2290
child 485963 9527800a8dcd455510083b6f788b0416a3b8f8b6
push id91562
push usergtatum@mozilla.com
push dateFri, 02 Aug 2019 15:11:46 +0000
treeherderautoland@9655b89e5f5a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerscanaltinova
bugs1567390
milestone70.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 1567390 - Add a stackwalking test to the profiler; r=canaltinova Differential Revision: https://phabricator.services.mozilla.com/D40308
tools/profiler/tests/xpcshell/head_profiler.js
tools/profiler/tests/xpcshell/test_feature_stackwalking.js
tools/profiler/tests/xpcshell/xpcshell.ini
--- a/tools/profiler/tests/xpcshell/head_profiler.js
+++ b/tools/profiler/tests/xpcshell/head_profiler.js
@@ -43,16 +43,24 @@ function getAllPayloadsOfType(profile, t
  */
 function wait(time) {
   return new Promise(resolve => {
     // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
     setTimeout(resolve, time);
   });
 }
 
+/**
+ * This function takes a thread, and a sample tuple from the "data" array, and inflates
+ * the frame to be an array of strings.
+ *
+ * @param {Object} thread - The thread from the profile.
+ * @param {Array} sample - The tuple from the thread.samples.data array.
+ * @returns {Array<string>} An array of function names.
+ */
 function getInflatedStackLocations(thread, sample) {
   let stackTable = thread.stackTable;
   let frameTable = thread.frameTable;
   let stringTable = thread.stringTable;
   let SAMPLE_STACK_SLOT = thread.samples.schema.stack;
   let STACK_PREFIX_SLOT = stackTable.schema.prefix;
   let STACK_FRAME_SLOT = stackTable.schema.frame;
   let FRAME_LOCATION_SLOT = frameTable.schema.location;
@@ -66,8 +74,90 @@ function getInflatedStackLocations(threa
     let frame = frameTable.data[stackEntry[STACK_FRAME_SLOT]];
     locations.push(stringTable[frame[FRAME_LOCATION_SLOT]]);
     stackIndex = stackEntry[STACK_PREFIX_SLOT];
   }
 
   // The profiler tree is inverted, so reverse the array.
   return locations.reverse();
 }
+
+/**
+ * It can be helpful to deterministically do at least one more profile sample. Sampling
+ * is done based on a timer. This function spins on a while loop until at least one more
+ * sample is collected.
+ *
+ * @return {number} The index of the collected sample.
+ */
+async function doAtLeastOnePeriodicSample() {
+  async function getProfileSampleCount() {
+    const profile = await Services.profiler.getProfileDataAsync();
+    return profile.threads[0].samples.data.length;
+  }
+
+  const sampleCount = await getProfileSampleCount();
+  // Create an infinite loop until a sample has been collected.
+  while (true) {
+    if (sampleCount < (await getProfileSampleCount())) {
+      return sampleCount;
+    }
+  }
+}
+
+/**
+ * This utility matches up stacks to see if they contain a certain sequence of stack
+ * frames. A correctly functioning profiler will have a certain sequence of stacks,
+ * but we can't always determine exactly which stacks will show up due to implementation
+ * changes, as well as memory addresses being arbitrary to that particular build.
+ *
+ * This function triggers a test failure with a nice debug message when it fails.
+ *
+ * @param {Array<string>} actualStackFrames - As generated by inflatedStackFrames.
+ * @param {Array<string | RegExp>} expectedStackFrames - Matches a subset of actualStackFrames
+ */
+function expectStackToContain(
+  actualStackFrames,
+  expectedStackFrames,
+  message = "The actual stack and expected stack do not match."
+) {
+  // Log the stacks that are being passed to this assertion, as it could be useful
+  // for when these tests fail.
+  console.log("Actual stack: ", actualStackFrames);
+  console.log(
+    "Expected to contain: ",
+    expectedStackFrames.map(s => s.toString())
+  );
+
+  let actualIndex = 0;
+
+  // Start walking the expected stack and look for matches.
+  for (
+    let expectedIndex = 0;
+    expectedIndex < expectedStackFrames.length;
+    expectedIndex++
+  ) {
+    const expectedStackFrame = expectedStackFrames[expectedIndex];
+
+    while (true) {
+      // Make sure that we haven't run out of actual stack frames.
+      if (actualIndex >= actualStackFrames.length) {
+        info(`Could not find a match for: "${expectedStackFrame.toString()}"`);
+        Assert.ok(false, message);
+      }
+
+      const actualStackFrame = actualStackFrames[actualIndex];
+      actualIndex++;
+
+      const itMatches =
+        typeof expectedStackFrame === "string"
+          ? expectedStackFrame === actualStackFrame
+          : actualStackFrame.match(expectedStackFrame);
+
+      if (itMatches) {
+        // We found a match, break out of this loop.
+        break;
+      }
+      // Keep on looping looking for a match.
+    }
+  }
+
+  Assert.ok(true, message);
+}
new file mode 100644
--- /dev/null
+++ b/tools/profiler/tests/xpcshell/test_feature_stackwalking.js
@@ -0,0 +1,51 @@
+/* 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/. */
+
+/**
+ * Do a basic test to see if native frames are being collected for stackwalking. This
+ * test is fairly naive, as it does not attempt to check that these are valid symbols,
+ * only that some kind of stack walking is happening. It does this by making sure at
+ * least two native frames are collected.
+ */
+add_task(async () => {
+  if (!AppConstants.MOZ_GECKO_PROFILER) {
+    return;
+  }
+  const entries = 10000;
+  const interval = 1;
+  const threads = [];
+  const features = ["stackwalk"];
+
+  Services.profiler.StartProfiler(entries, interval, features, threads);
+  const sampleIndex = await doAtLeastOnePeriodicSample();
+
+  const profile = await Services.profiler.getProfileDataAsync();
+  const [thread] = profile.threads;
+  const { samples } = thread;
+
+  const inflatedStackFrames = getInflatedStackLocations(
+    thread,
+    samples.data[sampleIndex]
+  );
+  const nativeStack = /^0x[0-9a-f]+$/;
+
+  expectStackToContain(
+    inflatedStackFrames,
+    [
+      "(root)",
+      // There are probably more native stacks here.
+      nativeStack,
+      nativeStack,
+      // Since this is an xpcshell test we know that JavaScript will run:
+      "js::RunScript",
+      // There are probably more native stacks here.
+      nativeStack,
+      nativeStack,
+    ],
+    "Expected native stacks to be interleaved between some frame labels. There should" +
+      "be more than one native stack if stack walking is working correctly. There " +
+      "is no attempt here to determine if the memory addresses point to the correct " +
+      "symbols"
+  );
+});
--- a/tools/profiler/tests/xpcshell/xpcshell.ini
+++ b/tools/profiler/tests/xpcshell/xpcshell.ini
@@ -13,8 +13,9 @@ skip-if = true
 skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536652
 [test_enterjit_osr_disabling.js]
 skip-if = !debug
 [test_enterjit_osr_enabling.js]
 skip-if = !debug
 [test_asm.js]
 [test_feature_mainthreadio.js]
 skip-if = release_or_beta || (os == "win" && processor == "aarch64") # The IOInterposer is in an ifdef, aarch64 due to 1536657
+[test_feature_stackwalking.js]