perf: Optimize getSlots function to handle large dateRanges arrays efficiently (#21371)
* perf: Optimize getSlots function to handle large dateRanges arrays efficiently Co-Authored-By: keith@cal.com <keithwillcode@gmail.com> * Update packages/lib/slots.test.ts * test: Update performance test to use 2000 date ranges Co-Authored-By: keith@cal.com <keithwillcode@gmail.com> * test: Update performance test to process 2000 date ranges across multiple days Co-Authored-By: keith@cal.com <keithwillcode@gmail.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: keith@cal.com <keithwillcode@gmail.com>
This commit is contained in:
co-authored by
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
keith@cal.com <keithwillcode@gmail.com>
parent
badd9a5698
commit
816df3704e
@@ -350,3 +350,52 @@ describe("Tests the slot logic with custom env variable", () => {
|
||||
).toHaveLength(11);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tests the slots function performance", () => {
|
||||
it("handles hundreds of date ranges efficiently", async () => {
|
||||
const startTime = process.hrtime();
|
||||
|
||||
const startDay = dayjs.utc().add(1, "day").startOf("day");
|
||||
const dateRanges: DateRange[] = [];
|
||||
|
||||
for (let day = 0; day < 7; day++) {
|
||||
const currentDay = startDay.add(day, "day");
|
||||
|
||||
for (let hour = 0; hour < 24; hour++) {
|
||||
for (let minute = 0; minute < 60; minute += 5) {
|
||||
if (dateRanges.length >= 2000) break;
|
||||
|
||||
dateRanges.push({
|
||||
start: currentDay.hour(hour).minute(minute),
|
||||
end: currentDay
|
||||
.hour(hour)
|
||||
.minute(minute + 4)
|
||||
.second(59),
|
||||
});
|
||||
}
|
||||
if (dateRanges.length >= 2000) break;
|
||||
}
|
||||
if (dateRanges.length >= 2000) break;
|
||||
}
|
||||
|
||||
const result = getSlots({
|
||||
inviteeDate: startDay,
|
||||
frequency: 5,
|
||||
minimumBookingNotice: 0,
|
||||
dateRanges: dateRanges,
|
||||
eventLength: 5,
|
||||
offsetStart: 0,
|
||||
});
|
||||
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
|
||||
const endTime = process.hrtime(startTime);
|
||||
const executionTimeInMs = endTime[0] * 1000 + endTime[1] / 1000000;
|
||||
|
||||
expect(executionTimeInMs).toBeLessThan(2000); // less than 2 seconds for 2000 date ranges
|
||||
|
||||
console.log(
|
||||
`Performance test completed in ${executionTimeInMs}ms with ${result.length} slots generated from ${dateRanges.length} date ranges`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+36
-23
@@ -40,6 +40,9 @@ function buildSlotsWithDateRanges({
|
||||
frequency = minimumOfOne(frequency);
|
||||
eventLength = minimumOfOne(eventLength);
|
||||
offsetStart = offsetStart ? minimumOfOne(offsetStart) : 0;
|
||||
|
||||
const orderedDateRanges = dateRanges.sort((a, b) => a.start.valueOf() - b.start.valueOf());
|
||||
|
||||
// there can only ever be one slot at a given start time, and based on duration also only a single length.
|
||||
const slots = new Map<
|
||||
string,
|
||||
@@ -66,7 +69,8 @@ function buildSlotsWithDateRanges({
|
||||
|
||||
const startTimeWithMinNotice = dayjs.utc().add(minimumBookingNotice, "minute");
|
||||
|
||||
const orderedDateRanges = dateRanges.sort((a, b) => a.start.valueOf() - b.start.valueOf());
|
||||
const slotBoundaries = new Map<string, true>();
|
||||
|
||||
orderedDateRanges.forEach((range) => {
|
||||
const dateYYYYMMDD = range.start.format("YYYY-MM-DD");
|
||||
|
||||
@@ -81,32 +85,41 @@ function buildSlotsWithDateRanges({
|
||||
|
||||
slotStartTime = slotStartTime.add(offsetStart ?? 0, "minutes").tz(timeZone);
|
||||
|
||||
// if the slotStartTime is between an existing slot, we need to adjust to the begin of the existing slot
|
||||
// but that adjusted startTime must be legal.
|
||||
const iterator = slots.keys();
|
||||
let result = iterator.next();
|
||||
// Find the nearest appropriate slot boundary if this time falls within an existing slot
|
||||
const slotBoundariesArray = Array.from(slotBoundaries.keys()).map((t) => dayjs(t));
|
||||
if (slotBoundariesArray.length > 0) {
|
||||
slotBoundariesArray.sort((a, b) => a.valueOf() - b.valueOf());
|
||||
|
||||
while (!result.done) {
|
||||
const utcResultValue = dayjs.utc(result.value);
|
||||
// if the slotStartTime is between an existing slot, we need to adjust to the begin of the existing slot
|
||||
if (
|
||||
utcResultValue.isBefore(slotStartTime) &&
|
||||
utcResultValue.add(frequency + (offsetStart ?? 0), "minutes").isAfter(slotStartTime)
|
||||
) {
|
||||
// however, the slot can now be before the start of this date range.
|
||||
if (!utcResultValue.isBefore(range.start)) {
|
||||
// it is between, if possible floor down to the start of the existing slot
|
||||
slotStartTime = utcResultValue;
|
||||
} else {
|
||||
// if not possible to floor, we need to ceil up to the next slot.
|
||||
slotStartTime = utcResultValue.add(frequency + (offsetStart ?? 0), "minutes");
|
||||
let prevBoundary = null;
|
||||
for (let i = slotBoundariesArray.length - 1; i >= 0; i--) {
|
||||
if (slotBoundariesArray[i].isBefore(slotStartTime)) {
|
||||
prevBoundary = slotBoundariesArray[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (prevBoundary) {
|
||||
const prevBoundaryEnd = prevBoundary.add(frequency + (offsetStart ?? 0), "minutes");
|
||||
if (prevBoundaryEnd.isAfter(slotStartTime)) {
|
||||
if (!prevBoundary.isBefore(range.start)) {
|
||||
slotStartTime = prevBoundary;
|
||||
} else {
|
||||
slotStartTime = prevBoundaryEnd;
|
||||
}
|
||||
slotStartTime = slotStartTime.tz(timeZone);
|
||||
}
|
||||
// and then convert to the correct timezone - UTC mode is just for performance.
|
||||
slotStartTime = slotStartTime.tz(timeZone);
|
||||
}
|
||||
result = iterator.next();
|
||||
}
|
||||
|
||||
while (!slotStartTime.add(eventLength, "minutes").subtract(1, "second").utc().isAfter(range.end)) {
|
||||
const slotKey = slotStartTime.toISOString();
|
||||
if (slots.has(slotKey)) {
|
||||
slotStartTime = slotStartTime.add(frequency + (offsetStart ?? 0), "minutes");
|
||||
continue;
|
||||
}
|
||||
|
||||
slotBoundaries.set(slotKey, true);
|
||||
|
||||
const dateOutOfOfficeExists = datesOutOfOffice?.[dateYYYYMMDD];
|
||||
let slotData: {
|
||||
time: Dayjs;
|
||||
@@ -133,7 +146,7 @@ function buildSlotsWithDateRanges({
|
||||
};
|
||||
}
|
||||
|
||||
slots.set(slotData.time.toISOString(), slotData);
|
||||
slots.set(slotKey, slotData);
|
||||
slotStartTime = slotStartTime.add(frequency + (offsetStart ?? 0), "minutes");
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user