Skip to content

Commit 47b3deb

Browse files
authored
Scheduler: Add timezones cache (T1296816) (DevExpress#30276)
Co-authored-by: Vladimir Bushmanov <vladimir.bushmanov@devexpress.com>
1 parent 90386b1 commit 47b3deb

File tree

5 files changed

+153
-9
lines changed

5 files changed

+153
-9
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import {
2+
describe, expect, it, jest,
3+
} from '@jest/globals';
4+
5+
import { Cache } from './global_cache';
6+
7+
describe('global cache', () => {
8+
it('should be empty at initialization', () => {
9+
const cache = new Cache();
10+
expect(cache.size).toBe(0);
11+
});
12+
13+
it('should get non-existed value', () => {
14+
const cache = new Cache();
15+
16+
expect(cache.get('test0')).toBe(undefined);
17+
});
18+
19+
it('should get existed value', () => {
20+
const cache = new Cache();
21+
cache.memo('test0', () => 'callbackValue');
22+
23+
expect(cache.get('test0')).toBe('callbackValue');
24+
});
25+
26+
it('should memo value', () => {
27+
const cache = new Cache();
28+
const valueCallback = jest.fn().mockReturnValue(1).mockReturnValueOnce(2);
29+
const memoValue = cache.memo('test', valueCallback);
30+
31+
expect(cache.get('test')).toBe(memoValue);
32+
expect(cache.size).toBe(1);
33+
});
34+
35+
it('should memo twice for deleted value', () => {
36+
const cache = new Cache();
37+
const valueCallback1 = jest.fn().mockReturnValue(1).mockReturnValueOnce(2);
38+
const valueCallback2 = jest.fn().mockReturnValue(1).mockReturnValueOnce(2);
39+
const memoValue1 = cache.memo('test1', valueCallback1);
40+
const memoValue2 = cache.memo('test2', valueCallback2);
41+
cache.delete('test1');
42+
43+
expect(cache.size).toBe(1);
44+
expect(cache.memo('test1', valueCallback1)).not.toBe(memoValue1);
45+
expect(cache.memo('test2', valueCallback2)).toBe(memoValue2);
46+
expect(cache.size).toBe(2);
47+
});
48+
49+
it('should delete existed value', () => {
50+
const cache = new Cache();
51+
cache.memo('test1', () => 'callbackValue1');
52+
cache.delete('test1');
53+
54+
expect(cache.get('test1')).toBe(undefined);
55+
expect(cache.size).toBe(0);
56+
});
57+
58+
it('should delete non-existed value', () => {
59+
const cache = new Cache();
60+
cache.memo('test1', () => 'callbackValue1');
61+
cache.memo('test2', () => 'callbackValue2');
62+
cache.delete('non-existed');
63+
64+
expect(cache.size).toBe(2);
65+
});
66+
67+
it('should clear all values', () => {
68+
const cache = new Cache();
69+
cache.memo('test0', () => 'callbackValue');
70+
cache.memo('test1', () => 'callbackValue');
71+
cache.clear();
72+
73+
expect(cache.size).toBe(0);
74+
});
75+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { isDefined } from '@js/core/utils/type';
2+
3+
export class Cache {
4+
private readonly cache = new Map();
5+
6+
get size(): number { return this.cache.size; }
7+
8+
clear(): void {
9+
this.cache.clear();
10+
}
11+
12+
get<R>(name: string): R | undefined {
13+
return this.cache.get(name) as R | undefined;
14+
}
15+
16+
memo<R>(name: string, valueCallback: () => R): R {
17+
if (!this.cache.has(name)) {
18+
const value = valueCallback();
19+
20+
if (isDefined(value)) {
21+
this.cache.set(name, value);
22+
}
23+
}
24+
25+
return this.cache.get(name) as R;
26+
}
27+
28+
delete(name: string): void {
29+
this.cache.delete(name);
30+
}
31+
}
32+
33+
export const globalCache = {
34+
timezones: new Cache(),
35+
};

packages/devextreme/js/__internal/scheduler/m_recurrence.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ class RecurrenceProcessor {
121121

122122
_getLocalMachineOffset(rruleDate) {
123123
const machineTimezoneOffset = timeZoneUtils.getClientTimezoneOffset(rruleDate);
124-
const machineTimezoneName = dateUtils.getMachineTimezoneName();
124+
const machineTimezoneName = timeZoneUtils.getMachineTimezoneName();
125125
const result = [machineTimezoneOffset];
126126

127127
// NOTE: Workaround for the RRule bug with timezones greater than GMT+12 (e.g. Apia Standard Time GMT+13)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {
2+
beforeAll, describe, expect, it,
3+
} from '@jest/globals';
4+
5+
import { globalCache } from './global_cache';
6+
import timeZoneUtils from './m_utils_time_zone';
7+
import timeZoneList from './timezones/timezone_list';
8+
9+
describe('timezone utils', () => {
10+
beforeAll(() => {
11+
globalCache.timezones.clear();
12+
});
13+
14+
describe('calculateTimezoneByValue', () => {
15+
it('should work faster after first run', () => {
16+
let now = Date.now();
17+
timeZoneList.value.forEach((timezone) => {
18+
timeZoneUtils.calculateTimezoneByValue(timezone);
19+
});
20+
const delta1 = Date.now() - now; // 41
21+
now = Date.now();
22+
timeZoneList.value.forEach((timezone) => {
23+
timeZoneUtils.calculateTimezoneByValue(timezone);
24+
});
25+
const delta2 = Date.now() - now; // 6
26+
27+
expect(globalCache.timezones.size).toBe(timeZoneList.value.length);
28+
expect(delta2).toBeLessThan(delta1 / 5);
29+
});
30+
});
31+
});

packages/devextreme/js/__internal/scheduler/m_utils_time_zone.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { dateUtilsTs } from '@ts/core/utils/date';
55
import { macroTaskArray } from '@ts/scheduler/utils/index';
66

77
import dateUtils from '../../core/utils/date';
8+
import { globalCache } from './global_cache';
89
import DateAdapter from './m_date_adapter';
910
import timeZoneDataUtils from './timezones/m_utils_timezones_data';
1011
import timeZoneList from './timezones/timezone_list';
@@ -21,6 +22,7 @@ export interface TimezoneData extends TimezoneLabel {
2122
offset?: number;
2223
}
2324

25+
const timeZoneListSet = new Set(timeZoneList.value);
2426
const toMs = dateUtils.dateToMilliseconds;
2527
const MINUTES_IN_HOUR = 60;
2628
const MS_IN_MINUTE = 60000;
@@ -103,7 +105,7 @@ const calculateTimezoneByValue = (timeZone: string | undefined, date = new Date(
103105
return undefined;
104106
}
105107

106-
const isValidTimezone = timeZoneList.value.includes(timeZone);
108+
const isValidTimezone = timeZoneListSet.has(timeZone);
107109
if (!isValidTimezone) {
108110
errors.log('W0009', timeZone);
109111
return undefined;
@@ -125,10 +127,10 @@ const calculateTimezoneByValue = (timeZone: string | undefined, date = new Date(
125127
const getStringOffset = (timeZone: string, date = new Date()): string | undefined => {
126128
let result = '';
127129
try {
128-
const dateTimeFormat = new Intl.DateTimeFormat('en-US', {
130+
const dateTimeFormat = globalCache.timezones.memo(`intl${timeZone}`, () => new Intl.DateTimeFormat('en-US', {
129131
timeZone,
130132
timeZoneName: 'longOffset',
131-
} as any);
133+
}));
132134

133135
result = dateTimeFormat
134136
.formatToParts(date)
@@ -227,12 +229,12 @@ const getClientTimezoneOffset = (date = new Date()) => date.getTimezoneOffset()
227229

228230
const getDiffBetweenClientTimezoneOffsets = (firstDate = new Date(), secondDate = new Date()) => getClientTimezoneOffset(firstDate) - getClientTimezoneOffset(secondDate);
229231

232+
const getMachineTimezoneName = () => globalCache.timezones.memo('localTimezone', () => dateUtils.getMachineTimezoneName());
233+
230234
const isEqualLocalTimeZone = (timeZoneName, date = new Date()) => {
231-
if (Intl) {
232-
const localTimeZoneName = Intl.DateTimeFormat().resolvedOptions().timeZone;
233-
if (localTimeZoneName === timeZoneName) {
234-
return true;
235-
}
235+
const localTimeZoneName = getMachineTimezoneName();
236+
if (localTimeZoneName && localTimeZoneName === timeZoneName) {
237+
return true;
236238
}
237239

238240
return isEqualLocalTimeZoneByDeclaration(timeZoneName, date);
@@ -380,6 +382,7 @@ const utils = {
380382
isTimezoneChangeInDate,
381383
getDateWithoutTimezoneChange,
382384
hasDSTInLocalTimeZone,
385+
getMachineTimezoneName,
383386
isEqualLocalTimeZone,
384387
isEqualLocalTimeZoneByDeclaration,
385388

0 commit comments

Comments
 (0)