summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/js-self-profiling/resources/profile-utils.js
blob: 2c32f346088bc7716fb22a9f119fbb98f7bfa816 (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
(function(global) {
  const TEST_SAMPLE_INTERVAL = 10;
  const ENSURE_SAMPLE_SPIN_WAIT_MS = 500;

  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 +
             ENSURE_SAMPLE_SPIN_WAIT_MS;
         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);
  }

  // Returns true if a trace contains a sample matching the given specification.
  // We define a "match" as follows: a sample A matches an expectation E if (and
  // only if) for each field of E, A has the same value.
  function containsSample(trace, expectedSample) {
    return trace.samples.find(sample => {
      return sampleMatches(sample, expectedSample);
    }) !== undefined;
  }

  // Compares each set field of `expected` against the given frame `actual`.
  function sampleMatches(actual, expected) {
    return (expected.timestamp === undefined ||
            expected.timestamp === actual.timestamp) &&
        (expected.stackId === undefined ||
         expected.stackId === actual.stackId) &&
        (expected.marker === undefined || expected.marker === actual.marker);
  }

  // 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,
    containsSample,

    // Cross-frame sampling
    forceSampleFrame,

    // Assertions
    testFunction,
  };
})(this);