Skip to content

Optimize EntityMetadata caching in GlobalWorkspaceDataSource#16146

Merged
Weiko merged 1 commit intomainfrom
c--improve-global-workspace-datasource-perf
Nov 28, 2025
Merged

Optimize EntityMetadata caching in GlobalWorkspaceDataSource#16146
Weiko merged 1 commit intomainfrom
c--improve-global-workspace-datasource-perf

Conversation

@Weiko
Copy link
Copy Markdown
Member

@Weiko Weiko commented Nov 27, 2025

Context

EntityMetadata was being rebuilt from scratch on every findMetadata()/getMetadata() call (~20 times per request). This involved running EntitySchemaTransformer.transform() and EntityMetadataBuilder.build() repeatedly, causing unnecessary CPU overhead.

Implementation

Cache entityMetadatas in ORMWorkspaceContext: Build EntityMetadata once during workspace context initialization instead of on every metadata lookup
Remove redundant entitySchemas caching: Since flatMetadata is already cached, the additional Redis cache for entitySchemaOptions was unnecessary overhead
Remove WorkspaceEntitiesStorage: Replaced with direct lookup from FlatObjectMetadataMap
Simplify getObjectMetadataFromEntityTarget: Now only accepts string targets, using flat metadata maps directly

Also:
Removed unused injections in some services

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Nov 27, 2025

Greptile Overview

Greptile Summary

This PR significantly improves performance by caching EntityMetadata objects at workspace context initialization instead of rebuilding them on every metadata lookup call. Previously, findMetadata() and getMetadata() were called ~20 times per request, each time triggering expensive EntitySchemaTransformer.transform() and EntityMetadataBuilder.build() operations.

Key improvements:

  • Caching strategy shift: entityMetadatas array is now built once during loadWorkspaceContext() and stored in ORMWorkspaceContext, eliminating redundant transformations
  • Redis cache removal: Removed unnecessary Redis caching layer for entitySchemaOptions since flatMetadata is already cached upstream
  • Code simplification: Removed WorkspaceEntitiesStorage class and simplified getObjectMetadataFromEntityTarget to only accept string targets
  • Cleanup: Removed unused service injections across multiple files

The implementation correctly uses AsyncLocalStorage for context isolation, ensuring no cross-request contamination. The metadata building logic has been consolidated in GlobalWorkspaceOrmManager where it executes once per workspace context initialization.

Confidence Score: 5/5

  • This PR is safe to merge with high confidence - it's a well-architected performance optimization that reduces CPU overhead without changing behavior
  • The changes demonstrate strong architectural understanding: entityMetadata building has been moved to initialization rather than on-demand, leveraging existing AsyncLocalStorage for proper request isolation. The removal of redundant Redis caching is justified since flatMetadata is already cached. All changes are backward compatible, with proper error handling maintained. The cleanup of unused injections follows best practices.
  • No files require special attention

Important Files Changed

File Analysis

Filename Score Overview
packages/twenty-server/src/engine/twenty-orm/storage/orm-workspace-context.storage.ts 5/5 New file that replaces workspace-context.storage.ts, stores entityMetadatas array instead of entitySchemas for caching optimization
packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.ts 5/5 Simplified findMetadata/getMetadata to directly use cached entityMetadatas, removed buildMetadatasFromSchemas method
packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager.ts 5/5 Moved entityMetadata building to initialization, removed Redis caching for entitySchemas, builds entityMetadatas once during context load
packages/twenty-server/src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util.ts 5/5 Simplified to only accept string entity targets, removed EntitySchema support and WorkspaceEntitiesStorage dependency

Sequence Diagram

sequenceDiagram
    participant Request
    participant GlobalWorkspaceOrmManager
    participant Cache as Various Cache Services
    participant EntitySchemaFactory
    participant TypeORM as TypeORM Builders
    participant Context as ORMWorkspaceContext
    participant DataSource as GlobalWorkspaceDataSource

    Request->>GlobalWorkspaceOrmManager: executeInWorkspaceContext(authContext)
    GlobalWorkspaceOrmManager->>GlobalWorkspaceOrmManager: loadWorkspaceContext(authContext)
    
    GlobalWorkspaceOrmManager->>Cache: getOrRecomputeManyOrAllFlatEntityMaps()
    Cache-->>GlobalWorkspaceOrmManager: flatObjectMetadataMaps, flatFieldMetadataMaps, flatIndexMaps
    
    GlobalWorkspaceOrmManager->>Cache: getWorkspaceFeatureFlagsMapAndVersion()
    Cache-->>GlobalWorkspaceOrmManager: featureFlagsMap
    
    GlobalWorkspaceOrmManager->>Cache: getRolesPermissionsFromCache()
    Cache-->>GlobalWorkspaceOrmManager: permissionsPerRoleId
    
    GlobalWorkspaceOrmManager->>GlobalWorkspaceOrmManager: buildEntitySchemas()
    loop For each flatObjectMetadata
        GlobalWorkspaceOrmManager->>EntitySchemaFactory: create(workspaceId, flatObjectMetadata)
        EntitySchemaFactory-->>GlobalWorkspaceOrmManager: EntitySchema
    end
    
    GlobalWorkspaceOrmManager->>GlobalWorkspaceOrmManager: buildEntityMetadatas(entitySchemas)
    GlobalWorkspaceOrmManager->>TypeORM: EntitySchemaTransformer.transform(entitySchemas)
    TypeORM-->>GlobalWorkspaceOrmManager: metadataArgsStorage
    GlobalWorkspaceOrmManager->>TypeORM: EntityMetadataBuilder.build()
    TypeORM-->>GlobalWorkspaceOrmManager: entityMetadatas[]
    
    GlobalWorkspaceOrmManager->>Context: Store in ORMWorkspaceContext
    Note over Context: entityMetadatas cached here<br/>for entire request lifecycle
    
    GlobalWorkspaceOrmManager->>GlobalWorkspaceOrmManager: withWorkspaceContext(context, fn)
    
    loop Multiple times per request (~20x)
        Request->>DataSource: findMetadata(target) or getMetadata(target)
        DataSource->>Context: getWorkspaceContext()
        Context-->>DataSource: { entityMetadatas }
        DataSource->>DataSource: entityMetadatas.find(metadata => metadata.target === target)
        DataSource-->>Request: EntityMetadata (cached)
        Note over DataSource: No rebuild needed!<br/>Direct array lookup
    end
Loading

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

11 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

@github-actions
Copy link
Copy Markdown
Contributor

🚀 Preview Environment Ready!

Your preview environment is available at: http://bore.pub:14435

This environment will automatically shut down when the PR is closed or after 5 hours.

entityTarget: EntityTarget<T>,
internalContext: WorkspaceInternalContext,
): FlatObjectMetadata => {
const objectMetadataName =
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've double checked, it seems this is never reached as an entityTarget but always as a string. I'm keeping the signature as it is for now and throwing to catch early if I'm wrong but otherwise this should simplify this part where WorkspaceEntitiesStorage can be completely deprecated

@Weiko Weiko merged commit 9620a4b into main Nov 28, 2025
55 of 60 checks passed
@Weiko Weiko deleted the c--improve-global-workspace-datasource-perf branch November 28, 2025 09:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants