跳转至

engines.cache · 图片缓存

绘制结果缓存。

为什么需要

tkfluent 的一次 hover 会让同一个 RoundRectSpec 被反复绘制(rest→hover 的 渐变动画最多几十帧,多个同尺寸按钮更是完全同参)。没有缓存时每次都要重新 光栅化并新建 PhotoImage——实测这正是端到端耗时的大头。

缓存键

(引擎名, spec)。spec 是 frozen dataclass,天然可哈希。

生命周期与"空图"陷阱

PhotoImage 绑定在具体的 Tk 解释器上,且 一旦被垃圾回收,引用它的 canvas item 会变成空白。因此这里刻意 不做 LRU 淘汰,而是采用 "像素预算 + 满了就不再写入"的策略:已经发出去的图片永远保持存活, 新的、缓存不下的规格就退化为"每次都重新渲染"——只损失速度,绝不损坏画面。

解释器用 id() 作键(_tkinter.tkapp 不支持 weakref),并用对象同一性 校验,避免 id 复用导致的串台。

ImageCache

单个 Tk 解释器对应的绘制结果缓存。

键是 (引擎名, 图元类型, spec);值是 (PhotoImage, 像素数)。 所有方法都是线程安全的(由模块级锁保护),可以直接在多线程里查询统计。

:param budget: 像素预算;超出后新规格不再写入缓存(见模块文档里的说明)

源代码位于: tkdeft\engines\cache.py
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
class ImageCache:
    """单个 Tk 解释器对应的绘制结果缓存。

    键是 ``(引擎名, 图元类型, spec)``;值是 ``(PhotoImage, 像素数)``。
    所有方法都是线程安全的(由模块级锁保护),可以直接在多线程里查询统计。

    :param budget: 像素预算;超出后新规格不再写入缓存(见模块文档里的说明)
    """

    __slots__ = ("_entries", "_pixels", "_budget", "_hits", "_misses", "_overflow")

    def __init__(self, budget: int = DEFAULT_PIXEL_BUDGET) -> None:
        self._entries: "OrderedDict[Any, Tuple[Any, int]]" = OrderedDict()
        self._pixels = 0
        self._budget = budget
        self._hits = 0
        self._misses = 0
        self._overflow = 0

    # -- 查询 -------------------------------------------------------------
    def get(self, key: Any):
        """取缓存图片;未命中返回 ``None``(同时累计命中率统计)。"""
        item = self._entries.get(key)
        if item is None:
            self._misses += 1
            return None
        self._hits += 1
        self._entries.move_to_end(key)
        return item[0]

    def __contains__(self, key: Any) -> bool:
        return key in self._entries

    def __len__(self) -> int:
        return len(self._entries)

    # -- 写入 -------------------------------------------------------------
    def put(self, key: Any, photo, pixels: int) -> bool:
        """写入缓存;超出预算时 **静默放弃**(已发出的图片保持有效)。

        :param key: ``(引擎名, 图元类型, spec)``
        :param photo: ``tkinter.PhotoImage``
        :param pixels: 该图片占用的像素数(用于预算核算)
        :returns: 是否真的写入了缓存;``False`` 表示这次只能"每次重新渲染"
        """
        if key in self._entries:
            return True
        if self._pixels + pixels > self._budget:
            self._overflow += 1
            return False
        self._entries[key] = (photo, pixels)
        self._pixels += pixels
        return True

    # -- 维护 -------------------------------------------------------------
    def clear(self) -> int:
        """清空缓存并返回被清掉的条目数。

        .. warning::
           已经发出去的 :class:`~tkinter.PhotoImage` 仍然由调用方(画布)持有,
           因此清空缓存不会让画面立刻变空白;真正的风险在于之后画布重新引用
           同一规格时会重新渲染。
        """
        count = len(self._entries)
        self._entries.clear()
        self._pixels = 0
        return count

    @property
    def budget(self) -> int:
        """像素预算。"""
        return self._budget

    @budget.setter
    def budget(self, value: int) -> None:
        self._budget = max(0, int(value))

    @property
    def pixels(self) -> int:
        """当前缓存占用的像素数。"""
        return self._pixels

    def stats(self) -> Dict[str, Any]:
        """这条缓存的统计信息(条目数、像素占用、命中率、溢出次数)。"""
        total = self._hits + self._misses
        return {
            "entries": len(self._entries),
            "pixels": self._pixels,
            "budget": self._budget,
            "hits": self._hits,
            "misses": self._misses,
            "hit_rate": (self._hits / total) if total else 0.0,
            "overflow": self._overflow,
        }

    def reset_stats(self) -> None:
        """把命中/未命中/溢出计数清零(不动缓存内容)。"""
        self._hits = self._misses = self._overflow = 0

budget property writable

像素预算。

pixels property

当前缓存占用的像素数。

clear()

清空缓存并返回被清掉的条目数。

.. warning:: 已经发出去的 :class:~tkinter.PhotoImage 仍然由调用方(画布)持有, 因此清空缓存不会让画面立刻变空白;真正的风险在于之后画布重新引用 同一规格时会重新渲染。

源代码位于: tkdeft\engines\cache.py
105
106
107
108
109
110
111
112
113
114
115
116
def clear(self) -> int:
    """清空缓存并返回被清掉的条目数。

    .. warning::
       已经发出去的 :class:`~tkinter.PhotoImage` 仍然由调用方(画布)持有,
       因此清空缓存不会让画面立刻变空白;真正的风险在于之后画布重新引用
       同一规格时会重新渲染。
    """
    count = len(self._entries)
    self._entries.clear()
    self._pixels = 0
    return count

get(key)

取缓存图片;未命中返回 None(同时累计命中率统计)。

源代码位于: tkdeft\engines\cache.py
70
71
72
73
74
75
76
77
78
def get(self, key: Any):
    """取缓存图片;未命中返回 ``None``(同时累计命中率统计)。"""
    item = self._entries.get(key)
    if item is None:
        self._misses += 1
        return None
    self._hits += 1
    self._entries.move_to_end(key)
    return item[0]

put(key, photo, pixels)

写入缓存;超出预算时 静默放弃(已发出的图片保持有效)。

:param key: (引擎名, 图元类型, spec) :param photo: tkinter.PhotoImage :param pixels: 该图片占用的像素数(用于预算核算) :returns: 是否真的写入了缓存;False 表示这次只能"每次重新渲染"

源代码位于: tkdeft\engines\cache.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def put(self, key: Any, photo, pixels: int) -> bool:
    """写入缓存;超出预算时 **静默放弃**(已发出的图片保持有效)。

    :param key: ``(引擎名, 图元类型, spec)``
    :param photo: ``tkinter.PhotoImage``
    :param pixels: 该图片占用的像素数(用于预算核算)
    :returns: 是否真的写入了缓存;``False`` 表示这次只能"每次重新渲染"
    """
    if key in self._entries:
        return True
    if self._pixels + pixels > self._budget:
        self._overflow += 1
        return False
    self._entries[key] = (photo, pixels)
    self._pixels += pixels
    return True

reset_stats()

把命中/未命中/溢出计数清零(不动缓存内容)。

源代码位于: tkdeft\engines\cache.py
145
146
147
def reset_stats(self) -> None:
    """把命中/未命中/溢出计数清零(不动缓存内容)。"""
    self._hits = self._misses = self._overflow = 0

stats()

这条缓存的统计信息(条目数、像素占用、命中率、溢出次数)。

源代码位于: tkdeft\engines\cache.py
132
133
134
135
136
137
138
139
140
141
142
143
def stats(self) -> Dict[str, Any]:
    """这条缓存的统计信息(条目数、像素占用、命中率、溢出次数)。"""
    total = self._hits + self._misses
    return {
        "entries": len(self._entries),
        "pixels": self._pixels,
        "budget": self._budget,
        "hits": self._hits,
        "misses": self._misses,
        "hit_rate": (self._hits / total) if total else 0.0,
        "overflow": self._overflow,
    }

cache_stats(master=None)

缓存统计,便于性能诊断。

:param master: 只看某个解释器;None 表示汇总所有解释器 (额外带一个 interpreters 字段)

::

>>> cache_stats()
{'interpreters': 1, 'entries': 62, 'pixels': 169244, 'budget': 8000000,
 'hits': 178, 'misses': 99, 'hit_rate': 0.642, 'overflow': 0}
源代码位于: tkdeft\engines\cache.py
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
def cache_stats(master=None) -> Dict[str, Any]:
    """缓存统计,便于性能诊断。

    :param master: 只看某个解释器;``None`` 表示汇总所有解释器
        (额外带一个 ``interpreters`` 字段)

    ::

        >>> cache_stats()
        {'interpreters': 1, 'entries': 62, 'pixels': 169244, 'budget': 8000000,
         'hits': 178, 'misses': 99, 'hit_rate': 0.642, 'overflow': 0}
    """
    with _LOCK:
        if master is not None:
            return _cache_for(master).stats()
        merged: Dict[str, Any] = {
            "interpreters": len(_CACHES),
            "entries": 0,
            "pixels": 0,
            "budget": _PIXEL_BUDGET,
            "hits": 0,
            "misses": 0,
            "overflow": 0,
        }
        for _, cache in _CACHES.values():
            stats = cache.stats()
            for field in ("entries", "pixels", "hits", "misses", "overflow"):
                merged[field] += stats[field]
        total = merged["hits"] + merged["misses"]
        merged["hit_rate"] = (merged["hits"] / total) if total else 0.0
        return merged

clear_cache(master=None)

清空缓存,返回被清掉的条目数。

:param master: 只清某个解释器的缓存;None 表示清空全部解释器

.. warning:: 清空后,仍然存在于画布上的 item 会因为图片被回收而变空白。 仅在确实要重建整个界面时调用。

源代码位于: tkdeft\engines\cache.py
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
def clear_cache(master=None) -> int:
    """清空缓存,返回被清掉的条目数。

    :param master: 只清某个解释器的缓存;``None`` 表示清空全部解释器

    .. warning::
       清空后,仍然存在于画布上的 item 会因为图片被回收而变空白。
       仅在确实要重建整个界面时调用。
    """
    with _LOCK:
        if master is None:
            total = sum(cache.clear() for _, cache in _CACHES.values())
            _CACHES.clear()
            return total
        return _cache_for(master).clear()

get_cache(master)

取(必要时创建)某个 Tk 解释器对应的缓存。

:param master: 任意 Tk 控件,或 tkapp 本身 :returns: 绑定到该解释器的 :class:ImageCache

源代码位于: tkdeft\engines\cache.py
196
197
198
199
200
201
202
def get_cache(master) -> ImageCache:
    """取(必要时创建)某个 Tk 解释器对应的缓存。

    :param master: 任意 Tk 控件,或 ``tkapp`` 本身
    :returns: 绑定到该解释器的 :class:`ImageCache`
    """
    return _cache_for(master)

set_cache_budget(pixels)

设置像素预算,并立即应用到所有已存在的解释器缓存。

:param pixels: 预算(像素数);None 表示恢复默认 :data:DEFAULT_PIXEL_BUDGET(注意:并不是"不限制")

源代码位于: tkdeft\engines\cache.py
222
223
224
225
226
227
228
229
230
231
232
def set_cache_budget(pixels: Optional[int]) -> None:
    """设置像素预算,并立即应用到所有已存在的解释器缓存。

    :param pixels: 预算(像素数);``None`` 表示恢复默认
        :data:`DEFAULT_PIXEL_BUDGET`(注意:并不是"不限制")
    """
    global _PIXEL_BUDGET
    with _LOCK:
        _PIXEL_BUDGET = DEFAULT_PIXEL_BUDGET if pixels is None else max(0, int(pixels))
        for _, cache in _CACHES.values():
            cache.budget = _PIXEL_BUDGET