From 49b673ae722c2b31ccb451ac7250f86a2c9a7182 Mon Sep 17 00:00:00 2001 From: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Date: Thu, 17 Oct 2024 17:32:35 +0300 Subject: [PATCH 1/2] chore: increase pagination managed users endpoint (#17147) --- packages/platform/types/api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/platform/types/api.ts b/packages/platform/types/api.ts index c870936b0bc39c..bf1a8262515790 100644 --- a/packages/platform/types/api.ts +++ b/packages/platform/types/api.ts @@ -43,7 +43,7 @@ export class Pagination { @Transform(({ value }: { value: string }) => value && parseInt(value)) @IsNumber() @Min(1) - @Max(100) + @Max(250) @IsOptional() limit?: number; @@ -51,7 +51,7 @@ export class Pagination { @ApiProperty({ required: false, description: "The number of items to skip", example: 0 }) @IsNumber() @Min(0) - @Max(100) + @Max(250) @IsOptional() offset?: number | null; } From f160bcda2f6bb582a4eecb87a22ff348c82037b4 Mon Sep 17 00:00:00 2001 From: Hariom Date: Fri, 18 Oct 2024 14:21:33 +0530 Subject: [PATCH 2/2] Add server-timings and used concurrency 2 for teamMembers matching --- .../routing-forms/lib/evaluateRaqbLogic.ts | 37 ++- .../trpc/__tests__/utils.test.ts | 165 ++++++++---- ...amMembersMatchingAttributeLogic.handler.ts | 49 +++- ...eamMembersMatchingAttributeLogic.schema.ts | 2 + .../app-store/routing-forms/trpc/raqbUtils.ts | 1 - .../routing-forms/trpc/response.handler.ts | 4 +- .../app-store/routing-forms/trpc/utils.ts | 249 ++++++++++++------ 7 files changed, 362 insertions(+), 145 deletions(-) diff --git a/packages/app-store/routing-forms/lib/evaluateRaqbLogic.ts b/packages/app-store/routing-forms/lib/evaluateRaqbLogic.ts index 3e48a3cfccce8f..28329143d483ec 100644 --- a/packages/app-store/routing-forms/lib/evaluateRaqbLogic.ts +++ b/packages/app-store/routing-forms/lib/evaluateRaqbLogic.ts @@ -12,17 +12,27 @@ export const enum RaqbLogicResult { LOGIC_NOT_FOUND_SO_MATCHED = "LOGIC_NOT_FOUND_SO_MATCHED", } -export const evaluateRaqbLogic = ({ - queryValue, - queryBuilderConfig, - data, - beStrictWithEmptyLogic = false, -}: { - queryValue: JsonTree; - queryBuilderConfig: any; - data: Record; - beStrictWithEmptyLogic?: boolean; -}): RaqbLogicResult => { +export const evaluateRaqbLogic = ( + { + queryValue, + queryBuilderConfig, + data, + beStrictWithEmptyLogic = false, + }: { + queryValue: JsonTree; + queryBuilderConfig: any; + data: Record; + beStrictWithEmptyLogic?: boolean; + }, + config: { + // 2 - Error/Warning + // 1 - Info + // 0 - Debug + logLevel: 0 | 1 | 2; + } = { + logLevel: 1, + } +): RaqbLogicResult => { const state = { tree: QbUtils.checkTree(QbUtils.loadTree(queryValue), queryBuilderConfig), config: queryBuilderConfig, @@ -40,7 +50,10 @@ export const evaluateRaqbLogic = ({ // If no logic is provided, then consider it a match return RaqbLogicResult.LOGIC_NOT_FOUND_SO_MATCHED; } - console.log("Checking logic with data", safeStringify({ logic, data })); + + if (config.logLevel >= 1) { + console.log("Checking logic with data", safeStringify({ logic, data })); + } // eslint-disable-next-line @typescript-eslint/no-explicit-any return !!jsonLogic.apply(logic as any, data) ? RaqbLogicResult.MATCH : RaqbLogicResult.NO_MATCH; }; diff --git a/packages/app-store/routing-forms/trpc/__tests__/utils.test.ts b/packages/app-store/routing-forms/trpc/__tests__/utils.test.ts index cd11301010d025..093eaa8dae6b76 100644 --- a/packages/app-store/routing-forms/trpc/__tests__/utils.test.ts +++ b/packages/app-store/routing-forms/trpc/__tests__/utils.test.ts @@ -1,6 +1,7 @@ import type { BaseWidget } from "react-awesome-query-builder"; import { describe, it, expect, vi, beforeEach } from "vitest"; +import logger from "@calcom/lib/logger"; import type { AttributeType } from "@calcom/prisma/enums"; import { RoutingFormFieldType } from "../../lib/FieldTypes"; @@ -31,6 +32,55 @@ function mockAttributesScenario({ ); } +function mockHugeAttributesOfTypeSingleSelect({ + numAttributes, + numOptionsPerAttribute, + numTeamMembers, + numAttributesUsedPerTeamMember, +}: { + numAttributes: number; + numOptionsPerAttribute: number; + numTeamMembers: number; + numAttributesUsedPerTeamMember: number; +}) { + if (numAttributesUsedPerTeamMember > numAttributes) { + throw new Error("numAttributesUsedPerTeamMember cannot be greater than numAttributes"); + } + const attributes = Array.from({ length: numAttributes }, (_, i) => ({ + id: `attr${i + 1}`, + name: `Attribute ${i + 1}`, + type: "SINGLE_SELECT" as const, + slug: `attribute-${i + 1}`, + options: Array.from({ length: numOptionsPerAttribute }, (_, i) => ({ + id: `opt${i + 1}`, + value: `Option ${i + 1}`, + slug: `option-${i + 1}`, + })), + })); + + const assignedAttributeOptionIdForEachMember = 1; + + const teamMembersWithAttributeOptionValuePerAttribute = Array.from({ length: numTeamMembers }, (_, i) => ({ + userId: i + 1, + attributes: Object.fromEntries( + Array.from({ length: numAttributesUsedPerTeamMember }, (_, j) => [ + attributes[j].id, + attributes[j].options[assignedAttributeOptionIdForEachMember].value, + ]) + ), + })); + + mockAttributesScenario({ + attributes, + teamMembersWithAttributeOptionValuePerAttribute, + }); + + return { + attributes, + teamMembersWithAttributeOptionValuePerAttribute, + }; +} + function buildQueryValue({ rules, }: { @@ -126,7 +176,7 @@ describe("findTeamMembersMatchingAttributeLogicOfRoute", () => { }); it("should return null if route is not found", async () => { - const result = await findTeamMembersMatchingAttributeLogicOfRoute({ + const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogicOfRoute({ form: { routes: [], fields: [] }, response: {}, routeId: "non-existent-route", @@ -137,7 +187,7 @@ describe("findTeamMembersMatchingAttributeLogicOfRoute", () => { }); it("should return null if the route does not have an attributesQueryValue set", async () => { - const result = await findTeamMembersMatchingAttributeLogicOfRoute({ + const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogicOfRoute({ form: { routes: [ { @@ -183,7 +233,7 @@ describe("findTeamMembersMatchingAttributeLogicOfRoute", () => { ], }) as AttributesQueryValue; - const result = await findTeamMembersMatchingAttributeLogicOfRoute({ + const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogicOfRoute({ form: { routes: [ { @@ -251,7 +301,7 @@ describe("findTeamMembersMatchingAttributeLogicOfRoute", () => { ], }) as AttributesQueryValue; - const result = await findTeamMembersMatchingAttributeLogicOfRoute({ + const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogicOfRoute({ form: { routes: [ buildDefaultCustomPageRoute({ @@ -286,50 +336,69 @@ describe("findTeamMembersMatchingAttributeLogicOfRoute", () => { ]); }); - describe("Error handling", () => { - it("should throw an error if the attribute type is not supported", async () => { - const Option1OfAttribute1 = { id: "opt1", value: "Option 1", slug: "option-1" }; - const Attribute1 = { - id: "attr1", - name: "Attribute 1", - type: "UNSUPPORTED_ATTRIBUTE_TYPE" as unknown as AttributeType, - slug: "attribute-1", - options: [Option1OfAttribute1], - }; - mockAttributesScenario({ - attributes: [Attribute1], - teamMembersWithAttributeOptionValuePerAttribute: [ - { - userId: 1, - attributes: { [Attribute1.id]: Option1OfAttribute1.value }, - }, - ], - }); - - await expect( - findTeamMembersMatchingAttributeLogicOfRoute({ - form: { - routes: [ - buildDefaultCustomPageRoute({ - id: "test-route", - attributesQueryValue: buildSelectTypeFieldQueryValue({ - rules: [ - { - raqbFieldId: Attribute1.id, - value: [Option1OfAttribute1.id], - operator: "select_equals", - }, - ], - }) as AttributesQueryValue, - }), - ], - fields: [], - }, - response: {}, - routeId: "test-route", - teamId: 1, - }) - ).rejects.toThrow("Unsupported attribute type"); + describe("Performance testing", () => { + describe("20 attributes, 4000 team members", async () => { + // In tests, the performance is actually really bad than real world. So, skipping this test for now + it.skip("should return matching team members with a SINGLE_SELECT attribute when 'all in' option is selected", async () => { + const { attributes } = mockHugeAttributesOfTypeSingleSelect({ + numAttributes: 20, + numOptionsPerAttribute: 30, + numTeamMembers: 4000, + numAttributesUsedPerTeamMember: 10, + }); + + const attributesQueryValue = buildSelectTypeFieldQueryValue({ + rules: [ + { + raqbFieldId: attributes[0].id, + value: [attributes[0].options[1].id], + operator: "select_equals", + }, + ], + }) as AttributesQueryValue; + + const { teamMembersMatchingAttributeLogic: result, timeTaken } = + await findTeamMembersMatchingAttributeLogicOfRoute( + { + form: { + routes: [ + buildDefaultCustomPageRoute({ + id: "test-route", + attributesQueryValue: attributesQueryValue, + }), + ], + fields: [], + }, + response: {}, + routeId: "test-route", + teamId: 1, + }, + { + concurrency: 1, + enablePerf: true, + } + ); + + expect(result).toEqual( + expect.arrayContaining([ + { + userId: 1, + result: RaqbLogicResult.MATCH, + }, + ]) + ); + + if (!timeTaken) { + throw new Error("Looks like performance testing is not enabled"); + } + const totalTimeTaken = Object.values(timeTaken).reduce((sum, time) => sum ?? 0 + (time || 0), 0); + console.log("Total time taken", totalTimeTaken, { + timeTaken, + }); + expect(totalTimeTaken).toBeLessThan(1000); + // All of them should match + expect(result?.length).toBe(4000); + }, 10000); }); }); }); diff --git a/packages/app-store/routing-forms/trpc/findTeamMembersMatchingAttributeLogic.handler.ts b/packages/app-store/routing-forms/trpc/findTeamMembersMatchingAttributeLogic.handler.ts index 600821440999fc..389e4cf5f6e8a8 100644 --- a/packages/app-store/routing-forms/trpc/findTeamMembersMatchingAttributeLogic.handler.ts +++ b/packages/app-store/routing-forms/trpc/findTeamMembersMatchingAttributeLogic.handler.ts @@ -1,3 +1,6 @@ +import type { ServerResponse } from "http"; +import type { NextApiResponse } from "next"; + import { entityPrismaWhereClause } from "@calcom/lib/entityPermissionUtils"; import { UserRepository } from "@calcom/lib/server/repository/user"; import type { PrismaClient } from "@calcom/prisma"; @@ -12,6 +15,7 @@ interface FindTeamMembersMatchingAttributeLogicHandlerOptions { ctx: { prisma: PrismaClient; user: NonNullable; + res: ServerResponse | NextApiResponse | undefined; }; input: TFindTeamMembersMatchingAttributeLogicInputSchema; } @@ -21,7 +25,7 @@ export const findTeamMembersMatchingAttributeLogicHandler = async ({ input, }: FindTeamMembersMatchingAttributeLogicHandlerOptions) => { const { prisma, user } = ctx; - const { formId, response, routeId } = input; + const { formId, response, routeId, _enablePerf, _concurrency } = input; const form = await prisma.app_RoutingForms_Form.findFirst({ where: { @@ -46,18 +50,32 @@ export const findTeamMembersMatchingAttributeLogicHandler = async ({ const serializableForm = await getSerializableForm({ form }); - const matchingTeamMembersWithResult = await findTeamMembersMatchingAttributeLogicOfRoute({ - response, - routeId, - form: serializableForm, - teamId: form.teamId, - }); + const { + teamMembersMatchingAttributeLogic: matchingTeamMembersWithResult, + timeTaken: teamMembersMatchingAttributeLogicTimeTaken, + } = await findTeamMembersMatchingAttributeLogicOfRoute( + { + response, + routeId, + form: serializableForm, + teamId: form.teamId, + }, + { + enablePerf: _enablePerf, + concurrency: _concurrency, + } + ); if (!matchingTeamMembersWithResult) { return null; } const matchingTeamMembersIds = matchingTeamMembersWithResult.map((member) => member.userId); const matchingTeamMembers = await UserRepository.findByIds({ ids: matchingTeamMembersIds }); + + if (_enablePerf) { + ctx.res?.setHeader("Server-Timing", getServerTimingHeader(teamMembersMatchingAttributeLogicTimeTaken)); + } + return matchingTeamMembers.map((user) => ({ id: user.id, name: user.name, @@ -65,4 +83,21 @@ export const findTeamMembersMatchingAttributeLogicHandler = async ({ })); }; +function getServerTimingHeader(timeTaken: { + gAtr: number | null; + gQryCnfg: number | null; + gMbrWtAtr: number | null; + lgcFrMbrs: number | null; + gQryVal: number | null; +}) { + const headerParts = Object.entries(timeTaken).map(([key, value]) => { + if (value !== null) { + return `${key};dur=${value}`; + } + return null; + }).filter(Boolean); + + return headerParts.join(', '); +} + export default findTeamMembersMatchingAttributeLogicHandler; diff --git a/packages/app-store/routing-forms/trpc/findTeamMembersMatchingAttributeLogic.schema.ts b/packages/app-store/routing-forms/trpc/findTeamMembersMatchingAttributeLogic.schema.ts index c3576490935d41..181ff69b5f8b5d 100644 --- a/packages/app-store/routing-forms/trpc/findTeamMembersMatchingAttributeLogic.schema.ts +++ b/packages/app-store/routing-forms/trpc/findTeamMembersMatchingAttributeLogic.schema.ts @@ -4,6 +4,8 @@ export const ZFindTeamMembersMatchingAttributeLogicInputSchema = z.object({ formId: z.string(), response: z.record(z.string(), z.any()), routeId: z.string(), + _enablePerf: z.boolean().optional(), + _concurrency: z.number().optional(), }); export type TFindTeamMembersMatchingAttributeLogicInputSchema = z.infer< diff --git a/packages/app-store/routing-forms/trpc/raqbUtils.ts b/packages/app-store/routing-forms/trpc/raqbUtils.ts index 796b0804843933..b3f7859385982c 100644 --- a/packages/app-store/routing-forms/trpc/raqbUtils.ts +++ b/packages/app-store/routing-forms/trpc/raqbUtils.ts @@ -231,7 +231,6 @@ function getAttributesQueryValue({ return acc; }, {} as Record); - console.log({ attributesMap }); const attributesQueryValueCompatibleForMatchingWithFormField: AttributesQueryValue = JSON.parse( replaceFieldTemplateVariableWithOptionLabel({ queryValueString: replaceAttributeOptionIdsWithOptionLabel({ diff --git a/packages/app-store/routing-forms/trpc/response.handler.ts b/packages/app-store/routing-forms/trpc/response.handler.ts index f66bc5ce09e5ac..701e60771e70f0 100644 --- a/packages/app-store/routing-forms/trpc/response.handler.ts +++ b/packages/app-store/routing-forms/trpc/response.handler.ts @@ -138,8 +138,8 @@ export const responseHandler = async ({ ctx, input }: ResponseHandlerOptions) => safeStringify({ teamMembersMatchingAttributeLogicWithResult }) ); - const teamMemberIdsMatchingAttributeLogic = teamMembersMatchingAttributeLogicWithResult - ? teamMembersMatchingAttributeLogicWithResult?.map((member) => member.userId) + const teamMemberIdsMatchingAttributeLogic = teamMembersMatchingAttributeLogicWithResult?.teamMembersMatchingAttributeLogic + ? teamMembersMatchingAttributeLogicWithResult.teamMembersMatchingAttributeLogic.map((member) => member.userId) : null; await onFormSubmission( { ...serializableFormWithFields, userWithEmails }, diff --git a/packages/app-store/routing-forms/trpc/utils.ts b/packages/app-store/routing-forms/trpc/utils.ts index 1f768395f35feb..a499e116f59459 100644 --- a/packages/app-store/routing-forms/trpc/utils.ts +++ b/packages/app-store/routing-forms/trpc/utils.ts @@ -1,4 +1,6 @@ import type { App_RoutingForms_Form, User } from "@prisma/client"; +import async from "async"; +import os from "os"; import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks"; import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload"; @@ -26,6 +28,24 @@ const { const moduleLogger = logger.getSubLogger({ prefix: ["routing-forms/trpc/utils"] }); +type SelectFieldWebhookResponse = string | number | string[] | { label: string; id: string | null }; +type FORM_SUBMITTED_WEBHOOK_RESPONSES = Record< + string, + { + /** + * Deprecates `value` prop as it now has both the id(that doesn't change) and the label(that can change but is human friendly) + */ + response: number | string | string[] | SelectFieldWebhookResponse | SelectFieldWebhookResponse[]; + /** + * @deprecated Use `response` instead + */ + value: FormResponse[keyof FormResponse]["value"]; + } +>; +type TeamMemberWithAttributeOptionValuePerAttribute = Awaited< + ReturnType +>[number]; + function isOptionsField(field: Pick) { return (field.type === "select" || field.type === "multiselect") && field.options; } @@ -77,100 +97,179 @@ function getFieldResponse({ }; } -type SelectFieldWebhookResponse = string | number | string[] | { label: string; id: string | null }; -type FORM_SUBMITTED_WEBHOOK_RESPONSES = Record< - string, - { - /** - * Deprecates `value` prop as it now has both the id(that doesn't change) and the label(that can change but is human friendly) - */ - response: number | string | string[] | SelectFieldWebhookResponse | SelectFieldWebhookResponse[]; - /** - * @deprecated Use `response` instead - */ - value: FormResponse[keyof FormResponse]["value"]; - } ->; +/** + * Performance wrapper for async functions + */ +async function asyncPerf(fn: () => Promise): Promise<[ReturnValue, number | null]> { + const start = performance.now(); + const result = await fn(); + const end = performance.now(); + return [result, end - start]; +} -export async function findTeamMembersMatchingAttributeLogicOfRoute({ - form, - response, - routeId, - teamId, -}: { - form: Pick, "routes" | "fields">; - response: FormResponse; - routeId: string; - teamId: number; -}) { +/** + * Performance wrapper for sync functions + */ +function perf(fn: () => ReturnValue): [ReturnValue, number | null] { + const start = performance.now(); + const result = fn(); + const end = performance.now(); + return [result, end - start]; +} + +export async function findTeamMembersMatchingAttributeLogicOfRoute( + { + form, + response, + routeId, + teamId, + }: { + form: Pick, "routes" | "fields">; + response: FormResponse; + routeId: string; + teamId: number; + }, + config: { + enablePerf?: boolean; + concurrency?: number; + } = {} +) { const route = form.routes?.find((route) => route.id === routeId); + + // Higher value of concurrency might not be performant as it might overwhelm the system. So, use a lower value as default. + const { enablePerf = false, concurrency = 2 } = config; + if (!route) { - return null; + return { + teamMembersMatchingAttributeLogic: null, + timeTaken: null, + }; + } + + if (isRouter(route)) { + return { + teamMembersMatchingAttributeLogic: null, + timeTaken: null, + }; } - const teamMembersMatchingAttributeLogic: { - userId: number; - result: RaqbLogicResult; - }[] = []; - if (!isRouter(route)) { - const attributesForTeam = await getAttributesForTeam({ teamId: teamId }); - const attributesQueryValue = getAttributesQueryValue({ + + const teamMembersMatchingAttributeLogicMap = new Map(); + + const [attributesForTeam, getAttributesForTeamTimeTaken] = await aPf( + async () => await getAttributesForTeam({ teamId: teamId }) + ); + + const [attributesQueryValue, getAttributesQueryValueTimeTaken] = pf(() => + getAttributesQueryValue({ attributesQueryValue: route.attributesQueryValue, attributes: attributesForTeam, response, fields: form.fields, getFieldResponse, - }); + }) + ); - if (!attributesQueryValue) { - return null; - } + if (!attributesQueryValue) { + return { + teamMembersMatchingAttributeLogic: null, + timeTaken: { + gAtr: getAttributesForTeamTimeTaken, + gQryVal: getAttributesQueryValueTimeTaken, + gQryCnfg: null, + gMbrWtAtr: null, + lgcFrMbrs: null, + }, + }; + } - const attributesQueryBuilderConfig = getAttributesQueryBuilderConfig({ + const [attributesQueryBuilderConfig, getAttributesQueryBuilderConfigTimeTaken] = pf(() => + getAttributesQueryBuilderConfig({ form, attributes: attributesForTeam, attributesQueryValue, - }); + }) + ); - moduleLogger.debug( - "Finding team members matching attribute logic", - safeStringify({ - form, - response, - routeId, - teamId, - attributesQueryBuilderConfigFields: attributesQueryBuilderConfig.fields, - }) - ); + moduleLogger.debug( + "Finding team members matching attribute logic", + safeStringify({ + form, + response, + routeId, + teamId, + attributesQueryBuilderConfigFields: attributesQueryBuilderConfig.fields, + }) + ); - const teamMembersWithAttributeOptionValuePerAttribute = - await getTeamMembersWithAttributeOptionValuePerAttribute({ teamId: teamId }); - - teamMembersWithAttributeOptionValuePerAttribute.forEach((member, index) => { - const attributesData = getAttributes({ - attributesData: member.attributes, - attributesQueryValue, - }); - moduleLogger.debug( - `Checking team member ${member.userId} with attributes logic`, - safeStringify({ attributes: attributesData, attributesQueryValue }) - ); - const result = evaluateRaqbLogic({ - queryValue: attributesQueryValue, - queryBuilderConfig: attributesQueryBuilderConfig, - data: attributesData, - beStrictWithEmptyLogic: true, - }); - - if (result === RaqbLogicResult.MATCH || result === RaqbLogicResult.LOGIC_NOT_FOUND_SO_MATCHED) { - moduleLogger.debug(`Team member ${member.userId} matches attributes logic`); - teamMembersMatchingAttributeLogic.push({ userId: member.userId, result }); - } else { - moduleLogger.debug(`Team member ${member.userId} does not match attributes logic`); + const [ + teamMembersWithAttributeOptionValuePerAttribute, + getTeamMembersWithAttributeOptionValuePerAttributeTimeTaken, + ] = await aPf(() => getTeamMembersWithAttributeOptionValuePerAttribute({ teamId: teamId })); + + const [_, teamMembersMatchingAttributeLogicTimeTaken] = await aPf(async () => { + return await async.mapLimit>( + teamMembersWithAttributeOptionValuePerAttribute, + concurrency, + async (member: TeamMemberWithAttributeOptionValuePerAttribute) => { + const attributesData = getAttributes({ + attributesData: member.attributes, + attributesQueryValue, + }); + moduleLogger.debug( + `Checking team member ${member.userId} with attributes logic`, + safeStringify({ attributes: attributesData, attributesQueryValue }) + ); + const result = evaluateRaqbLogic( + { + queryValue: attributesQueryValue, + queryBuilderConfig: attributesQueryBuilderConfig, + data: attributesData, + beStrictWithEmptyLogic: true, + }, + { + // This logic runs too many times as it is per team member and we don't want to spam the console with logs. It might also take a performance hit otherwise + logLevel: 2, + } + ); + + if (result === RaqbLogicResult.MATCH || result === RaqbLogicResult.LOGIC_NOT_FOUND_SO_MATCHED) { + moduleLogger.debug(`Team member ${member.userId} matches attributes logic`); + teamMembersMatchingAttributeLogicMap.set(member.userId, result); + } else { + moduleLogger.debug(`Team member ${member.userId} does not match attributes logic`); + return; + } } - }); + ); + }); + + return { + teamMembersMatchingAttributeLogic: Array.from(teamMembersMatchingAttributeLogicMap).map((item) => ({ + userId: item[0], + result: item[1], + })), + timeTaken: { + gAtr: getAttributesForTeamTimeTaken, + gQryCnfg: getAttributesQueryBuilderConfigTimeTaken, + gMbrWtAtr: getTeamMembersWithAttributeOptionValuePerAttributeTimeTaken, + lgcFrMbrs: teamMembersMatchingAttributeLogicTimeTaken, + gQryVal: getAttributesQueryValueTimeTaken, + }, + }; + + function pf(fn: () => ReturnValue): [ReturnValue, number | null] { + if (!enablePerf) { + return [fn(), null]; + } + return perf(fn); } - return teamMembersMatchingAttributeLogic; + async function aPf(fn: () => Promise): Promise<[ReturnValue, number | null]> { + if (!enablePerf) { + return [await fn(), null]; + } + return asyncPerf(fn); + } } export async function onFormSubmission(