Skip to content

Commit ad845e0

Browse files
committed
feat: add cache for DateFormat
1 parent 4ac0d74 commit ad845e0

4 files changed

Lines changed: 159 additions & 12 deletions

File tree

packages/localization/src/DateFormat.ts

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,134 @@ type DateFormatOptions = {
2828
const DateFormatWrapped = DateFormatNative as typeof DateFormatT;
2929

3030
class DateFormat extends DateFormatWrapped {
31+
/**
32+
* Central cache for all DateFormat instances across the entire library.
33+
* Shared by Calendar, DatePicker, TimePicker, and all other date components.
34+
* Key format: "type:JSON.stringify(options):locale"
35+
* @private
36+
*/
37+
private static _cache = new Map<string, DateFormat>();
38+
39+
private static _stats = {
40+
totalCalls: 0,
41+
cacheHits: 0,
42+
cacheMisses: 0,
43+
uniqueInstances: 0,
44+
};
45+
46+
static logCacheStats() {
47+
console.log("===== DateFormat Cache Statistics =====");
48+
console.log(`Total calls: ${DateFormat._stats.totalCalls}`);
49+
console.log(`Cache hits: ${DateFormat._stats.cacheHits} (${DateFormat._stats.totalCalls > 0 ? ((DateFormat._stats.cacheHits / DateFormat._stats.totalCalls) * 100).toFixed(1) : 0}%)`);
50+
console.log(`Cache misses: ${DateFormat._stats.cacheMisses} (${DateFormat._stats.totalCalls > 0 ? ((DateFormat._stats.cacheMisses / DateFormat._stats.totalCalls) * 100).toFixed(1) : 0}%)`);
51+
console.log(`Unique instances: ${DateFormat._cache.size}`);
52+
console.log(`Total instances: ${(window as any).dateformatinstances?.size || 0}`);
53+
console.log("Cache keys:");
54+
Array.from(DateFormat._cache.keys()).forEach((key, index) => {
55+
console.log(` ${index + 1}. ${key}`);
56+
});
57+
console.log("=========================================\n");
58+
59+
return {
60+
totalCalls: DateFormat._stats.totalCalls,
61+
cacheHits: DateFormat._stats.cacheHits,
62+
cacheMisses: DateFormat._stats.cacheMisses,
63+
uniqueInstances: DateFormat._cache.size,
64+
totalInstances: (window as any).dateformatinstances?.size || 0,
65+
};
66+
}
67+
3168
static getDateInstance(oFormatOptions?: DateFormatOptions, oLocale?: LocaleWrapped): DateFormat;
3269
static getDateInstance(oLocale?: LocaleWrapped): DateFormat;
3370
static getDateInstance(oFormatOptionsOrLocale?: DateFormatOptions | LocaleWrapped, oLocale?: LocaleWrapped): DateFormat {
71+
DateFormat._stats.totalCalls++;
72+
73+
if (oFormatOptionsOrLocale instanceof LocaleWrapped) {
74+
return DateFormatWrapped.getDateInstance(undefined, oFormatOptionsOrLocale);
75+
}
76+
77+
const nativeLocale = oLocale ?? new LocaleWrapped(getLocale().toString());
78+
const cacheKey = `date:${JSON.stringify(oFormatOptionsOrLocale || {})}:${nativeLocale.toString()}`;
79+
80+
if (!DateFormat._cache.has(cacheKey)) {
81+
DateFormat._stats.cacheMisses++;
82+
console.log(`[DateFormat CACHE MISS #${DateFormat._stats.cacheMisses}] Creating NEW date instance | Total instances: ${DateFormat._cache.size + 1}`);
83+
console.log(` Key: ${cacheKey}`);
84+
const date = DateFormatWrapped.getDateInstance(oFormatOptionsOrLocale, nativeLocale);
85+
DateFormat._cache.set(cacheKey, date);
86+
} else {
87+
DateFormat._stats.cacheHits++;
88+
console.log(`[DateFormat CACHE HIT #${DateFormat._stats.cacheHits}] Reusing CACHED date instance`);
89+
}
90+
91+
return DateFormat._cache.get(cacheKey)!;
92+
}
93+
94+
static getTimeInstance(oFormatOptions?: DateFormatOptions, oLocale?: LocaleWrapped): DateFormat;
95+
static getTimeInstance(oLocale?: LocaleWrapped): DateFormat;
96+
static getTimeInstance(oFormatOptionsOrLocale?: DateFormatOptions | LocaleWrapped, oLocale?: LocaleWrapped): DateFormat {
97+
DateFormat._stats.totalCalls++;
98+
3499
if (oFormatOptionsOrLocale instanceof LocaleWrapped) {
35-
return DateFormatWrapped.getDateInstance(undefined, oFormatOptionsOrLocale);
100+
return DateFormatWrapped.getTimeInstance(undefined, oFormatOptionsOrLocale);
36101
}
102+
37103
const nativeLocale = oLocale ?? new LocaleWrapped(getLocale().toString());
38-
return DateFormatWrapped.getDateInstance(oFormatOptionsOrLocale, nativeLocale);
104+
const cacheKey = `time:${JSON.stringify(oFormatOptionsOrLocale || {})}:${nativeLocale.toString()}`;
105+
106+
if (!DateFormat._cache.has(cacheKey)) {
107+
DateFormat._stats.cacheMisses++;
108+
console.log(`[DateFormat CACHE MISS #${DateFormat._stats.cacheMisses}] Creating NEW time instance | Total instances: ${DateFormat._cache.size + 1}`);
109+
console.log(` Key: ${cacheKey}`);
110+
const time = DateFormatWrapped.getTimeInstance(oFormatOptionsOrLocale, nativeLocale);
111+
DateFormat._cache.set(cacheKey, time);
112+
} else {
113+
DateFormat._stats.cacheHits++;
114+
console.log(`[DateFormat CACHE HIT #${DateFormat._stats.cacheHits}] Reusing CACHED time instance`);
115+
}
116+
117+
return DateFormat._cache.get(cacheKey)!;
39118
}
119+
120+
static getDateTimeInstance(oFormatOptions?: DateFormatOptions, oLocale?: LocaleWrapped): DateFormat;
121+
static getDateTimeInstance(oLocale?: LocaleWrapped): DateFormat;
122+
static getDateTimeInstance(oFormatOptionsOrLocale?: DateFormatOptions | LocaleWrapped, oLocale?: LocaleWrapped): DateFormat {
123+
DateFormat._stats.totalCalls++;
124+
125+
if (oFormatOptionsOrLocale instanceof LocaleWrapped) {
126+
return DateFormatWrapped.getDateTimeInstance(undefined, oFormatOptionsOrLocale);
127+
}
128+
129+
const nativeLocale = oLocale ?? new LocaleWrapped(getLocale().toString());
130+
const cacheKey = `datetime:${JSON.stringify(oFormatOptionsOrLocale || {})}:${nativeLocale.toString()}`;
131+
132+
if (!DateFormat._cache.has(cacheKey)) {
133+
DateFormat._stats.cacheMisses++;
134+
console.log(`[DateFormat CACHE MISS #${DateFormat._stats.cacheMisses}] Creating NEW datetime instance | Total instances: ${DateFormat._cache.size + 1}`);
135+
console.log(` Key: ${cacheKey}`);
136+
const datetime = DateFormatWrapped.getDateTimeInstance(oFormatOptionsOrLocale, nativeLocale);
137+
DateFormat._cache.set(cacheKey, datetime);
138+
} else {
139+
DateFormat._stats.cacheHits++;
140+
console.log(`[DateFormat CACHE HIT #${DateFormat._stats.cacheHits}] Reusing CACHED datetime instance`);
141+
}
142+
143+
return DateFormat._cache.get(cacheKey)!;
144+
}
145+
}
146+
147+
// @ts-ignore
148+
if (typeof window !== "undefined") {
149+
// @ts-ignore
150+
window.DateFormatStats = {
151+
log: () => DateFormat.logCacheStats(),
152+
get: () => ({
153+
totalCalls: DateFormat["_stats"].totalCalls,
154+
cacheHits: DateFormat["_stats"].cacheHits,
155+
cacheMisses: DateFormat["_stats"].cacheMisses,
156+
uniqueInstances: DateFormat["_cache"].size,
157+
}),
158+
};
40159
}
41160

42161
export default DateFormat;

packages/main/src/Calendar.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ class Calendar extends CalendarPart {
447447
_getHeaderTextForMonth(monthTimestamp: number): { monthText: string, yearText: string, secondMonthText?: string, secondYearText?: string } {
448448
const calendarDate = CalendarDateComponent.fromTimestamp(monthTimestamp * 1000, this._primaryCalendarType);
449449
const localeData = getCachedLocaleDataInstance(getLocale());
450-
const yearFormat = DateFormat.getDateInstance({ format: "y", calendarType: this._primaryCalendarType });
450+
const yearFormat = this._primaryYearFormat;
451451

452452
const monthText = localeData.getMonthsStandAlone("wide", this._primaryCalendarType)[calendarDate.getMonth()];
453453
const localDate = calendarDate.toLocalJSDate();
@@ -463,7 +463,7 @@ class Calendar extends CalendarPart {
463463
const secondaryCalendarDate = secondaryDate.firstDate || secondaryDate.lastDate;
464464
const secondaryLocaleData = getCachedLocaleDataInstance(getLocale());
465465
result.secondMonthText = secondaryLocaleData.getMonthsStandAlone("wide", this._secondaryCalendarType)[secondaryCalendarDate.getMonth()];
466-
const secondaryYearFormat = DateFormat.getDateInstance({ format: "y", calendarType: this._secondaryCalendarType });
466+
const secondaryYearFormat = this._secondaryYearFormat;
467467
result.secondYearText = String(secondaryYearFormat.format(secondaryCalendarDate.toLocalJSDate(), true));
468468
}
469469

@@ -641,11 +641,12 @@ class Calendar extends CalendarPart {
641641
}
642642

643643
async onAfterRendering() {
644+
console.log(`[Calendar ${this._id}] ===== RENDER CYCLE =====`);
644645
await renderFinished(); // Await for the current picker to render and then ask if it has previous/next pages
645646
this._previousButtonDisabled = !this._currentPickerDOM._hasPreviousPage();
646647
this._nextButtonDisabled = !this._currentPickerDOM._hasNextPage();
647648

648-
const yearFormat = DateFormat.getDateInstance({ format: "y", calendarType: this.primaryCalendarType });
649+
const yearFormat = this._primaryYearFormat;
649650
const localeData = getCachedLocaleDataInstance(getLocale());
650651
this._headerMonthButtonText = localeData.getMonthsStandAlone("wide", this.primaryCalendarType)[this._calendarDate.getMonth()];
651652
this._headerYearButtonText = String(yearFormat.format(this._localDate, true));
@@ -724,6 +725,14 @@ class Calendar extends CalendarPart {
724725
return this.shadowRoot!.querySelector(`[ui5-${this._currentPicker}picker]`)! as unknown as ICalendarPicker;
725726
}
726727

728+
get _primaryYearFormat() {
729+
return DateFormat.getDateInstance({ format: "y", calendarType: this._primaryCalendarType });
730+
}
731+
732+
get _secondaryYearFormat() {
733+
return DateFormat.getDateInstance({ format: "y", calendarType: this._secondaryCalendarType });
734+
}
735+
727736
/**
728737
* Returns the focusable element inside the Calendar (the current picker)
729738
* @override
@@ -747,7 +756,7 @@ class Calendar extends CalendarPart {
747756
}
748757

749758
_setSecondaryCalendarTypeButtonText() {
750-
const yearFormatSecType = DateFormat.getDateInstance({ format: "y", calendarType: this._secondaryCalendarType });
759+
const yearFormatSecType = this._secondaryYearFormat;
751760
this._headerYearButtonTextSecType = String(yearFormatSecType.format(this._localDate, true));
752761

753762
const currentYearRange = this._currentYearRange;
@@ -767,7 +776,7 @@ class Calendar extends CalendarPart {
767776
}
768777

769778
const localDate = UI5Date.getInstance(this._timestamp * 1000);
770-
const secondYearFormat = DateFormat.getDateInstance({ format: "y", calendarType: this._secondaryCalendarType });
779+
const secondYearFormat = this._secondaryYearFormat;
771780
const dateInSecType = transformDateToSecondaryType(this._primaryCalendarType, this._secondaryCalendarType, this._timestamp);
772781
const secondMonthInfo = convertMonthNumbersToMonthNames(dateInSecType.firstDate.getMonth(), dateInSecType.lastDate.getMonth(), this._secondaryCalendarType);
773782
const secondYearText = secondYearFormat.format(localDate);
@@ -1035,7 +1044,7 @@ class Calendar extends CalendarPart {
10351044
* @private
10361045
*/
10371046
_formatYearRangeText(yearRange: CalendarYearRangeT) {
1038-
const yearFormat = DateFormat.getDateInstance({ format: "y", calendarType: this.primaryCalendarType });
1047+
const yearFormat = this._primaryYearFormat
10391048
const { rangeStart, rangeEnd } = this._createYearRangeDates(yearRange, this.primaryCalendarType);
10401049

10411050
const rangeStartText = yearFormat.format(rangeStart.toLocalJSDate());

packages/main/src/DayPicker.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ class DayPicker extends CalendarPart implements ICalendarPicker {
209209
_focusableDay!: HTMLElement;
210210

211211
_autoFocus?: boolean;
212+
_weekNumberFormat?: DateFormat;
212213

213214
@i18n("@ui5/webcomponents")
214215
static i18nBundle: I18nBundle;
@@ -367,8 +368,16 @@ class DayPicker extends CalendarPart implements ICalendarPicker {
367368
}
368369

369370
_calculateWeekNumber(date: Date): number {
370-
const oDateFormat = DateFormat.getDateInstance({ pattern: "w", calendarType: this.primaryCalendarType, calendarWeekNumbering: this.calendarWeekNumbering });
371-
const weekNumber = oDateFormat.format(date);
371+
// Cache DateFormat instance for week number calculation
372+
if (!this._weekNumberFormat) {
373+
this._weekNumberFormat = DateFormat.getDateInstance({
374+
pattern: "w",
375+
calendarType: this.primaryCalendarType,
376+
calendarWeekNumbering: this.calendarWeekNumbering
377+
});
378+
}
379+
380+
const weekNumber = this._weekNumberFormat.format(date);
372381

373382
return Number(weekNumber);
374383
}

packages/main/src/YearRangePicker.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ class YearRangePicker extends CalendarPart implements ICalendarPicker {
126126
_currentYearRange?: CalendarYearRangeT;
127127

128128
_gridStartYear?: number;
129+
_yearFormatPrimary?: DateFormat;
130+
_yearFormatSecondary?: DateFormat;
129131

130132
@i18n("@ui5/webcomponents")
131133
static i18nBundle: I18nBundle;
@@ -199,8 +201,16 @@ class YearRangePicker extends CalendarPart implements ICalendarPicker {
199201
}
200202

201203
_getYearRanges() {
202-
const yearFormat = DateFormat.getDateInstance({ format: "y", calendarType: this._primaryCalendarType });
203-
const yearFormatInSecType = DateFormat.getDateInstance({ format: "y", calendarType: this._secondaryCalendarType });
204+
// Cache DateFormat instances
205+
if (!this._yearFormatPrimary) {
206+
this._yearFormatPrimary = DateFormat.getDateInstance({ format: "y", calendarType: this._primaryCalendarType });
207+
}
208+
if (!this._yearFormatSecondary) {
209+
this._yearFormatSecondary = DateFormat.getDateInstance({ format: "y", calendarType: this._secondaryCalendarType });
210+
}
211+
212+
const yearFormat = this._yearFormatPrimary;
213+
const yearFormatInSecType = this._yearFormatSecondary;
204214

205215
const pageSize = this._getPageSize();
206216
const rowSize = this._getRowSize();

0 commit comments

Comments
 (0)