diff --git a/packages/localization/src/DateFormat.ts b/packages/localization/src/DateFormat.ts index 5596e83b7f92..196775773218 100644 --- a/packages/localization/src/DateFormat.ts +++ b/packages/localization/src/DateFormat.ts @@ -28,15 +28,135 @@ type DateFormatOptions = { const DateFormatWrapped = DateFormatNative as typeof DateFormatT; class DateFormat extends DateFormatWrapped { + /** + * Central cache for all DateFormat instances across the entire library. + * Shared by Calendar, DatePicker, TimePicker, and all other date components. + * Key format: "type:JSON.stringify(options):locale" + * @private + */ + private static _cache = new Map(); + + private static _stats = { + totalCalls: 0, + cacheHits: 0, + cacheMisses: 0, + uniqueInstances: 0, + }; + + static logCacheStats() { + console.log("===== DateFormat Cache Statistics ====="); + console.log(`Total calls: ${DateFormat._stats.totalCalls}`); + console.log(`Cache hits: ${DateFormat._stats.cacheHits} (${DateFormat._stats.totalCalls > 0 ? ((DateFormat._stats.cacheHits / DateFormat._stats.totalCalls) * 100).toFixed(1) : 0}%)`); + console.log(`Cache misses: ${DateFormat._stats.cacheMisses} (${DateFormat._stats.totalCalls > 0 ? ((DateFormat._stats.cacheMisses / DateFormat._stats.totalCalls) * 100).toFixed(1) : 0}%)`); + console.log(`Unique instances: ${DateFormat._cache.size}`); + console.log(`Total instances: ${(window as any).dateformatinstances?.size || 0}`); + console.log("Cache keys:"); + Array.from(DateFormat._cache.keys()).forEach((key, index) => { + console.log(` ${index + 1}. ${key}`); + }); + console.log("=========================================\n"); + + return { + totalCalls: DateFormat._stats.totalCalls, + cacheHits: DateFormat._stats.cacheHits, + cacheMisses: DateFormat._stats.cacheMisses, + uniqueInstances: DateFormat._cache.size, + totalInstances: (window as any).dateformatinstances?.size || 0, + }; + } + static getDateInstance(oFormatOptions?: DateFormatOptions, oLocale?: LocaleWrapped): DateFormat; static getDateInstance(oLocale?: LocaleWrapped): DateFormat; static getDateInstance(oFormatOptionsOrLocale?: DateFormatOptions | LocaleWrapped, oLocale?: LocaleWrapped): DateFormat { + DateFormat._stats.totalCalls++; + if (oFormatOptionsOrLocale instanceof LocaleWrapped) { return DateFormatWrapped.getDateInstance(undefined, oFormatOptionsOrLocale); } + + const nativeLocale = oLocale ?? new LocaleWrapped(getLocale().toString()); + const cacheKey = `date:${JSON.stringify(oFormatOptionsOrLocale || {})}:${nativeLocale.toString()}`; + + if (!DateFormat._cache.has(cacheKey)) { + DateFormat._stats.cacheMisses++; + console.log(`[DateFormat CACHE MISS #${DateFormat._stats.cacheMisses}] Creating NEW date instance | Total instances: ${DateFormat._cache.size + 1}`); + console.log(` Key: ${cacheKey}`); + const date = DateFormatWrapped.getDateInstance(oFormatOptionsOrLocale, nativeLocale); + DateFormat._cache.set(cacheKey, date); + } else { + DateFormat._stats.cacheHits++; + console.log(`[DateFormat CACHE HIT #${DateFormat._stats.cacheHits}] Reusing CACHED date instance`); + } + + return DateFormat._cache.get(cacheKey)!; + } + + static getTimeInstance(oFormatOptions?: DateFormatOptions, oLocale?: LocaleWrapped): DateFormat; + static getTimeInstance(oLocale?: LocaleWrapped): DateFormat; + static getTimeInstance(oFormatOptionsOrLocale?: DateFormatOptions | LocaleWrapped, oLocale?: LocaleWrapped): DateFormat { + DateFormat._stats.totalCalls++; + + if (oFormatOptionsOrLocale instanceof LocaleWrapped) { + return DateFormatWrapped.getTimeInstance(undefined, oFormatOptionsOrLocale); + } + + const nativeLocale = oLocale ?? new LocaleWrapped(getLocale().toString()); + const cacheKey = `time:${JSON.stringify(oFormatOptionsOrLocale || {})}:${nativeLocale.toString()}`; + + if (!DateFormat._cache.has(cacheKey)) { + DateFormat._stats.cacheMisses++; + console.log(`[DateFormat CACHE MISS #${DateFormat._stats.cacheMisses}] Creating NEW time instance | Total instances: ${DateFormat._cache.size + 1}`); + console.log(` Key: ${cacheKey}`); + const time = DateFormatWrapped.getTimeInstance(oFormatOptionsOrLocale, nativeLocale); + DateFormat._cache.set(cacheKey, time); + } else { + DateFormat._stats.cacheHits++; + console.log(`[DateFormat CACHE HIT #${DateFormat._stats.cacheHits}] Reusing CACHED time instance`); + } + + return DateFormat._cache.get(cacheKey)!; + } + + static getDateTimeInstance(oFormatOptions?: DateFormatOptions, oLocale?: LocaleWrapped): DateFormat; + static getDateTimeInstance(oLocale?: LocaleWrapped): DateFormat; + static getDateTimeInstance(oFormatOptionsOrLocale?: DateFormatOptions | LocaleWrapped, oLocale?: LocaleWrapped): DateFormat { + DateFormat._stats.totalCalls++; + + if (oFormatOptionsOrLocale instanceof LocaleWrapped) { + return DateFormatWrapped.getDateTimeInstance(undefined, oFormatOptionsOrLocale); + } + const nativeLocale = oLocale ?? new LocaleWrapped(getLocale().toString()); - return DateFormatWrapped.getDateInstance(oFormatOptionsOrLocale, nativeLocale); + const cacheKey = `datetime:${JSON.stringify(oFormatOptionsOrLocale || {})}:${nativeLocale.toString()}`; + + if (!DateFormat._cache.has(cacheKey)) { + DateFormat._stats.cacheMisses++; + console.log(`[DateFormat CACHE MISS #${DateFormat._stats.cacheMisses}] Creating NEW datetime instance | Total instances: ${DateFormat._cache.size + 1}`); + console.log(` Key: ${cacheKey}`); + const datetime = DateFormatWrapped.getDateTimeInstance(oFormatOptionsOrLocale, nativeLocale); + DateFormat._cache.set(cacheKey, datetime); + } else { + DateFormat._stats.cacheHits++; + console.log(`[DateFormat CACHE HIT #${DateFormat._stats.cacheHits}] Reusing CACHED datetime instance`); + } + + return DateFormat._cache.get(cacheKey)!; } } +// @ts-ignore +window.DateFormatStats = { + log: () => DateFormat.logCacheStats(), + get: () => ({ + // @ts-ignore + totalCalls: DateFormat._stats.totalCalls, + // @ts-ignore + cacheHits: DateFormat._stats.cacheHits, + // @ts-ignore + cacheMisses: DateFormat._stats.cacheMisses, + // @ts-ignore + uniqueInstances: DateFormat._cache.size, + }), +}; + export default DateFormat; diff --git a/packages/main/src/Calendar.ts b/packages/main/src/Calendar.ts index 3ee8e8ab30cf..f19f41dc86ef 100644 --- a/packages/main/src/Calendar.ts +++ b/packages/main/src/Calendar.ts @@ -447,7 +447,7 @@ class Calendar extends CalendarPart { _getHeaderTextForMonth(monthTimestamp: number): { monthText: string, yearText: string, secondMonthText?: string, secondYearText?: string } { const calendarDate = CalendarDateComponent.fromTimestamp(monthTimestamp * 1000, this._primaryCalendarType); const localeData = getCachedLocaleDataInstance(getLocale()); - const yearFormat = DateFormat.getDateInstance({ format: "y", calendarType: this._primaryCalendarType }); + const yearFormat = this._primaryYearFormat; const monthText = localeData.getMonthsStandAlone("wide", this._primaryCalendarType)[calendarDate.getMonth()]; const localDate = calendarDate.toLocalJSDate(); @@ -463,7 +463,7 @@ class Calendar extends CalendarPart { const secondaryCalendarDate = secondaryDate.firstDate || secondaryDate.lastDate; const secondaryLocaleData = getCachedLocaleDataInstance(getLocale()); result.secondMonthText = secondaryLocaleData.getMonthsStandAlone("wide", this._secondaryCalendarType)[secondaryCalendarDate.getMonth()]; - const secondaryYearFormat = DateFormat.getDateInstance({ format: "y", calendarType: this._secondaryCalendarType }); + const secondaryYearFormat = this._secondaryYearFormat; result.secondYearText = String(secondaryYearFormat.format(secondaryCalendarDate.toLocalJSDate(), true)); } @@ -641,11 +641,12 @@ class Calendar extends CalendarPart { } async onAfterRendering() { + console.log(`[Calendar ${this._id}] ===== RENDER CYCLE =====`); await renderFinished(); // Await for the current picker to render and then ask if it has previous/next pages this._previousButtonDisabled = !this._currentPickerDOM._hasPreviousPage(); this._nextButtonDisabled = !this._currentPickerDOM._hasNextPage(); - const yearFormat = DateFormat.getDateInstance({ format: "y", calendarType: this.primaryCalendarType }); + const yearFormat = this._primaryYearFormat; const localeData = getCachedLocaleDataInstance(getLocale()); this._headerMonthButtonText = localeData.getMonthsStandAlone("wide", this.primaryCalendarType)[this._calendarDate.getMonth()]; this._headerYearButtonText = String(yearFormat.format(this._localDate, true)); @@ -724,6 +725,14 @@ class Calendar extends CalendarPart { return this.shadowRoot!.querySelector(`[ui5-${this._currentPicker}picker]`)! as unknown as ICalendarPicker; } + get _primaryYearFormat() { + return DateFormat.getDateInstance({ format: "y", calendarType: this._primaryCalendarType }); + } + + get _secondaryYearFormat() { + return DateFormat.getDateInstance({ format: "y", calendarType: this._secondaryCalendarType }); + } + /** * Returns the focusable element inside the Calendar (the current picker) * @override @@ -747,7 +756,7 @@ class Calendar extends CalendarPart { } _setSecondaryCalendarTypeButtonText() { - const yearFormatSecType = DateFormat.getDateInstance({ format: "y", calendarType: this._secondaryCalendarType }); + const yearFormatSecType = this._secondaryYearFormat; this._headerYearButtonTextSecType = String(yearFormatSecType.format(this._localDate, true)); const currentYearRange = this._currentYearRange; @@ -767,7 +776,7 @@ class Calendar extends CalendarPart { } const localDate = UI5Date.getInstance(this._timestamp * 1000); - const secondYearFormat = DateFormat.getDateInstance({ format: "y", calendarType: this._secondaryCalendarType }); + const secondYearFormat = this._secondaryYearFormat; const dateInSecType = transformDateToSecondaryType(this._primaryCalendarType, this._secondaryCalendarType, this._timestamp); const secondMonthInfo = convertMonthNumbersToMonthNames(dateInSecType.firstDate.getMonth(), dateInSecType.lastDate.getMonth(), this._secondaryCalendarType); const secondYearText = secondYearFormat.format(localDate); @@ -1035,7 +1044,7 @@ class Calendar extends CalendarPart { * @private */ _formatYearRangeText(yearRange: CalendarYearRangeT) { - const yearFormat = DateFormat.getDateInstance({ format: "y", calendarType: this.primaryCalendarType }); + const yearFormat = this._primaryYearFormat const { rangeStart, rangeEnd } = this._createYearRangeDates(yearRange, this.primaryCalendarType); const rangeStartText = yearFormat.format(rangeStart.toLocalJSDate());