From 56ea79d98c9d4ec4c263094ace7d70a5dfbb1873 Mon Sep 17 00:00:00 2001 From: Etienne <45695613+etiennejouan@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:41:15 +0100 Subject: [PATCH] Perf investigation - Time qgl query (#18911) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added a new IS_GRAPHQL_QUERY_TIMING_ENABLED feature flag that, when activated per workspace, logs the execution time and operation name of every GraphQL query across both the standard Yoga pipeline and the direct execution fast path. The timing context propagates via AsyncLocalStorage to also instrument buildColumnsToSelect and formatResult — giving a breakdown of column selection and result transformation costs without any caller changes. --- .../src/generated-metadata/graphql.ts | 1 + .../clients/generated/metadata/schema.graphql | 1 + .../src/clients/generated/metadata/schema.ts | 5 +- .../hooks/use-direct-execution.hook.ts | 43 +++++++++++-- .../graphql-config/graphql-config.service.ts | 4 ++ .../utils/build-columns-to-select.ts | 20 +++++- .../graphql/metadata-graphql-api.module.ts | 2 + .../api/graphql/metadata.module-factory.ts | 4 ++ .../hooks/use-graphql-query-timing.hook.ts | 63 +++++++++++++++++++ .../storage/query-timing-context.storage.ts | 6 ++ .../workspace-entity-manager.spec.ts | 1 + .../twenty-orm/utils/format-result.util.ts | 41 +++++++++++- .../twenty-shared/src/types/FeatureFlagKey.ts | 1 + 13 files changed, 181 insertions(+), 11 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/graphql/hooks/use-graphql-query-timing.hook.ts create mode 100644 packages/twenty-server/src/engine/core-modules/graphql/storage/query-timing-context.storage.ts diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 3cb4f3cfcc..07a98063fd 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1708,6 +1708,7 @@ 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-sdk/src/clients/generated/metadata/schema.graphql b/packages/twenty-sdk/src/clients/generated/metadata/schema.graphql index a2604a295d..f86e936f62 100644 --- a/packages/twenty-sdk/src/clients/generated/metadata/schema.graphql +++ b/packages/twenty-sdk/src/clients/generated/metadata/schema.graphql @@ -1559,6 +1559,7 @@ enum FeatureFlagKey { IS_DIRECT_GRAPHQL_EXECUTION_ENABLED IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED IS_CONNECTED_ACCOUNT_MIGRATED + IS_GRAPHQL_QUERY_TIMING_ENABLED } type SSOIdentityProvider { diff --git a/packages/twenty-sdk/src/clients/generated/metadata/schema.ts b/packages/twenty-sdk/src/clients/generated/metadata/schema.ts index fe3df37128..0925f45145 100644 --- a/packages/twenty-sdk/src/clients/generated/metadata/schema.ts +++ b/packages/twenty-sdk/src/clients/generated/metadata/schema.ts @@ -1256,7 +1256,7 @@ export interface FeatureFlag { __typename: 'FeatureFlag' } -export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_AI_ENABLED' | 'IS_APPLICATION_ENABLED' | 'IS_MARKETPLACE_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_DASHBOARD_V2_ENABLED' | 'IS_ATTACHMENT_MIGRATED' | 'IS_NOTE_TARGET_MIGRATED' | 'IS_TASK_TARGET_MIGRATED' | 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_ENABLED' | 'IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_EDITING_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' +export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_AI_ENABLED' | 'IS_APPLICATION_ENABLED' | 'IS_MARKETPLACE_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_DASHBOARD_V2_ENABLED' | 'IS_ATTACHMENT_MIGRATED' | 'IS_NOTE_TARGET_MIGRATED' | 'IS_TASK_TARGET_MIGRATED' | 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_ENABLED' | 'IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_EDITING_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' export interface SSOIdentityProvider { id: Scalars['UUID'] @@ -9081,7 +9081,8 @@ export const enumFeatureFlagKey = { IS_RICH_TEXT_V1_MIGRATED: 'IS_RICH_TEXT_V1_MIGRATED' as const, 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_CONNECTED_ACCOUNT_MIGRATED: 'IS_CONNECTED_ACCOUNT_MIGRATED' as const, + IS_GRAPHQL_QUERY_TIMING_ENABLED: 'IS_GRAPHQL_QUERY_TIMING_ENABLED' as const } export const enumRelationType = { 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 b4a719396a..4c52be6bc1 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,3 +1,5 @@ +import { Logger } from '@nestjs/common'; + import { type Request } from 'express'; import { DocumentNode, parse } from 'graphql'; import { type Plugin } from 'graphql-yoga'; @@ -5,12 +7,15 @@ 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; @@ -27,12 +32,13 @@ export function useDirectExecution( return; } - const isEnabled = await config.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_DIRECT_GRAPHQL_EXECUTION_ENABLED, - req.workspace.id, - ); + const isDirectExecutionEnabled = + await config.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_DIRECT_GRAPHQL_EXECUTION_ENABLED, + req.workspace.id, + ); - if (!isEnabled) { + if (!isDirectExecutionEnabled) { return; } @@ -83,6 +89,33 @@ 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 9df77aad62..e1a6c6baf6 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 @@ -27,6 +27,7 @@ import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/service import { useSentryTracing } from 'src/engine/core-modules/exception-handler/hooks/use-sentry-tracing'; 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'; @@ -61,6 +62,9 @@ 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/graphql-query-runner/utils/build-columns-to-select.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select.ts index 0381a85189..b6005b41ea 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select.ts @@ -1,6 +1,9 @@ +import { Logger } from '@nestjs/common'; + import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; +import { isQueryTimingEnabled } from 'src/engine/core-modules/graphql/storage/query-timing-context.storage'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; @@ -10,6 +13,8 @@ import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-m import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +const logger = new Logger('buildColumnsToSelect'); + export const buildColumnsToSelect = ({ select, relations, @@ -23,6 +28,9 @@ export const buildColumnsToSelect = ({ flatObjectMetadataMaps: FlatEntityMaps; flatFieldMetadataMaps: FlatEntityMaps; }) => { + const timingEnabled = isQueryTimingEnabled(); + const startTime = timingEnabled ? performance.now() : 0; + const requiredRelationColumns = getRequiredRelationColumns( relations, flatObjectMetadata, @@ -40,7 +48,17 @@ export const buildColumnsToSelect = ({ fieldsToSelect[columnName] = true; } - return { ...fieldsToSelect, id: true }; + const result = { ...fieldsToSelect, id: true }; + + if (timingEnabled) { + const durationMs = (performance.now() - startTime).toFixed(2); + + logger.log( + `${flatObjectMetadata.nameSingular} — ${durationMs}ms (${Object.keys(select).length} select, ${flatObjectMetadata.fieldIds.length} fields)`, + ); + } + + return result; }; const getRequiredRelationColumns = ( diff --git a/packages/twenty-server/src/engine/api/graphql/metadata-graphql-api.module.ts b/packages/twenty-server/src/engine/api/graphql/metadata-graphql-api.module.ts index dbecd9faf7..5bce582544 100644 --- a/packages/twenty-server/src/engine/api/graphql/metadata-graphql-api.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/metadata-graphql-api.module.ts @@ -7,6 +7,7 @@ import { GraphQLConfigModule } from 'src/engine/api/graphql/graphql-config/graph import { metadataModuleFactory } from 'src/engine/api/graphql/metadata.module-factory'; import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { I18nModule } from 'src/engine/core-modules/i18n/i18n.module'; import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module'; @@ -34,6 +35,7 @@ import { MetadataEngineModule } from 'src/engine/metadata-modules/metadata-engin CacheStorageNamespace.EngineWorkspace, MetricsService, I18nService, + FeatureFlagService, ], }), MetadataEngineModule, 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 d9dfc1f526..53c85f4b99 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 @@ -8,8 +8,10 @@ import { MetadataGraphQLApiModule } from 'src/engine/api/graphql/metadata-graphq 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 { 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'; @@ -24,6 +26,7 @@ export const metadataModuleFactory = async ( cacheStorageService: CacheStorageService, metricsService: MetricsService, i18nService: I18nService, + featureFlagService: FeatureFlagService, ): Promise => { const config: YogaDriverConfig = { autoSchemaFile: true, @@ -37,6 +40,7 @@ export const metadataModuleFactory = async ( }, resolvers: { JSON: GraphQLJSON }, plugins: [ + useGraphQLQueryTiming({ featureFlagService }), useGraphQLErrorHandlerHook({ metricsService: metricsService, exceptionHandlerService, 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 new file mode 100644 index 0000000000..810aea2034 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/graphql/hooks/use-graphql-query-timing.hook.ts @@ -0,0 +1,63 @@ +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/graphql/storage/query-timing-context.storage.ts b/packages/twenty-server/src/engine/core-modules/graphql/storage/query-timing-context.storage.ts new file mode 100644 index 0000000000..6b4c5045ca --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/graphql/storage/query-timing-context.storage.ts @@ -0,0 +1,6 @@ +import { AsyncLocalStorage } from 'async_hooks'; + +export const queryTimingContextStorage = new AsyncLocalStorage(); + +export const isQueryTimingEnabled = (): boolean => + queryTimingContextStorage.getStore() === true; 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 0b6e647c91..c114f66c56 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 @@ -250,6 +250,7 @@ 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, }, userWorkspaceRoleMap: {}, eventEmitterService: { diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts index bb213c86a2..8e08761274 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts @@ -1,3 +1,4 @@ +import { Logger } from '@nestjs/common'; import { isPlainObject } from '@nestjs/common/utils/shared.utils'; import { isNonEmptyString, isNull } from '@sniptt/guards'; @@ -26,6 +27,10 @@ import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object import { getCompositeFieldMetadataCollection } from 'src/engine/twenty-orm/utils/get-composite-field-metadata-collection'; import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; +import { isQueryTimingEnabled } from 'src/engine/core-modules/graphql/storage/query-timing-context.storage'; + +const formatResultLogger = new Logger('formatResult'); + export function formatResult( // oxlint-disable-next-line @typescripttypescript/no-explicit-any data: any, @@ -33,6 +38,36 @@ export function formatResult( flatObjectMetadataMaps: FlatEntityMaps, flatFieldMetadataMaps: FlatEntityMaps, fieldMapsForObject?: FieldMapsForObject, +): T { + const timingEnabled = isQueryTimingEnabled(); + const startTime = timingEnabled ? performance.now() : 0; + const result = formatResultInternal( + data, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + fieldMapsForObject, + ); + + if (timingEnabled && isDefined(flatObjectMetadata)) { + const durationMs = (performance.now() - startTime).toFixed(2); + const recordCount = Array.isArray(data) ? data.length : 1; + + formatResultLogger.log( + `${flatObjectMetadata.nameSingular} — ${durationMs}ms (${recordCount} records)`, + ); + } + + return result; +} + +function formatResultInternal( + // oxlint-disable-next-line @typescripttypescript/no-explicit-any + data: any, + flatObjectMetadata: FlatObjectMetadata | undefined, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, + fieldMapsForObject?: FieldMapsForObject, ): T { if (!isDefined(data)) { return data; @@ -41,7 +76,7 @@ export function formatResult( if (!isPlainObject(data)) { if (Array.isArray(data)) { return data.map((item) => - formatResult( + formatResultInternal( item, flatObjectMetadata, flatObjectMetadataMaps, @@ -93,7 +128,7 @@ export function formatResult( if (!compositePropertyArgs && !isRelation) { if (isPlainObject(value)) { // @ts-expect-error legacy noImplicitAny - newData[key] = formatResult( + newData[key] = formatResultInternal( value, flatObjectMetadata, flatObjectMetadataMaps, @@ -130,7 +165,7 @@ export function formatResult( } // @ts-expect-error legacy noImplicitAny - newData[key] = formatResult( + newData[key] = formatResultInternal( value, targetObjectMetadata, flatObjectMetadataMaps, diff --git a/packages/twenty-shared/src/types/FeatureFlagKey.ts b/packages/twenty-shared/src/types/FeatureFlagKey.ts index cd6aa908b4..ef526afd64 100644 --- a/packages/twenty-shared/src/types/FeatureFlagKey.ts +++ b/packages/twenty-shared/src/types/FeatureFlagKey.ts @@ -23,4 +23,5 @@ 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', }