perf: add native automatic live append backend

This commit is contained in:
Kinneyzhang 2026-08-20 09:35:35 +08:00
parent cfa7c18beb
commit 3d3dda6688
7 changed files with 159 additions and 12 deletions

View File

@ -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.

View File

@ -58,6 +58,14 @@
"Seconds before retrying a live layout deferred by IME composition."
: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
"Maximum hard-paragraph characters planned automatically.
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)))
(let* ((context (ekp-buffer--policy-context width))
(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
(ekp-layout-plan-append
old-plan planning-text width))))

60
ekp.el
View File

@ -142,6 +142,9 @@ Set to nil to force pure Elisp implementation."
:type 'boolean
: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 = flexible space between boxes (Knuth-Plass terminology)
;; 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))))
(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)
"Return the box index in OFFSETS beginning at CUTOFF."
(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))
(cutoff (and (> (length old) 0)
(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
(and cutoff
(equal (ekp-para-latin-font para)
(ekp-latin-font string))
(equal (ekp-para-cjk-font para)
(ekp-cjk-font string))))
(equal (ekp-para-latin-font para) latin-font)
(equal (ekp-para-cjk-font para) cjk-font)))
(old-offsets (ekp-para-box-offsets-memo para))
(stable (and fonts-stable old-offsets
(ekp--append-stable-box-count old-offsets cutoff))))
(when (and stable (> stable 0))
(let* ((tail (substring string cutoff))
(split (ekp--split-with-hyphen tail))
(let* ((split (ekp--split-with-hyphen tail))
(tail-boxes (car split))
(boxes (ekp--append-prefix-vector
(ekp-para-boxes para) stable tail-boxes))
@ -2787,16 +2809,30 @@ HYPHEN-COUNT)."
"Get cached DP result from PARA for LINE-PIXEL, or nil."
(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 ()
"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
(boundp 'ekp-c-module-loaded) ekp-c-module-loaded
(fboundp 'ekp-c-break-with-arrays)
(ekp--c-module-ready-p)
;; looseness and parshape need the (position × line-count) DP,
;; Elisp only; first-line indent is a scalar the C engine takes
(= ekp-looseness 0)
(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 ()
"Push current K-P penalty settings to the C module."
(when (fboundp 'ekp-c-set-penalties)
@ -2819,8 +2855,10 @@ HYPHEN-COUNT)."
(ekp--dp-cache-elisp para line-pixel))))
(defun ekp--dp-cache-append (para previous stable line-pixel)
"Compute PARA at LINE-PIXEL reusing PREVIOUS states through STABLE."
(if (ekp--c-available-p)
"Compute PARA at LINE-PIXEL reusing PREVIOUS states through STABLE.
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)
(let* ((old (ekp--dp-get-cached previous line-pixel))
(state (and old (plist-get old :state)))

View File

@ -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
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)
`ekp-buffer.el` turns the string API into buffer-level commands:

View File

@ -74,6 +74,12 @@ Elisp 与 C 两个引擎的输出**完全一致**;未启用模块或 C 返回 ni
Elisp。已启用模块若 signal,则作为后端契约错误直接呈现。若磁盘上的
模块版本旧于 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)
`ekp-buffer.el` 把字符串 API 变成 buffer 级命令:

View File

@ -826,6 +826,34 @@
(should-not (get-text-property
(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 ()
"Deleting into the previous native row keeps the live transaction local."
(let ((text

View File

@ -11,6 +11,7 @@
(require 'json)
(defvar ekp-use-c-module)
(defvar ekp-auto-justify-native-append)
(defvar ekp-buffer--conflicts)
(defvar ekp-auto-justify-paragraph-limit)
(declare-function ekp-auto-justify-mode "ekp-buffer")
@ -327,7 +328,11 @@
(defun ekp-live-commit-evaluator--metadata (engine gc-mode width rows)
"Return sample metadata for ENGINE, GC-MODE, WIDTH, and ROWS."
`((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)
"Collect a fixed structural-commit sample count for METADATA."