summaryrefslogtreecommitdiffstats
path: root/tools/profiler/tests/xpcshell/head.js
blob: ce87b32fd57e802cf911ca512b8197f23947be0b (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
/* 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/. */

/* import-globals-from ../shared-head.js */

// This Services declaration may shadow another from head.js, so define it as
// a var rather than a const.

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

// Load the shared head
const sharedHead = do_get_file("shared-head.js", false);
if (!sharedHead) {
  throw new Error("Could not load the shared head.");
}
Services.scriptloader.loadSubScript(
  Services.io.newFileURI(sharedHead).spec,
  this
);

/**
 * 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;

  // Build the stack from the raw data and accumulate the locations in
  // an array.
  let stackIndex = sample[SAMPLE_STACK_SLOT];
  let locations = [];
  while (stackIndex !== null) {
    let stackEntry = stackTable.data[stackIndex];
    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();
}

/**
 * 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);
}

/**
 * @param {Thread} thread
 * @param {string} filename - The filename used to trigger FileIO.
 * @returns {InflatedMarkers[]}
 */
function getInflatedFileIOMarkers(thread, filename) {
  const markers = getInflatedMarkerData(thread);
  return markers.filter(
    marker =>
      marker.data?.type === "FileIO" &&
      marker.data?.filename?.endsWith(filename)
  );
}

/**
 * Checks properties common to all FileIO markers.
 *
 * @param {InflatedMarkers[]} markers
 * @param {string} filename
 */
function checkInflatedFileIOMarkers(markers, filename) {
  greater(markers.length, 0, "Found some markers");

  // See IOInterposeObserver::Observation::ObservedOperationString
  const validOperations = new Set([
    "write",
    "fsync",
    "close",
    "stat",
    "create/open",
    "read",
  ]);
  const validSources = new Set(["PoisonIOInterposer", "NSPRIOInterposer"]);

  for (const marker of markers) {
    try {
      ok(
        marker.name.startsWith("FileIO"),
        "Has a marker.name that starts with FileIO"
      );
      equal(marker.data.type, "FileIO", "Has a marker.data.type");
      ok(isIntervalMarker(marker), "All FileIO markers are interval markers");
      ok(
        validOperations.has(marker.data.operation),
        `The markers have a known operation - "${marker.data.operation}"`
      );
      ok(
        validSources.has(marker.data.source),
        `The FileIO marker has a known source "${marker.data.source}"`
      );
      ok(marker.data.filename.endsWith(filename));
      ok(Boolean(marker.data.stack), "A stack was collected");
    } catch (error) {
      console.error("Failing inflated FileIO marker:", marker);
      throw error;
    }
  }
}

/**
 * Do deep equality checks for schema, but then surface nice errors for a user to know
 * what to do if the check fails.
 */
function checkSchema(actual, expected) {
  const schemaName = expected.name;
  info(`Checking marker schema for "${schemaName}"`);

  try {
    ok(
      actual,
      `Schema was found for "${schemaName}". See the test output for more information.`
    );
    // Check individual properties to surface easier to debug errors.
    deepEqual(
      expected.display,
      actual.display,
      `The "display" property for ${schemaName} schema matches. See the test output for more information.`
    );
    if (expected.data) {
      ok(actual.data, `Schema was found for "${schemaName}"`);
      for (const expectedDatum of expected.data) {
        const actualDatum = actual.data.find(d => d.key === expectedDatum.key);
        deepEqual(
          expectedDatum,
          actualDatum,
          `The "${schemaName}" field "${expectedDatum.key}" matches expectations. See the test output for more information.`
        );
      }
      equal(
        expected.data.length,
        actual.data.length,
        "The expected and actual data have the same number of items"
      );
    }

    // Finally do a true deep equal.
    deepEqual(expected, actual, "The entire schema is deepEqual");
  } catch (error) {
    // The test results are not very human readable. This is a bit of a hacky
    // solution to make it more readable.
    dump("-----------------------------------------------------\n");
    dump("The expected marker schema:\n");
    dump("-----------------------------------------------------\n");
    dump(JSON.stringify(expected, null, 2));
    dump("\n");
    dump("-----------------------------------------------------\n");
    dump("The actual marker schema:\n");
    dump("-----------------------------------------------------\n");
    dump(JSON.stringify(actual, null, 2));
    dump("\n");
    dump("-----------------------------------------------------\n");
    dump("A marker schema was not equal to expectations. If you\n");
    dump("are modifying the schema, then please copy and paste\n");
    dump("the new schema into this test.\n");
    dump("-----------------------------------------------------\n");
    dump("Copy this: " + JSON.stringify(actual));
    dump("\n");
    dump("-----------------------------------------------------\n");

    throw error;
  }
}