UNPKG

chrome-devtools-frontend

Version:
704 lines (644 loc) • 34.9 kB
// 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)); }); });