Skip to content

Commit 0a582ff

Browse files
committed
Speed up non-ISO calendar tests about 6x
I was wrong about what was making non-ISO calendars so slow. I thought the problem was `formatToParts()`, but it turns out that the `DateTimeFormat` constructor is really slow and also allocates ridiculous amounts of RAM. See more details here: https://bugs.chromium.org/p/v8/issues/detail?id=6528 @littledan in https://bugs.chromium.org/p/v8/issues/detail?id=6528#c4 recommended to cache DateTimeFormat instances, so that's what this commit does. The result is a 6x speedup in non-ISO calendar tests. Before: 6398.83ms After: 1062.26ms A similar speedup is likely for `ES.GetCanonicalTimeZoneIdentifier`. Caching time zone canonicalization (in a separate PR) should have a big positive impact on ZonedDateTIme and TimeZone perf. Many thanks to @fer22f for uncovering this optimization in js-temporal/temporal-polyfill#7.
1 parent faaefc3 commit 0a582ff

File tree

1 file changed

+29
-20
lines changed

1 file changed

+29
-20
lines changed

Diff for: polyfill/lib/calendar.mjs

+29-20
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import { CALENDAR_ID, ISO_YEAR, ISO_MONTH, ISO_DAY, CreateSlots, GetSlot, HasSlo
77
const ArrayIncludes = Array.prototype.includes;
88
const ArrayPrototypePush = Array.prototype.push;
99
const ObjectAssign = Object.assign;
10+
const ObjectEntries = Object.entries;
11+
const IntlDateTimeFormat = globalThis.Intl.DateTimeFormat;
12+
const MathAbs = Math.abs;
13+
const MathFloor = Math.floor;
1014

1115
const impl = {};
1216

@@ -427,19 +431,31 @@ function simpleDateDiff(one, two) {
427431
*/
428432
const nonIsoHelperBase = {
429433
// The properties and methods below here should be the same for all lunar/lunisolar calendars.
434+
getFormatter() {
435+
// `new Intl.DateTimeFormat()` is amazingly slow and chews up RAM. Per
436+
// https://bugs.chromium.org/p/v8/issues/detail?id=6528#c4, we cache one
437+
// DateTimeFormat instance per calendar. Caching is lazy so we only pay for
438+
// calendars that are used. Note that the nonIsoHelperBase object is spread
439+
// into each each calendar's implementation before any cache is created, so
440+
// each calendar gets its own separate cached formatter.
441+
if (typeof this.formatter === 'undefined') {
442+
this.formatter = new IntlDateTimeFormat(`en-US-u-ca-${this.id}`, {
443+
day: 'numeric',
444+
month: 'numeric',
445+
year: 'numeric',
446+
era: this.eraLength,
447+
timeZone: 'UTC'
448+
});
449+
}
450+
return this.formatter;
451+
},
430452
isoToCalendarDate(isoDate, cache) {
431453
let { year: isoYear, month: isoMonth, day: isoDay } = isoDate;
432454
const key = JSON.stringify({ func: 'isoToCalendarDate', isoYear, isoMonth, isoDay, id: this.id });
433455
const cached = cache.get(key);
434456
if (cached) return cached;
435457

436-
const dateTimeFormat = new Intl.DateTimeFormat(`en-US-u-ca-${this.id}`, {
437-
day: 'numeric',
438-
month: 'numeric',
439-
year: 'numeric',
440-
era: this.eraLength,
441-
timeZone: 'UTC'
442-
});
458+
const dateTimeFormat = this.getFormatter();
443459
let parts, isoString;
444460
try {
445461
isoString = toUtcIsoDateString({ isoYear, isoMonth, isoDay });
@@ -763,7 +779,7 @@ const nonIsoHelperBase = {
763779
},
764780
addMonthsCalendar(calendarDate, months, overflow, cache) {
765781
const { day } = calendarDate;
766-
for (let i = 0, absMonths = Math.abs(months); i < absMonths; i++) {
782+
for (let i = 0, absMonths = MathAbs(months); i < absMonths; i++) {
767783
const days = months < 0 ? -this.daysInPreviousMonth(calendarDate, cache) : this.daysInMonth(calendarDate, cache);
768784
const isoDate = this.calendarToIsoDate(calendarDate, 'constrain', cache);
769785
const addedIso = this.addDaysIso(isoDate, days, cache);
@@ -970,7 +986,7 @@ const helperHebrew = ObjectAssign({}, nonIsoHelperBase, {
970986
minMaxMonthLength(calendarDate, minOrMax) {
971987
const { month, year } = calendarDate;
972988
const monthCode = this.getMonthCode(year, month);
973-
const monthInfo = Object.entries(this.months).find((m) => m[1].monthCode === monthCode);
989+
const monthInfo = ObjectEntries(this.months).find((m) => m[1].monthCode === monthCode);
974990
if (monthInfo === undefined) throw new RangeError(`unmatched Hebrew month: ${month}`);
975991
const daysInMonth = monthInfo[1].days;
976992
return typeof daysInMonth === 'number' ? daysInMonth : daysInMonth[minOrMax];
@@ -1096,7 +1112,7 @@ const helperIslamic = ObjectAssign({}, nonIsoHelperBase, {
10961112
constantEra: 'ah',
10971113
estimateIsoDate(calendarDate) {
10981114
const { year } = this.adjustCalendarDate(calendarDate);
1099-
return { year: Math.floor((year * this.DAYS_PER_ISLAMIC_YEAR) / this.DAYS_PER_ISO_YEAR) + 622, month: 1, day: 1 };
1115+
return { year: MathFloor((year * this.DAYS_PER_ISLAMIC_YEAR) / this.DAYS_PER_ISO_YEAR) + 622, month: 1, day: 1 };
11001116
}
11011117
});
11021118

@@ -1587,7 +1603,7 @@ const helperChinese = ObjectAssign({}, nonIsoHelperBase, {
15871603
calendarType: 'lunisolar',
15881604
inLeapYear(calendarDate, cache) {
15891605
const months = this.getMonthList(calendarDate.year, cache);
1590-
return Object.entries(months).length === 13;
1606+
return ObjectEntries(months).length === 13;
15911607
},
15921608
monthsInYear(calendarDate, cache) {
15931609
return this.inLeapYear(calendarDate, cache) ? 13 : 12;
@@ -1601,14 +1617,7 @@ const helperChinese = ObjectAssign({}, nonIsoHelperBase, {
16011617
const key = JSON.stringify({ func: 'getMonthList', calendarYear, id: this.id });
16021618
const cached = cache.get(key);
16031619
if (cached) return cached;
1604-
const dateTimeFormat = new Intl.DateTimeFormat(`en-US-u-ca-${this.id}`, {
1605-
day: 'numeric',
1606-
month: 'numeric',
1607-
year: 'numeric',
1608-
era: 'short',
1609-
timeZone: 'UTC'
1610-
});
1611-
1620+
const dateTimeFormat = this.getFormatter();
16121621
const getCalendarDate = (isoYear, daysPastFeb1) => {
16131622
const isoStringFeb1 = toUtcIsoDateString({ isoYear, isoMonth: 2, isoDay: 1 });
16141623
const legacyDate = new Date(isoStringFeb1);
@@ -1723,7 +1732,7 @@ const helperChinese = ObjectAssign({}, nonIsoHelperBase, {
17231732
}
17241733
} else if (monthCode === undefined) {
17251734
const months = this.getMonthList(year, cache);
1726-
const monthEntries = Object.entries(months);
1735+
const monthEntries = ObjectEntries(months);
17271736
const largestMonth = monthEntries.length;
17281737
if (overflow === 'reject') {
17291738
ES.RejectToRange(month, 1, largestMonth);

0 commit comments

Comments
 (0)