From 3ae8cda360c5fa29a7228a4cb87f8464bddb9382 Mon Sep 17 00:00:00 2001 From: Kirill Chernakov Date: Tue, 21 Apr 2026 19:40:10 +0400 Subject: [PATCH] [workflows_management] Lazy-load Zod connector schemas to cut idle memory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single lazy boundary in schema.ts (the sole consumer of connector_action_schema.ts) defers ~16 MB of zod-schema heap until the first workflow edit/execute call. connector_action_schema.ts is left untouched — no getter wrappers, no test-only reset API. Removes the unused WORKFLOW_ZOD_SCHEMA / WORKFLOW_ZOD_SCHEMA_LOOSE module-level constants whose eager generateYamlSchemaFromConnectors() calls contributed to the startup heap. Heap snapshot (built Kibana, allocation tracking, idle): @kbn/workflows-management-plugin alloc site: 17.2 → 4.2 MB (-13 MB) Closes #264175 Made-with: Cursor --- .../connector_action_schema.lazy.test.ts | 60 +++++++++++++++++++ .../workflows_management/common/schema.ts | 43 +++++++------ 2 files changed, 84 insertions(+), 19 deletions(-) create mode 100644 src/platform/plugins/shared/workflows_management/common/connector_action_schema.lazy.test.ts diff --git a/src/platform/plugins/shared/workflows_management/common/connector_action_schema.lazy.test.ts b/src/platform/plugins/shared/workflows_management/common/connector_action_schema.lazy.test.ts new file mode 100644 index 0000000000000..905e92ca75897 --- /dev/null +++ b/src/platform/plugins/shared/workflows_management/common/connector_action_schema.lazy.test.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +/** + * Regression tests for the lazy-loading boundary in schema.ts (see #264175). + * + * schema.ts is the sole consumer of connector_action_schema.ts. It defers the + * require() so the heavy stack_connectors_schema/* and @kbn/connector-specs + * modules are not loaded at Kibana startup. These tests guard that invariant. + */ + +const SEP = __dirname.includes('\\') ? '\\' : '/'; +const CONNECTOR_ACTION_SCHEMA_PATH = require.resolve('./connector_action_schema'); +const STACK_CONNECTOR_SCHEMA_DIR = `${__dirname}${SEP}stack_connectors_schema`; +const SCHEMA_PATH = require.resolve('./schema'); + +const CONNECTOR_SPECS_RESOLVED_PATH: string = require.resolve('@kbn/connector-specs'); +const CONNECTOR_SPECS_DIR = CONNECTOR_SPECS_RESOLVED_PATH.slice( + 0, + CONNECTOR_SPECS_RESOLVED_PATH.lastIndexOf(SEP) +); + +const isHeavyModule = (p: string) => + p === CONNECTOR_ACTION_SCHEMA_PATH || + p.startsWith(STACK_CONNECTOR_SCHEMA_DIR + SEP) || + p.startsWith(CONNECTOR_SPECS_DIR); + +const getLoadedHeavyModules = () => Object.keys(require.cache).filter(isHeavyModule); + +describe('schema.ts lazy-loading boundary', () => { + beforeEach(() => { + jest.resetModules(); + for (const modulePath of Object.keys(require.cache)) { + if (modulePath === SCHEMA_PATH || isHeavyModule(modulePath)) { + delete require.cache[modulePath]; + } + } + }); + + it('does not load connector_action_schema or its transitive deps when schema.ts is imported', () => { + require('./schema'); + expect(getLoadedHeavyModules()).toEqual([]); + }); + + it('loads connector_action_schema after a consumer function triggers the boundary', () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { getAllConnectors } = require('./schema') as typeof import('./schema'); + expect(getLoadedHeavyModules()).toEqual([]); + + getAllConnectors(); + + expect(getLoadedHeavyModules().length).toBeGreaterThan(0); + }); +}); diff --git a/src/platform/plugins/shared/workflows_management/common/schema.ts b/src/platform/plugins/shared/workflows_management/common/schema.ts index ba06f944b9ed3..ebe7478c2a2e9 100644 --- a/src/platform/plugins/shared/workflows_management/common/schema.ts +++ b/src/platform/plugins/shared/workflows_management/common/schema.ts @@ -25,22 +25,28 @@ import { } from '@kbn/workflows'; import { z } from '@kbn/zod/v4'; -// Import connector schemas from the organized structure -import { - ConnectorActionInputSchemas, - ConnectorActionOutputSchemas, - ConnectorInputSchemas, - ConnectorOutputSchemas, - ConnectorSpecsInputSchemas, - staticConnectors, -} from './connector_action_schema'; // Import the singleton instance of StepSchemas import { stepSchemas } from './step_schemas'; +// Defers ~16 MB of zod-schema heap until the first workflow edit/execute call. +// connector_action_schema.ts eagerly builds Maps of Zod schemas from +// stack_connectors_schema/* and @kbn/connector-specs; keeping it behind a +// lazy require() avoids that cost at Kibana startup. See #264175. +let _connectorSchemas: typeof import('./connector_action_schema') | null = null; +function getConnectorSchemas(): typeof import('./connector_action_schema') { + if (_connectorSchemas === null) { + _connectorSchemas = require('./connector_action_schema'); + } + return _connectorSchemas as typeof import('./connector_action_schema'); +} + /** * Get parameter schema for a specific sub-action */ function getSubActionParamsSchema(actionTypeId: string, subActionName: string): z.ZodSchema { + const { ConnectorInputSchemas, ConnectorActionInputSchemas, ConnectorSpecsInputSchemas } = + getConnectorSchemas(); + const schema = ConnectorInputSchemas.get(actionTypeId); if (schema) { return schema; @@ -70,6 +76,8 @@ function getSubActionParamsSchema(actionTypeId: string, subActionName: string): * Get output schema for a specific sub-action */ function getSubActionOutputSchema(actionTypeId: string, subActionName: string): z.ZodSchema { + const { ConnectorOutputSchemas, ConnectorActionOutputSchemas } = getConnectorSchemas(); + const schema = ConnectorOutputSchemas.get(actionTypeId); if (schema) { return schema; @@ -200,14 +208,11 @@ function convertDynamicConnectorsToContractsInternal( export type WorkflowZodSchemaType = z.infer>; export type WorkflowZodSchemaLooseType = z.infer>; -// Legacy exports for backward compatibility - these will be deprecated -// TODO: Remove these once all consumers are updated to use the lazy-loaded versions -export const WORKFLOW_ZOD_SCHEMA = generateYamlSchemaFromConnectors(staticConnectors); -export const WORKFLOW_ZOD_SCHEMA_LOOSE = generateYamlSchemaFromConnectors( - staticConnectors, - [], - true -); +// NOTE: The former `WORKFLOW_ZOD_SCHEMA` / `WORKFLOW_ZOD_SCHEMA_LOOSE` +// module-level constants were removed in favour of `getWorkflowZodSchema()` / +// `getWorkflowZodSchemaLoose()`. They were unreferenced and their eager +// `generateYamlSchemaFromConnectors(...)` calls were a significant contributor +// to the startup heap described in https://github.com/elastic/kibana/issues/264175. /** * Combine static connectors with dynamic Elasticsearch and Kibana connectors @@ -227,7 +232,7 @@ export function getAllConnectorsInternal(): ConnectorContractUnion[] { const elasticsearchConnectors = getElasticsearchConnectors(); const kibanaConnectors = getKibanaConnectors(); const allConnectors = [ - ...staticConnectors, + ...getConnectorSchemas().staticConnectors, ...elasticsearchConnectors, ...kibanaConnectors, ...registeredStepDefinitions, @@ -318,7 +323,7 @@ export function addDynamicConnectorsToCache( const elasticsearchConnectors = getElasticsearchConnectors(); const kibanaConnectors = getKibanaConnectors(); const baseConnectors = [ - ...staticConnectors, + ...getConnectorSchemas().staticConnectors, ...elasticsearchConnectors, ...kibanaConnectors, ...registeredStepDefinitions,