From 3d3dda6688d1566eb2551f672bff10abc21b58f9 Mon Sep 17 00:00:00 2001 From: Kinneyzhang Date: Thu, 20 Aug 2026 09:35:35 +0800 Subject: [PATCH] perf: add native automatic live append backend --- ...adr_native_live_append_backend_20260820.md | 52 ++++++++++++++++ ekp-buffer.el | 11 ++++ ekp.el | 60 +++++++++++++++---- readme.md | 7 +++ readme_zh.md | 6 ++ tests/ekp-buffer-tests.el | 28 +++++++++ tests/ekp-live-commit-evaluator.el | 7 ++- 7 files changed, 159 insertions(+), 12 deletions(-) create mode 100644 .phrase/phases/phase-kp-overhaul-20260726/adr_native_live_append_backend_20260820.md diff --git a/.phrase/phases/phase-kp-overhaul-20260726/adr_native_live_append_backend_20260820.md b/.phrase/phases/phase-kp-overhaul-20260726/adr_native_live_append_backend_20260820.md new file mode 100644 index 0000000..4137a74 --- /dev/null +++ b/.phrase/phases/phase-kp-overhaul-20260726/adr_native_live_append_backend_20260820.md @@ -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. diff --git a/ekp-buffer.el b/ekp-buffer.el index 981543e..1be87d1 100644 --- a/ekp-buffer.el +++ b/ekp-buffer.el @@ -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)))) diff --git a/ekp.el b/ekp.el index 5a07f91..a3616da 100644 --- a/ekp.el +++ b/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))) diff --git a/readme.md b/readme.md index 71f638e..3ebed99 100644 --- a/readme.md +++ b/readme.md @@ -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: diff --git a/readme_zh.md b/readme_zh.md index 76ab645..3c0ffcd 100644 --- a/readme_zh.md +++ b/readme_zh.md @@ -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 级命令: diff --git a/tests/ekp-buffer-tests.el b/tests/ekp-buffer-tests.el index 67a954e..40868e8 100644 --- a/tests/ekp-buffer-tests.el +++ b/tests/ekp-buffer-tests.el @@ -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 diff --git a/tests/ekp-live-commit-evaluator.el b/tests/ekp-live-commit-evaluator.el index d5adc2b..92f78c8 100644 --- a/tests/ekp-live-commit-evaluator.el +++ b/tests/ekp-live-commit-evaluator.el @@ -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."