Skip to content

Commit

Permalink
Implemented debug profiler wrapper around every module in dev mode (#316
Browse files Browse the repository at this point in the history
)
  • Loading branch information
rubenthoms authored Sep 19, 2023
1 parent 8d22ea0 commit faa08d0
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from "react";
import { ImportState } from "@framework/Module";
import { ModuleInstance, ModuleInstanceState } from "@framework/ModuleInstance";
import { Workbench } from "@framework/Workbench";
import { DebugProfiler } from "@framework/internal/components/DebugProfiler";
import { ErrorBoundary } from "@framework/internal/components/ErrorBoundary";
import { CircularProgress } from "@lib/components/CircularProgress";

Expand Down Expand Up @@ -114,13 +115,15 @@ export const ViewContent = React.memo((props: ViewContentProps) => {
return (
<ErrorBoundary moduleInstance={props.moduleInstance}>
<div className="p-4 h-full w-full">
<View
moduleContext={props.moduleInstance.getContext()}
workbenchSession={props.workbench.getWorkbenchSession()}
workbenchServices={props.workbench.getWorkbenchServices()}
workbenchSettings={props.workbench.getWorkbenchSettings()}
initialSettings={props.moduleInstance.getInitialSettings() || undefined}
/>
<DebugProfiler id={`${props.moduleInstance.getId()}-view`}>
<View
moduleContext={props.moduleInstance.getContext()}
workbenchSession={props.workbench.getWorkbenchSession()}
workbenchServices={props.workbench.getWorkbenchServices()}
workbenchSettings={props.workbench.getWorkbenchSettings()}
initialSettings={props.moduleInstance.getInitialSettings() || undefined}
/>
</DebugProfiler>
</div>
</ErrorBoundary>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import React from "react";

import { isDevMode } from "@lib/utils/devMode";

type DebugProfilerRenderInfoProps = {
children: React.ReactNode | React.ReactNode[];
title: string;
};

const DebugProfilerRenderInfo: React.FC<DebugProfilerRenderInfoProps> = (props) => {
return (
<span className="text-pink-300" title={props.title}>
{props.children}
</span>
);
};

DebugProfilerRenderInfo.displayName = "DebugProfilerRenderInfo";

type DebugProfilerWrapperProps = {
id: string;
children: React.ReactNode;
onRender: React.ProfilerOnRenderCallback;
};

const DebugProfilerWrapper = React.memo((props: DebugProfilerWrapperProps) => {
return (
<React.Profiler id={props.id} onRender={props.onRender}>
{props.children}
</React.Profiler>
);
});

DebugProfilerWrapper.displayName = "DebugProfilerWrapper";

export type DebugProfilerProps = {
id: string;
children: React.ReactNode;
};

type RenderInfo = {
phase: "mount" | "update";
actualDuration: number;
baseDuration: number;
startTime: number;
commitTime: number;
interactions: Set<any>;
renderCount: number;
minTime: number;
maxTime: number;
totalTime: number;
avgTime: number;
};

export const DebugProfiler: React.FC<DebugProfilerProps> = (props) => {
const [renderInfo, setRenderInfo] = React.useState<RenderInfo | null>(null);

const handleRender = React.useCallback(
(
_: string,
phase: "mount" | "update",
actualDuration: number,
baseDuration: number,
startTime: number,
commitTime: number,
interactions: Set<any>
) => {
setRenderInfo((prev) => ({
phase,
actualDuration,
baseDuration,
startTime,
commitTime,
interactions,
renderCount: (prev?.renderCount ?? 0) + 1,
minTime: Math.min(prev?.minTime ?? actualDuration, actualDuration),
maxTime: Math.max(prev?.maxTime ?? actualDuration, actualDuration),
totalTime: (prev?.totalTime ?? 0) + actualDuration,
avgTime: ((prev?.totalTime ?? 0) + actualDuration) / ((prev?.renderCount ?? 0) + 1),
}));
},
[]
);

if (isDevMode()) {
return (
<>
<DebugProfilerWrapper id={props.id} onRender={handleRender}>
{props.children}
</DebugProfilerWrapper>
<div className="absolute bottom-1 w-full flex gap-2 flex-wrap">
{renderInfo && (
<>
<DebugProfilerRenderInfo title="Render count">
RC: {renderInfo.renderCount}
</DebugProfilerRenderInfo>
<DebugProfilerRenderInfo title="Phase">P: {renderInfo.phase}</DebugProfilerRenderInfo>
<DebugProfilerRenderInfo
title={
"Actual duration: The number of milliseconds spent rendering the module and its descendants for the current update. " +
"This indicates how well the subtree makes use of memoization (e.g. memo and useMemo). " +
"Ideally this value should decrease significantly after the initial mount as many of the descendants will only " +
"need to re-render if their specific props change."
}
>
AD: {renderInfo.actualDuration.toFixed(2)}ms
</DebugProfilerRenderInfo>
<DebugProfilerRenderInfo
title={
"Base Duration: The number of milliseconds estimating how much time it would take to re-render the entire module subtree without any optimizations. " +
"It is calculated by summing up the most recent render durations of each component in the tree. " +
"This value estimates a worst-case cost of rendering (e.g. the initial mount or a tree with no memoization). " +
"Compare actualDuration against it to see if memoization is working."
}
>
BD: {renderInfo.baseDuration.toFixed(2)}ms
</DebugProfilerRenderInfo>
<DebugProfilerRenderInfo title="The number of milliseconds of the fastest render duration.">
MIN: {renderInfo.minTime.toFixed(2)}ms
</DebugProfilerRenderInfo>
<DebugProfilerRenderInfo title="The number of milliseconds of the slowest render duration.">
MAX: {renderInfo.maxTime.toFixed(2)}ms
</DebugProfilerRenderInfo>
<DebugProfilerRenderInfo title="The number of milliseconds of the average render duration.">
AVG: {renderInfo.avgTime.toFixed(2)}ms
</DebugProfilerRenderInfo>
</>
)}
</div>
</>
);
}

return <>{props.children}</>;
};

DebugProfiler.displayName = "DebugProfiler";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { DebugProfiler } from "./debugProfiler";
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { Cog6ToothIcon } from "@heroicons/react/20/solid";
import { CircularProgress } from "@lib/components/CircularProgress";
import { resolveClassNames } from "@lib/utils/resolveClassNames";

import { DebugProfiler } from "../../DebugProfiler";

type SettingProps = {
moduleInstance: ModuleInstance<any>;
activeModuleId: string;
Expand Down Expand Up @@ -74,7 +76,7 @@ export const Setting: React.FC<SettingProps> = (props) => {
key={props.moduleInstance.getId()}
className={resolveClassNames(
props.activeModuleId === props.moduleInstance.getId() ? "flex" : "hidden",
"flex-col h-full w-full"
"flex-col h-full w-full relative"
)}
>
<ErrorBoundary moduleInstance={props.moduleInstance}>
Expand All @@ -89,13 +91,15 @@ export const Setting: React.FC<SettingProps> = (props) => {
</div>
<div className="flex flex-col gap-4 overflow-auto">
<div className="p-2">
<Settings
moduleContext={props.moduleInstance.getContext()}
workbenchSession={props.workbench.getWorkbenchSession()}
workbenchServices={props.workbench.getWorkbenchServices()}
workbenchSettings={props.workbench.getWorkbenchSettings()}
initialSettings={props.moduleInstance.getInitialSettings() || undefined}
/>
<DebugProfiler id={`${props.moduleInstance.getId()}-settings`}>
<Settings
moduleContext={props.moduleInstance.getContext()}
workbenchSession={props.workbench.getWorkbenchSession()}
workbenchServices={props.workbench.getWorkbenchServices()}
workbenchSettings={props.workbench.getWorkbenchSettings()}
initialSettings={props.moduleInstance.getInitialSettings() || undefined}
/>
</DebugProfiler>
</div>
</div>
</ErrorBoundary>
Expand Down

0 comments on commit faa08d0

Please sign in to comment.