perf: add native automatic live append backend
This commit is contained in:
parent
cfa7c18beb
commit
3d3dda6688
@ -0,0 +1,52 @@
|
|||||||
|
# ADR: Native Backend for Automatic Live Append 2026-08-20
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Source-loaded automatic live append exceeded the interaction budget because
|
||||||
|
the strict Elisp append DP interpreted every structural transition. The
|
||||||
|
existing C backend already accepts the prepared paragraph arrays and produces
|
||||||
|
exactly the same break result, but `ekp-use-c-module=nil` previously disabled
|
||||||
|
it even inside the live append path.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Add `ekp-auto-justify-native-append`, defaulting to non-nil. When auto mode is
|
||||||
|
publishing an already prepared, context-safe 1D live append and the compatible
|
||||||
|
C module is loaded, `ekp--dp-cache-append` may use the native DP regardless of
|
||||||
|
the ordinary full-layout `ekp-use-c-module` setting. The string API and full
|
||||||
|
paragraph layout continue to obey `ekp-use-c-module` directly. Setting the new
|
||||||
|
option to nil restores pure Elisp live append; an unavailable module always
|
||||||
|
falls back to Elisp.
|
||||||
|
|
||||||
|
The native call receives the same prepared 15-field arrays and is validated by
|
||||||
|
the existing C/Elisp parity contract. No C ABI field or source projection
|
||||||
|
representation changes.
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
|
||||||
|
- Duplicate the strict Elisp DP for append: rejected after the parity
|
||||||
|
experiment required a second 160-line transition kernel and returned nil.
|
||||||
|
- Reuse final-pass transient state: rejected because artificial candidates and
|
||||||
|
surviving-path arrays are not part of the persisted state.
|
||||||
|
- Keep the source stress debt open: insufficient after the user selected the
|
||||||
|
native live-append architecture.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- A loaded C module accelerates live append even when full layout is explicitly
|
||||||
|
configured for Elisp; this is documented and user-controllable.
|
||||||
|
- The remaining source latency belongs to Elisp-owned append preparation and
|
||||||
|
semantic plan assembly, which remain exact and testable.
|
||||||
|
- Native-unavailable environments retain the previous pure-Elisp behavior.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- A public buffer regression proves native calls occur only when the option is
|
||||||
|
enabled and are absent when it is disabled.
|
||||||
|
- Existing append-chain, C/Elisp parity, fuzz, and source-clean tests remain
|
||||||
|
required; source-fresh evaluator reports native-live usage explicitly.
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
Set `ekp-auto-justify-native-append` to nil or revert the dispatch change;
|
||||||
|
the full layout API and valid C contract remain independently usable.
|
||||||
@ -58,6 +58,14 @@
|
|||||||
"Seconds before retrying a live layout deferred by IME composition."
|
"Seconds before retrying a live layout deferred by IME composition."
|
||||||
:type 'number)
|
:type 'number)
|
||||||
|
|
||||||
|
(defcustom ekp-auto-justify-native-append t
|
||||||
|
"Use the loaded native module for automatic live append DP.
|
||||||
|
This affects only the already prepared live append path. Explicit string
|
||||||
|
layout and full buffer layout still obey `ekp-use-c-module' directly; when
|
||||||
|
the native module is unavailable, live append falls back to Elisp."
|
||||||
|
:type 'boolean
|
||||||
|
:group 'ekp-buffer)
|
||||||
|
|
||||||
(defcustom ekp-auto-justify-paragraph-limit 2048
|
(defcustom ekp-auto-justify-paragraph-limit 2048
|
||||||
"Maximum hard-paragraph characters planned automatically.
|
"Maximum hard-paragraph characters planned automatically.
|
||||||
Longer paragraphs stay natural so enabling the mode, pasting, and
|
Longer paragraphs stay natural so enabling the mode, pasting, and
|
||||||
@ -1490,6 +1498,9 @@ Optional CONTEXT supplies a precomputed policy context."
|
|||||||
(equal (cadr key) (cadr old-key)))
|
(equal (cadr key) (cadr old-key)))
|
||||||
(let* ((context (ekp-buffer--policy-context width))
|
(let* ((context (ekp-buffer--policy-context width))
|
||||||
(planning-text (ekp-buffer--planning-text text context))
|
(planning-text (ekp-buffer--planning-text text context))
|
||||||
|
(ekp--allow-native-live-append
|
||||||
|
(and ekp-auto-justify-native-append
|
||||||
|
(bound-and-true-p ekp-c-module-loaded)))
|
||||||
(plan (ekp-buffer--with-policy-context context
|
(plan (ekp-buffer--with-policy-context context
|
||||||
(ekp-layout-plan-append
|
(ekp-layout-plan-append
|
||||||
old-plan planning-text width))))
|
old-plan planning-text width))))
|
||||||
|
|||||||
60
ekp.el
60
ekp.el
@ -142,6 +142,9 @@ Set to nil to force pure Elisp implementation."
|
|||||||
:type 'boolean
|
:type 'boolean
|
||||||
:group 'ekp)
|
:group 'ekp)
|
||||||
|
|
||||||
|
(defvar ekp--allow-native-live-append nil
|
||||||
|
"Non-nil when auto live append may use a loaded native DP module.")
|
||||||
|
|
||||||
;;;; Glue Parameters
|
;;;; Glue Parameters
|
||||||
;; Glue = flexible space between boxes (Knuth-Plass terminology)
|
;; Glue = flexible space between boxes (Knuth-Plass terminology)
|
||||||
;; lws = Latin Word Space, mws = Mixed (Latin-CJK), cws = CJK
|
;; lws = Latin Word Space, mws = Mixed (Latin-CJK), cws = CJK
|
||||||
@ -1791,6 +1794,17 @@ POLICY-ANALYSIS is a precomputed result from `ekp--analyze-policies'."
|
|||||||
(cl-position ?\s old :from-end t :end (1+ tail))))
|
(cl-position ?\s old :from-end t :end (1+ tail))))
|
||||||
(1+ space))))))
|
(1+ space))))))
|
||||||
|
|
||||||
|
(defun ekp--para-has-box-type-p (para type)
|
||||||
|
"Return non-nil when PARA has a box with TYPE at either edge."
|
||||||
|
(seq-some
|
||||||
|
(lambda (box-type)
|
||||||
|
(if (eq type 'cjk)
|
||||||
|
(or (memq (car box-type) '(cjk cjk-open cjk-close))
|
||||||
|
(memq (cdr box-type) '(cjk cjk-open cjk-close)))
|
||||||
|
(or (eq type (car box-type))
|
||||||
|
(eq type (cdr box-type)))))
|
||||||
|
(append (ekp-para-boxes-types para) nil)))
|
||||||
|
|
||||||
(defun ekp--append-stable-box-count (offsets cutoff)
|
(defun ekp--append-stable-box-count (offsets cutoff)
|
||||||
"Return the box index in OFFSETS beginning at CUTOFF."
|
"Return the box index in OFFSETS beginning at CUTOFF."
|
||||||
(let ((position (1- (length offsets)))
|
(let ((position (1- (length offsets)))
|
||||||
@ -1971,18 +1985,26 @@ Return nil when the tokenizer prefix cannot be reused exactly."
|
|||||||
(let* ((old (ekp-para-string para))
|
(let* ((old (ekp-para-string para))
|
||||||
(cutoff (and (> (length old) 0)
|
(cutoff (and (> (length old) 0)
|
||||||
(ekp--append-cutoff old string)))
|
(ekp--append-cutoff old string)))
|
||||||
|
(tail (and cutoff (substring string cutoff)))
|
||||||
|
(latin-font
|
||||||
|
(and tail
|
||||||
|
(if (ekp--para-has-box-type-p para 'latin)
|
||||||
|
(ekp-para-latin-font para)
|
||||||
|
(ekp-latin-font tail))))
|
||||||
|
(cjk-font
|
||||||
|
(and tail
|
||||||
|
(if (ekp--para-has-box-type-p para 'cjk)
|
||||||
|
(ekp-para-cjk-font para)
|
||||||
|
(ekp-cjk-font tail))))
|
||||||
(fonts-stable
|
(fonts-stable
|
||||||
(and cutoff
|
(and cutoff
|
||||||
(equal (ekp-para-latin-font para)
|
(equal (ekp-para-latin-font para) latin-font)
|
||||||
(ekp-latin-font string))
|
(equal (ekp-para-cjk-font para) cjk-font)))
|
||||||
(equal (ekp-para-cjk-font para)
|
|
||||||
(ekp-cjk-font string))))
|
|
||||||
(old-offsets (ekp-para-box-offsets-memo para))
|
(old-offsets (ekp-para-box-offsets-memo para))
|
||||||
(stable (and fonts-stable old-offsets
|
(stable (and fonts-stable old-offsets
|
||||||
(ekp--append-stable-box-count old-offsets cutoff))))
|
(ekp--append-stable-box-count old-offsets cutoff))))
|
||||||
(when (and stable (> stable 0))
|
(when (and stable (> stable 0))
|
||||||
(let* ((tail (substring string cutoff))
|
(let* ((split (ekp--split-with-hyphen tail))
|
||||||
(split (ekp--split-with-hyphen tail))
|
|
||||||
(tail-boxes (car split))
|
(tail-boxes (car split))
|
||||||
(boxes (ekp--append-prefix-vector
|
(boxes (ekp--append-prefix-vector
|
||||||
(ekp-para-boxes para) stable tail-boxes))
|
(ekp-para-boxes para) stable tail-boxes))
|
||||||
@ -2787,16 +2809,30 @@ HYPHEN-COUNT)."
|
|||||||
"Get cached DP result from PARA for LINE-PIXEL, or nil."
|
"Get cached DP result from PARA for LINE-PIXEL, or nil."
|
||||||
(gethash (ekp--dp-key line-pixel) (ekp-para-dp-cache para)))
|
(gethash (ekp--dp-key line-pixel) (ekp-para-dp-cache para)))
|
||||||
|
|
||||||
|
(defun ekp--c-module-ready-p ()
|
||||||
|
"Return non-nil when the loaded C module exposes the DP entry point."
|
||||||
|
(and (boundp 'ekp-c-module-loaded)
|
||||||
|
ekp-c-module-loaded
|
||||||
|
(fboundp 'ekp-c-break-with-arrays)))
|
||||||
|
|
||||||
(defun ekp--c-available-p ()
|
(defun ekp--c-available-p ()
|
||||||
"Return non-nil when the C module can be used for DP."
|
"Return non-nil when the C module can be used for ordinary DP."
|
||||||
(and ekp-use-c-module
|
(and ekp-use-c-module
|
||||||
(boundp 'ekp-c-module-loaded) ekp-c-module-loaded
|
(ekp--c-module-ready-p)
|
||||||
(fboundp 'ekp-c-break-with-arrays)
|
|
||||||
;; looseness and parshape need the (position × line-count) DP,
|
;; looseness and parshape need the (position × line-count) DP,
|
||||||
;; Elisp only; first-line indent is a scalar the C engine takes
|
;; Elisp only; first-line indent is a scalar the C engine takes
|
||||||
(= ekp-looseness 0)
|
(= ekp-looseness 0)
|
||||||
(not ekp-parshape)))
|
(not ekp-parshape)))
|
||||||
|
|
||||||
|
(defun ekp--c-append-available-p ()
|
||||||
|
"Return non-nil when live append may use the native 1D DP path."
|
||||||
|
(and (= ekp-looseness 0)
|
||||||
|
(not ekp-parshape)
|
||||||
|
(or (ekp--c-available-p)
|
||||||
|
(and ekp--allow-native-live-append
|
||||||
|
(bound-and-true-p ekp-auto-justify-native-append)
|
||||||
|
(ekp--c-module-ready-p)))))
|
||||||
|
|
||||||
(defun ekp--c-sync-params ()
|
(defun ekp--c-sync-params ()
|
||||||
"Push current K-P penalty settings to the C module."
|
"Push current K-P penalty settings to the C module."
|
||||||
(when (fboundp 'ekp-c-set-penalties)
|
(when (fboundp 'ekp-c-set-penalties)
|
||||||
@ -2819,8 +2855,10 @@ HYPHEN-COUNT)."
|
|||||||
(ekp--dp-cache-elisp para line-pixel))))
|
(ekp--dp-cache-elisp para line-pixel))))
|
||||||
|
|
||||||
(defun ekp--dp-cache-append (para previous stable line-pixel)
|
(defun ekp--dp-cache-append (para previous stable line-pixel)
|
||||||
"Compute PARA at LINE-PIXEL reusing PREVIOUS states through STABLE."
|
"Compute PARA at LINE-PIXEL reusing PREVIOUS states through STABLE.
|
||||||
(if (ekp--c-available-p)
|
Automatic live append may use the loaded native 1D DP even when the
|
||||||
|
ordinary full-layout engine is explicitly set to Elisp."
|
||||||
|
(if (ekp--c-append-available-p)
|
||||||
(ekp--dp-cache-via-c para line-pixel)
|
(ekp--dp-cache-via-c para line-pixel)
|
||||||
(let* ((old (ekp--dp-get-cached previous line-pixel))
|
(let* ((old (ekp--dp-get-cached previous line-pixel))
|
||||||
(state (and old (plist-get old :state)))
|
(state (and old (plist-get old :state)))
|
||||||
|
|||||||
@ -85,6 +85,13 @@ is surfaced as a backend contract failure. If the module on disk is older
|
|||||||
than the Elisp code expects, loading refuses with a message asking you to
|
than the Elisp code expects, loading refuses with a message asking you to
|
||||||
rebuild.
|
rebuild.
|
||||||
|
|
||||||
|
Automatic live append has a separate `ekp-auto-justify-native-append`
|
||||||
|
switch, enabled by default. When a compatible module is already loaded,
|
||||||
|
auto-mode may use it for the prepared append DP even if
|
||||||
|
`ekp-use-c-module` is nil; full string/buffer layout still follows
|
||||||
|
`ekp-use-c-module`. Set the new switch to nil to force pure-Elisp live
|
||||||
|
append, or when the module is unavailable it falls back automatically.
|
||||||
|
|
||||||
## Interactive Use (buffer & region)
|
## Interactive Use (buffer & region)
|
||||||
|
|
||||||
`ekp-buffer.el` turns the string API into buffer-level commands:
|
`ekp-buffer.el` turns the string API into buffer-level commands:
|
||||||
|
|||||||
@ -74,6 +74,12 @@ Elisp 与 C 两个引擎的输出**完全一致**;未启用模块或 C 返回 ni
|
|||||||
Elisp。已启用模块若 signal,则作为后端契约错误直接呈现。若磁盘上的
|
Elisp。已启用模块若 signal,则作为后端契约错误直接呈现。若磁盘上的
|
||||||
模块版本旧于 Elisp 代码的要求,加载会拒绝并提示重新编译。
|
模块版本旧于 Elisp 代码的要求,加载会拒绝并提示重新编译。
|
||||||
|
|
||||||
|
自动 live append 另有 `ekp-auto-justify-native-append` 开关,默认开启。
|
||||||
|
当兼容模块已经加载时,auto-mode 可让已准备好的 append DP 走 native,
|
||||||
|
即使 `ekp-use-c-module` 为 nil;完整字符串/buffer 排版仍遵守
|
||||||
|
`ekp-use-c-module`。将该开关设为 nil 可强制 live append 使用纯 Elisp;
|
||||||
|
模块不可用时会自动回退。
|
||||||
|
|
||||||
## 交互使用(buffer 与 region)
|
## 交互使用(buffer 与 region)
|
||||||
|
|
||||||
`ekp-buffer.el` 把字符串 API 变成 buffer 级命令:
|
`ekp-buffer.el` 把字符串 API 变成 buffer 级命令:
|
||||||
|
|||||||
@ -826,6 +826,34 @@
|
|||||||
(should-not (get-text-property
|
(should-not (get-text-property
|
||||||
(1- (point)) 'ekp-buffer--display))))))
|
(1- (point)) 'ekp-buffer--display))))))
|
||||||
|
|
||||||
|
(ert-deftest ekp-buffer-test-live-native-append-bridge-respects-setting ()
|
||||||
|
"Native live append is optional and falls back to the Elisp engine."
|
||||||
|
(skip-unless
|
||||||
|
(and (fboundp #'ekp-c-module-load)
|
||||||
|
(ignore-errors (ekp-c-module-load))
|
||||||
|
(bound-and-true-p ekp-c-module-loaded)))
|
||||||
|
(let (disabled-lines enabled-lines)
|
||||||
|
(dolist (enabled '(nil t))
|
||||||
|
(let ((ekp-use-c-module nil)
|
||||||
|
(ekp-auto-justify-native-append enabled)
|
||||||
|
(calls 0))
|
||||||
|
(ekp-buffer-test--with-mode "alpha beta gamma delt" 20
|
||||||
|
(let ((original (symbol-function 'ekp-c-break-with-arrays)))
|
||||||
|
(cl-letf (((symbol-function 'ekp-c-break-with-arrays)
|
||||||
|
(lambda (&rest arguments)
|
||||||
|
(cl-incf calls)
|
||||||
|
(apply original arguments))))
|
||||||
|
(goto-char (point-max))
|
||||||
|
(ekp-buffer-test--type-string
|
||||||
|
"a long continuation with more words")))
|
||||||
|
(if enabled
|
||||||
|
(progn
|
||||||
|
(should (> calls 0))
|
||||||
|
(setq enabled-lines (ekp-buffer-test--display-lines)))
|
||||||
|
(should (= calls 0))
|
||||||
|
(setq disabled-lines (ekp-buffer-test--display-lines))))))
|
||||||
|
(should (equal enabled-lines disabled-lines))))
|
||||||
|
|
||||||
(ert-deftest ekp-buffer-test-live-backward-wrap-crossing-stays-local ()
|
(ert-deftest ekp-buffer-test-live-backward-wrap-crossing-stays-local ()
|
||||||
"Deleting into the previous native row keeps the live transaction local."
|
"Deleting into the previous native row keeps the live transaction local."
|
||||||
(let ((text
|
(let ((text
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
(require 'json)
|
(require 'json)
|
||||||
|
|
||||||
(defvar ekp-use-c-module)
|
(defvar ekp-use-c-module)
|
||||||
|
(defvar ekp-auto-justify-native-append)
|
||||||
(defvar ekp-buffer--conflicts)
|
(defvar ekp-buffer--conflicts)
|
||||||
(defvar ekp-auto-justify-paragraph-limit)
|
(defvar ekp-auto-justify-paragraph-limit)
|
||||||
(declare-function ekp-auto-justify-mode "ekp-buffer")
|
(declare-function ekp-auto-justify-mode "ekp-buffer")
|
||||||
@ -327,7 +328,11 @@
|
|||||||
(defun ekp-live-commit-evaluator--metadata (engine gc-mode width rows)
|
(defun ekp-live-commit-evaluator--metadata (engine gc-mode width rows)
|
||||||
"Return sample metadata for ENGINE, GC-MODE, WIDTH, and ROWS."
|
"Return sample metadata for ENGINE, GC-MODE, WIDTH, and ROWS."
|
||||||
`((engine . ,engine) (gc_mode . ,gc-mode)
|
`((engine . ,engine) (gc_mode . ,gc-mode)
|
||||||
(width . ,width) (rows . ,rows)))
|
(width . ,width) (rows . ,rows)
|
||||||
|
(live_append_backend
|
||||||
|
. ,(if (bound-and-true-p ekp-auto-justify-native-append)
|
||||||
|
"native-c"
|
||||||
|
engine))))
|
||||||
|
|
||||||
(defun ekp-live-commit-evaluator--collect (metadata)
|
(defun ekp-live-commit-evaluator--collect (metadata)
|
||||||
"Collect a fixed structural-commit sample count for METADATA."
|
"Collect a fixed structural-commit sample count for METADATA."
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user