Skip to content

Commit cfb1489

Browse files
perf: batch DB operations in createAttributesScenario to avoid CI timeout (calcom#28320)
* perf: batch DB operations in performance test setup to avoid CI timeout Replace ~5200 sequential DB inserts with batch createMany() operations in createHugeAttributesOfTypeSingleSelect. The test was timing out at 60s on 2-vCPU CI runners because the setup phase alone (creating 400 users, 800 memberships, 4000 attribute assignments one-by-one) exceeded the timeout before the actual logic under test even ran. Co-Authored-By: romitgabani1 <romitgabani1.work@gmail.com> * perf: optimize createAttributesScenario with batch DB operations Move batch DB operations (createMany) into the shared createAttributesScenario helper so all tests benefit, not just the performance test. Simplify createHugeAttributesOfTypeSingleSelect to delegate to the optimized helper. Co-Authored-By: romitgabani1 <romitgabani1.work@gmail.com> * Revert "perf: optimize createAttributesScenario with batch DB operations" This reverts commit c739edd. * Revert "perf: batch DB operations in performance test setup to avoid CI timeout" This reverts commit ff6642a. * perf: optimize createAttributesScenario with batch DB operations Replace sequential prisma.*.create() calls in the shared createAttributesScenario helper with batch createMany() calls, reducing ~5,200 sequential DB operations to ~10 batch operations. Simplify createHugeAttributesOfTypeSingleSelect to delegate to the optimized helper instead of duplicating batch logic. Co-Authored-By: romitgabani1 <romitgabani1.work@gmail.com> * fix: preserve deterministic user ordering in createAttributesScenario Map users by email after findMany to ensure the index-based attribute assignment matches the original userDataList order, since findMany does not guarantee return order. Co-Authored-By: romitgabani1 <romitgabani1.work@gmail.com> * revert: make test timeout back to 1 min * refactor: extract batch helpers createTestUsers, createTestMembershipsForUsers, createTestAttributeAssignments Organize batch DB operations from createAttributesScenario into reusable helper functions mirroring the existing single-record helpers. Co-Authored-By: romitgabani1 <romitgabani1.work@gmail.com> * cleanup: remove unused single-record helpers and JSDoc comments Remove createTestUser, createTestMemberships, createTestAttributeAssignment (now replaced by batch versions). Remove added comments. Co-Authored-By: romitgabani1 <romitgabani1.work@gmail.com> * chore --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent cc01d11 commit cfb1489

1 file changed

Lines changed: 115 additions & 78 deletions

File tree

packages/features/routing-forms/lib/findTeamMembersMatchingAttributeLogic.integration-test.ts

Lines changed: 115 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -53,42 +53,6 @@ const createTestTeam = async (overrides: { parentId: number }) => {
5353
return team;
5454
};
5555

56-
const createTestUser = async (overrides?: { email?: string; username?: string }) => {
57-
const timestamp = Date.now() + Math.random();
58-
const user = await prisma.user.create({
59-
data: {
60-
email: overrides?.email ?? `test-user-${timestamp}@example.com`,
61-
username: overrides?.username ?? `test-user-${timestamp}`,
62-
},
63-
});
64-
createdResources.users.push(user.id);
65-
return user;
66-
};
67-
68-
const createTestMemberships = async (params: { userId: number; orgId: number; teamId: number }) => {
69-
const orgMembership = await prisma.membership.create({
70-
data: {
71-
userId: params.userId,
72-
teamId: params.orgId,
73-
role: "MEMBER",
74-
accepted: true,
75-
},
76-
});
77-
createdResources.memberships.push(orgMembership.id);
78-
79-
const teamMembership = await prisma.membership.create({
80-
data: {
81-
userId: params.userId,
82-
teamId: params.teamId,
83-
role: "MEMBER",
84-
accepted: true,
85-
},
86-
});
87-
createdResources.memberships.push(teamMembership.id);
88-
89-
return { orgMembership, teamMembership };
90-
};
91-
9256
const createTestAttribute = async (params: {
9357
orgId: number;
9458
id: string;
@@ -122,18 +86,98 @@ const createTestAttribute = async (params: {
12286
return attribute;
12387
};
12488

125-
const createTestAttributeAssignment = async (params: {
126-
orgMembershipId: number;
127-
attributeOptionId: string;
89+
const createTestUsers = async (userDataList: { email: string; username: string }[]) => {
90+
await prisma.user.createMany({ data: userDataList });
91+
92+
const users = await prisma.user.findMany({
93+
where: { email: { in: userDataList.map((u) => u.email) } },
94+
select: { id: true, email: true },
95+
});
96+
97+
const emailToUser = new Map(users.map((u) => [u.email, u]));
98+
const orderedUsers = userDataList.map((ud) => emailToUser.get(ud.email)!);
99+
100+
createdResources.users.push(...orderedUsers.map((u) => u.id));
101+
return orderedUsers;
102+
};
103+
104+
const createTestMembershipsForUsers = async (params: {
105+
userIds: number[];
106+
orgId: number;
107+
teamId: number;
128108
}) => {
129-
const assignment = await prisma.attributeToUser.create({
130-
data: {
131-
memberId: params.orgMembershipId,
132-
attributeOptionId: params.attributeOptionId,
133-
},
109+
const { userIds, orgId, teamId } = params;
110+
111+
await prisma.membership.createMany({
112+
data: userIds.map((userId) => ({
113+
userId,
114+
teamId: orgId,
115+
role: "MEMBER" as const,
116+
accepted: true,
117+
})),
118+
});
119+
120+
const orgMemberships = await prisma.membership.findMany({
121+
where: { userId: { in: userIds }, teamId: orgId },
122+
select: { id: true, userId: true },
123+
});
124+
125+
createdResources.memberships.push(...orgMemberships.map((m) => m.id));
126+
127+
await prisma.membership.createMany({
128+
data: userIds.map((userId) => ({
129+
userId,
130+
teamId,
131+
role: "MEMBER" as const,
132+
accepted: true,
133+
})),
134134
});
135-
createdResources.attributeToUsers.push(assignment.id);
136-
return assignment;
135+
136+
const teamMemberships = await prisma.membership.findMany({
137+
where: { userId: { in: userIds }, teamId },
138+
select: { id: true },
139+
});
140+
141+
createdResources.memberships.push(...teamMemberships.map((m) => m.id));
142+
143+
const userIdToOrgMembershipId = new Map(orgMemberships.map((m) => [m.userId, m.id]));
144+
return { userIdToOrgMembershipId };
145+
};
146+
147+
const createTestAttributeAssignments = async (params: {
148+
orderedUserIds: number[];
149+
userIdToOrgMembershipId: Map<number, number>;
150+
membersAttributes: Record<string, string | string[]>[];
151+
optionValueToId: Map<string, string>;
152+
}) => {
153+
const { orderedUserIds, userIdToOrgMembershipId, membersAttributes, optionValueToId } = params;
154+
155+
const assignments: { memberId: number; attributeOptionId: string }[] = [];
156+
for (let i = 0; i < orderedUserIds.length; i++) {
157+
const orgMembershipId = userIdToOrgMembershipId.get(orderedUserIds[i]);
158+
if (!orgMembershipId) continue;
159+
for (const [attrId, value] of Object.entries(membersAttributes[i])) {
160+
const values = Array.isArray(value) ? value : [value];
161+
for (const v of values) {
162+
const optionId = optionValueToId.get(`${attrId}:${v}`);
163+
if (optionId) {
164+
assignments.push({ memberId: orgMembershipId, attributeOptionId: optionId });
165+
}
166+
}
167+
}
168+
}
169+
170+
if (assignments.length > 0) {
171+
await prisma.attributeToUser.createMany({ data: assignments });
172+
173+
const orgMembershipIds = Array.from(new Set(assignments.map((a) => a.memberId)));
174+
const created = await prisma.attributeToUser.findMany({
175+
where: { memberId: { in: orgMembershipIds } },
176+
select: { id: true },
177+
});
178+
179+
createdResources.attributeToUsers.push(...created.map((a) => a.id));
180+
}
137181
};
138182

139183
async function createAttributesScenario(params: {
@@ -167,40 +211,33 @@ async function createAttributesScenario(params: {
167211
}
168212
}
169213

170-
const createdUsers: { userId: number; orgMembershipId: number }[] = [];
171-
172-
for (const member of teamMembersWithAttributeOptionValuePerAttribute) {
173-
let userId: number;
174-
if (member.userId) {
175-
userId = member.userId;
176-
const user = await createTestUser();
177-
userId = user.id;
178-
} else {
179-
const user = await createTestUser();
180-
userId = user.id;
181-
}
214+
const numMembers = teamMembersWithAttributeOptionValuePerAttribute.length;
215+
const timestamp = Date.now();
182216

183-
const { orgMembership } = await createTestMemberships({
184-
userId,
185-
orgId: testFixtures.org.id,
186-
teamId: testFixtures.team.id,
187-
});
217+
const userDataList = Array.from({ length: numMembers }, (_, i) => ({
218+
email: `test-user-${timestamp}-${i}@example.com`,
219+
username: `test-user-${timestamp}-${i}`,
220+
}));
188221

189-
for (const [attrId, value] of Object.entries(member.attributes)) {
190-
const values = Array.isArray(value) ? value : [value];
191-
for (const v of values) {
192-
const optionId = optionValueToId.get(`${attrId}:${v}`);
193-
if (optionId) {
194-
await createTestAttributeAssignment({
195-
orgMembershipId: orgMembership.id,
196-
attributeOptionId: optionId,
197-
});
198-
}
199-
}
200-
}
222+
const orderedUsers = await createTestUsers(userDataList);
201223

202-
createdUsers.push({ userId, orgMembershipId: orgMembership.id });
203-
}
224+
const { userIdToOrgMembershipId } = await createTestMembershipsForUsers({
225+
userIds: orderedUsers.map((u) => u.id),
226+
orgId: testFixtures.org.id,
227+
teamId: testFixtures.team.id,
228+
});
229+
230+
await createTestAttributeAssignments({
231+
orderedUserIds: orderedUsers.map((u) => u.id),
232+
userIdToOrgMembershipId,
233+
membersAttributes: teamMembersWithAttributeOptionValuePerAttribute.map((m) => m.attributes),
234+
optionValueToId,
235+
});
236+
237+
const createdUsers = orderedUsers.map((user) => ({
238+
userId: user.id,
239+
orgMembershipId: userIdToOrgMembershipId.get(user.id)!,
240+
}));
204241

205242
return { createdAttributes, createdUsers };
206243
}
@@ -232,7 +269,7 @@ async function createHugeAttributesOfTypeSingleSelect(params: {
232269

233270
const assignedAttributeOptionIdForEachMember = 1;
234271

235-
const teamMembersWithAttributeOptionValuePerAttribute = Array.from({ length: numTeamMembers }, (_, i) => ({
272+
const teamMembersWithAttributeOptionValuePerAttribute = Array.from({ length: numTeamMembers }, (_, _i) => ({
236273
attributes: Object.fromEntries(
237274
Array.from({ length: numAttributesUsedPerTeamMember }, (_, j) => [
238275
attributes[j].id,

0 commit comments

Comments
 (0)