Skip to content

Instantly share code, notes, and snippets.

@getify
Last active October 31, 2024 15:49
Show Gist options
  • Save getify/22b977479992988c2b2a606896872e70 to your computer and use it in GitHub Desktop.
Save getify/22b977479992988c2b2a606896872e70 to your computer and use it in GitHub Desktop.
horribly slow date functions -- need help to optimize!!

Overview

I'm in need of help to profile where the slow part(s) of this relative-date util logic is, and find reasonable tweaks or work-arounds to thse bottlenecks.

I'm actually happy to even pay a bit of a bounty ($USD) for someone who can produce an analysis of the actual problem(s) and clear, workable solutions.

Here's an example estimated threshold for improvement that might be helpful (and earn a bounty): the 500-iteration loop above might be taking 200-300ms (on my system), and I need it to be able to run in less than 10ms (which should be completely doable I think, it's not that complex of logic).

IOW, we're going to need at least an order of magnitude improvement, not just 5-10% improvement.

In my testing, the code I've provided here runs in about 85ms in my chrome dev console, and it takes about 300ms in a Node16 program.

Notes

  1. There are zero libraries/dependencies here, and it must stay that way. I'm not going to just use an external date lib (or some huge timezone database, etc). And unfortuantely, Intl the long-hoped-for built-in JS Temporal API (date/time capability upgrades) have still not landed -- seemingly stuck in stage-3.

  2. I deliberately have chosen a fixed timezone of "America/Chicago", which is where I am located, and where my app entirely runs. I don't need or care to support every possible timezone.

  3. That said, even though I'm normalizing to "12:00:00.000" on a day, and only printing the date (not time) here for the purposes of testing, I do actually need to support DST, and stably ensure that every date has that same clock-time.

  4. I'm intentionally detecting the TZ offset dynamically, because unlike normalizing to 12:00:00.000 (as here), the date/time such code might run could be at like 1:03am or at 4:09am, and depending on whether it's before or after the magic DST 2am changeover, there might be hour shifting if I didn't compute the TZ offset for every date.

    Unfortunately, while modern browsers support the timeZoneName: "longOffset" feature (of Intl API), unfortunately Node 16 (which I'm stuck on) does not, so I need the fallback code for computing the tz-offset by counting hours/minutes between two timestamps.

  5. I have another dozen or so date utils like the ones above... and these utils are used many thousands of times across my app, both in Node (v16 -- no, I unfortunately cannot upgrade) and in the browser. I never noticed they were slow until I just recently did some heavier data aggregation (over several thousand records/iterations), and it turns out some of my loops, like a reduce(..) call (which do this kind of relative date math), are absurdly slow, like ~400ms to run a thousand iterations.

    I've narrowed it down that these date functions are the culprit, because when I comment them out (fake hard-code the data), the loops complete in less ~1ms.

var HOST_TZ = "America/Chicago";
var longOffsetSupported = true;
function parseDateParts(date) {
var [ month, day, fullYear, ] = formatLocaleDate(date).split(/[^\d]+/);
return {
fullYear: Number(fullYear),
month: Number(month),
day: Number(day),
};
}
function getDateStr(date) {
var { fullYear, month, day, } = parseDateParts(date);
return `${fullYear}-${String(month).padStart(2,"0")}-${String(day).padStart(2,"0")}`;
}
function formatLocaleDate(date) {
return date.toLocaleDateString("en-US",{ timeZone: HOST_TZ, });
}
function normalizeLocaleDate(
dateStr,
TZ_OFFSET = computeTZOffset(HOST_TZ,new Date(Date.parse(`${dateStr}T12:00:00.000`)))
) {
return new Date(Date.parse(`${dateStr}T12:00:00.000${TZ_OFFSET}`));
}
function getTodayDate() {
return normalizeLocaleDate(getDateStr(new Date()));
}
function getDateRelative(
fromDate = getTodayDate(),
deltaYear = 0,
deltaMonth = 0,
deltaDay = 0
) {
// note: to keep from having timezone shift, we intentionally
// use timezone-sensitive `fromDate` (getDate(), getHours(), etc)
// in the constructor new Date() call; then normalizeLocaleDate()
// re-normalizes it to noon in America/Chicago timezone
var newDate = new Date(
fromDate.getFullYear() + deltaYear,
fromDate.getMonth() + deltaMonth,
fromDate.getDate() + deltaDay,
fromDate.getHours(),
fromDate.getMinutes(),
fromDate.getSeconds(),
fromDate.getMilliseconds()
);
return normalizeLocaleDate(
getDateStr(newDate),
computeTZOffset(HOST_TZ,newDate)
);
}
function getDayBehindDate(fromDate = getTodayDate()) {
return getDateRelative(
fromDate,
/*deltaYear=*/0,
/*deltaMonth=*/0,
/*deltaDay=*/-1
);
}
// adapted from: https://www.npmjs.com/package/get-timezone-offset
function computeTZOffset(timezoneLabel,forDate = new Date()) {
if (longOffsetSupported) {
try {
let offset = (
forDate.toLocaleString("en-US",{
timeZone: timezoneLabel,
timeZoneName: "longOffset",
})
.match(/GMT(.*)$/)[1]
);
if (/^[\-\+]\d{2}:\d{2}$/.test(offset)) {
return offset;
}
}
catch (err) {
longOffsetSupported = false;
}
}
var formatOptions = {
timeZone: "UTC",
hourCycle: "h23",
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
};
var utcFormat = new Intl.DateTimeFormat("en-US",formatOptions);
var tzFormat = new Intl.DateTimeFormat("en-US",{ ...formatOptions, timeZone: timezoneLabel, });
return formatHourMin(diffMinutes(
parseDate(utcFormat.format(forDate)),
parseDate(tzFormat.format(forDate))
));
// *****************************
function parseDate(dateStr) {
return Object.fromEntries(
Object.entries(
((
dateStr.replace(/[\u200E\u200F]/g,"")
.match(/^\d+.(?<day>\d+).\d+,?\s+(?<hour>\d+).(?<min>\d+)/)
) || { groups: {}, })
.groups
)
.map(([name,val]) => [ name, Number(val), ])
);
}
function diffMinutes(d1,d2) {
var day = d1.day - d2.day;
var hour = d1.hour - d2.hour;
var min = d1.min - d2.min;
if (day > 15 ) day = -1;
if (day < -15 ) day = 1;
return 60 * (24 * day + hour) + min;
}
function formatHourMin(minutes) {
var hour = Math.floor(minutes / 60);
minutes -= (hour * 60);
return `${minutes > 0 ? "+" : "-"}${String(hour).padStart(2,"0")}:${String(minutes).padStart(2,"0")}`;
}
}
var prevDates = Array(500);
var startTS = Date.now();
// start: benchmark/optimize
var curDate = getTodayDate();
// collect the previous 500 days before today
for (let i = 0; i < 500; i++) {
curDate = getDayBehindDate(curDate);
prevDates[i] = getDateStr(curDate);
}
// end: benchmark/optimization
var endTS = Date.now();
if (endTS - startTS > 0) {
console.log(`That took ~${endTS - startTS}ms`);
}
console.log(prevDates);
@getify
Copy link
Author

getify commented Oct 31, 2024

Interesting exploration of this problem space with chatGPT:

https://chatgpt.com/share/6723a6d1-917c-8001-aee4-18f960fd1e53

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment