Skip to content

Commit bdd77ed

Browse files
authored
perf: table domain s cache (#2105)
* perf: table domain s cache * perf: optimize link recursive sql perf
1 parent 20c0afd commit bdd77ed

File tree

11 files changed

+992
-309
lines changed

11 files changed

+992
-309
lines changed

apps/nestjs-backend/src/features/field/field-calculate/field-calculate.module.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
22
import { DbProvider } from '../../../db-provider/db.provider';
33
import { CalculationModule } from '../../calculation/calculation.module';
44
import { CollaboratorModule } from '../../collaborator/collaborator.module';
5+
import { ComputedModule } from '../../record/computed/computed.module';
56
import { TableIndexService } from '../../table/table-index.service';
67
import { TableDomainQueryModule } from '../../table-domain';
78
import { ViewModule } from '../../view/view.module';
@@ -16,7 +17,14 @@ import { FormulaFieldService } from './formula-field.service';
1617
import { LinkFieldQueryService } from './link-field-query.service';
1718

1819
@Module({
19-
imports: [FieldModule, CalculationModule, ViewModule, CollaboratorModule, TableDomainQueryModule],
20+
imports: [
21+
FieldModule,
22+
CalculationModule,
23+
ViewModule,
24+
CollaboratorModule,
25+
TableDomainQueryModule,
26+
ComputedModule,
27+
],
2028
providers: [
2129
DbProvider,
2230
FieldDeletingService,

apps/nestjs-backend/src/features/field/field-calculate/field-converting.service.ts

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import type { IOpsMap } from '../../calculation/utils/compose-maps';
4141
import { composeOpMaps } from '../../calculation/utils/compose-maps';
4242
import { isLinkCellValue } from '../../calculation/utils/detect-link';
4343
import { CollaboratorService } from '../../collaborator/collaborator.service';
44+
import { ComputedOrchestratorService } from '../../record/computed/services/computed-orchestrator.service';
4445
import { TableIndexService } from '../../table/table-index.service';
4546
import { FieldService } from '../field.service';
4647
import type { IFieldInstance, IFieldMap } from '../model/factory';
@@ -71,6 +72,7 @@ export class FieldConvertingService {
7172
private readonly fieldCalculationService: FieldCalculationService,
7273
private readonly collaboratorService: CollaboratorService,
7374
private readonly tableIndexService: TableIndexService,
75+
private readonly computedOrchestrator: ComputedOrchestratorService,
7476
@InjectModel('CUSTOM_KNEX') private readonly knex: Knex
7577
) {}
7678

@@ -955,6 +957,37 @@ export class FieldConvertingService {
955957
};
956958
}
957959

960+
private buildCellContextsFromOps(opsMap: IOpsMap[string] | undefined) {
961+
const contexts: ICellContext[] = [];
962+
if (!opsMap) {
963+
return contexts;
964+
}
965+
for (const [recordId, ops] of Object.entries(opsMap)) {
966+
for (const op of ops) {
967+
const context = RecordOpBuilder.editor.setRecord.detect(op);
968+
if (!context) {
969+
continue;
970+
}
971+
contexts.push({
972+
recordId,
973+
fieldId: context.fieldId,
974+
oldValue: context.oldCellValue,
975+
newValue: context.newCellValue,
976+
});
977+
}
978+
}
979+
return contexts;
980+
}
981+
982+
private buildComputedSources(recordOpsMap: IOpsMap) {
983+
return Object.entries(recordOpsMap)
984+
.map(([tableId, ops]) => ({
985+
tableId,
986+
cellContexts: this.buildCellContextsFromOps(ops),
987+
}))
988+
.filter((source) => source.cellContexts.length);
989+
}
990+
958991
// eslint-disable-next-line sonarjs/cognitive-complexity
959992
private async calculateAndSaveRecords(
960993
tableId: string,
@@ -985,7 +1018,17 @@ export class FieldConvertingService {
9851018
}
9861019
}
9871020

988-
await this.batchService.updateRecords(recordOpsMap);
1021+
const computedSources = this.buildComputedSources(recordOpsMap);
1022+
if (computedSources.length) {
1023+
await this.computedOrchestrator.computeCellChangesForRecordsMulti(
1024+
computedSources,
1025+
async () => {
1026+
await this.batchService.updateRecords(recordOpsMap);
1027+
}
1028+
);
1029+
} else {
1030+
await this.batchService.updateRecords(recordOpsMap);
1031+
}
9891032
}
9901033

9911034
private async getExistRecords(tableId: string, newField: IFieldInstance) {
@@ -1295,7 +1338,35 @@ export class FieldConvertingService {
12951338
return false;
12961339
}
12971340

1298-
return majorFieldKeysChanged(oldField, newField);
1341+
if (majorFieldKeysChanged(oldField, newField)) {
1342+
return true;
1343+
}
1344+
1345+
if (this.hasConditionalLookupDiff(newField, oldField)) {
1346+
return true;
1347+
}
1348+
1349+
if (this.hasConditionalRollupDiff(newField, oldField)) {
1350+
return true;
1351+
}
1352+
1353+
return false;
1354+
}
1355+
1356+
private hasConditionalLookupDiff(newField: IFieldInstance, oldField: IFieldInstance) {
1357+
if (!newField.isConditionalLookup) {
1358+
return false;
1359+
}
1360+
1361+
return !isEqual(newField.lookupOptions, oldField.lookupOptions);
1362+
}
1363+
1364+
private hasConditionalRollupDiff(newField: IFieldInstance, oldField: IFieldInstance) {
1365+
if (newField.type !== FieldType.ConditionalRollup) {
1366+
return false;
1367+
}
1368+
1369+
return !isEqual(newField.options, oldField.options);
12991370
}
13001371

13011372
private async calculateField(
@@ -1307,7 +1378,11 @@ export class FieldConvertingService {
13071378
return;
13081379
}
13091380

1310-
if (!majorFieldKeysChanged(oldField, newField)) {
1381+
const hasMajorChange = majorFieldKeysChanged(oldField, newField);
1382+
const conditionalLookupDiff = this.hasConditionalLookupDiff(newField, oldField);
1383+
const conditionalRollupDiff = this.hasConditionalRollupDiff(newField, oldField);
1384+
1385+
if (!hasMajorChange && !conditionalLookupDiff && !conditionalRollupDiff) {
13111386
return;
13121387
}
13131388

apps/nestjs-backend/src/features/field/open-api/field-open-api.service.ts

Lines changed: 91 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -947,6 +947,7 @@ export class FieldOpenApiService {
947947
oldField,
948948
modifiedOps,
949949
supplementChange,
950+
dependentFieldIds,
950951
}: {
951952
tableId: string;
952953
newField: IFieldInstance;
@@ -957,6 +958,7 @@ export class FieldOpenApiService {
957958
newField: IFieldInstance;
958959
oldField: IFieldInstance;
959960
};
961+
dependentFieldIds?: string[];
960962
}): Promise<{ compatibilityIssue: boolean }> {
961963
let encounteredCompatibilityIssue = false;
962964

@@ -983,19 +985,47 @@ export class FieldOpenApiService {
983985
}
984986
};
985987

988+
const sourceMap = new Map<string, Set<string>>();
989+
const shouldRecomputeSelf = this.fieldConvertingService.needCalculate(newField, oldField);
990+
const addSource = (tid: string, fieldIds: string[]) => {
991+
const set = sourceMap.get(tid) ?? new Set<string>();
992+
fieldIds.forEach((id) => set.add(id));
993+
sourceMap.set(tid, set);
994+
};
995+
996+
if (shouldRecomputeSelf) {
997+
addSource(tableId, [newField.id]);
998+
}
999+
1000+
if (dependentFieldIds?.length) {
1001+
const dependentFields = await this.prismaService.field.findMany({
1002+
where: { id: { in: dependentFieldIds }, deletedTime: null },
1003+
select: { id: true, tableId: true },
1004+
});
1005+
dependentFields
1006+
.filter(
1007+
({ id, tableId: depTableId }) =>
1008+
shouldRecomputeSelf || id !== newField.id || depTableId !== tableId
1009+
)
1010+
.forEach(({ id, tableId: depTableId }) => addSource(depTableId, [id]));
1011+
}
1012+
1013+
if (supplementChange) {
1014+
addSource(supplementChange.tableId, [supplementChange.newField.id]);
1015+
}
1016+
1017+
const sources = Array.from(sourceMap.entries()).map(([tid, ids]) => ({
1018+
tableId: tid,
1019+
fieldIds: Array.from(ids),
1020+
}));
1021+
const hasSources = sources.length > 0;
1022+
9861023
// 1. stage close constraint
9871024
await this.fieldConvertingService.closeConstraint(tableId, newField, oldField);
9881025

9891026
// 2. stage alter + apply record changes and calculate field with computed publishing (atomic)
9901027
await this.prismaService.$tx(
9911028
async () => {
992-
const sources = [{ tableId, fieldIds: [newField.id] }];
993-
if (supplementChange)
994-
sources.push({
995-
tableId: supplementChange.tableId,
996-
fieldIds: [supplementChange.newField.id],
997-
});
998-
9991029
const runCompute = async () => {
10001030
// Update dependencies and schema first so evaluate() sees new schema
10011031
await this.fieldViewSyncService.convertDependenciesByFieldIds(
@@ -1027,14 +1057,19 @@ export class FieldOpenApiService {
10271057
}
10281058
};
10291059

1030-
try {
1031-
await this.computedOrchestrator.computeCellChangesForFields(sources, runCompute);
1032-
} catch (error) {
1033-
if (this.isFieldReferenceCompatibilityError(error)) {
1034-
encounteredCompatibilityIssue = true;
1035-
return;
1060+
if (hasSources) {
1061+
try {
1062+
await this.computedOrchestrator.computeCellChangesForFields(sources, runCompute);
1063+
} catch (error) {
1064+
if (this.isFieldReferenceCompatibilityError(error)) {
1065+
encounteredCompatibilityIssue = true;
1066+
return;
1067+
}
1068+
1069+
throw error;
10361070
}
1037-
throw error;
1071+
} else {
1072+
await runCompute();
10381073
}
10391074
},
10401075
{ timeout: this.thresholdConfig.bigTransactionTimeout }
@@ -1086,12 +1121,26 @@ export class FieldOpenApiService {
10861121
const { newField, oldField, modifiedOps, supplementChange, references } =
10871122
await this.fieldConvertingService.stageAnalysis(tableId, fieldId, updateFieldRo);
10881123

1124+
const dependentRefs = await this.prismaService.reference.findMany({
1125+
where: { fromFieldId: fieldId },
1126+
select: { toFieldId: true },
1127+
});
1128+
const dependentFieldIds = Array.from(
1129+
new Set([...(references ?? []), ...dependentRefs.map((ref) => ref.toFieldId)])
1130+
);
1131+
1132+
const shouldRecomputeSelf = this.fieldConvertingService.needCalculate(newField, oldField);
1133+
const filteredDependentFieldIds = shouldRecomputeSelf
1134+
? dependentFieldIds
1135+
: dependentFieldIds.filter((id) => id !== newField.id);
1136+
10891137
const { compatibilityIssue } = await this.performConvertField({
10901138
tableId,
10911139
newField,
10921140
oldField,
10931141
modifiedOps,
10941142
supplementChange,
1143+
dependentFieldIds: filteredDependentFieldIds,
10951144
});
10961145

10971146
const shouldForceLookupError =
@@ -1102,20 +1151,37 @@ export class FieldOpenApiService {
11021151
((newField.options as ILinkFieldOptions | undefined)?.foreignTableId ?? null) !==
11031152
((oldField.options as ILinkFieldOptions | undefined)?.foreignTableId ?? null));
11041153

1105-
const dependentRefs = await this.prismaService.reference.findMany({
1106-
where: { fromFieldId: fieldId },
1107-
select: { toFieldId: true },
1108-
});
1109-
const dependentFieldIds = Array.from(
1110-
new Set([...(references ?? []), ...dependentRefs.map((ref) => ref.toFieldId)])
1111-
);
1112-
1113-
if (dependentFieldIds.length) {
1154+
if (filteredDependentFieldIds.length) {
11141155
try {
1115-
await this.restoreReference(dependentFieldIds);
1156+
await this.restoreReference(filteredDependentFieldIds);
11161157
const dependentFieldRaws = await this.prismaService.field.findMany({
1117-
where: { id: { in: dependentFieldIds }, deletedTime: null },
1158+
where: { id: { in: filteredDependentFieldIds }, deletedTime: null },
11181159
});
1160+
1161+
if (dependentFieldRaws.length) {
1162+
const dependentSourceMap = dependentFieldRaws.reduce<Record<string, Set<string>>>(
1163+
(acc, field) => {
1164+
const set = acc[field.tableId] ?? new Set<string>();
1165+
set.add(field.id);
1166+
acc[field.tableId] = set;
1167+
return acc;
1168+
},
1169+
{}
1170+
);
1171+
const dependentSources = Object.entries(dependentSourceMap).map(([tid, ids]) => ({
1172+
tableId: tid,
1173+
fieldIds: Array.from(ids),
1174+
}));
1175+
if (dependentSources.length) {
1176+
await this.computedOrchestrator.computeCellChangesForFields(
1177+
dependentSources,
1178+
async () => {
1179+
// schema/meta already up to date; nothing additional to run here
1180+
}
1181+
);
1182+
}
1183+
}
1184+
11191185
for (const raw of dependentFieldRaws) {
11201186
const instance = createFieldInstanceByRaw(raw);
11211187
const isValid = await this.isFieldConfigurationValid(raw.tableId, instance);

0 commit comments

Comments
 (0)