diff --git a/src/backend/agent.js b/src/backend/agent.js index 7a7c7a62bb..34a5613520 100644 --- a/src/backend/agent.js +++ b/src/backend/agent.js @@ -46,6 +46,9 @@ export default class Agent extends EventEmitter { addBridge(bridge: Bridge) { this._bridge = bridge; + // TODO (profiling) Component commits + // TODO (profiling) Interactions + bridge.addListener('getCommitDetails', this.getCommitDetails); bridge.addListener('getProfilingStatus', this.getProfilingStatus); bridge.addListener('getProfilingSummary', this.getProfilingSummary); diff --git a/src/backend/renderer.js b/src/backend/renderer.js index a6510e1310..f58d34c6b6 100644 --- a/src/backend/renderer.js +++ b/src/backend/renderer.js @@ -693,30 +693,32 @@ export function attach( ) { debug('enqueueUpdateIfNecessary()', fiber); - if (isProfiling) { - if (haveProfilerTimesChanged(fiber.alternate, fiber)) { - const id = getFiberID(getPrimaryFiber(fiber)); - const { actualDuration, treeBaseDuration } = fiber; + const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration'); + if (isProfilingSupported) { + const id = getFiberID(getPrimaryFiber(fiber)); + const { actualDuration, treeBaseDuration } = fiber; - const operation = new Uint32Array(3); - operation[0] = TREE_OPERATION_UPDATE_TREE_BASE_DURATION; - operation[1] = id; - operation[2] = treeBaseDuration; - addOperation(operation); + idToTreeBaseDurationMap.set(id, fiber.treeBaseDuration); - idToTreeBaseDurationMap.set(id, treeBaseDuration); + if (isProfiling) { + if (treeBaseDuration !== fiber.alternate.treeBaseDuration) { + const operation = new Uint32Array(3); + operation[0] = TREE_OPERATION_UPDATE_TREE_BASE_DURATION; + operation[1] = getFiberID(getPrimaryFiber(fiber)); + operation[2] = treeBaseDuration; + addOperation(operation); + } - if (actualDuration > 0) { - // If profiling is active, store durations for elements that were rendered during the commit. - const metadata = ((currentCommitProfilingMetadata: any): CommitProfilingData); - metadata.committedFibers.push({ - id, - actualDuration, - }); - metadata.maxActualDuration = Math.max( - metadata.maxActualDuration, - actualDuration - ); + if (haveProfilerTimesChanged(fiber.alternate, fiber)) { + if (actualDuration > 0) { + // If profiling is active, store durations for elements that were rendered during the commit. + const metadata = ((currentCommitProfilingMetadata: any): CommitProfilingData); + metadata.actualDurations.push(id, actualDuration); + metadata.maxActualDuration = Math.max( + metadata.maxActualDuration, + actualDuration + ); + } } } } @@ -880,7 +882,7 @@ export function attach( // If profiling is active, store commit time and duration, and the current interactions. // The frontend may request this information after profiling has stopped. currentCommitProfilingMetadata = { - committedFibers: [], + actualDurations: [], commitTime: performance.now() - profilingStartTime, interactions: Array.from(root.memoizedInteractions).map( (interaction: Interaction) => ({ @@ -1377,13 +1379,8 @@ export function attach( } } - type CommittedFiber = {| - actualDuration: number, - id: number, - |}; - type CommitProfilingData = {| - committedFibers: Array, + actualDurations: Array, commitTime: number, interactions: Array, maxActualDuration: number, @@ -1410,7 +1407,7 @@ export function attach( return { commitIndex, interactions: commitProfilingData.interactions, - committedFibers: commitProfilingData.committedFibers, + actualDurations: commitProfilingData.actualDurations, rootID, }; } @@ -1419,7 +1416,7 @@ export function attach( return { commitIndex, interactions: [], - committedFibers: [], + actualDurations: [], rootID, }; } diff --git a/src/backend/types.js b/src/backend/types.js index 64c6169a69..48da3b85f3 100644 --- a/src/backend/types.js +++ b/src/backend/types.js @@ -56,12 +56,9 @@ export type Interaction = {| |}; export type CommitDetails = {| + actualDurations: Array, commitIndex: number, interactions: Array, - committedFibers: Array<{| - actualDuration: number, - id: number, - |}>, rootID: number, |}; diff --git a/src/devtools/ProfilingCache.js b/src/devtools/ProfilingCache.js index faf6a60b02..7c76d0421d 100644 --- a/src/devtools/ProfilingCache.js +++ b/src/devtools/ProfilingCache.js @@ -59,7 +59,7 @@ export default class ProfilingCache { if (!store.profilingOperations.has(rootID)) { // If no profiling data was recorded for this root, skip the round trip. resolve({ - committedFibers: [], + actualDurations: new Map(), interactions: [], }); } else { @@ -139,7 +139,7 @@ export default class ProfilingCache { onCommitDetails = ({ commitIndex, - committedFibers, + actualDurations, interactions, rootID, }: CommitDetailsBackend) => { @@ -148,8 +148,13 @@ export default class ProfilingCache { if (resolve != null) { this._pendingCommitDetailsMap.delete(key); + const actualDurationsMap = new Map(); + for (let i = 0; i < actualDurations.length; i += 2) { + actualDurationsMap.set(actualDurations[i], actualDurations[i + 1]); + } + resolve({ - committedFibers, + actualDurations: actualDurationsMap, interactions, }); } @@ -166,7 +171,7 @@ export default class ProfilingCache { if (resolve != null) { this._pendingProfileSummaryMap.delete(rootID); const initialTreeBaseDurationsMap = new Map(); - for (let i = 0; i < initialTreeBaseDurations.length; i++) { + for (let i = 0; i < initialTreeBaseDurations.length; i += 2) { initialTreeBaseDurationsMap.set( initialTreeBaseDurations[i], initialTreeBaseDurations[i + 1] diff --git a/src/devtools/store.js b/src/devtools/store.js index 76fa683fa4..9884897acf 100644 --- a/src/devtools/store.js +++ b/src/devtools/store.js @@ -575,33 +575,37 @@ export default class Store extends EventEmitter { // DEBUG __printTree = () => { - console.group('__printTree()'); - this._roots.forEach((rootID: number) => { - const printElement = (id: number) => { - const element = ((this._idToElement.get(id): any): Element); - console.log( - `${'•'.repeat(element.depth)}${element.id}:${element.displayName || - ''}${element.key ? `key:"${element.key}"` : ''} (${element.weight})` - ); - element.children.forEach(printElement); - }; - const root = ((this._idToElement.get(rootID): any): Element); - console.group(`${rootID}:root (${root.weight})`); - root.children.forEach(printElement); - console.groupEnd(); - }); - console.group(`List of ${this.numElements} elements`); - for (let i = 0; i < this.numElements; i++) { - //if (i === 4) { debugger } - const element = this.getElementAtIndex(i); - if (element != null) { - console.log( - `${'•'.repeat(element.depth)}${i}: ${element.displayName || - 'Unknown'}` - ); + if (__DEBUG__) { + console.group('__printTree()'); + this._roots.forEach((rootID: number) => { + const printElement = (id: number) => { + const element = ((this._idToElement.get(id): any): Element); + console.log( + `${'•'.repeat(element.depth)}${element.id}:${element.displayName || + ''} ${element.key ? `key:"${element.key}"` : ''} (${ + element.weight + })` + ); + element.children.forEach(printElement); + }; + const root = ((this._idToElement.get(rootID): any): Element); + console.group(`${rootID}:root (${root.weight})`); + root.children.forEach(printElement); + console.groupEnd(); + }); + console.group(`List of ${this.numElements} elements`); + for (let i = 0; i < this.numElements; i++) { + //if (i === 4) { debugger } + const element = this.getElementAtIndex(i); + if (element != null) { + console.log( + `${'•'.repeat(element.depth)}${i}: ${element.displayName || + 'Unknown'}` + ); + } } + console.groupEnd(); + console.groupEnd(); } - console.groupEnd(); - console.groupEnd(); }; } diff --git a/src/devtools/views/Profiler/CommitRanked.js b/src/devtools/views/Profiler/CommitRanked.js index 8e50869a37..4e856656a4 100644 --- a/src/devtools/views/Profiler/CommitRanked.js +++ b/src/devtools/views/Profiler/CommitRanked.js @@ -2,10 +2,13 @@ import React, { useContext } from 'react'; import { ProfilerContext } from './ProfilerContext'; +import { calculateSelfDuration } from './utils'; import { StoreContext } from '../context'; import styles from './CommitRanked.css'; +import type { CommitDetails, CommitTree, Node } from './types'; + export default function CommitRanked(_: {||}) { const { rendererID, rootID, selectedCommitIndex } = useContext( ProfilerContext @@ -31,5 +34,57 @@ export default function CommitRanked(_: {||}) { rootID: ((rootID: any): number), }); + const chartData = generateChartData(commitTree, commitDetails); + return 'Coming soon: Ranked'; } + +type ChartNode = {| + id: number, + label: string, + name: string, + title: string, + value: number, +|}; + +type ChartData = {| + maxValue: number, + nodes: Array, +|}; + +const generateChartData = ( + commitTree: CommitTree, + commitDetails: CommitDetails +): ChartData => { + const { nodes } = commitTree; + + let maxSelfDuration = 0; + + const chartNodes: Array = []; + commitDetails.actualDurations.forEach((actualDuration, id) => { + const node = ((nodes.get(id): any): Node); + + // Don't show the root node in this chart. + if (node.parentID === 0) { + return; + } + + const selfDuration = calculateSelfDuration(id, commitTree, commitDetails); + maxSelfDuration = Math.max(maxSelfDuration, selfDuration); + + const name = node.displayName || 'Unknown'; + const label = `${name} (${selfDuration.toFixed(1)}ms)`; + chartNodes.push({ + id, + label, + name, + title: label, + value: selfDuration, + }); + }); + + return { + maxValue: maxSelfDuration, + nodes: chartNodes.sort((a, b) => b.value - a.value), + }; +}; diff --git a/src/devtools/views/Profiler/CommitTreeBuilder.js b/src/devtools/views/Profiler/CommitTreeBuilder.js index af5200bc44..24b92d665a 100644 --- a/src/devtools/views/Profiler/CommitTreeBuilder.js +++ b/src/devtools/views/Profiler/CommitTreeBuilder.js @@ -1,6 +1,7 @@ // @flow import { + __DEBUG__, TREE_OPERATION_ADD, TREE_OPERATION_REMOVE, TREE_OPERATION_RESET_CHILDREN, @@ -17,6 +18,17 @@ import type { ProfilingSummary as ProfilingSummaryFrontend, } from 'src/devtools/views/Profiler/types'; +const debug = (methodName, ...args) => { + if (__DEBUG__) { + console.log( + `%cCommitTreeBuilder %c${methodName}`, + 'color: pink; font-weight: bold;', + 'font-weight: bold;', + ...args + ); + } +}; + const rootToCommitTreeMap: Map> = new Map(); export function getCommitTree({ @@ -49,40 +61,29 @@ export function getCommitTree({ // If this is the very first commit, start with the cached snapshot and apply the first mutation. // Otherwise load (or generate) the previous commit and append a mutation to it. if (commitIndex === 0) { - const initialCommitTree = { - nodes: new Map(), - rootID, - }; + const nodes = new Map(); // Construct the initial tree. - const queue: Array = [rootID]; - while (queue.length > 0) { - const currentID = queue.pop(); - const currentNode = ((store.profilingSnapshot.get( - currentID - ): any): Node); - - initialCommitTree.nodes.set(currentID, { - id: currentID, - children: currentNode.children, - displayName: currentNode.displayName, - key: currentNode.key, - parentID: 0, - treeBaseDuration: ((profilingSummary.initialTreeBaseDurations.get( - currentID - ): any): number), - }); - - queue.push(...currentNode.children); - } + recursivelyIniitliazeTree( + rootID, + 0, + nodes, + profilingSummary.initialTreeBaseDurations, + store + ); // Mutate the tree const commitOperations = store.profilingOperations.get(rootID); if (commitOperations != null && commitIndex < commitOperations.length) { const commitTree = updateTree( - initialCommitTree, + { nodes, rootID }, commitOperations[commitIndex] ); + + if (__DEBUG__) { + __printTree(commitTree); + } + commitTrees.push(commitTree); return commitTree; } @@ -100,6 +101,11 @@ export function getCommitTree({ previousCommitTree, commitOperations[commitIndex] ); + + if (__DEBUG__) { + __printTree(commitTree); + } + commitTrees.push(commitTree); return commitTree; } @@ -113,6 +119,35 @@ export function getCommitTree({ }; } +function recursivelyIniitliazeTree( + id: number, + parentID: number, + nodes: Map, + initialTreeBaseDurations: Map, + store: Store +): void { + const node = ((store.profilingSnapshot.get(id): any): Node); + + nodes.set(id, { + id, + children: node.children, + displayName: node.displayName, + key: node.key, + parentID, + treeBaseDuration: ((initialTreeBaseDurations.get(id): any): number), + }); + + node.children.forEach(childID => + recursivelyIniitliazeTree( + childID, + id, + nodes, + initialTreeBaseDurations, + store + ) + ); +} + function updateTree( commitTree: CommitTree, operations: Uint32Array @@ -170,6 +205,11 @@ function updateTree( parentNode = ((nodes.get(parentID): any): Node); parentNode.children = parentNode.children.concat(id); + debug( + 'Add', + `fiber ${id} (${displayName || 'null'}) as child of ${parentID}` + ); + const node: Node = { children: [], displayName, @@ -197,6 +237,8 @@ function updateTree( if (parentNode == null) { // No-op } else { + debug('Remove', `fiber ${id} from parent ${parentID}`); + parentNode.children = parentNode.children.filter( childID => childID !== id ); @@ -212,8 +254,11 @@ function updateTree( i = i + 3 + numChildren; + debug('Re-order', `fiber ${id} children ${children.join(',')}`); + node = ((nodes.get(id): any): Node); node.children = Array.from(children); + break; case TREE_OPERATION_UPDATE_TREE_BASE_DURATION: id = operations[i + 1]; @@ -221,6 +266,11 @@ function updateTree( node = ((nodes.get(id): any): Node); node.treeBaseDuration = operations[i + 2]; + debug( + 'Update', + `fiber ${id} treeBaseDuration to ${node.treeBaseDuration}` + ); + i = i + 3; break; default: @@ -237,3 +287,29 @@ function updateTree( export function invalidateCommitTrees(): void { rootToCommitTreeMap.clear(); } + +// DEBUG +const __printTree = (commitTree: CommitTree) => { + if (__DEBUG__) { + const { nodes, rootID } = commitTree; + console.group('__printTree()'); + const queue = [rootID, 0]; + while (queue.length > 0) { + const id = queue.shift(); + const depth = queue.shift(); + + const node = ((nodes.get(id): any): Node); + + console.log( + `${'•'.repeat(depth)}${node.id}:${node.displayName || ''} ${ + node.key ? `key:"${node.key}"` : '' + } (${node.treeBaseDuration})` + ); + + node.children.forEach(childID => { + queue.push(childID, depth + 1); + }); + } + console.groupEnd(); + } +}; diff --git a/src/devtools/views/Profiler/SnapshotSelector.js b/src/devtools/views/Profiler/SnapshotSelector.js index c7293018f9..fa6469b54e 100644 --- a/src/devtools/views/Profiler/SnapshotSelector.js +++ b/src/devtools/views/Profiler/SnapshotSelector.js @@ -55,6 +55,8 @@ export default function SnapshotSelector(_: Props) { return null; }, [filteredCommitIndices, selectedCommitIndex]); + // TODO (profiling) This should be managed by the context controller (reducer). + // TODO (profiling) We should also reset the selected index to 0 between profiling sessions. if (selectedFilteredCommitIndex === null) { if (numFilteredCommits > 0) { setSelectedCommitIndex(0); diff --git a/src/devtools/views/Profiler/types.js b/src/devtools/views/Profiler/types.js index bf87074dfc..0b42424578 100644 --- a/src/devtools/views/Profiler/types.js +++ b/src/devtools/views/Profiler/types.js @@ -21,11 +21,8 @@ export type Interaction = {| |}; export type CommitDetails = {| + actualDurations: Map, interactions: Array, - committedFibers: Array<{| - actualDuration: number, - id: number, - |}>, |}; export type ProfilingSummary = {| diff --git a/src/devtools/views/Profiler/utils.js b/src/devtools/views/Profiler/utils.js index ed9897f544..d94c2ecd4e 100644 --- a/src/devtools/views/Profiler/utils.js +++ b/src/devtools/views/Profiler/utils.js @@ -1,5 +1,7 @@ // @flow +import type { CommitDetails, CommitTree, Node } from './types'; + const commitGradient = [ 'var(--color-commit-gradient-0)', 'var(--color-commit-gradient-1)', @@ -13,6 +15,30 @@ const commitGradient = [ 'var(--color-commit-gradient-9)', ]; +export const calculateSelfDuration = ( + id: number, + commitTree: CommitTree, + commitDetails: CommitDetails +): number => { + const { actualDurations } = commitDetails; + const { nodes } = commitTree; + + if (!actualDurations.has(id)) { + return 0; + } + + let selfDuration = ((actualDurations.get(id): any): number); + + const node = ((nodes.get(id): any): Node); + node.children.forEach(childID => { + if (actualDurations.has(childID)) { + selfDuration -= ((actualDurations.get(childID): any): number); + } + }); + + return selfDuration; +}; + export const getGradientColor = (value: number) => { const maxIndex = commitGradient.length - 1; let index;