diff --git a/config/kibana.yml b/config/kibana.yml index 06e019804ce51..4754d2fe4ffe9 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -5,6 +5,7 @@ # Kibana is served by a back end server. This setting specifies the port to use. #server.port: 5601 +data.search.sessions.enabled: true # Specifies the address to which the Kibana server will bind. IP addresses and host names are both valid values. # The default is 'localhost', which usually means remote machines will not be able to connect. # To allow connections from remote users, set this parameter to a non-loopback address. diff --git a/src/core/packages/http/browser-internal/src/fetch.ts b/src/core/packages/http/browser-internal/src/fetch.ts index e49ab9db3328e..aa898c7267869 100644 --- a/src/core/packages/http/browser-internal/src/fetch.ts +++ b/src/core/packages/http/browser-internal/src/fetch.ts @@ -82,6 +82,8 @@ export class Fetch { const optionsWithPath = validateFetchArguments(pathOrOptions, options); const controller = new HttpInterceptController(); + const mark = typeof pathOrOptions !== 'string' && pathOrOptions.mark; + // We wrap the interception in a separate promise to ensure that when // a halt is called we do not resolve or reject, halting handling of the promise. return new Promise>(async (resolve, reject) => { @@ -93,7 +95,7 @@ export class Fetch { controller ); const initialResponse = interceptFetch( - this.fetchResponse.bind(this), + this.fetchResponse.bind(this, !!mark), interceptedOptions, this.interceptors, controller @@ -164,12 +166,17 @@ export class Fetch { } private async fetchResponse( + mark: boolean, fetchOptions: HttpFetchOptionsWithPath ): Promise> { const request = this.createRequest(fetchOptions); let response: Response; let body = null; + if (mark) { + performance.mark('search_initiated'); + } + try { response = await window.fetch(request); } catch (err) { @@ -189,6 +196,9 @@ export class Fetch { body = await response.arrayBuffer(); } else { const text = await response.text(); + if (mark) { + performance.mark('search_response_received'); + } try { body = JSON.parse(text); @@ -210,13 +220,14 @@ export class Fetch { private shorthand(method: string): HttpHandler { return ( pathOrOptions: string | HttpFetchOptionsWithPath, - options?: HttpFetchOptions + options?: HttpFetchOptions, + mark?: boolean ) => { const optionsWithPath: HttpFetchOptionsWithPath = validateFetchArguments( pathOrOptions, options ); - return this.fetch>({ ...optionsWithPath, method }); + return this.fetch>({ ...optionsWithPath, method, mark }); }; } } diff --git a/src/core/packages/http/browser/src/types.ts b/src/core/packages/http/browser/src/types.ts index fee0ceb15f68a..0aa72d4e9c8e3 100644 --- a/src/core/packages/http/browser/src/types.ts +++ b/src/core/packages/http/browser/src/types.ts @@ -338,6 +338,7 @@ export interface HttpFetchOptionsWithPath extends HttpFetchOptions { * The path on the Kibana server to send the request to. Should not include the basePath. */ path: string; + mark?: boolean; } /** @@ -352,7 +353,8 @@ export interface HttpFetchOptionsWithPath extends HttpFetchOptions { export interface HttpHandler { ( path: string, - options: HttpFetchOptions & { asResponse: true } + options: HttpFetchOptions & { asResponse: true }, + mark?: boolean ): Promise>; (options: HttpFetchOptionsWithPath & { asResponse: true }): Promise< diff --git a/src/core/packages/rendering/server-internal/src/bootstrap/render_template.ts b/src/core/packages/rendering/server-internal/src/bootstrap/render_template.ts index ee32366dca52a..f231f7887a5cc 100644 --- a/src/core/packages/rendering/server-internal/src/bootstrap/render_template.ts +++ b/src/core/packages/rendering/server-internal/src/bootstrap/render_template.ts @@ -159,6 +159,22 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { detail: 'load_started', }) + // Set up performance observer to log marks and measures as they happen + if (typeof PerformanceObserver !== 'undefined' && false) { + const observer = new PerformanceObserver((list) => { + const entries = list.getEntries(); + entries.forEach((entry) => { + if (entry.entryType === 'mark') { + console.log( + \`Performance Mark: \${entry.name} at \${entry.startTime.toFixed(2)}ms\`, + entry.toJSON() + ); + } + }); + }); + observer.observe({ entryTypes: ['mark', 'measure'] }); + } + load([ ${jsDependencyPaths.map((path) => `'${path}'`).join(',')} ], function () { diff --git a/src/platform/packages/shared/content-management/table_list_view_table/src/components/item_details.tsx b/src/platform/packages/shared/content-management/table_list_view_table/src/components/item_details.tsx index 4e7551588fab1..18a313313f7e7 100644 --- a/src/platform/packages/shared/content-management/table_list_view_table/src/components/item_details.tsx +++ b/src/platform/packages/shared/content-management/table_list_view_table/src/components/item_details.tsx @@ -89,7 +89,9 @@ export function ItemDetails({ {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} { + performance.mark('navigate_to_dashboard'); + }} data-test-subj={`${id}ListingTitleLink-${item.attributes.title.split(' ').join('-')}`} > diff --git a/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx b/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx index f532385433501..53f0a8fef1d39 100644 --- a/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx +++ b/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx @@ -301,6 +301,8 @@ export function XYChart({ const onRenderChange = useCallback( (isRendered: boolean = true) => { if (isRendered) { + performance.mark('render_complete'); + renderComplete(); } }, diff --git a/src/platform/plugins/shared/chart_expressions/expression_xy/public/expression_renderers/xy_chart_renderer.tsx b/src/platform/plugins/shared/chart_expressions/expression_xy/public/expression_renderers/xy_chart_renderer.tsx index 91162ad03545d..d114e27b042ce 100644 --- a/src/platform/plugins/shared/chart_expressions/expression_xy/public/expression_renderers/xy_chart_renderer.tsx +++ b/src/platform/plugins/shared/chart_expressions/expression_xy/public/expression_renderers/xy_chart_renderer.tsx @@ -313,5 +313,7 @@ export const getXyChartRenderer = ({ , domNode ); + + performance.mark('charts_lib_invoked'); }, }); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/dashboard_app.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/dashboard_app.tsx index 34b47b81d3efc..574c52e3df31a 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/dashboard_app.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/dashboard_app.tsx @@ -257,7 +257,25 @@ export function DashboardApp({ locator={locator} onApiAvailable={(dashboard) => { if (dashboard && !dashboardApi) { + // performance.mark('setDashboardApi-start'); setDashboardApi(dashboard); + // performance.mark('setDashboardApi-end'); + // performance.measure( + // 'setDashboardApi-execution', + // 'setDashboardApi-start', + // 'setDashboardApi-end' + // ); + + const setDashboardApiMeasure = performance.getEntriesByName( + 'setDashboardApi-execution' + )[0]; + if (setDashboardApiMeasure) { + // eslint-disable-next-line no-console + console.log( + `setDashboardApi execution took: ${setDashboardApiMeasure.duration.toFixed(2)}ms` + ); + } + if (expandedPanelId) { dashboard?.expandPanel(expandedPanelId); } diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/dashboard_router.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/dashboard_router.tsx index 7a004f8f44dd0..fbac5a25135e7 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/dashboard_router.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/dashboard_router.tsx @@ -96,6 +96,7 @@ export async function mountApp({ const renderDashboard = ( routeProps: RouteComponentProps<{ id?: string; expandedPanelId?: string }> ) => { + performance.mark('dashboard_render_initiated'); const routeParams = parse(routeProps.history.location.search); if (routeParams.embed === 'true' && !globalEmbedSettings) { globalEmbedSettings = getDashboardEmbedSettings(routeParams); diff --git a/src/platform/plugins/shared/data/common/search/expressions/esql.ts b/src/platform/plugins/shared/data/common/search/expressions/esql.ts index 482706d4f8a7c..4d3bfe86c9f48 100644 --- a/src/platform/plugins/shared/data/common/search/expressions/esql.ts +++ b/src/platform/plugins/shared/data/common/search/expressions/esql.ts @@ -173,6 +173,8 @@ export const getEsqlFn = ({ getStartDependencies }: EsqlFnArguments) => { }, { abortSignal, inspectorAdapters, getKibanaRequest, getSearchSessionId } ) { + performance.mark('requester_fn'); + return defer(() => getStartDependencies(() => { const request = getKibanaRequest?.(); diff --git a/src/platform/plugins/shared/data/public/search/expressions/esaggs.ts b/src/platform/plugins/shared/data/public/search/expressions/esaggs.ts index c13e6b4086eb8..35d9907238294 100644 --- a/src/platform/plugins/shared/data/public/search/expressions/esaggs.ts +++ b/src/platform/plugins/shared/data/public/search/expressions/esaggs.ts @@ -47,6 +47,7 @@ export function getFunctionDefinition({ args, { inspectorAdapters, abortSignal, getSearchSessionId, getExecutionContext, getSearchContext } ) { + performance.mark('requester_fn'); return defer(async () => { const [{ aggs, indexPatterns, searchSource, getNow }, { handleEsaggsRequest }] = await Promise.all([getStartDependencies(), import('../../../common/search/expressions')]); diff --git a/src/platform/plugins/shared/data/public/search/search_interceptor/search_interceptor.ts b/src/platform/plugins/shared/data/public/search/search_interceptor/search_interceptor.ts index c3b58599a2b50..0e1614659f3aa 100644 --- a/src/platform/plugins/shared/data/public/search/search_interceptor/search_interceptor.ts +++ b/src/platform/plugins/shared/data/public/search/search_interceptor/search_interceptor.ts @@ -288,7 +288,8 @@ export class SearchInterceptor { private runSearch$( { id, ...request }: IKibanaSearchRequest, options: IAsyncSearchOptions, - searchAbortController: SearchAbortController + searchAbortController: SearchAbortController, + mark: boolean = false ) { const { sessionId, strategy } = options; @@ -304,7 +305,8 @@ export class SearchInterceptor { ...this.deps.session.getSearchOptions(sessionId), abortSignal: searchAbortController.getSignal(), isSearchStored, - } + }, + mark ) .then((result) => { afterPoll({ isSearchStored: result.isStored ?? false }); @@ -439,7 +441,8 @@ export class SearchInterceptor { */ private runSearch( request: IKibanaSearchRequest, - options?: ISearchOptions + options?: ISearchOptions, + mark: boolean = false ): Promise { const { abortSignal } = options || {}; @@ -460,7 +463,8 @@ export class SearchInterceptor { strategy === undefined, // undefined strategy is treated as enhanced ES }), asResponse: true, - } + }, + mark ) .then((rawResponse) => { const warning = rawResponse.response?.headers.get('warning'); @@ -534,7 +538,8 @@ export class SearchInterceptor { private getSearchResponse$( request: IKibanaSearchRequest, options: IAsyncSearchOptions, - requestHash?: string + requestHash?: string, + mark: boolean = false ) { const cached = requestHash ? this.responseCache.get(requestHash) : undefined; @@ -544,7 +549,8 @@ export class SearchInterceptor { // Create a new abort signal if one was not passed. This fake signal will never be aborted, // So the underlaying search will not be aborted, even if the other consumers abort. searchAbortController.addAbortSignal(options.abortSignal ?? new AbortController().signal); - const response$ = cached?.response$ || this.runSearch$(request, options, searchAbortController); + const response$ = + cached?.response$ || this.runSearch$(request, options, searchAbortController, mark); if (requestHash && !this.responseCache.has(requestHash)) { this.responseCache.set(requestHash, { @@ -568,7 +574,11 @@ export class SearchInterceptor { * @options * @returns `Observable` emitting the search response or an error. */ - public search({ id, ...request }: IKibanaSearchRequest, options: IAsyncSearchOptions = {}) { + public search( + { id, ...request }: IKibanaSearchRequest, + options: IAsyncSearchOptions = {}, + mark: boolean = false + ) { const searchOptions = { ...options, }; @@ -583,7 +593,8 @@ export class SearchInterceptor { const { searchAbortController, response$ } = this.getSearchResponse$( request, searchOptions, - requestHash + requestHash, + mark ); this.pendingCount$.next(this.pendingCount$.getValue() + 1); diff --git a/src/platform/plugins/shared/data/public/search/search_service.ts b/src/platform/plugins/shared/data/public/search/search_service.ts index 7e20c6e3a8c5f..d64412324fee5 100644 --- a/src/platform/plugins/shared/data/public/search/search_service.ts +++ b/src/platform/plugins/shared/data/public/search/search_service.ts @@ -231,7 +231,7 @@ export class SearchService implements Plugin { const { http, uiSettings, chrome, application, notifications, ...startServices } = coreStart; const search = ((request, options = {}) => { - return this.searchInterceptor.search(request, options); + return this.searchInterceptor.search(request, options, true); }) as ISearchGeneric; const loadingCount$ = new BehaviorSubject(0); diff --git a/src/platform/plugins/shared/data_views/server/fetcher/index_patterns_fetcher.ts b/src/platform/plugins/shared/data_views/server/fetcher/index_patterns_fetcher.ts index 1910a2b6502f7..0efc6f62b14ae 100644 --- a/src/platform/plugins/shared/data_views/server/fetcher/index_patterns_fetcher.ts +++ b/src/platform/plugins/shared/data_views/server/fetcher/index_patterns_fetcher.ts @@ -102,6 +102,8 @@ export class IndexPatternsFetcher { const expandWildcards = allowHidden ? 'all' : 'open'; + performance.mark('getFieldsForWildcard:start'); + const fieldCapsResponse = await getFieldCapabilities({ callCluster: this.elasticsearchClient, uiSettingsClient: this.uiSettingsClient, @@ -120,6 +122,8 @@ export class IndexPatternsFetcher { abortSignal, }); + performance.mark('getFieldsForWildcard:getFieldCapsEnd'); + if (this.rollupsEnabled && type === DataViewType.ROLLUP && rollupIndex) { const rollupFields: FieldDescriptor[] = []; const capabilities = getCapabilitiesForRollupIndices( @@ -152,6 +156,29 @@ export class IndexPatternsFetcher { indices: fieldCapsResponse.indices, }; } + + performance.mark('getFieldsForWildcard:end'); + + // add measures and print all measures + performance.measure('getFieldsForWildcard:total', 'getFieldsForWildcard:start'); + performance.measure( + 'getFieldsForWildcard:getFieldCaps', + 'getFieldsForWildcard:start', + 'getFieldsForWildcard:getFieldCapsEnd' + ); + performance.measure( + 'getFieldsForWildcard:rollupProcessing', + 'getFieldsForWildcard:getFieldCapsEnd', + 'getFieldsForWildcard:end' + ); + + performance.getEntriesByType('measure').forEach((m) => { + // eslint-disable-next-line no-console + console.log(`${m.name}: ${m.duration}`); + }); + performance.clearMarks(); + performance.clearMeasures(); + return fieldCapsResponse; } diff --git a/src/platform/plugins/shared/data_views/server/fetcher/lib/field_capabilities/field_capabilities.ts b/src/platform/plugins/shared/data_views/server/fetcher/lib/field_capabilities/field_capabilities.ts index e2783be13e271..be344e184f40f 100644 --- a/src/platform/plugins/shared/data_views/server/fetcher/lib/field_capabilities/field_capabilities.ts +++ b/src/platform/plugins/shared/data_views/server/fetcher/lib/field_capabilities/field_capabilities.ts @@ -60,7 +60,9 @@ export async function getFieldCapabilities(params: FieldCapabilitiesParams) { abortSignal, } = params; + performance.mark('getFieldCapabilities:start'); const excludedTiers = await uiSettingsClient?.get(DATA_VIEWS_FIELDS_EXCLUDED_TIERS); + performance.mark('getFieldCapabilities:uiSettingsEnd'); const esFieldCaps = await callFieldCapsApi({ callCluster, indices, @@ -73,7 +75,10 @@ export async function getFieldCapabilities(params: FieldCapabilitiesParams) { runtimeMappings, abortSignal, }); - const fieldCapsArr = readFieldCapsResponse(esFieldCaps.body); + performance.mark('getFieldCapabilities:responseEnd'); + const fieldCapsArr = await readFieldCapsResponse(esFieldCaps.body); + performance.mark('getFieldCapabilities:readEnd'); + const fieldsFromFieldCapsByName = keyBy(fieldCapsArr, 'name'); const allFieldsUnsorted = Object.keys(fieldsFromFieldCapsByName) @@ -105,6 +110,30 @@ export async function getFieldCapabilities(params: FieldCapabilitiesParams) { ) .map(mergeOverrides); + performance.mark('getFieldCapabilities:allFieldsCollectedEnd'); + + // add measures and print all measures + performance.measure( + 'getFieldCapabilities:uiSettings', + 'getFieldCapabilities:start', + 'getFieldCapabilities:uiSettingsEnd' + ); + performance.measure( + 'getFieldCapabilities:response', + 'getFieldCapabilities:uiSettingsEnd', + 'getFieldCapabilities:responseEnd' + ); + performance.measure( + 'getFieldCapabilities:read', + 'getFieldCapabilities:responseEnd', + 'getFieldCapabilities:readEnd' + ); + performance.measure( + 'getFieldCapabilities:allFieldsCollected', + 'getFieldCapabilities:readEnd', + 'getFieldCapabilities:allFieldsCollectedEnd' + ); + return { fields: sortBy(allFieldsUnsorted, 'name'), indices: esFieldCaps.body.indices as string[], diff --git a/x-pack/platform/plugins/shared/lens/public/data_views_service/loader.ts b/x-pack/platform/plugins/shared/lens/public/data_views_service/loader.ts index d49a08b6507d4..edbf9d4fb6c5a 100644 --- a/x-pack/platform/plugins/shared/lens/public/data_views_service/loader.ts +++ b/x-pack/platform/plugins/shared/lens/public/data_views_service/loader.ts @@ -200,6 +200,7 @@ export async function loadIndexPatterns({ )) ); + const start = performance.now(); const indexPatternsObject = indexPatterns.reduce( (acc, indexPattern) => ({ [indexPattern.id!]: convertDataViewIntoLensIndexPattern(indexPattern, onRestrictionMapping), @@ -208,6 +209,9 @@ export async function loadIndexPatterns({ { ...cache } ); + const end = performance.now(); + // console.log(`Data view transformations took: ${(end - start).toFixed(2)}ms`); + return indexPatternsObject; } diff --git a/x-pack/platform/plugins/shared/lens/public/plugin.ts b/x-pack/platform/plugins/shared/lens/public/plugin.ts index 1307faf54cf29..0b02b357e4ec2 100644 --- a/x-pack/platform/plugins/shared/lens/public/plugin.ts +++ b/x-pack/platform/plugins/shared/lens/public/plugin.ts @@ -383,6 +383,7 @@ export class LensPlugin { if (embeddable) { // Let Kibana know about the Lens embeddable embeddable.registerReactEmbeddableFactory(LENS_EMBEDDABLE_TYPE, async () => { + performance.mark('embeddable_factory_requested'); const [deps, { createLensEmbeddableFactory }] = await Promise.all([ getStartServicesForEmbeddable(), import('./async_services'), diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts index 62b3718f3170b..5181b9e001f2b 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts @@ -112,6 +112,57 @@ export function loadEmbeddableData( }; const onRenderComplete = () => { + // list performance marks + performance.measure('time_to_navigate', 'navigate_to_dashboard', 'dashboard_render_initiated'); + performance.measure( + 'time_until_embeddable_factory_requested', + 'dashboard_render_initiated', + 'embeddable_factory_requested' + ); + performance.measure( + 'time_until_embeddable_requested', + 'embeddable_factory_requested', + 'embeddable_requested' + ); + performance.measure( + 'time_until_embeddable_executes_expression', + 'embeddable_requested', + 'expression_sent_to_engine' + ); + performance.measure('time_to_requester_fn', 'expression_sent_to_engine', 'requester_fn'); + performance.measure('time_until_data_request', 'requester_fn', 'search_initiated'); + performance.measure('time_to_data', 'search_initiated', 'search_response_received'); + performance.measure('time_to_charts_lib', 'search_response_received', 'charts_lib_invoked'); + performance.measure('time_in_charts_lib', 'charts_lib_invoked', 'render_complete'); + + performance.measure('total', 'navigate_to_dashboard', 'render_complete'); + + // log them + const measures = performance.getEntriesByType('measure').sort((a, b) => { + // Keep "total" measure at the end + if (a.name === 'total') return 1; + if (b.name === 'total') return -1; + return a.startTime - b.startTime; + }); + + let totalTime = 0; + for (const measure of measures) { + totalTime += measure.duration; + } + + // Print performance data in TSV format for Google Sheets + let tsvOutput = ''; + for (let i = 0; i < measures.length; i++) { + const measure = measures[i]; + const percentage = totalTime > 0 ? ((measure.duration / totalTime) * 100).toFixed(1) : '0.0'; + tsvOutput += `${measure.name}\t${measure.duration.toFixed(2)}\t${percentage}\n`; + } + + console.log(`Performance TSV:\nMeasure\tDuration (ms)\tPercentage\n${tsvOutput}`); + + performance.clearMarks(); + performance.clearMeasures(); + updateMessages(getUserMessages('embeddableBadge')); // No issues so far, blocking errors are handled directly by Lens from this point on if (!dispatchBlockingErrorIfAny()) { diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/expression_wrapper.tsx b/x-pack/platform/plugins/shared/lens/public/react_embeddable/expression_wrapper.tsx index 24f4b62de7190..ace72c6a36866 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/expression_wrapper.tsx +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/expression_wrapper.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useRef } from 'react'; import type { ExpressionRendererEvent, ReactExpressionRendererProps, @@ -74,7 +74,15 @@ export function ExpressionWrapper({ noPadding, abortController, }: ExpressionWrapperProps) { + const hasMarkedExpressionSent = useRef(false); + if (!expression) return null; + + if (!hasMarkedExpressionSent.current) { + performance.mark('expression_sent_to_engine'); + hasMarkedExpressionSent.current = true; + } + return (
{ + performance.mark('embeddable_requested'); + const titleManager = initializeTitleManager(initialState.rawState); const dynamicActionsManager = services.embeddableEnhanced?.initializeEmbeddableDynamicActions( @@ -220,6 +222,8 @@ export const createLensEmbeddableFactory = ( searchContextConfig.cleanup(); }; + performance.mark('embeddable_built'); + return { api, Component: () => ( diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/renderer/lens_embeddable_component.tsx b/x-pack/platform/plugins/shared/lens/public/react_embeddable/renderer/lens_embeddable_component.tsx index 4864b6e7ea39d..11090ee964422 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/renderer/lens_embeddable_component.tsx +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/renderer/lens_embeddable_component.tsx @@ -6,7 +6,7 @@ */ import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; import { ExpressionWrapper } from '../expression_wrapper'; import type { LensInternalApi, LensApi } from '../types'; import { UserMessages } from '../user_messages/container'; @@ -46,6 +46,9 @@ export function LensEmbeddableComponent({ const [warningOrErrors, infoMessages] = useMessages(internalApi); + // Track the previous expression to detect changes + const previousExpression = useRef(null); + // On unmount call all the cleanups useEffect(() => { addLog(`Mounting Lens Embeddable component: ${api.defaultTitle$?.getValue()}`); @@ -69,6 +72,20 @@ export function LensEmbeddableComponent({ } : undefined; + // // Check if expression has changed across renders and throw error + // if (expressionParams?.expression) { + // if (previousExpression.current === null) { + // // First time we have an expression, store it + // previousExpression.current = expressionParams.expression; + // } else if (previousExpression.current !== expressionParams.expression) { + // // Expression has changed, throw an error + // throw new Error( + // 'Expression cannot change across component renders. ' + + // `Previous: ${previousExpression.current}, Current: ${expressionParams.expression}` + // ); + // } + // } + return (