Teact: Debug useMemo hit rate

This commit is contained in:
Alexander Zinchuk 2023-07-05 13:15:09 +02:00
parent 8cdc1b87be
commit 1c82160285
2 changed files with 90 additions and 27 deletions

View File

@ -303,10 +303,23 @@ function buildEmptyElement(): VirtualElementEmpty {
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
const DEBUG_components: AnyLiteral = { TOTAL: { componentName: 'TOTAL', renderCount: 0 } }; const DEBUG_components: AnyLiteral = { TOTAL: { componentName: 'TOTAL', renderCount: 0 } };
// eslint-disable-next-line @typescript-eslint/naming-convention
const DEBUG_memos: Record<string, { key: string; calls: number; misses: number; hitRate: number }> = {};
const DEBUG_MEMOS_CALLS_THRESHOLD = 20;
document.addEventListener('dblclick', () => { document.addEventListener('dblclick', () => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn('COMPONENTS', orderBy(Object.values(DEBUG_components), 'renderCount', 'desc')); console.warn('COMPONENTS', orderBy(Object.values(DEBUG_components), 'renderCount', 'desc'));
// eslint-disable-next-line no-console
console.warn('MEMOS', orderBy(
Object
.values(DEBUG_memos)
.filter(({ calls }) => calls >= DEBUG_MEMOS_CALLS_THRESHOLD)
.map((state) => ({ ...state, hitRate: state.hitRate.toFixed(2) })),
'hitRate',
'asc',
));
}); });
let instancesPendingUpdate = new Set<ComponentInstance>(); let instancesPendingUpdate = new Set<ComponentInstance>();
@ -587,19 +600,13 @@ export function useState<T>(initial?: T, debugKey?: string): [T, StateHookSetter
runUpdatePassOnRaf(); runUpdatePassOnRaf();
if (DEBUG_MORE) { if (DEBUG_MORE) {
if (componentInstance.name !== 'TeactNContainer') { // eslint-disable-next-line no-console
// eslint-disable-next-line no-console console.log(
console.log( '[Teact.useState]',
'[Teact.useState]', DEBUG_resolveComponentName(componentInstance.Component),
componentInstance.name, `State update at cursor #${cursor}${debugKey ? ` (${debugKey})` : ''}, next value: `,
// `componentInstance.Component` may be set to `null` by GC helper byCursor[cursor].nextValue,
componentInstance.Component && (componentInstance.Component as FC_withDebug).DEBUG_contentComponentName );
? `> ${(componentInstance.Component as FC_withDebug).DEBUG_contentComponentName}`
: '',
`State update at cursor #${cursor}${debugKey ? ` (${debugKey})` : ''}, next value: `,
byCursor[cursor].nextValue,
);
}
} }
})(renderingInstance), })(renderingInstance),
}; };
@ -764,19 +771,56 @@ export function useMemo<T extends any>(resolver: () => T, dependencies: any[], d
const { cursor, byCursor } = renderingInstance.hooks.memos; const { cursor, byCursor } = renderingInstance.hooks.memos;
let { value } = byCursor[cursor] || {}; let { value } = byCursor[cursor] || {};
// eslint-disable-next-line @typescript-eslint/naming-convention
let DEBUG_state: typeof DEBUG_memos[string] | undefined;
if (DEBUG && debugKey) {
const instanceKey = `${debugKey}#${renderingInstance.id}`;
DEBUG_state = DEBUG_memos[instanceKey];
if (!DEBUG_state) {
DEBUG_state = {
key: instanceKey, calls: 0, misses: 0, hitRate: 0,
};
DEBUG_memos[instanceKey] = DEBUG_state;
}
DEBUG_state.calls++;
DEBUG_state.hitRate = (DEBUG_state.calls - DEBUG_state.misses) / DEBUG_state.calls;
}
if ( if (
byCursor[cursor] === undefined byCursor[cursor] === undefined
|| dependencies.length !== byCursor[cursor].dependencies.length || dependencies.length !== byCursor[cursor].dependencies.length
|| dependencies.some((dependency, i) => dependency !== byCursor[cursor].dependencies[i]) || dependencies.some((dependency, i) => dependency !== byCursor[cursor].dependencies[i])
) { ) {
if (DEBUG && debugKey) { if (DEBUG && debugKey) {
// eslint-disable-next-line no-console if (DEBUG_state) {
console.log( DEBUG_state.misses++;
`[Teact.useMemo] ${renderingInstance.name} (${debugKey}): Update is caused by:`, DEBUG_state.hitRate = (DEBUG_state.calls - DEBUG_state.misses) / DEBUG_state.calls;
byCursor[cursor]
? getUnequalProps(byCursor[cursor].dependencies, dependencies).join(', ') if (
: '[first render]', DEBUG_state.calls % 10 === 0
); && DEBUG_state.calls >= DEBUG_MEMOS_CALLS_THRESHOLD
&& DEBUG_state.hitRate < 0.5
) {
// eslint-disable-next-line no-console
console.warn(
// eslint-disable-next-line max-len
`[Teact] ${DEBUG_state.key}: Hit rate is ${DEBUG_state.hitRate.toFixed(2)} for ${DEBUG_state.calls} calls`,
);
}
}
// Ignore default `debugKey`
if (!debugKey.startsWith('memo#')) {
// eslint-disable-next-line no-console
console.log(
`[Teact.useMemo] ${renderingInstance.name} (${debugKey}): Update is caused by:`,
byCursor[cursor]
? getUnequalProps(byCursor[cursor].dependencies, dependencies).join(', ')
: '[first render]',
);
}
} }
value = resolver(); value = resolver();
@ -816,11 +860,28 @@ export function useRef<T>(initial?: T | null) {
export function memo<T extends FC>(Component: T, debugKey?: string) { export function memo<T extends FC>(Component: T, debugKey?: string) {
return function TeactMemoWrapper(props: Props) { return function TeactMemoWrapper(props: Props) {
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps return useMemo(
return useMemo(() => createElement(Component, props), Object.values(props), debugKey); () => createElement(Component, props),
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
Object.values(props),
debugKey ?? `memo#${DEBUG_resolveComponentName(Component)}`,
);
} as T; } as T;
} }
// eslint-disable-next-line @typescript-eslint/naming-convention
export function DEBUG_resolveComponentName(Component: FC_withDebug) {
// eslint-disable-next-line @typescript-eslint/naming-convention
const { name, DEBUG_contentComponentName } = Component;
// Shorthand
if (name === 'TeactNContainer') {
return `@${DEBUG_contentComponentName}`;
}
return name + (DEBUG_contentComponentName ? ` > ${DEBUG_contentComponentName}` : '');
}
export default { export default {
createElement, createElement,
Fragment, Fragment,

View File

@ -1,6 +1,6 @@
/* eslint-disable eslint-multitab-tt/set-global-only-variable */ /* eslint-disable eslint-multitab-tt/set-global-only-variable */
import type { FC, FC_withDebug, Props } from './teact'; import type { FC, FC_withDebug, Props } from './teact';
import React, { useEffect } from './teact'; import React, { DEBUG_resolveComponentName, useEffect } from './teact';
import { requestMeasure } from '../fasterdom/fasterdom'; import { requestMeasure } from '../fasterdom/fasterdom';
import { DEBUG, DEBUG_MORE } from '../../config'; import { DEBUG, DEBUG_MORE } from '../../config';
@ -250,9 +250,7 @@ export function withGlobal<OwnProps extends AnyLiteral>(
mapStateToProps: MapStateToProps<OwnProps> = () => ({}), mapStateToProps: MapStateToProps<OwnProps> = () => ({}),
) { ) {
return (Component: FC) => { return (Component: FC) => {
return function TeactNContainer(props: OwnProps) { function TeactNContainer(props: OwnProps) {
(TeactNContainer as FC_withDebug).DEBUG_contentComponentName = Component.name;
const id = useUniqueId(); const id = useUniqueId();
const forceUpdate = useForceUpdate(); const forceUpdate = useForceUpdate();
@ -301,7 +299,11 @@ export function withGlobal<OwnProps extends AnyLiteral>(
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
return <Component {...container.mappedProps} {...props} />; return <Component {...container.mappedProps} {...props} />;
}; }
(TeactNContainer as FC_withDebug).DEBUG_contentComponentName = DEBUG_resolveComponentName(Component);
return TeactNContainer;
}; };
} }