WIP: memoize creating hashable tuples from ot.Paint
to speed up https://github.com/googlefonts/nanoemoji/issues/218
diff --git a/Lib/fontTools/colorLib/builder.py b/Lib/fontTools/colorLib/builder.py
index d5084f4..3912914 100644
--- a/Lib/fontTools/colorLib/builder.py
+++ b/Lib/fontTools/colorLib/builder.py
@@ -436,20 +436,6 @@
raise TypeError(obj)
-def _as_tuple(obj) -> Tuple[Any, ...]:
- # start simple, who even cares about cyclic graphs or interesting field types
- def _tuple_safe(value):
- if isinstance(value, enum.Enum):
- return value
- elif hasattr(value, "__dict__"):
- return tuple((k, _tuple_safe(v)) for k, v in value.__dict__.items())
- elif isinstance(value, collections.abc.MutableSequence):
- return tuple(_tuple_safe(e) for e in value)
- return value
-
- return tuple(_tuple_safe(obj))
-
-
def _reuse_ranges(num_layers: int) -> Generator[Tuple[int, int], None, None]:
# TODO feels like something itertools might have already
for lbound in range(num_layers):
@@ -465,11 +451,13 @@
slices: List[ot.Paint]
layers: List[ot.Paint]
reusePool: Mapping[Tuple[Any, ...], int]
+ paintTuples: Mapping[int, Tuple[Any, ...]]
def __init__(self):
self.slices = []
self.layers = []
self.reusePool = {}
+ self.paintTuples = {}
def buildPaintSolid(
self, paletteIndex: int, alpha: _ScalarInput = _DEFAULT_ALPHA
@@ -605,7 +593,9 @@
reverse=True,
)
for lbound, ubound in ranges:
- reuse_lbound = self.reusePool.get(_as_tuple(paints[lbound:ubound]), -1)
+ reuse_lbound = self.reusePool.get(
+ self._paints_as_tuple(paints[lbound:ubound]), -1
+ )
if reuse_lbound == -1:
continue
new_slice = ot.Paint()
@@ -622,7 +612,7 @@
# Register our parts for reuse
for lbound, ubound in _reuse_ranges(len(paints)):
- self.reusePool[_as_tuple(paints[lbound:ubound])] = (
+ self.reusePool[self._paints_as_tuple(paints[lbound:ubound])] = (
lbound + ot_paint.FirstLayerIndex
)
@@ -660,6 +650,32 @@
layers.Paint = self.layers
return layers
+ def _paints_as_tuple(self, paints: Sequence[ot.Paint]) -> Tuple[Any, ...]:
+ # Return a hashable tuple of tuples from the ot.Paint sequence.
+ # Cache result by id(paint) to avoid computing more than once per paint.
+ # DO NOT MUTATE the paint after this, otherwise you invalidate the cache!
+
+ # Start simple, who even cares about cyclic graphs or interesting field types
+ def _tuple_safe(value):
+ is_paint = isinstance(value, ot.Paint)
+
+ if is_paint and id(value) in self.paintTuples:
+ return self.paintTuples[id(value)]
+
+ result = value
+ if isinstance(value, enum.Enum):
+ result = value
+ elif hasattr(value, "__dict__"):
+ result = tuple((k, _tuple_safe(v)) for k, v in value.__dict__.items())
+ elif isinstance(value, collections.abc.MutableSequence):
+ result = tuple(_tuple_safe(e) for e in value)
+
+ if is_paint:
+ self.paintTuples[id(value)] = result
+ return result
+
+ return tuple(_tuple_safe(paint) for paint in paints)
+
LayerV1ListBuilder._buildFunctions = {
pf.value: getattr(LayerV1ListBuilder, "build" + pf.name)