ebox/ebox-cache.el
2026-08-26 00:09:53 +08:00

217 lines
7.7 KiB
EmacsLisp

;;; ebox-cache.el --- Cache ownership metadata for Ebox -*- lexical-binding: t; -*-
;;; Commentary:
;; Owns shared cache contracts, cache invalidation metadata, and cache
;; reporting helpers. It does not own rendering, layout, or buffer mutation.
;;; Code:
(require 'cl-lib)
(require 'subr-x)
(cl-defstruct (ebox-cache-spec
(:constructor ebox-cache-spec-create))
name owner scope key-deps invalidates lifetime value-owner)
(defvar ebox-cache--registry (make-hash-table :test 'eq)
"Registry of cache specs keyed by cache name.")
(defvar ebox-cache--buffer-report-table (make-hash-table :test 'eq)
"Buffer-keyed cache report facts for the current incremental operation.")
(defvar ebox-cache-report-buffer nil
"Dynamically bound buffer whose cache report should be appended to reports.")
(defconst ebox-cache--default-specs
'((:name measurement
:owner ebox-measure
:scope display
:key-deps (string face text-scale display-signature)
:invalidates (font frame text-scale display-signature)
:lifetime global-or-display
:value-owner ebox-measure)
(:name computed-style
:owner ebox-style
:scope node
:key-deps (specified-style-signature class-version theme-version)
:invalidates (style class theme)
:lifetime buffer-runtime
:value-owner ebox-style)
(:name viewport-dependency
:owner ebox-incremental
:scope buffer-runtime
:key-deps (root-id computed-style-tree-version viewport-width)
:invalidates (width display children style viewport)
:lifetime buffer-runtime
:value-owner ebox-incremental)
(:name layout-fragment
:owner ebox-layout
:scope formatting-context
:key-deps (node-id computed-style-signature child-signatures available-size)
:invalidates (geometry structure viewport)
:lifetime buffer-runtime
:value-owner ebox-layout)
(:name render-body
:owner ebox-buffer-backend
:scope buffer-render
:key-deps (fragment-signature paint-signature backend-display-signature)
:invalidates (paint geometry structure backend-display)
:lifetime buffer-runtime
:value-owner ebox-buffer-backend)
(:name snapshot-signature
:owner ebox-fragment
:scope buffer-runtime
:key-deps (node-id fragment-version style-signature)
:invalidates (style geometry placement structure)
:lifetime buffer-runtime
:value-owner ebox-fragment))
"Default cache contracts for existing Ebox engine caches.")
(defun ebox-cache-clear-registry ()
"Clear the cache spec registry."
(clrhash ebox-cache--registry))
(cl-defun ebox-cache-register
(&key name owner scope key-deps invalidates lifetime value-owner)
"Register and return a cache spec."
(unless name
(error "ebox-cache: cache spec requires :name"))
(let ((spec (ebox-cache-spec-create
:name name
:owner owner
:scope scope
:key-deps key-deps
:invalidates invalidates
:lifetime lifetime
:value-owner value-owner)))
(puthash name spec ebox-cache--registry)
spec))
(defun ebox-cache-spec (name)
"Return registered cache spec NAME."
(gethash name ebox-cache--registry))
(defun ebox-cache-register-default-specs ()
"Register default cache specs and return the registry."
(dolist (spec ebox-cache--default-specs)
(apply #'ebox-cache-register spec))
ebox-cache--registry)
(defun ebox-cache-reset-default-registry ()
"Reset the registry to the default engine cache specs."
(ebox-cache-clear-registry)
(ebox-cache-register-default-specs))
(defun ebox-cache--intersects-p (left right)
"Return non-nil when LEFT and RIGHT share a symbol."
(cl-intersection left right :test #'eq))
(defun ebox-cache-invalidated-spec-names (dirty-kinds)
"Return cache spec names whose invalidation rules intersect DIRTY-KINDS."
(let (names)
(maphash
(lambda (name spec)
(when (ebox-cache--intersects-p
dirty-kinds (ebox-cache-spec-invalidates spec))
(push name names)))
ebox-cache--registry)
names))
(defun ebox-cache--empty-report ()
"Return a new empty cache report plist."
(list :cache-invalidated nil
:cache-hit-count 0
:cache-miss-count 0
:cache-scope nil
:cache-fallback-reason nil))
(defun ebox-cache-clear-report (buffer)
"Clear cache report facts for BUFFER."
(when buffer
(ebox-cache-publish-report buffer (ebox-cache--empty-report))))
(defun ebox-cache-publish-report (buffer report)
"Atomically replace BUFFER's cache REPORT facts."
(when buffer
(puthash buffer (copy-sequence report)
ebox-cache--buffer-report-table)))
(defun ebox-cache-snapshot-report (buffer)
"Return an exact restorable snapshot of BUFFER's cache report entry."
(let* ((missing (make-symbol "missing"))
(report (and buffer
(gethash buffer ebox-cache--buffer-report-table missing))))
(list :present-p (and buffer (not (eq report missing)))
:report (unless (eq report missing) (copy-sequence report)))))
(defun ebox-cache-restore-report (buffer snapshot)
"Restore BUFFER's cache report entry from SNAPSHOT."
(when buffer
(if (plist-get snapshot :present-p)
(puthash buffer (copy-sequence (plist-get snapshot :report))
ebox-cache--buffer-report-table)
(remhash buffer ebox-cache--buffer-report-table))))
(defun ebox-cache-discard-report (buffer)
"Discard cache report facts owned by BUFFER."
(when buffer
(remhash buffer ebox-cache--buffer-report-table)))
(defun ebox-cache--report-state (buffer)
"Return mutable cache report state for BUFFER."
(when buffer
(or (gethash buffer ebox-cache--buffer-report-table)
(let ((state (ebox-cache--empty-report)))
(puthash buffer state ebox-cache--buffer-report-table)
state))))
(defun ebox-cache--push-unique-values (state key values)
"Append VALUES uniquely to STATE at KEY."
(dolist (value values)
(when value
(cl-pushnew value (plist-get state key) :test #'equal))))
(defun ebox-cache--spec-scopes (cache-names)
"Return distinct scopes for CACHE-NAMES."
(let (scopes)
(dolist (name cache-names)
(when-let* ((spec (ebox-cache-spec name)))
(cl-pushnew (ebox-cache-spec-scope spec) scopes :test #'equal)))
scopes))
(defun ebox-cache-record-hit (buffer cache-name)
"Record a CACHE-NAME hit for BUFFER's current update report."
(when-let* ((state (ebox-cache--report-state buffer)))
(plist-put state :cache-hit-count
(1+ (or (plist-get state :cache-hit-count) 0)))
(ebox-cache--push-unique-values
state :cache-scope (ebox-cache--spec-scopes (list cache-name)))))
(defun ebox-cache-record-miss (buffer cache-name)
"Record a CACHE-NAME miss for BUFFER's current update report."
(when-let* ((state (ebox-cache--report-state buffer)))
(plist-put state :cache-miss-count
(1+ (or (plist-get state :cache-miss-count) 0)))
(ebox-cache--push-unique-values
state :cache-scope (ebox-cache--spec-scopes (list cache-name)))))
(defun ebox-cache-record-invalidation (buffer cache-names reason)
"Record invalidated CACHE-NAMES and REASON for BUFFER."
(when-let* ((state (ebox-cache--report-state buffer)))
(ebox-cache--push-unique-values state :cache-invalidated cache-names)
(ebox-cache--push-unique-values
state :cache-scope (ebox-cache--spec-scopes cache-names))
(plist-put state :cache-fallback-reason reason)))
(defun ebox-cache-report (buffer)
"Return cache report plist for BUFFER."
(copy-sequence
(or (gethash buffer ebox-cache--buffer-report-table)
(ebox-cache--empty-report))))
(ebox-cache-register-default-specs)
(provide 'ebox-cache)
;;; ebox-cache.el ends here