Skip to content

Commit

Permalink
[Lens] Add lens editor performance metrics (#163089)
Browse files Browse the repository at this point in the history
## Summary
This PR instruments the code to track a few key editor performance
metrics. This is to prepare for adding a Lens editor performance
journey.

Metrics
- initial load of main chart
  - time to load data
  - time to render
- time to initially load and render all suggestions

For this PR, each metric is reported to the console (commented out to
pass the linter). When the journey is added, the console statements will
be converted to analytics service calls so that they show up as metrics
in the journey dashboard.

I made a few changes to increase the accuracy of the metrics.
- wrapping render-complete callbacks in `requestAnimationFrame` calls as
a temporary solution to
elastic/elastic-charts#2124
- fixing a multiple-subscription issue in the workspace panel

---------

Co-authored-by: Stratoula Kalafateli <[email protected]>
  • Loading branch information
drewdaemon and stratoula committed Aug 7, 2023
1 parent 2240c71 commit c6df737
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 76 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,10 @@ export const GaugeComponent: FC<GaugeRenderProps> = memo(
const onRenderChange = useCallback(
(isRendered: boolean = true) => {
if (isRendered) {
renderComplete();
// this requestAnimationFrame call is a temporary fix for https://github.com/elastic/elastic-charts/issues/2124
window.requestAnimationFrame(() => {
renderComplete();
});
}
},
[renderComplete]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,10 @@ export const HeatmapComponent: FC<HeatmapRenderProps> = memo(
const onRenderChange = useCallback(
(isRendered: boolean = true) => {
if (isRendered) {
renderComplete();
// this requestAnimationFrame call is a temporary fix for https://github.com/elastic/elastic-charts/issues/2124
window.requestAnimationFrame(() => {
renderComplete();
});
}
},
[renderComplete]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,11 @@ describe('MetricVisComponent', function () {
});

it('should report render complete', () => {
jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
cb(0);
return 0;
});

const renderCompleteSpy = jest.fn();
const component = shallow(
<MetricVis
Expand All @@ -995,6 +1000,8 @@ describe('MetricVisComponent', function () {
component.find(Settings).props().onRenderChange!(true);

expect(renderCompleteSpy).toHaveBeenCalledTimes(1);

(window.requestAnimationFrame as jest.Mock).mockRestore();
});

it('should convert null values to NaN', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,10 @@ export const MetricVis = ({
const onRenderChange = useCallback<RenderChangeListener>(
(isRendered) => {
if (isRendered) {
renderComplete();
// this requestAnimationFrame call is a temporary fix for https://github.com/elastic/elastic-charts/issues/2124
window.requestAnimationFrame(() => {
renderComplete();
});
}
},
[renderComplete]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,11 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => {
const onRenderChange = useCallback(
(isRendered: boolean = true) => {
if (isRendered) {
props.renderComplete();
setChartIsLoaded(true);
// this requestAnimationFrame call is a temporary fix for https://github.com/elastic/elastic-charts/issues/2124
window.requestAnimationFrame(() => {
props.renderComplete();
setChartIsLoaded(true);
});
}
},
[props]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,10 @@ export const TagCloudChart = ({
const onRenderChange = useCallback<RenderChangeListener>(
(isRendered) => {
if (isRendered) {
renderComplete();
// this requestAnimationFrame call is a temporary fix for https://github.com/elastic/elastic-charts/issues/2124
window.requestAnimationFrame(() => {
renderComplete();
});
}
},
[renderComplete]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,10 @@ export function XYChart({
const onRenderChange = useCallback(
(isRendered: boolean = true) => {
if (isRendered) {
renderComplete();
// this requestAnimationFrame call is a temporary fix for https://github.com/elastic/elastic-charts/issues/2124
window.requestAnimationFrame(() => {
renderComplete();
});
}
},
[renderComplete]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ import {
selectFrameDatasourceAPI,
} from '../../state_management';
import { filterAndSortUserMessages } from '../../app_plugin/get_application_user_messages';

const MAX_SUGGESTIONS_DISPLAYED = 5;
const LOCAL_STORAGE_SUGGESTIONS_PANEL = 'LENS_SUGGESTIONS_PANEL_HIDDEN';

Expand Down Expand Up @@ -108,11 +107,13 @@ const PreviewRenderer = ({
ExpressionRendererComponent,
expression,
hasError,
onRender,
}: {
withLabel: boolean;
expression: string | null | undefined;
ExpressionRendererComponent: ReactExpressionRendererType;
hasError: boolean;
onRender: () => void;
}) => {
const onErrorMessage = (
<div className="lnsSuggestionPanel__suggestionIcon">
Expand Down Expand Up @@ -143,6 +144,7 @@ const PreviewRenderer = ({
padding="s"
renderMode="preview"
expression={expression}
onRender$={onRender}
debounce={2000}
renderError={() => {
return onErrorMessage;
Expand All @@ -159,6 +161,7 @@ const SuggestionPreview = ({
selected,
onSelect,
showTitleAsLabel,
onRender,
}: {
onSelect: () => void;
preview: {
Expand All @@ -170,6 +173,7 @@ const SuggestionPreview = ({
ExpressionRenderer: ReactExpressionRendererType;
selected: boolean;
showTitleAsLabel?: boolean;
onRender: () => void;
}) => {
return (
<EuiToolTip content={preview.title}>
Expand All @@ -194,6 +198,7 @@ const SuggestionPreview = ({
expression={preview.expression && toExpression(preview.expression)}
withLabel={Boolean(showTitleAsLabel)}
hasError={Boolean(preview.error)}
onRender={onRender}
/>
) : (
<span className="lnsSuggestionPanel__suggestionIcon">
Expand Down Expand Up @@ -358,20 +363,36 @@ export function SuggestionPanel({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [existsStagedPreview]);

if (!activeDatasourceId) {
return null;
}

if (suggestions.length === 0) {
return null;
}
const startTime = useRef<number>(0);
const initialRenderComplete = useRef<boolean>(false);
const suggestionsRendered = useRef<boolean[]>([]);
const totalSuggestions = suggestions.length + 1;

const onSuggestionRender = useCallback((suggestionIndex: number) => {
suggestionsRendered.current[suggestionIndex] = true;
if (initialRenderComplete.current === false && suggestionsRendered.current.every(Boolean)) {
initialRenderComplete.current = true;
// console.log(
// 'time to fetch data and perform initial render for all suggestions',
// performance.now() - startTime.current
// );
}
}, []);

function rollbackToCurrentVisualization() {
const rollbackToCurrentVisualization = useCallback(() => {
if (lastSelectedSuggestion !== -1) {
setLastSelectedSuggestion(-1);
dispatchLens(rollbackSuggestion());
dispatchLens(applyChanges());
}
}, [dispatchLens, lastSelectedSuggestion]);

if (!activeDatasourceId) {
return null;
}

if (suggestions.length === 0) {
return null;
}

const renderApplyChangesPrompt = () => (
Expand Down Expand Up @@ -400,52 +421,58 @@ export function SuggestionPanel({
</EuiPanel>
);

const renderSuggestionsUI = () => (
<>
{currentVisualization.activeId && !hideSuggestions && (
<SuggestionPreview
preview={{
error: currentStateError,
expression: currentStateExpression,
icon:
visualizationMap[currentVisualization.activeId].getDescription(
currentVisualization.state
).icon || 'empty',
title: i18n.translate('xpack.lens.suggestions.currentVisLabel', {
defaultMessage: 'Current visualization',
}),
}}
ExpressionRenderer={AutoRefreshExpressionRenderer}
onSelect={rollbackToCurrentVisualization}
selected={lastSelectedSuggestion === -1}
showTitleAsLabel
/>
)}
{!hideSuggestions &&
suggestions.map((suggestion, index) => {
return (
<SuggestionPreview
preview={{
expression: suggestion.previewExpression,
icon: suggestion.previewIcon,
title: suggestion.title,
}}
ExpressionRenderer={AutoRefreshExpressionRenderer}
key={index}
onSelect={() => {
if (lastSelectedSuggestion === index) {
rollbackToCurrentVisualization();
} else {
setLastSelectedSuggestion(index);
switchToSuggestion(dispatchLens, suggestion, { applyImmediately: true });
}
}}
selected={index === lastSelectedSuggestion}
/>
);
})}
</>
);
const renderSuggestionsUI = () => {
suggestionsRendered.current = new Array(totalSuggestions).fill(false);
startTime.current = performance.now();
return (
<>
{currentVisualization.activeId && !hideSuggestions && (
<SuggestionPreview
preview={{
error: currentStateError,
expression: currentStateExpression,
icon:
visualizationMap[currentVisualization.activeId].getDescription(
currentVisualization.state
).icon || 'empty',
title: i18n.translate('xpack.lens.suggestions.currentVisLabel', {
defaultMessage: 'Current visualization',
}),
}}
ExpressionRenderer={AutoRefreshExpressionRenderer}
onSelect={rollbackToCurrentVisualization}
selected={lastSelectedSuggestion === -1}
showTitleAsLabel
onRender={() => onSuggestionRender(0)}
/>
)}
{!hideSuggestions &&
suggestions.map((suggestion, index) => {
return (
<SuggestionPreview
preview={{
expression: suggestion.previewExpression,
icon: suggestion.previewIcon,
title: suggestion.title,
}}
ExpressionRenderer={AutoRefreshExpressionRenderer}
key={index}
onSelect={() => {
if (lastSelectedSuggestion === index) {
rollbackToCurrentVisualization();
} else {
setLastSelectedSuggestion(index);
switchToSuggestion(dispatchLens, suggestion, { applyImmediately: true });
}
}}
selected={index === lastSelectedSuggestion}
onRender={() => onSuggestionRender(index + 1)}
/>
);
})}
</>
);
};

return (
<div className="lnsSuggestionPanel">
Expand Down
Loading

0 comments on commit c6df737

Please sign in to comment.