chrome-devtools-frontend
Version:
Chrome DevTools UI
704 lines (644 loc) • 34.9 kB
text/typescript
// Copyright 2023 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Copyright 2023222 The Chromium Author2s. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Trace from '../../models/trace/trace.js';
import {describeWithEnvironment} from '../../testing/EnvironmentHelpers.js';
import {getMainThread} from '../../testing/TraceHelpers.js';
import {TraceLoader} from '../../testing/TraceLoader.js';
import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js';
import * as Timeline from './timeline.js';
function findFirstEntry(
allEntries: readonly Trace.Types.Events.Event[],
predicate: (entry: Trace.Types.Events.Event) => boolean): Trace.Types.Events.Event {
const entry = allEntries.find(entry => predicate(entry));
if (!entry) {
throw new Error('Could not find expected entry.');
}
return entry;
}
describeWithEnvironment('EntriesFilter', function() {
it('parses a stack and returns an empty list of invisible entries', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'basic-stack.json.gz');
const stack = new Timeline.EntriesFilter.EntriesFilter(parsedTrace);
assert.deepEqual([], stack?.invisibleEntries());
});
it('supports the user merging an entry into its parent', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'basic-stack.json.gz');
const mainThread = getMainThread(parsedTrace.Renderer);
/** This stack looks roughly like so (with some events omitted):
* ======== basicStackOne ============
* =========== basicTwo ==============
* =========== basicThree ============
* ======== fibonacci ===
* ======== fibonacci ===
* ======== fibonacci ===
* ======== fibonacci ===
* ======== fibonacci ===
* ==== fibonacci ===
*
* In this test we want to test the user merging basicTwo into its parent, so the resulting trace should look like so:
* ======== basicStackOne ============
* =========== basicThree ============ << No more basicTwo, it has been merged.
* ======== fibonacci ===
* ======== fibonacci ===
* ======== fibonacci ===
* ======== fibonacci ===
* ======== fibonacci ===
* ==== fibonacci ===
*
**/
const entryTwo = findFirstEntry(mainThread.entries, entry => {
// Processing this trace ends up with two distinct stacks for basicTwo()
// So we find the first one so we can focus this test on just one stack.
return Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === 'basicTwo' &&
entry.dur === 827;
});
const stack = new Timeline.EntriesFilter.EntriesFilter(parsedTrace);
if (!stack) {
throw new Error('EntriesFilter does not exist');
}
stack.applyFilterAction({type: PerfUI.FlameChart.FilterAction.MERGE_FUNCTION, entry: entryTwo});
assert.isTrue(stack.invisibleEntries().includes(entryTwo), 'entryTwo is invisble');
// Only one entry - the one for the `basicTwo` function - should have been hidden.
assert.lengthOf(stack.invisibleEntries(), 1);
});
it('adds the parent of the merged entry into the expandableEntries array', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'basic-stack.json.gz');
const mainThread = getMainThread(parsedTrace.Renderer);
/** This stack looks roughly like so (with some events omitted):
* ======== basicStackOne ============
* =========== basicTwo ==============
* =========== basicThree ============
* ======== fibonacci ===
* ======== fibonacci ===
* ======== fibonacci ===
* ======== fibonacci ===
*
* In this test we want to test that the parent of the merged entry is added to the expandableEntries array,
* so that later an array decoration is added to it and the merged entry could be shown again if the array is clicked.
* the user merging basicTwo into its parent, so the resulting trace should look like so:
* ======== basicStackOne ============ << As parent of basicTwo, it belongs to the expandableEntries array
* =========== basicThree ============ << No more basicTwo, it has been merged.
* ======== fibonacci ===
* ======== fibonacci ===
* ======== fibonacci ===
* ======== fibonacci ===
*
**/
const entryTwo = findFirstEntry(mainThread.entries, entry => {
// Processing this trace ends up with two distinct stacks for basicTwo()
// So we find the first one so we can focus this test on just one stack.
return Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === 'basicTwo' &&
entry.dur === 827;
});
const stack = new Timeline.EntriesFilter.EntriesFilter(parsedTrace);
if (!stack) {
throw new Error('EntriesFilter does not exist');
}
stack.applyFilterAction({type: PerfUI.FlameChart.FilterAction.MERGE_FUNCTION, entry: entryTwo});
assert.isTrue(stack.invisibleEntries().includes(entryTwo), 'entryTwo is invisble');
// Get the parent of basicTwo, which is basicStackOne.
const basicStackOne = findFirstEntry(mainThread.entries, entry => {
return Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === 'basicStackOne' &&
entry.dur === 827;
});
// Get the parent of basicTwo marked as expandable.
assert.isTrue(stack.isEntryExpandable(basicStackOne));
});
it('adds the collapsed entry into the expandableEntries array', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'basic-stack.json.gz');
const mainThread = getMainThread(parsedTrace.Renderer);
/** This stack looks roughly like so (with some events omitted):
* ======== basicStackOne ============
* =========== basicTwo ==============
* =========== basicThree ============
* ======== fibonacci ===
* ======== fibonacci ===
* ======== fibonacci ===
* ======== fibonacci ===
*
* In this test we want to test that the collapsed entry is added to the expandableEntries array,
* so that later an arrow decoration is added to it and the collapsed entries could be shown again if the arraw is clicked.
*
* The user collapses basicTwo, so the resulting trace should look like so:
* ======== basicStackOne ============
* =========== basicTwo ============ << All entries under basicTwo merged collapsed and it belongs to the expandableEntries array
*
**/
const entryTwo = findFirstEntry(mainThread.entries, entry => {
// Processing this trace ends up with two distinct stacks for basicTwo()
// So we find the first one so we can focus this test on just one stack.
return Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === 'basicTwo' &&
entry.dur === 827;
});
const stack = new Timeline.EntriesFilter.EntriesFilter(parsedTrace);
if (!stack) {
throw new Error('EntriesFilter does not exist');
}
stack.applyFilterAction({type: PerfUI.FlameChart.FilterAction.COLLAPSE_FUNCTION, entry: entryTwo});
// basicTwo is marked as expandable.
assert.isTrue(stack.isEntryExpandable(entryTwo));
});
it('adds the next visible parent of the merged entry into the expandableEntries array if the direct parent is hidden',
async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'two-functions-recursion.json.gz');
const mainThread = getMainThread(parsedTrace.Renderer);
/** This stack looks roughly like so (with some events omitted):
* ======== onclick ============
* =========== foo =============
* ==== foo2 =====
* ===== foo =====
* ==== foo2 =====
* ===== foo =====
* ==== foo2 =====
* ===== foo =====
*
* In this test we want to test that the next visible parent of the merged entry is added to the
* expandableEntries array even if the direct one is hidden by some other action,
* so that later an array decoration is added to it and the merged entry could be shown again if the array is clicked.
*
* collapse all repeating calls of foo after the first one:
* ======== onclick ============
* =========== foo ============= << all foo except first removed
* ===== foo2 ====
* ==== foo2 ===== << direct parent is not visible anymore
* ==== foo2 =====
*
* merge second foo2 and add the next visible parent to the expandableEntries array:
* ======== onclick ============
* =========== foo =============
* ===== foo2 ==== << added to expandableEntries as the next visible parent of the merged entry
* ==== foo2 =====
*
**/
const firstFooCallEntry = findFirstEntry(mainThread.entries, entry => {
return Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === 'foo' && entry.dur === 233;
});
const firstFooCallEndTime = Trace.Helpers.Timing.eventTimingsMicroSeconds(firstFooCallEntry).endTime;
const fooCalls = mainThread.entries.filter(entry => {
const isFooCall = Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === 'foo';
if (!isFooCall) {
return false;
}
const {endTime} = Trace.Helpers.Timing.eventTimingsMicroSeconds(entry);
return endTime <= firstFooCallEndTime;
});
const stack = new Timeline.EntriesFilter.EntriesFilter(parsedTrace);
if (!stack) {
throw new Error('EntriesFilter does not exist');
}
// Collapse all foo calls after the first one.
stack.applyFilterAction({
type: PerfUI.FlameChart.FilterAction.COLLAPSE_REPEATING_DESCENDANTS,
entry: firstFooCallEntry,
});
// First foo call is marked as expandable since its' children are hidden.
assert.isTrue(stack.isEntryExpandable(firstFooCallEntry));
// Make sure all foo calls after first are hidden.
const allFooExceptFirstInStackAreHidden = fooCalls.every((fooCall, i) => {
if (i === 0) {
// First foo should not be invisible.
return !stack.invisibleEntries().includes(fooCall);
}
return stack.invisibleEntries().includes(fooCall);
});
assert.isTrue(
allFooExceptFirstInStackAreHidden, 'First foo is invisible or some following foo calls are still visible');
const foo2Calls = mainThread.entries.filter(entry => {
const isFoo2Call = Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === 'foo2';
if (!isFoo2Call) {
return false;
}
const {endTime} = Trace.Helpers.Timing.eventTimingsMicroSeconds(entry);
return endTime <= firstFooCallEndTime;
});
// Merge second foo2 entry.
stack.applyFilterAction({type: PerfUI.FlameChart.FilterAction.MERGE_FUNCTION, entry: foo2Calls[1]});
// First foo2 entry should be in the expandableEntries array.
assert.isTrue(stack.isEntryExpandable(foo2Calls[0]));
});
it('supports collapsing an entry', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'basic-stack.json.gz');
const mainThread = getMainThread(parsedTrace.Renderer);
/** This stack looks roughly like so (with some events omitted):
* ======== basicStackOne ============
* =========== basicTwo ==============
* =========== basicThree ============
* ======== fibonacci ===
* ======== fibonacci ===
* ======== fibonacci ===
* ======== fibonacci ===
* ======== fibonacci ===
* ==== fibonacci ===
*
* In this test we want to test the user collapsing basicTwo, which should have the effect of keeping basicTwo visible, but removing all of its children:
* ======== basicStackOne ============
* =========== basicTwo ==============
* << all children removed
**/
const basicTwoCallEntry = findFirstEntry(mainThread.entries, entry => {
// Processing this trace ends up with two distinct stacks for basicTwo()
// So we find the first one so we can focus this test on just one stack.
return Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === 'basicTwo' &&
entry.dur === 827;
});
// Gather the fib() calls under the first basicTwo stack, by finding all
// the calls whose end time is less than or equal to the end time of the
// `basicTwo` function.
const fibonacciCalls = mainThread.entries.filter(entry => {
const isFibCall = Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === 'fibonacci';
if (!isFibCall) {
return false;
}
const {endTime} = Trace.Helpers.Timing.eventTimingsMicroSeconds(entry);
const basicTwoCallEndTime = Trace.Helpers.Timing.eventTimingsMicroSeconds(basicTwoCallEntry).endTime;
return endTime <= basicTwoCallEndTime;
});
const stack = new Timeline.EntriesFilter.EntriesFilter(parsedTrace);
if (!stack) {
throw new Error('EntriesFilter does not exist');
}
stack.applyFilterAction({type: PerfUI.FlameChart.FilterAction.COLLAPSE_FUNCTION, entry: basicTwoCallEntry});
// We collapsed at the `basicTwo` entry - so it should not be included in the invisible list itself.
assert.isFalse(stack.invisibleEntries().includes(basicTwoCallEntry), 'entryTwo is not visible');
// But all fib() calls below it in the stack should now be invisible.
const allFibonacciInStackAreHidden = fibonacciCalls.every(fibCall => {
return stack.invisibleEntries().includes(fibCall);
});
assert.isTrue(allFibonacciInStackAreHidden, 'Some fibonacci calls are still visible');
});
it('supports collapsing all repeating entries among descendants', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'two-functions-recursion.json.gz');
const mainThread = getMainThread(parsedTrace.Renderer);
/** This stack looks roughly like so (with some events omitted):
* ======== onclick ============
* =========== foo =============
* ==== foo2 =====
* ===== foo =====
* ==== foo2 =====
* ===== foo =====
* ==== foo2 =====
* ===== foo =====
*
* In this test we want to test the user collapsing all descendant foo calls of the first first one,
* which should have the effect of keeping the first foo visible, but removing all of its other calls:
* ======== onclick ============
* =========== foo =============
* ===== foo2 ==== << all foo except first removed
* ==== foo2 =====
* ==== foo2 =====
**/
const firstFooCallEntry = findFirstEntry(mainThread.entries, entry => {
return Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === 'foo' && entry.dur === 233;
});
// Gather the foo() and foo2() calls under and including the first foo entry, by finding all
// the calls whose end time is less than or equal to the end time of the first `foo` function.
const firstFooCallEndTime = Trace.Helpers.Timing.eventTimingsMicroSeconds(firstFooCallEntry).endTime;
const fooCalls = mainThread.entries.filter(entry => {
const isFooCall = Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === 'foo';
if (!isFooCall) {
return false;
}
const {endTime} = Trace.Helpers.Timing.eventTimingsMicroSeconds(entry);
return endTime <= firstFooCallEndTime;
});
const foo2Calls = mainThread.entries.filter(entry => {
const isFoo2Call = Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === 'foo2';
if (!isFoo2Call) {
return false;
}
const {endTime} = Trace.Helpers.Timing.eventTimingsMicroSeconds(entry);
return endTime <= firstFooCallEndTime;
});
const stack = new Timeline.EntriesFilter.EntriesFilter(parsedTrace);
if (!stack) {
throw new Error('EntriesFilter does not exist');
}
stack.applyFilterAction(
{type: PerfUI.FlameChart.FilterAction.COLLAPSE_REPEATING_DESCENDANTS, entry: firstFooCallEntry});
// We collapsed identical descendants after the first `foo` entry - so it should not be included in the invisible list itself,
// but all foo() calls below it in the stack should now be invisible.
const allFooExceptFirstInStackAreHidden = fooCalls.every((fooCall, i) => {
if (i === 0) {
// First foo should not be invisible.
return !stack.invisibleEntries().includes(fooCall);
}
return stack.invisibleEntries().includes(fooCall);
});
assert.isTrue(
allFooExceptFirstInStackAreHidden, 'First foo is invisible or some following foo calls are still visible');
// All of the foo2 calls that were inbetween foo calls should still be visible.
const allFoo2InStackAreVisible = foo2Calls.every(fooCall => {
return !stack.invisibleEntries().includes(fooCall);
});
assert.isTrue(allFoo2InStackAreVisible, 'Some foo2 calls are invisible');
});
it('supports undo all filter actions by applying context menu undo action', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'basic-stack.json.gz');
const mainThread = getMainThread(parsedTrace.Renderer);
/** This stack looks roughly like so (with some events omitted):
* ======== basicStackOne ============
* =========== basicTwo ==============
* =========== basicThree ============
* ======== fibonacci ===
* ======== fibonacci ===
* ======== fibonacci ===
* ======== fibonacci ===
* ======== fibonacci ===
* ==== fibonacci ===
*
* In this test we want to test the user undoing all actions with 'undo all actions' after applying merge, collapse repeating descendants and collapse function.
*
* First, collapse all repeating descendants of the first fibonacci call:
* ======== basicStackOne ============
* =========== basicTwo ==============
* =========== basicThree ============
* ======== fibonacci === << repeating children removed
*
* Then, merge basicStackOne:
* =========== basicTwo ==============
* =========== basicThree ============
* ======== fibonacci ===
*
* Finally, collapse basicTwo():
* =========== basicTwo ==============
*
* Applying 'undo all actions' should bring the stack to the original state.
**/
const stack = new Timeline.EntriesFilter.EntriesFilter(parsedTrace);
if (!stack) {
throw new Error('EntriesFilter does not exist');
}
const basicTwoCallEntry = findFirstEntry(mainThread.entries, entry => {
// Processing this trace ends up with two distinct stacks for basicTwo()
// So we find the first one so we can focus this test on just one stack.
return Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === 'basicTwo' &&
entry.dur === 827;
});
const fibonacciCalls = mainThread.entries.filter(entry => {
const isFibCall = Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === 'fibonacci';
if (!isFibCall) {
return false;
}
const {endTime} = Trace.Helpers.Timing.eventTimingsMicroSeconds(entry);
const basicTwoCallEndTime = Trace.Helpers.Timing.eventTimingsMicroSeconds(basicTwoCallEntry).endTime;
return endTime <= basicTwoCallEndTime;
});
// Collapse all repeating descendants of the first fibonacci call:
stack.applyFilterAction(
{type: PerfUI.FlameChart.FilterAction.COLLAPSE_REPEATING_DESCENDANTS, entry: fibonacciCalls[0]});
// We collapsed identical descendants after the first `foo` entry - so it should not be included in the invisible list itself,
// but all foo() calls below it in the stack should now be invisible.
const allFibExceptFirstInStackAreHidden = fibonacciCalls.every((fibCall, i) => {
if (i === 0) {
// First foo should not be invisible.
return !stack.invisibleEntries().includes(fibCall);
}
return stack.invisibleEntries().includes(fibCall);
});
assert.isTrue(
allFibExceptFirstInStackAreHidden, 'First fib is invisible or some following fib calls are still visible');
// Merge basicStackOne:
const basicStackOne = findFirstEntry(mainThread.entries, entry => {
return Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === 'basicStackOne' &&
entry.dur === 827;
});
stack.applyFilterAction({type: PerfUI.FlameChart.FilterAction.MERGE_FUNCTION, entry: basicStackOne});
assert.isTrue(stack.invisibleEntries().includes(basicStackOne), 'entrybasicStackOneTwo is visble');
// Collapse basicTwo():
stack.applyFilterAction({type: PerfUI.FlameChart.FilterAction.COLLAPSE_FUNCTION, entry: basicTwoCallEntry});
// basicThree and first fibnacci should now be hidden:
const basicThree = findFirstEntry(mainThread.entries, entry => {
return Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === 'basicThree' &&
entry.dur === 827;
});
assert.isTrue(stack.invisibleEntries().includes(basicThree), 'basicThree is visble');
assert.isTrue(stack.invisibleEntries().includes(fibonacciCalls[0]), 'first fibonacci is visble');
// Apply UNDO_ALL_ACTIONS to bring back all of the hidden entries:
// UNDO_ALL_ACTIONS can be called on any visible entry
stack.applyFilterAction({type: PerfUI.FlameChart.FilterAction.UNDO_ALL_ACTIONS, entry: basicTwoCallEntry});
// If the length of invisibleEntries list is 0, all of the entries added earlier were removed and are now visible.
assert.lengthOf(stack.invisibleEntries(), 0);
});
it('supports resetting children of the closest expandable parent when a hidden entry is provided', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'basic-stack.json.gz');
const mainThread = getMainThread(parsedTrace.Renderer);
/** This stack looks roughly like so (with some events omitted):
* ======== basicStackOne ============
* =========== basicTwo ==============
* =========== basicThree ============
* ======== fibonacci ===
* ======== fibonacci ===
* ======== fibonacci ===
* ======== fibonacci ===
* ======== fibonacci ===
* ==== fibonacci ===
*
* In this test we want to test the user selecting an entry that is hidden via a link.
* If this happens, we should reveal this entry to resetting children of the closest expandable parent.
*
* First, collapse all children of the basicTwo:
* ======== basicStackOne ============
* =========== basicTwo ============== << children collapsed
* =========== basicThree ============
* ======== fibonacci === << repeating children removed
*
* Then, reveal the first fibonacci entry that is hidden:
* ======== basicStackOne ============
* =========== basicTwo ==============
* =========== basicThree ============
* ======== fibonacci === << reveal this hidden entry
* ======== fibonacci ===
*
* This should result in all basicTwo children being removed from the invisible array and stack being in the initial state.
**/
const stack = new Timeline.EntriesFilter.EntriesFilter(parsedTrace);
if (!stack) {
throw new Error('EntriesFilter does not exist');
}
const basicTwoCallEntry = findFirstEntry(mainThread.entries, entry => {
// Processing this trace ends up with two distinct stacks for basicTwo()
// So we find the first one so we can focus this test on just one stack.
return Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === 'basicTwo' &&
entry.dur === 827;
});
// Make sure no entries are hidden
assert.lengthOf(stack.invisibleEntries(), 0);
// Collapse all children of basicTwo call:
stack.applyFilterAction({type: PerfUI.FlameChart.FilterAction.COLLAPSE_FUNCTION, entry: basicTwoCallEntry});
// Make sure all 37 of basicTwo descdendants are hidden
assert.lengthOf(stack.invisibleEntries(), 37);
// Get the first fibonacci call that is one of the hidden children and make sure it is hidden
const firstFibCallEntry = findFirstEntry(mainThread.entries, entry => {
return Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === 'fibonacci';
});
assert.isTrue(stack.invisibleEntries().includes(firstFibCallEntry));
// Reveal the first fibonacci call and make sure that the all of the entries are now visible because the closest
// expandable parent to the fib call is basicTwo and, therefore, we need to reset its children.
stack.revealEntry(firstFibCallEntry);
assert.lengthOf(stack.invisibleEntries(), 0);
});
it('supports resetting all hidden children of a selected entry', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'two-functions-recursion.json.gz');
const mainThread = getMainThread(parsedTrace.Renderer);
/** This stack looks roughly like so (with some events omitted):
* ======== onclick ============
* =========== foo =============
* ==== foo2 =====
* ===== foo =====
* ==== foo2 =====
* ===== foo =====
* ==== foo2 =====
* ===== foo =====
*
* In this test we want to test the user collapsing all descendant foo calls of the first first one,
* which should have the effect of keeping the first foo visible, but removing all of its other calls:
* ======== onclick ============
* =========== foo =============
* ===== foo2 ==== << all foo except first removed
* ==== foo2 =====
* ==== foo2 =====
*
* Then, reset children on the second visible foo2.
* ======== onclick ============
* =========== foo =============
* ===== foo2 ==== << foo() after this entry still hidden
* ==== foo2 ===== << all children from this node are visible
* ===== foo =====
* ==== foo2 =====
* ===== foo =====
*
* This results in a stack where all children of an entry children were reset on (second foo2)
* are visible, but the entries hidden above the entry children were reset on stay hidden.
**/
const firstFooCallEntry = findFirstEntry(mainThread.entries, entry => {
return Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === 'foo' && entry.dur === 233;
});
// Gather the foo() and foo2() calls under and including the first foo entry, by finding all
// the calls whose end time is less than or equal to the end time of the first `foo` function.
const firstFooCallEndTime = Trace.Helpers.Timing.eventTimingsMicroSeconds(firstFooCallEntry).endTime;
const fooCalls = mainThread.entries.filter(entry => {
const isFooCall = Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === 'foo';
if (!isFooCall) {
return false;
}
const {endTime} = Trace.Helpers.Timing.eventTimingsMicroSeconds(entry);
return endTime <= firstFooCallEndTime;
});
const foo2Calls = mainThread.entries.filter(entry => {
const isFoo2Call = Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === 'foo2';
if (!isFoo2Call) {
return false;
}
const {endTime} = Trace.Helpers.Timing.eventTimingsMicroSeconds(entry);
return endTime <= firstFooCallEndTime;
});
const stack = new Timeline.EntriesFilter.EntriesFilter(parsedTrace);
if (!stack) {
throw new Error('EntriesFilter does not exist');
}
stack.applyFilterAction(
{type: PerfUI.FlameChart.FilterAction.COLLAPSE_REPEATING_DESCENDANTS, entry: firstFooCallEntry});
// We collapsed identical descendants after the first `foo` entry - so it should not be included in the invisible list itself,
// but all foo() calls below it in the stack should now be invisible.
const allFooExceptFirstInStackAreHidden = fooCalls.every((fooCall, i) => {
if (i === 0) {
// First foo should not be invisible.
return !stack.invisibleEntries().includes(fooCall);
}
return stack.invisibleEntries().includes(fooCall);
});
assert.isTrue(
allFooExceptFirstInStackAreHidden, 'First foo is invisible or some following foo calls are still visible');
// All of the foo2 calls that were inbetween foo calls should still be visible.
let allFoo2InStackAreVisible = foo2Calls.every(fooCall => {
return !stack.invisibleEntries().includes(fooCall);
});
assert.isTrue(allFoo2InStackAreVisible, 'Some foo2 calls are invisible');
// Reset all children after second foo2 call
assert.lengthOf(foo2Calls, 3);
stack.applyFilterAction({type: PerfUI.FlameChart.FilterAction.RESET_CHILDREN, entry: foo2Calls[1]});
// All foo and foo2 calls except the second foo cll should now be visible
allFoo2InStackAreVisible = foo2Calls.every(fooCall => {
return !stack.invisibleEntries().includes(fooCall);
});
assert.isTrue(allFoo2InStackAreVisible, 'Some foo2 calls are invisible');
const allFooExceptSecondInStackAreVisible = fooCalls.every((fooCall, i) => {
if (i === 1) {
// Second foo should be invisible.
return stack.invisibleEntries().includes(fooCall);
}
return !stack.invisibleEntries().includes(fooCall);
});
assert.isTrue(
allFooExceptSecondInStackAreVisible,
'Some foo calls except the second one are invisible or the second one is visible');
});
it('correctly returns the amount of hidden children of a node', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'two-functions-recursion.json.gz');
const mainThread = getMainThread(parsedTrace.Renderer);
/** This stack looks roughly like so (with some erlier events omitted):
* ======== onclick ============
* =========== foo =============
* ==== foo2 =====
* ===== foo =====
* ==== foo2 =====
* ===== foo =====
* ==== foo2 =====
* ===== foo =====
*
* In this test we want to test if the amount of hidden children returned is correct:
* If we collapse repeating children on the first foo call, the 3 child foo calls should be removed.
* Therefore, the amount of hidden children should be equal to 3.
* ======== onclick ============
* =========== foo ============= << all foo except first hidden
* ===== foo2 ====
* ==== foo2 =====
* ==== foo2 =====
**/
const firstFooCallEntry = findFirstEntry(mainThread.entries, entry => {
return Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === 'foo' && entry.dur === 233;
});
const stack = new Timeline.EntriesFilter.EntriesFilter(parsedTrace);
if (!stack) {
throw new Error('EntriesFilter does not exist');
}
// Before applying any action on a node, there should be no entries hidden under it
assert.strictEqual(stack.findHiddenDescendantsAmount(firstFooCallEntry), 0);
stack.applyFilterAction(
{type: PerfUI.FlameChart.FilterAction.COLLAPSE_REPEATING_DESCENDANTS, entry: firstFooCallEntry});
// There should be 3 foo() entries hidden under the first foo call entry
assert.strictEqual(stack.findHiddenDescendantsAmount(firstFooCallEntry), 3);
});
it('correctly assigns a visible parent to expandable entries if the direct parent is not visible', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'basic-stack.json.gz');
const mainThread = getMainThread(parsedTrace.Renderer);
/** This stack looks roughly like so (with some events omitted):
* ======== Task ===============
* ======== (anonymous) ======== << entry with an invisible in the timeline direct parent. We need to make sure that we correctly add Task to the expandable entries
* ======== RegisterFrameID ====
* ======== postMessage ========
**/
const anonymousEntryWithInvisibleParent = findFirstEntry(mainThread.entries, entry => {
return Trace.Types.Events.isProfileCall(entry) && entry.nodeId === 42;
});
const stack = new Timeline.EntriesFilter.EntriesFilter(parsedTrace);
if (!stack) {
throw new Error('EntriesFilter does not exist');
}
const taskEntry = findFirstEntry(mainThread.entries, entry => {
return entry.name === 'RunTask' && entry.dur === 978 && entry.ts === 164397762991;
});
// Make sure the expandable entries are empty at first
assert.lengthOf(stack.expandableEntries(), 0);
// Hide the anonymous function
stack.applyFilterAction(
{type: PerfUI.FlameChart.FilterAction.MERGE_FUNCTION, entry: anonymousEntryWithInvisibleParent});
// Make sure Task entry is added to expandable entries
assert.lengthOf(stack.expandableEntries(), 1);
assert.isTrue(stack.expandableEntries().includes(taskEntry));
});
});