1818
1919import contextlib
2020from functools import lru_cache
21+ import os
2122import re
22- from typing import List , Optional , TYPE_CHECKING , Tuple
23+ from typing import TYPE_CHECKING , List , Optional , Tuple
2324
24- from metomi .isodatetime .data import Calendar , CALENDAR , Duration
25+ from metomi .isodatetime .data import CALENDAR , Calendar , Duration
2526from metomi .isodatetime .dumpers import TimePointDumper
26- from metomi .isodatetime .timezone import (
27- get_local_time_zone , get_local_time_zone_format , TimeZoneFormatMode )
2827from metomi .isodatetime .exceptions import IsodatetimeError
2928from metomi .isodatetime .parsers import ISO8601SyntaxError
30- from cylc .flow .time_parser import CylcTimeParser
29+ from metomi .isodatetime .timezone import (
30+ TimeZoneFormatMode ,
31+ get_local_time_zone ,
32+ get_local_time_zone_format ,
33+ )
34+
3135from cylc .flow .cycling import (
32- PointBase , IntervalBase , SequenceBase , ExclusionBase , cmp
36+ ExclusionBase ,
37+ IntervalBase ,
38+ PointBase ,
39+ SequenceBase ,
40+ cmp ,
3341)
3442from cylc .flow .exceptions import (
3543 CylcConfigError ,
3644 IntervalParsingError ,
3745 PointParsingError ,
3846 SequenceDegenerateError ,
39- WorkflowConfigError
47+ WorkflowConfigError ,
4048)
41- from cylc .flow .wallclock import get_current_time_string
4249from cylc .flow .parsec .validate import IllegalValueError
50+ from cylc .flow .time_parser import CylcTimeParser
51+ from cylc .flow .wallclock import get_current_time_string
52+
4353
4454if TYPE_CHECKING :
4555 from metomi .isodatetime .data import TimePoint
4656 from metomi .isodatetime .parsers import (
47- DurationParser , TimePointParser , TimeRecurrenceParser )
57+ DurationParser ,
58+ TimePointParser ,
59+ TimeRecurrenceParser ,
60+ )
4861
4962CYCLER_TYPE_ISO8601 = "iso8601"
5063CYCLER_TYPE_SORT_KEY_ISO8601 = 1
5770 "(incompatible with [cylc]cycle point num expanded year digits = %s ?)" )
5871
5972
73+ # NOTE: We cache some datetime cycling operations to improve compute
74+ # perforance. For profiling, this can be disabled by setting the environment
75+ # variable CYLC_CYCLER_LRU_CACHE_SIZE=0.
76+
77+ # The number of cycling operations to cache:
78+ _LRU_CACHE_SIZE = int (os .environ .get ('CYLC_CYCLER_LRU_CACHE_SIZE' , '10000' ))
79+
80+ # A smaller cache for use with larger objecs (to reduce memory impact):
81+ _LARGE_LRU_CACHE_SIZE = int (_LRU_CACHE_SIZE / 100 ) if _LRU_CACHE_SIZE else 0
82+
83+
6084class WorkflowSpecifics :
6185
6286 """Store workflow-setup-specific constants and utilities here."""
@@ -123,7 +147,7 @@ def sub(self, other):
123147 ))
124148
125149 @staticmethod
126- @lru_cache (10000 )
150+ @lru_cache (_LRU_CACHE_SIZE )
127151 def _iso_point_add (point_string , interval_string , _calendar_mode ):
128152 """Add the parsed point_string to the parsed interval_string."""
129153 point = point_parse (point_string )
@@ -134,23 +158,23 @@ def _cmp(self, other: 'ISO8601Point') -> int:
134158 return self ._iso_point_cmp (self .value , other .value , CALENDAR .mode )
135159
136160 @staticmethod
137- @lru_cache (10000 )
161+ @lru_cache (_LRU_CACHE_SIZE )
138162 def _iso_point_cmp (point_string , other_point_string , _calendar_mode ):
139163 """Compare the parsed point_string to the other one."""
140164 point = point_parse (point_string )
141165 other_point = point_parse (other_point_string )
142166 return cmp (point , other_point )
143167
144168 @staticmethod
145- @lru_cache (10000 )
169+ @lru_cache (_LRU_CACHE_SIZE )
146170 def _iso_point_sub_interval (point_string , interval_string , _calendar_mode ):
147171 """Return the parsed point_string minus the parsed interval_string."""
148172 point = point_parse (point_string )
149173 interval = interval_parse (interval_string )
150174 return str (point - interval )
151175
152176 @staticmethod
153- @lru_cache (10000 )
177+ @lru_cache (_LRU_CACHE_SIZE )
154178 def _iso_point_sub_point (point_string , other_point_string , _calendar_mode ):
155179 """Return the difference between the two parsed point strings."""
156180 point = point_parse (point_string )
@@ -216,7 +240,7 @@ def __bool__(self):
216240 return self ._iso_interval_nonzero (self .value )
217241
218242 @staticmethod
219- @lru_cache (10000 )
243+ @lru_cache (_LRU_CACHE_SIZE )
220244 def _iso_interval_abs (interval_string , other_interval_string ):
221245 """Return the absolute (non-negative) value of an interval_string."""
222246 interval = interval_parse (interval_string )
@@ -226,38 +250,38 @@ def _iso_interval_abs(interval_string, other_interval_string):
226250 return interval_string
227251
228252 @staticmethod
229- @lru_cache (10000 )
253+ @lru_cache (_LRU_CACHE_SIZE )
230254 def _iso_interval_add (interval_string , other_interval_string ):
231255 """Return one parsed interval_string plus the other one."""
232256 interval = interval_parse (interval_string )
233257 other = interval_parse (other_interval_string )
234258 return str (interval + other )
235259
236260 @staticmethod
237- @lru_cache (10000 )
261+ @lru_cache (_LRU_CACHE_SIZE )
238262 def _iso_interval_cmp (interval_string , other_interval_string ):
239263 """Compare one parsed interval_string with the other one."""
240264 interval = interval_parse (interval_string )
241265 other = interval_parse (other_interval_string )
242266 return cmp (interval , other )
243267
244268 @staticmethod
245- @lru_cache (10000 )
269+ @lru_cache (_LRU_CACHE_SIZE )
246270 def _iso_interval_sub (interval_string , other_interval_string ):
247271 """Subtract one parsed interval_string from the other one."""
248272 interval = interval_parse (interval_string )
249273 other = interval_parse (other_interval_string )
250274 return str (interval - other )
251275
252276 @staticmethod
253- @lru_cache (10000 )
277+ @lru_cache (_LRU_CACHE_SIZE )
254278 def _iso_interval_mul (interval_string , factor ):
255279 """Multiply one parsed interval_string's values by factor."""
256280 interval = interval_parse (interval_string )
257281 return str (interval * factor )
258282
259283 @staticmethod
260- @lru_cache (10000 )
284+ @lru_cache (_LRU_CACHE_SIZE )
261285 def _iso_interval_nonzero (interval_string ):
262286 """Return whether the parsed interval_string is a null interval."""
263287 interval = interval_parse (interval_string )
@@ -318,7 +342,6 @@ class ISO8601Sequence(SequenceBase):
318342
319343 TYPE = CYCLER_TYPE_ISO8601
320344 TYPE_SORT_KEY = CYCLER_TYPE_SORT_KEY_ISO8601
321- _MAX_CACHED_POINTS = 100
322345
323346 __slots__ = ('dep_section' , 'context_start_point' , 'context_end_point' ,
324347 'offset' , '_cached_first_point_values' ,
@@ -346,7 +369,9 @@ def __init__(
346369
347370 # cache is_on_sequence
348371 # see B019 - https://github.com/PyCQA/flake8-bugbear#list-of-warnings
349- self .is_on_sequence = lru_cache (maxsize = 100 )(self ._is_on_sequence )
372+ self .is_on_sequence = lru_cache (_LARGE_LRU_CACHE_SIZE )(
373+ self ._is_on_sequence
374+ )
350375
351376 if (
352377 context_start_point is None
@@ -462,8 +487,7 @@ def is_valid(self, point):
462487 return self ._cached_valid_point_booleans [point .value ]
463488 except KeyError :
464489 is_valid = self .is_on_sequence (point )
465- if (len (self ._cached_valid_point_booleans ) >
466- self ._MAX_CACHED_POINTS ):
490+ if len (self ._cached_valid_point_booleans ) > _LARGE_LRU_CACHE_SIZE :
467491 self ._cached_valid_point_booleans .popitem ()
468492 self ._cached_valid_point_booleans [point .value ] = is_valid
469493 return is_valid
@@ -555,14 +579,15 @@ def _check_and_cache_next_point(self, point, next_point):
555579 )
556580
557581 # Cache the answer for point -> next_point.
558- if (len (self ._cached_next_point_values ) >
559- self ._MAX_CACHED_POINTS ):
582+ if len (self ._cached_next_point_values ) > _LARGE_LRU_CACHE_SIZE :
560583 self ._cached_next_point_values .popitem ()
561584 self ._cached_next_point_values [point .value ] = next_point .value
562585
563586 # Cache next_point as a valid starting point for this recurrence.
564- if (len (self ._cached_next_point_values ) >
565- self ._MAX_CACHED_POINTS ):
587+ if (
588+ _LARGE_LRU_CACHE_SIZE
589+ and len (self ._cached_next_point_values ) > _LARGE_LRU_CACHE_SIZE
590+ ):
566591 self ._cached_recent_valid_points .pop (0 )
567592 self ._cached_recent_valid_points .append (next_point )
568593
@@ -600,8 +625,10 @@ def get_first_point(
600625 # Check multiple exclusions
601626 if ret and ret in self .exclusions :
602627 return self .get_next_point_on_sequence (ret )
603- if (len (self ._cached_first_point_values ) >
604- self ._MAX_CACHED_POINTS ):
628+ if (
629+ len (self ._cached_first_point_values )
630+ > _LARGE_LRU_CACHE_SIZE
631+ ):
605632 self ._cached_first_point_values .popitem ()
606633 self ._cached_first_point_values [point .value ] = (
607634 first_point_value )
@@ -950,7 +977,7 @@ def is_offset_absolute(offset_string):
950977 return False
951978
952979
953- @lru_cache (10000 )
980+ @lru_cache (_LRU_CACHE_SIZE )
954981def _interval_parse (interval_string ):
955982 """Parse an interval_string into a proper Duration object."""
956983 return WorkflowSpecifics .interval_parser .parse (interval_string )
@@ -965,7 +992,7 @@ def point_parse(point_string: str) -> 'TimePoint':
965992 )
966993
967994
968- @lru_cache (10000 )
995+ @lru_cache (_LRU_CACHE_SIZE )
969996def _point_parse (point_string : str , _dump_fmt , _tz ) -> 'TimePoint' :
970997 """Parse a point_string into a proper TimePoint object.
971998
0 commit comments