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."
|
||||
: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
60
ekp.el
@ -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)))
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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 级命令:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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."
|
||||
|
||||
Loading…
Reference in New Issue
Block a user