From 579714b62fb3d4794b0ed92fe69354b323afa6bf Mon Sep 17 00:00:00 2001 From: Etienne <45695613+etiennejouan@users.noreply.github.com> Date: Thu, 2 Apr 2026 13:23:10 +0200 Subject: [PATCH] Investigate memory leak (#19213) In search for cache leak + Remove old instrumentation --- .../src/metadata/generated/schema.graphql | 1 - .../src/metadata/generated/schema.ts | 3 +- .../src/generated-metadata/graphql.ts | 1 - .../__tests__/workspace.factory.spec.ts | 5 ++ .../api/graphql/core-graphql-api.module.ts | 2 + .../hooks/use-direct-execution.hook.ts | 32 ---------- .../graphql-config/graphql-config.service.ts | 6 +- .../api/graphql/metadata.module-factory.ts | 4 +- .../api/graphql/workspace-schema.factory.ts | 8 +++ .../services/core-entity-cache.service.ts | 38 +++++++++-- .../hooks/use-graphql-query-timing.hook.ts | 63 ------------------- .../metrics/types/metrics-keys.type.ts | 1 + .../workspace-entity-manager.spec.ts | 1 - .../storage/promise-memoizer.storage.ts | 4 ++ .../services/workspace-cache.service.ts | 30 +++++++++ .../twenty-shared/src/types/FeatureFlagKey.ts | 1 - 16 files changed, 87 insertions(+), 113 deletions(-) delete mode 100644 packages/twenty-server/src/engine/core-modules/graphql/hooks/use-graphql-query-timing.hook.ts diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index d6400c6d58..4fd762cb9d 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -1732,7 +1732,6 @@ enum FeatureFlagKey { IS_DIRECT_GRAPHQL_EXECUTION_ENABLED IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED IS_CONNECTED_ACCOUNT_MIGRATED - IS_GRAPHQL_QUERY_TIMING_ENABLED IS_RECORD_TABLE_WIDGET_ENABLED IS_DATASOURCE_MIGRATED } diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index 475dbbc78f..c3dbbf7937 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -1422,7 +1422,7 @@ export interface PublicFeatureFlag { __typename: 'PublicFeatureFlag' } -export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_AI_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_MARKETPLACE_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_DRAFT_EMAIL_ENABLED' | 'IS_USAGE_ANALYTICS_ENABLED' | 'IS_RICH_TEXT_V1_MIGRATED' | 'IS_DIRECT_GRAPHQL_EXECUTION_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' | 'IS_CONNECTED_ACCOUNT_MIGRATED' | 'IS_GRAPHQL_QUERY_TIMING_ENABLED' | 'IS_RECORD_TABLE_WIDGET_ENABLED' | 'IS_DATASOURCE_MIGRATED' +export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_AI_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_MARKETPLACE_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_DRAFT_EMAIL_ENABLED' | 'IS_USAGE_ANALYTICS_ENABLED' | 'IS_RICH_TEXT_V1_MIGRATED' | 'IS_DIRECT_GRAPHQL_EXECUTION_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' | 'IS_CONNECTED_ACCOUNT_MIGRATED' | 'IS_RECORD_TABLE_WIDGET_ENABLED' | 'IS_DATASOURCE_MIGRATED' export interface ClientConfigMaintenanceMode { startAt: Scalars['DateTime'] @@ -9096,7 +9096,6 @@ export const enumFeatureFlagKey = { IS_DIRECT_GRAPHQL_EXECUTION_ENABLED: 'IS_DIRECT_GRAPHQL_EXECUTION_ENABLED' as const, IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED: 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' as const, IS_CONNECTED_ACCOUNT_MIGRATED: 'IS_CONNECTED_ACCOUNT_MIGRATED' as const, - IS_GRAPHQL_QUERY_TIMING_ENABLED: 'IS_GRAPHQL_QUERY_TIMING_ENABLED' as const, IS_RECORD_TABLE_WIDGET_ENABLED: 'IS_RECORD_TABLE_WIDGET_ENABLED' as const, IS_DATASOURCE_MIGRATED: 'IS_DATASOURCE_MIGRATED' as const } diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index daa6add585..5097aa51a9 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1725,7 +1725,6 @@ export enum FeatureFlagKey { IS_DIRECT_GRAPHQL_EXECUTION_ENABLED = 'IS_DIRECT_GRAPHQL_EXECUTION_ENABLED', IS_DRAFT_EMAIL_ENABLED = 'IS_DRAFT_EMAIL_ENABLED', IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED', - IS_GRAPHQL_QUERY_TIMING_ENABLED = 'IS_GRAPHQL_QUERY_TIMING_ENABLED', IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED', IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED', IS_MARKETPLACE_ENABLED = 'IS_MARKETPLACE_ENABLED', diff --git a/packages/twenty-server/src/engine/api/graphql/__tests__/workspace.factory.spec.ts b/packages/twenty-server/src/engine/api/graphql/__tests__/workspace.factory.spec.ts index 627eeb9974..328112ec34 100644 --- a/packages/twenty-server/src/engine/api/graphql/__tests__/workspace.factory.spec.ts +++ b/packages/twenty-server/src/engine/api/graphql/__tests__/workspace.factory.spec.ts @@ -5,6 +5,7 @@ import { WorkspaceResolverFactory } from 'src/engine/api/graphql/workspace-resol import { WorkspaceGraphQLSchemaGenerator } from 'src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory'; import { WorkspaceSchemaFactory } from 'src/engine/api/graphql/workspace-schema.factory'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; +import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; @@ -54,6 +55,10 @@ describe('WorkspaceSchemaFactory', () => { provide: TwentyConfigService, useValue: {}, }, + { + provide: MetricsService, + useValue: {}, + }, ], }).compile(); diff --git a/packages/twenty-server/src/engine/api/graphql/core-graphql-api.module.ts b/packages/twenty-server/src/engine/api/graphql/core-graphql-api.module.ts index 0c2e477729..90979cd318 100644 --- a/packages/twenty-server/src/engine/api/graphql/core-graphql-api.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/core-graphql-api.module.ts @@ -4,6 +4,7 @@ import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars- import { WorkspaceResolverBuilderModule } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.module'; import { WorkspaceSchemaBuilderModule } from 'src/engine/api/graphql/workspace-schema-builder/workspace-schema-builder.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; +import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; @@ -18,6 +19,7 @@ import { WorkspaceSchemaFactory } from './workspace-schema.factory'; WorkspaceCacheStorageModule, WorkspaceManyOrAllFlatEntityMapsCacheModule, FeatureFlagModule, + MetricsModule, ], providers: [WorkspaceSchemaFactory, ScalarsExplorerService], exports: [WorkspaceSchemaFactory], diff --git a/packages/twenty-server/src/engine/api/graphql/direct-execution/hooks/use-direct-execution.hook.ts b/packages/twenty-server/src/engine/api/graphql/direct-execution/hooks/use-direct-execution.hook.ts index 4c52be6bc1..d5eafd6826 100644 --- a/packages/twenty-server/src/engine/api/graphql/direct-execution/hooks/use-direct-execution.hook.ts +++ b/packages/twenty-server/src/engine/api/graphql/direct-execution/hooks/use-direct-execution.hook.ts @@ -1,5 +1,3 @@ -import { Logger } from '@nestjs/common'; - import { type Request } from 'express'; import { DocumentNode, parse } from 'graphql'; import { type Plugin } from 'graphql-yoga'; @@ -7,15 +5,12 @@ import { FeatureFlagKey } from 'twenty-shared/types'; import { isNull } from '@sniptt/guards'; import { type DirectExecutionService } from 'src/engine/api/graphql/direct-execution/direct-execution.service'; -import { queryTimingContextStorage } from 'src/engine/core-modules/graphql/storage/query-timing-context.storage'; import { computeSkipWorkspaceSchemaCreation } from 'src/engine/api/graphql/direct-execution/utils/compute-skip-workspace-schema-creation.util'; import { findOperationDefinition } from 'src/engine/api/graphql/direct-execution/utils/find-operation-definition.util'; import { hasOnlyGeneratedWorkspaceResolvers } from 'src/engine/api/graphql/direct-execution/utils/has-only-generated-workspace-resolvers.util'; import { isSubscriptionOperation } from 'src/engine/api/graphql/direct-execution/utils/is-subscription-operation.util'; import { type FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; -const logger = new Logger('GraphQLQueryTiming'); - export type DirectExecutionPluginConfig = { directExecutionService: DirectExecutionService; featureFlagService: FeatureFlagService; @@ -89,33 +84,6 @@ export function useDirectExecution( return; } - const isTimingEnabled = await config.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_GRAPHQL_QUERY_TIMING_ENABLED, - req.workspace.id, - ); - - const resolvedOperationName = operationName ?? 'Anonymous'; - - if (isTimingEnabled) { - const startTime = performance.now(); - - const timedResult = await queryTimingContextStorage.run(true, () => - config.directExecutionService.execute(req, document), - ); - - const durationMs = (performance.now() - startTime).toFixed(2); - - logger.log( - `[direct-execution] ${resolvedOperationName} — ${durationMs}ms (workspace: ${req.workspace.id})`, - ); - - if (isNull(timedResult)) { - return; - } - - return endResponse(Response.json(timedResult)); - } - const result = await config.directExecutionService.execute(req, document); if (isNull(result)) { diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.service.ts index 3922b9a003..b54c35a777 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.service.ts @@ -21,18 +21,17 @@ import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interface import { DirectExecutionService } from 'src/engine/api/graphql/direct-execution/direct-execution.service'; import { useDirectExecution } from 'src/engine/api/graphql/direct-execution/hooks/use-direct-execution.hook'; import { WorkspaceSchemaFactory } from 'src/engine/api/graphql/workspace-schema.factory'; +import { type FlatAuthContextUser } from 'src/engine/core-modules/auth/types/flat-auth-context-user.type'; import { CoreEngineModule } from 'src/engine/core-modules/core-engine.module'; import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; import { useSentryTracing } from 'src/engine/core-modules/exception-handler/hooks/use-sentry-tracing'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { useDisableIntrospectionAndSuggestionsForUnauthenticatedUsers } from 'src/engine/core-modules/graphql/hooks/use-disable-introspection-and-suggestions-for-unauthenticated-users.hook'; import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook'; -import { useGraphQLQueryTiming } from 'src/engine/core-modules/graphql/hooks/use-graphql-query-timing.hook'; import { useValidateGraphqlQueryComplexity } from 'src/engine/core-modules/graphql/hooks/use-validate-graphql-query-complexity.hook'; import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; -import { type FlatAuthContextUser } from 'src/engine/core-modules/auth/types/flat-auth-context-user.type'; import { type FlatWorkspace } from 'src/engine/core-modules/workspace/types/flat-workspace.type'; import { DataloaderService } from 'src/engine/dataloaders/dataloader.service'; import { handleExceptionAndConvertToGraphQLError } from 'src/engine/utils/global-exception-handler.util'; @@ -62,9 +61,6 @@ export class GraphQLConfigService const isDebugMode = this.twentyConfigService.get('NODE_ENV') === NodeEnvironment.DEVELOPMENT; const plugins = [ - useGraphQLQueryTiming({ - featureFlagService: this.featureFlagService, - }), useDirectExecution({ directExecutionService: this.directExecutionService, featureFlagService: this.featureFlagService, diff --git a/packages/twenty-server/src/engine/api/graphql/metadata.module-factory.ts b/packages/twenty-server/src/engine/api/graphql/metadata.module-factory.ts index 53c85f4b99..bd45d3f2e5 100644 --- a/packages/twenty-server/src/engine/api/graphql/metadata.module-factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/metadata.module-factory.ts @@ -5,13 +5,12 @@ import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interface import { useCachedMetadata } from 'src/engine/api/graphql/graphql-config/hooks/use-cached-metadata'; import { MetadataGraphQLApiModule } from 'src/engine/api/graphql/metadata-graphql-api.module'; -import { ClientConfig } from 'src/engine/core-modules/client-config/client-config.entity'; import { type CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; +import { ClientConfig } from 'src/engine/core-modules/client-config/client-config.entity'; import { type ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; import { type FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { useDisableIntrospectionAndSuggestionsForUnauthenticatedUsers } from 'src/engine/core-modules/graphql/hooks/use-disable-introspection-and-suggestions-for-unauthenticated-users.hook'; import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook'; -import { useGraphQLQueryTiming } from 'src/engine/core-modules/graphql/hooks/use-graphql-query-timing.hook'; import { useValidateGraphqlQueryComplexity } from 'src/engine/core-modules/graphql/hooks/use-validate-graphql-query-complexity.hook'; import { type I18nService } from 'src/engine/core-modules/i18n/i18n.service'; import { type MetricsService } from 'src/engine/core-modules/metrics/metrics.service'; @@ -40,7 +39,6 @@ export const metadataModuleFactory = async ( }, resolvers: { JSON: GraphQLJSON }, plugins: [ - useGraphQLQueryTiming({ featureFlagService }), useGraphQLErrorHandlerHook({ metricsService: metricsService, exceptionHandlerService, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts index 985f6d6214..5a26229e18 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts @@ -12,6 +12,8 @@ import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/work import { WorkspaceResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory'; import { WorkspaceGraphQLSchemaGenerator } from 'src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; +import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service'; +import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type'; import { FlatWorkspace } from 'src/engine/core-modules/workspace/types/flat-workspace.type'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { @@ -38,6 +40,7 @@ export class WorkspaceSchemaFactory { private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly featureFlagService: FeatureFlagService, private readonly dataSourceService: DataSourceService, + private readonly metricsService: MetricsService, ) {} async createGraphQLSchema( @@ -203,6 +206,11 @@ export class WorkspaceSchemaFactory { }, }); + await this.metricsService.incrementCounter({ + key: MetricsKeys.GraphqlSchemaBuild, + shouldStoreInCache: false, + }); + return executableSchema; } diff --git a/packages/twenty-server/src/engine/core-entity-cache/services/core-entity-cache.service.ts b/packages/twenty-server/src/engine/core-entity-cache/services/core-entity-cache.service.ts index fef15e88df..b80383f527 100644 --- a/packages/twenty-server/src/engine/core-entity-cache/services/core-entity-cache.service.ts +++ b/packages/twenty-server/src/engine/core-entity-cache/services/core-entity-cache.service.ts @@ -7,10 +7,6 @@ import { isDefined, isValidUuid } from 'twenty-shared/utils'; import { CoreEntityCacheProvider } from 'src/engine/core-entity-cache/interfaces/core-entity-cache-provider.service'; -import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; -import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; -import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; -import { PromiseMemoizer } from 'src/engine/twenty-orm/storage/promise-memoizer.storage'; import { CORE_ENTITY_CACHE_KEY } from 'src/engine/core-entity-cache/decorators/core-entity-cache.decorator'; import { CORE_ENTITY_CACHE_KEYS, @@ -18,6 +14,10 @@ import { type CoreEntityCacheDataMap, } from 'src/engine/core-entity-cache/types/core-entity-cache-key.type'; import { type CoreEntityLocalCacheEntry } from 'src/engine/core-entity-cache/types/core-entity-local-cache-entry.type'; +import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; +import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; +import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; +import { PromiseMemoizer } from 'src/engine/twenty-orm/storage/promise-memoizer.storage'; const LOCAL_TTL_MS = 100; // 100ms const LOCAL_ENTRY_TTL_MS = 30 * 60 * 1000; // 30 minutes @@ -76,6 +76,36 @@ export class CoreEntityCacheService implements OnModuleInit { this.coreEntityCacheProviders.set(coreEntityCacheKeyName, instance); } } + + setInterval(() => { + const stats = this.getStats(); + + this.logger.log( + `Cache stats: localCacheSize=${stats.localCacheSize} memoizerCacheSize=${stats.memoizerCacheSize} memoizerPendingSize=${stats.memoizerPendingSize} entriesByKey=${JSON.stringify(stats.entriesByKey)} versionsByKey=${JSON.stringify(stats.versionsByKey)}`, + ); + }, 60_000).unref(); + } + + public getStats() { + const memoizerSize = this.memoizer.getSize(); + const entriesByKey: Record = {}; + const versionsByKey: Record = {}; + + for (const [key, entry] of this.localCache.entries()) { + const prefix = key.substring(0, key.lastIndexOf(':')); + + entriesByKey[prefix] = (entriesByKey[prefix] ?? 0) + 1; + versionsByKey[prefix] = + (versionsByKey[prefix] ?? 0) + entry.versions.size; + } + + return { + localCacheSize: this.localCache.size, + memoizerCacheSize: memoizerSize.cache, + memoizerPendingSize: memoizerSize.pending, + entriesByKey, + versionsByKey, + }; } public async get( diff --git a/packages/twenty-server/src/engine/core-modules/graphql/hooks/use-graphql-query-timing.hook.ts b/packages/twenty-server/src/engine/core-modules/graphql/hooks/use-graphql-query-timing.hook.ts deleted file mode 100644 index 810aea2034..0000000000 --- a/packages/twenty-server/src/engine/core-modules/graphql/hooks/use-graphql-query-timing.hook.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Logger } from '@nestjs/common'; - -import { type Plugin } from '@envelop/core'; -import { Kind, type OperationDefinitionNode } from 'graphql'; -import { FeatureFlagKey } from 'twenty-shared/types'; - -import { type GraphQLContext } from 'src/engine/api/graphql/graphql-config/interfaces/graphql-context.interface'; -import { type FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; -import { queryTimingContextStorage } from 'src/engine/core-modules/graphql/storage/query-timing-context.storage'; - -type GraphQLQueryTimingOptions = { - featureFlagService: FeatureFlagService; -}; - -const logger = new Logger('GraphQLQueryTiming'); - -export const useGraphQLQueryTiming = ( - options: GraphQLQueryTimingOptions, -): Plugin => { - return { - async onExecute({ args, executeFn, setExecuteFn }) { - const workspaceId = args.contextValue.req?.workspace?.id; - - if (!workspaceId) { - return; - } - - const isEnabled = await options.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_GRAPHQL_QUERY_TIMING_ENABLED, - workspaceId, - ); - - if (!isEnabled) { - return; - } - - const rootOperation = args.document.definitions.find( - (definition: { kind: string }) => - definition.kind === Kind.OPERATION_DEFINITION, - ) as OperationDefinitionNode | undefined; - - const operationName = - args.operationName || rootOperation?.name?.value || 'Anonymous'; - - const operationType = rootOperation?.operation ?? 'unknown'; - const startTime = performance.now(); - - setExecuteFn((executeArgs) => - queryTimingContextStorage.run(true, () => executeFn(executeArgs)), - ); - - return { - onExecuteDone() { - const durationMs = (performance.now() - startTime).toFixed(2); - - logger.log( - `[${operationType}] ${operationName} — ${durationMs}ms (workspace: ${workspaceId})`, - ); - }, - }; - }, - }; -}; diff --git a/packages/twenty-server/src/engine/core-modules/metrics/types/metrics-keys.type.ts b/packages/twenty-server/src/engine/core-modules/metrics/types/metrics-keys.type.ts index f36c11c4aa..15b9c66d2c 100644 --- a/packages/twenty-server/src/engine/core-modules/metrics/types/metrics-keys.type.ts +++ b/packages/twenty-server/src/engine/core-modules/metrics/types/metrics-keys.type.ts @@ -34,4 +34,5 @@ export enum MetricsKeys { JobCompleted = 'job/completed', JobFailed = 'job/failed', JobWaiting = 'job/waiting', + GraphqlSchemaBuild = 'graphql/schema-build', } diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts index 1eb1911811..d0c4d5c664 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts @@ -244,7 +244,6 @@ describe('WorkspaceEntityManager', () => { IS_DIRECT_GRAPHQL_EXECUTION_ENABLED: false, IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED: false, IS_CONNECTED_ACCOUNT_MIGRATED: false, - IS_GRAPHQL_QUERY_TIMING_ENABLED: false, IS_RECORD_TABLE_WIDGET_ENABLED: false, IS_DATASOURCE_MIGRATED: false, IS_COMMAND_MENU_ITEM_ENABLED: false, diff --git a/packages/twenty-server/src/engine/twenty-orm/storage/promise-memoizer.storage.ts b/packages/twenty-server/src/engine/twenty-orm/storage/promise-memoizer.storage.ts index f0533b4929..456727b422 100644 --- a/packages/twenty-server/src/engine/twenty-orm/storage/promise-memoizer.storage.ts +++ b/packages/twenty-server/src/engine/twenty-orm/storage/promise-memoizer.storage.ts @@ -95,6 +95,10 @@ export class PromiseMemoizer { } } + getSize(): { cache: number; pending: number } { + return { cache: this.cache.size, pending: this.pending.size }; + } + async clearAll(onDelete?: (value: T) => Promise | void): Promise { for (const [, entry] of this.cache.entries()) { await onDelete?.(entry.value); diff --git a/packages/twenty-server/src/engine/workspace-cache/services/workspace-cache.service.ts b/packages/twenty-server/src/engine/workspace-cache/services/workspace-cache.service.ts index d67246f4a7..220293b1ed 100644 --- a/packages/twenty-server/src/engine/workspace-cache/services/workspace-cache.service.ts +++ b/packages/twenty-server/src/engine/workspace-cache/services/workspace-cache.service.ts @@ -94,6 +94,36 @@ export class WorkspaceCacheService implements OnModuleInit { } } } + + setInterval(() => { + const stats = this.getStats(); + + this.logger.log( + `Cache stats: localCacheSize=${stats.localCacheSize} memoizerCacheSize=${stats.memoizerCacheSize} memoizerPendingSize=${stats.memoizerPendingSize} entriesByKey=${JSON.stringify(stats.entriesByKey)} versionsByKey=${JSON.stringify(stats.versionsByKey)}`, + ); + }, 60_000).unref(); + } + + public getStats() { + const memoizerSize = this.memoizer.getSize(); + const entriesByKey: Record = {}; + const versionsByKey: Record = {}; + + for (const [key, entry] of this.localCache.entries()) { + const prefix = key.substring(0, key.lastIndexOf(':')); + + entriesByKey[prefix] = (entriesByKey[prefix] ?? 0) + 1; + versionsByKey[prefix] = + (versionsByKey[prefix] ?? 0) + entry.versions.size; + } + + return { + localCacheSize: this.localCache.size, + memoizerCacheSize: memoizerSize.cache, + memoizerPendingSize: memoizerSize.pending, + entriesByKey, + versionsByKey, + }; } public async getOrRecompute( diff --git a/packages/twenty-shared/src/types/FeatureFlagKey.ts b/packages/twenty-shared/src/types/FeatureFlagKey.ts index 1d14cb4366..a1d9b3286e 100644 --- a/packages/twenty-shared/src/types/FeatureFlagKey.ts +++ b/packages/twenty-shared/src/types/FeatureFlagKey.ts @@ -14,7 +14,6 @@ export enum FeatureFlagKey { IS_DIRECT_GRAPHQL_EXECUTION_ENABLED = 'IS_DIRECT_GRAPHQL_EXECUTION_ENABLED', IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED = 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED', IS_CONNECTED_ACCOUNT_MIGRATED = 'IS_CONNECTED_ACCOUNT_MIGRATED', - IS_GRAPHQL_QUERY_TIMING_ENABLED = 'IS_GRAPHQL_QUERY_TIMING_ENABLED', IS_RECORD_TABLE_WIDGET_ENABLED = 'IS_RECORD_TABLE_WIDGET_ENABLED', IS_DATASOURCE_MIGRATED = 'IS_DATASOURCE_MIGRATED', }