Fix performances on view loading (#14859)

## Context

We are experiencing bad performance on Twenty. One of the root cause
hypothesis is that computing `currentUser.currentWorkspace.views` is CPU
consuming. Without views, the GetCurrentUser response is ~1000 lines.
With its ~10000 lines.

As graphql is going through all fields recursively this can be quite
heavy on CPU. We had a similar issues on ObjectMetadataItems 2 years ago
and came with storing the response in redis.

Note: I thought there was also a cache in RAM but this is not the case,
so to invalidate the cache we can just empty redis.

## How

- Extract getting all views from GetCurrentUser and update frontend to
perform both queries
- Add views to cached graphql operations
- invalidate the cache manually on view or related core entities update
/ create / delete / destroy

## Tests

I have tested a lot on v1
This commit is contained in:
Charles Bochet
2025-10-02 22:53:03 +02:00
committed by GitHub
parent 87256e82f5
commit 5dcd0607b3
26 changed files with 385 additions and 151 deletions
@@ -107,7 +107,7 @@ export class CacheStorageService {
do {
const result = await redisClient.scan(cursor, {
MATCH: scanPattern,
MATCH: `${this.namespace}:${scanPattern}`,
COUNT: 100,
});
@@ -53,6 +53,7 @@ describe('ViewFieldService', () => {
provide: ViewService,
useValue: {
findByIdWithRelatedObjectMetadata: jest.fn(),
flushGraphQLCache: jest.fn(),
},
},
],
@@ -13,6 +13,7 @@ import {
generateViewFilterGroupUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-filter-group.exception';
import { ViewFilterGroupService } from 'src/engine/core-modules/view/services/view-filter-group.service';
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
describe('ViewFilterGroupService', () => {
let viewFilterGroupService: ViewFilterGroupService;
@@ -44,6 +45,12 @@ describe('ViewFilterGroupService', () => {
delete: jest.fn(),
},
},
{
provide: ViewService,
useValue: {
flushGraphQLCache: jest.fn(),
},
},
],
}).compile();
@@ -13,6 +13,7 @@ import {
generateViewFilterUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-filter.exception';
import { ViewFilterService } from 'src/engine/core-modules/view/services/view-filter.service';
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
describe('ViewFilterService', () => {
let viewFilterService: ViewFilterService;
@@ -35,6 +36,12 @@ describe('ViewFilterService', () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ViewFilterService,
{
provide: ViewService,
useValue: {
flushGraphQLCache: jest.fn(),
},
},
{
provide: getRepositoryToken(ViewFilterEntity),
useValue: {
@@ -12,6 +12,7 @@ import {
generateViewGroupUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-group.exception';
import { ViewGroupService } from 'src/engine/core-modules/view/services/view-group.service';
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
describe('ViewGroupService', () => {
let viewGroupService: ViewGroupService;
@@ -45,6 +46,12 @@ describe('ViewGroupService', () => {
delete: jest.fn(),
},
},
{
provide: ViewService,
useValue: {
flushGraphQLCache: jest.fn(),
},
},
],
}).compile();
@@ -13,6 +13,7 @@ import {
generateViewSortUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-sort.exception';
import { ViewSortService } from 'src/engine/core-modules/view/services/view-sort.service';
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
describe('ViewSortService', () => {
let viewSortService: ViewSortService;
@@ -44,6 +45,12 @@ describe('ViewSortService', () => {
delete: jest.fn(),
},
},
{
provide: ViewService,
useValue: {
flushGraphQLCache: jest.fn(),
},
},
],
}).compile();
@@ -15,6 +15,7 @@ import {
generateViewUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view.exception';
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
describe('ViewService', () => {
let viewService: ViewService;
@@ -56,6 +57,12 @@ describe('ViewService', () => {
delete: jest.fn(),
},
},
{
provide: WorkspaceCacheStorageService,
useValue: {
flushGraphQLOperation: jest.fn(),
},
},
{
provide: I18nService,
useValue: {
@@ -95,6 +95,9 @@ export class ViewFieldService {
);
const savedViewField = await this.viewFieldRepository.save(viewField);
await this.viewService.flushGraphQLCache(viewFieldData.workspaceId);
const createdViewField = await this.findById(
savedViewField.id,
viewFieldData.workspaceId,
@@ -223,6 +226,8 @@ export class ViewFieldService {
...updateData,
});
await this.viewService.flushGraphQLCache(workspaceId);
return { ...existingViewField, ...updatedViewField };
}
@@ -241,6 +246,8 @@ export class ViewFieldService {
await this.viewFieldRepository.softDelete(id);
await this.viewService.flushGraphQLCache(workspaceId);
return viewField;
}
@@ -259,6 +266,8 @@ export class ViewFieldService {
await this.viewFieldRepository.delete(id);
await this.viewService.flushGraphQLCache(workspaceId);
return viewField;
}
@@ -12,12 +12,14 @@ import {
generateViewFilterGroupExceptionMessage,
generateViewFilterGroupUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-filter-group.exception';
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
@Injectable()
export class ViewFilterGroupService {
constructor(
@InjectRepository(ViewFilterGroupEntity)
private readonly viewFilterGroupRepository: Repository<ViewFilterGroupEntity>,
private readonly viewService: ViewService,
) {}
async findByWorkspaceId(
@@ -118,7 +120,14 @@ export class ViewFilterGroupService {
const viewFilterGroup =
this.viewFilterGroupRepository.create(viewFilterGroupData);
return this.viewFilterGroupRepository.save(viewFilterGroup);
await this.viewService.flushGraphQLCache(viewFilterGroupData.workspaceId);
const savedViewFilterGroup =
await this.viewFilterGroupRepository.save(viewFilterGroup);
await this.viewService.flushGraphQLCache(viewFilterGroupData.workspaceId);
return savedViewFilterGroup;
}
async update(
@@ -143,6 +152,8 @@ export class ViewFilterGroupService {
...updateData,
});
await this.viewService.flushGraphQLCache(workspaceId);
return { ...existingViewFilterGroup, ...updatedViewFilterGroup };
}
@@ -182,6 +193,8 @@ export class ViewFilterGroupService {
await this.viewFilterGroupRepository.delete(id);
await this.viewService.flushGraphQLCache(workspaceId);
return true;
}
}
@@ -12,12 +12,14 @@ import {
generateViewFilterExceptionMessage,
generateViewFilterUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-filter.exception';
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
@Injectable()
export class ViewFilterService {
constructor(
@InjectRepository(ViewFilterEntity)
private readonly viewFilterRepository: Repository<ViewFilterEntity>,
private readonly viewService: ViewService,
) {}
async findByWorkspaceId(workspaceId: string): Promise<ViewFilterEntity[]> {
@@ -109,7 +111,13 @@ export class ViewFilterService {
const viewFilter = this.viewFilterRepository.create(viewFilterData);
return this.viewFilterRepository.save(viewFilter);
await this.viewService.flushGraphQLCache(viewFilterData.workspaceId);
const savedViewFilter = await this.viewFilterRepository.save(viewFilter);
await this.viewService.flushGraphQLCache(viewFilterData.workspaceId);
return savedViewFilter;
}
async update(
@@ -134,6 +142,8 @@ export class ViewFilterService {
...updateData,
});
await this.viewService.flushGraphQLCache(workspaceId);
return { ...existingViewFilter, ...updatedViewFilter };
}
@@ -152,6 +162,8 @@ export class ViewFilterService {
await this.viewFilterRepository.softDelete(id);
await this.viewService.flushGraphQLCache(workspaceId);
return viewFilter;
}
@@ -170,6 +182,8 @@ export class ViewFilterService {
await this.viewFilterRepository.delete(id);
await this.viewService.flushGraphQLCache(workspaceId);
return true;
}
}
@@ -12,12 +12,14 @@ import {
generateViewGroupExceptionMessage,
generateViewGroupUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-group.exception';
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
@Injectable()
export class ViewGroupService {
constructor(
@InjectRepository(ViewGroupEntity)
private readonly viewGroupRepository: Repository<ViewGroupEntity>,
private readonly viewService: ViewService,
) {}
async findByWorkspaceId(workspaceId: string): Promise<ViewGroupEntity[]> {
@@ -109,7 +111,13 @@ export class ViewGroupService {
const viewGroup = this.viewGroupRepository.create(viewGroupData);
return this.viewGroupRepository.save(viewGroup);
await this.viewService.flushGraphQLCache(viewGroupData.workspaceId);
const savedViewGroup = await this.viewGroupRepository.save(viewGroup);
await this.viewService.flushGraphQLCache(viewGroupData.workspaceId);
return savedViewGroup;
}
async update(
@@ -134,6 +142,8 @@ export class ViewGroupService {
...updateData,
});
await this.viewService.flushGraphQLCache(workspaceId);
return { ...existingViewGroup, ...updatedViewGroup };
}
@@ -152,6 +162,8 @@ export class ViewGroupService {
await this.viewGroupRepository.softDelete(id);
await this.viewService.flushGraphQLCache(workspaceId);
return viewGroup;
}
@@ -170,6 +182,8 @@ export class ViewGroupService {
await this.viewGroupRepository.delete(id);
await this.viewService.flushGraphQLCache(workspaceId);
return true;
}
}
@@ -12,12 +12,14 @@ import {
generateViewSortExceptionMessage,
generateViewSortUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-sort.exception';
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
@Injectable()
export class ViewSortService {
constructor(
@InjectRepository(ViewSortEntity)
private readonly viewSortRepository: Repository<ViewSortEntity>,
private readonly viewService: ViewService,
) {}
async findByWorkspaceId(workspaceId: string): Promise<ViewSortEntity[]> {
@@ -105,7 +107,11 @@ export class ViewSortService {
const viewSort = this.viewSortRepository.create(viewSortData);
return this.viewSortRepository.save(viewSort);
const savedViewSort = await this.viewSortRepository.save(viewSort);
await this.viewService.flushGraphQLCache(viewSortData.workspaceId);
return savedViewSort;
}
async update(
@@ -130,6 +136,8 @@ export class ViewSortService {
...updateData,
});
await this.viewService.flushGraphQLCache(workspaceId);
return { ...existingViewSort, ...updatedViewSort };
}
@@ -148,6 +156,8 @@ export class ViewSortService {
await this.viewSortRepository.softDelete(id);
await this.viewService.flushGraphQLCache(workspaceId);
return viewSort;
}
@@ -166,6 +176,8 @@ export class ViewSortService {
await this.viewSortRepository.delete(id);
await this.viewService.flushGraphQLCache(workspaceId);
return true;
}
}
@@ -15,6 +15,7 @@ import {
generateViewExceptionMessage,
generateViewUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view.exception';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
@Injectable()
export class ViewService {
@@ -22,6 +23,7 @@ export class ViewService {
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
private readonly i18nService: I18nService,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
) {}
async findByWorkspaceId(workspaceId: string): Promise<ViewEntity[]> {
@@ -134,7 +136,11 @@ export class ViewService {
isCustom: true,
});
return this.viewRepository.save(view);
const savedView = await this.viewRepository.save(view);
await this.flushGraphQLCache(viewData.workspaceId);
return savedView;
}
async update(
@@ -159,6 +165,8 @@ export class ViewService {
...updateData,
});
await this.flushGraphQLCache(workspaceId);
return { ...existingView, ...updatedView };
}
@@ -177,6 +185,8 @@ export class ViewService {
await this.viewRepository.softDelete(id);
await this.flushGraphQLCache(workspaceId);
return view;
}
@@ -194,6 +204,7 @@ export class ViewService {
}
await this.viewRepository.delete(id);
await this.flushGraphQLCache(workspaceId);
return true;
}
@@ -235,4 +246,11 @@ export class ViewService {
return viewName;
}
async flushGraphQLCache(workspaceId: string): Promise<void> {
await this.workspaceCacheStorageService.flushGraphQLOperation({
operationName: 'FindAllCoreViews',
workspaceId,
});
}
}