diff --git a/DEVELOPER.md b/DEVELOPER.md index fa4a1d4..1f0ffc6 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -193,6 +193,16 @@ C module 1.4: `ekp-c-break-with-arrays` takes 14 args (…, forbidden-positions, tail-protrudes, hyphen-protrude); batch vectors have 14 elements; `ekp-c-set-penalties` takes 4–7. +Performance after the feature wave (byte-compiled + C, Apple +Silicon, batch): justify zh w=200 ≈ 54 ms, range zh ≈ 117 ms — +justify at parity with the pre-feature numbers, range ≈ +55 % from +the larger box count. Hot-path caches: per-char `ekp--str-type' +memo, interned glue strings, per-(paragraph, width) rendered-output +cache in the dp-cache (capped at 64 widths). Continuous-reflow +reality check (60-paragraph, 26 k-char article, region layer +included): ≈ 73 ms per width change cold, less on revisit; +incremental single-paragraph re-justify after an edit ≈ 17 ms. + ## 6. Looseness `ekp-looseness` ≠ 0 switches to `ekp--dp-run-loose`, a full diff --git a/DEVELOPER_ZH.md b/DEVELOPER_ZH.md index f9f3406..b97ac0a 100644 --- a/DEVELOPER_ZH.md +++ b/DEVELOPER_ZH.md @@ -170,6 +170,13 @@ C 模块 1.4:`ekp-c-break-with-arrays` 14 参(…、 forbidden-positions、tail-protrudes、hyphen-protrude);batch 向量 14 元;`ekp-c-set-penalties` 4–7 参。 +特性完成后的性能(字节编译 + C,Apple Silicon,batch):justify zh +w=200 ≈ 54 ms、range zh ≈ 117 ms——justify 与特性前持平,range 因盒 +数增加约 +55%。热路径缓存:`ekp--str-type` 按字符记忆化、glue 字符 +串驻留、(段落, 宽度) 渲染结果缓存进 dp-cache(上限 64 个宽度)。连 +续变宽实测(60 段 2.6 万字文章,含 region 层全链路):每次变宽约 +73 ms,重访宽度更快;编辑后单段增量重排约 17 ms。 + ## 6. Looseness `ekp-looseness` ≠ 0 时切换到 `ekp--dp-run-loose`:完整的 diff --git a/ekp-region.el b/ekp-region.el index 1878627..a7653db 100644 --- a/ekp-region.el +++ b/ekp-region.el @@ -238,7 +238,13 @@ tails. Text the user typed into the justified region is preserved." (remove-text-properties pos (1+ pos) '(ekp-hidden nil display nil)) (forward-char 1)) - (t (forward-char 1))))) + ;; Plain text: our markers are sparse, so hop straight + ;; to the next property boundary instead of stepping + ;; char by char. + (t (goto-char (min (marker-position end-m) + (next-property-change pos nil + (marker-position + end-m)))))))) (remove-text-properties (min beg end) end-m '(ekp-justified nil))) (set-marker end-m nil)))) diff --git a/ekp.el b/ekp.el index 8f45e82..9a3c69d 100644 --- a/ekp.el +++ b/ekp.el @@ -173,6 +173,9 @@ when non-zero the C module is bypassed automatically.") ;; zeros when `ekp-protrusion' is off); hyphen-protrude = same for ;; the soft hyphen at a hyphenated break. tail-protrudes hyphen-protrude + ;; Lazily memoized (START . END) offsets of each box in the source + ;; string (render-time lossless payloads); content-invariant. + (box-offsets-memo nil) ;; Glue params snapshot at para creation time (plist) glue-params (dp-cache nil :type hash-table)) @@ -305,11 +308,20 @@ Returns (boxes-vector . hyphen-positions-vector)." (cons (vconcat (apply #'append (nreverse new-boxes))) (vconcat (nreverse hyphen-idxs))))) +(defvar ekp--str-type-table (make-char-table 'ekp-str-type) + "Per-character memo for `ekp--str-type' (a pure classification).") + (defun ekp--str-type (str) "Classify single-character string STR. Returns one of `space', `latin', `cjk', `cjk-open', `cjk-close'. `cjk-open' must not end a line; `cjk-close' must not start one \(kinsoku) — enforced via `ekp-para-breaks-allowed'." + (let ((c (aref str 0))) + (or (aref ekp--str-type-table c) + (aset ekp--str-type-table c (ekp--str-type-1 str))))) + +(defun ekp--str-type-1 (str) + "Uncached `ekp--str-type'." (cond ;; Whitespace or zero-width characters ((or (string-blank-p str) (= (string-width str) 0)) 'space) @@ -390,9 +402,12 @@ class instead.") Same box-level rule as `ekp--no-line-start-chars'; fullwidth openers are covered by the `cjk-open' class.") -(defun ekp--box-pure-set-p (box set) - "Non-nil when BOX is non-empty and every char is a member of SET." - (let ((len (length box)) (chars (append set nil)) (i 0) (all t)) +(defconst ekp--no-line-start-char-list (append ekp--no-line-start-chars nil)) +(defconst ekp--no-line-end-char-list (append ekp--no-line-end-chars nil)) + +(defun ekp--box-pure-set-p (box chars) + "Non-nil when BOX is non-empty and every char is a member of CHARS." + (let ((len (length box)) (i 0) (all t)) (when (> len 0) (while (and all (< i len)) (unless (memq (aref box i) chars) @@ -403,12 +418,12 @@ are covered by the `cjk-open' class.") (defun ekp--box-no-line-start-p (box box-type) "Non-nil if BOX must not appear at the start of a line." (or (eq (car box-type) 'cjk-close) - (ekp--box-pure-set-p box ekp--no-line-start-chars))) + (ekp--box-pure-set-p box ekp--no-line-start-char-list))) (defun ekp--box-no-line-end-p (box box-type) "Non-nil if BOX must not appear at the end of a line." (or (eq (cdr box-type) 'cjk-open) - (ekp--box-pure-set-p box ekp--no-line-end-chars))) + (ekp--box-pure-set-p box ekp--no-line-end-char-list))) (defun ekp--compute-glue-types (boxes boxes-types hyphen-positions) "Compute glue types for BOXES. Positions after HYPHEN-POSITIONS are `nws'." @@ -546,7 +561,7 @@ reduces the number of `string-pixel-width' calls." ((eq tail-type 'cjk-close) (alist-get 'cjk-close ekp-protrusion-ratios 0)) ((memq (aref box (1- (length box))) - (append ekp--no-line-start-chars nil)) + ekp--no-line-start-char-list) (alist-get 'latin-close ekp-protrusion-ratios 0)) (t 0)))) (if (> ratio 0) @@ -1819,14 +1834,34 @@ leftmost scan aligns them unambiguously." string (propertize string 'ekp-hidden t 'display ""))) +(defvar ekp--glue-string-cache (make-hash-table :test 'eql) + "PIXEL → shared glue string for empty payloads (pure, shareable).") + +(defvar ekp--glue-space-string-cache (make-hash-table :test 'eql) + "PIXEL → shared glue string for a plain single-space payload.") + (defun ekp--render-glue (pixel payload) "Render a glue of PIXEL width that replaced original text PAYLOAD. Zero-width glue renders as the hidden PAYLOAD itself, so no original -character is ever dropped." +character is ever dropped. The common payloads (empty, plain space) +are interned per width: glue strings are immutable, so sharing is +safe and avoids re-allocating properties for every gap." (cond - ((> pixel 0) - (propertize " " 'display `(space :width (,pixel)) 'ekp-glue payload)) - (t (ekp--hide-string payload)))) + ((and (= pixel 0) (string-empty-p payload)) "") + ((<= pixel 0) (ekp--hide-string payload)) + ((string-empty-p payload) + (or (gethash pixel ekp--glue-string-cache) + (puthash pixel + (propertize " " 'display `(space :width (,pixel)) + 'ekp-glue payload) + ekp--glue-string-cache))) + ((and (string= payload " ") (null (object-intervals payload))) + (or (gethash pixel ekp--glue-space-string-cache) + (puthash pixel + (propertize " " 'display `(space :width (,pixel)) + 'ekp-glue payload) + ekp--glue-space-string-cache))) + (t (propertize " " 'display `(space :width (,pixel)) 'ekp-glue payload)))) (defun ekp--hyphen-for-box (box) "Return a hyphen string styled like the end of BOX. @@ -1837,6 +1872,22 @@ The `ekp-soft-hyphen' property marks it as synthesized, so (apply #'propertize "-" 'ekp-soft-hyphen t props))) (defun ekp--pixel-justify (string line-pixel) + "Justify single-paragraph STRING to LINE-PIXEL, with render caching. +The rendered string for a (paragraph, width) pair is deterministic, +so it is stored in the paragraph's dp-cache entry and reused — resize +sweeps that revisit a width pay nothing." + (let* ((para (ekp--get-para string)) + (dp (ekp-dp-data string line-pixel)) + (hit (plist-get dp :rendered))) + (or hit + (let ((rendered (ekp--pixel-justify-1 string line-pixel)) + (cache (ekp-para-dp-cache para))) + ;; keep memory bounded during long resize sessions + (when (<= (hash-table-count cache) 64) + (puthash line-pixel (plist-put dp :rendered rendered) cache)) + rendered)))) + +(defun ekp--pixel-justify-1 (string line-pixel) "Justify single-paragraph STRING to LINE-PIXEL. The output is lossless with respect to STRING: @@ -1848,8 +1899,11 @@ The output is lossless with respect to STRING: survives as zero-display `ekp-hidden' text, - break hyphens carry `ekp-soft-hyphen'. `ekp-unjustify-region' inverts all four structurally." - (let* ((boxes (append (ekp--boxes string) nil)) - (offsets (ekp--box-offsets string boxes)) + (let* ((para (ekp--get-para string)) + (boxes (append (ekp-para-boxes para) nil)) + (offsets (or (ekp-para-box-offsets-memo para) + (setf (ekp-para-box-offsets-memo para) + (ekp--box-offsets string boxes)))) (breaks (ekp-line-breaks string line-pixel)) (num (length breaks)) (lines-glues (ekp-line-glues string line-pixel))