Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions TeXmacs/progs/kernel/gui/menu-widget.scm
Original file line number Diff line number Diff line change
Expand Up @@ -1502,6 +1502,17 @@
) ;cond
) ;tm-define

(tm-define (menu-cache-normalize r)
(:type (-> object object))
(:synopsis "Normalize expanded menu @r for use as menu cache key")
;; invisible 项的载荷(如 push-focus 注入的文档路径)不影响 widget 构建,
;; 但会让展开结果随光标位置变化,毒化缓存判等,故归一化剔除。
(cond ((and (pair? r) (== (car r) 'invisible)) (list 'invisible))
((pair? r) (cons (menu-cache-normalize (car r)) (menu-cache-normalize (cdr r))))
(else r)
) ;cond
) ;tm-define

(define-table menu-expand-table
(--- ,(lambda (p) `(--- ,@(menu-expand-list (cdr p)))))
(| ,(lambda (p) `(| ,@(menu-expand-list (cdr p)))))
Expand Down
107 changes: 107 additions & 0 deletions TeXmacs/tests/1145.scm
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; MODULE : 1145.scm
;; DESCRIPTION : GUI 复现:连续输入与进出数学模式时观察 mode 工具栏重建
;; COPYRIGHT : (C) 2026 Mogan STEM
;;
;; PURPOSE
;; 配合 C++ 侧 [1145] 诊断日志(get_menu_widget 的 EQUAL/CACHE_HIT/MISS
;; 计数、replaceButtons 的 SAME/REPLACE 计数与耗时),在真实 GUI 里驱动:
;; A. 纯文本连续输入 —— 展开结果应不变,期望全 EQUAL、零 REPLACE。
;; B. 反复进出数学模式 —— 上下文切换,展开结果变化,观察每次切换是否
;; 全量重建所有按钮(REPLACE n=...)。
;;
;; USAGE
;; xmake b stem
;; MOGAN_TEST_GUI=1 xmake r 1145
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(texmacs-module (texmacs tests 1145))

;; 步骤间隔:给 typeset + idle update_menus 足够时间(idle >= 1/60s 才触发)

(define step-delay-ms 1500)

(define (log-step label)
(display "[1145-step] ")
(display label)
(newline)
) ;define

;; 每步间隔 step-delay-ms:lambda 返回剩余毫秒表示继续等待,返回 #t 表示完成。

(define (run-chain steps on-done)
(if (null? steps)
(on-done)
(exec-delayed-pause (let ((start (texmacs-time)))
(lambda ()
(let ((left (- (+ start step-delay-ms) (texmacs-time))))
(if (> left 0)
left
(begin
(log-step (caar steps))
((cdar steps))
;; 同步触发 C++ update_menus,不依赖 idle/焦点
(update-menus)
(run-chain (cdr steps) on-done)
#t
) ;begin
) ;if
) ;let
) ;lambda
) ;let
) ;exec-delayed-pause
) ;if
) ;define

(tm-define (test_1145)
(new-document)
(let* ((phase-a
;; A: 纯文本输入 5 步,每步插 6 个字符
(let loop
((i 0) (acc '()))
(if (>= i 5)
(reverse acc)
(loop (+ i 1)
(cons (cons (string-append "A: type text " (number->string i))
(lambda () (insert "abcdef"))
) ;cons
acc
) ;cons
) ;loop
) ;if
) ;let
) ;phase-a
(phase-b
;; B: 5 轮 进出数学模式
(let loop
((i 0) (acc '()))
(if (>= i 5)
acc
(loop (+ i 1)
(append acc
(list (cons (string-append "B" (number->string i) ": insert math")
(lambda () (insert '(math "x")))
) ;cons
(cons (string-append "B" (number->string i) ": enter math")
(lambda () (go-left))
) ;cons
(cons (string-append "B" (number->string i) ": type in math")
(lambda () (insert "y"))
) ;cons
(cons (string-append "B" (number->string i) ": exit math")
(lambda () (go-right))
) ;cons
) ;list
) ;append
) ;loop
) ;if
) ;let
) ;phase-b
) ;
(run-chain (append phase-a phase-b)
(lambda () (display "[1145-step] done, quit") (newline) (quit-TeXmacs))
) ;run-chain
) ;let*
) ;tm-define
130 changes: 130 additions & 0 deletions devel/1145.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# [1145] 工具栏(replaceButtons)重复重建性能优化

## 1 相关文档
- [dddd.md](dddd.md) - 任务文档模板

## 2 任务相关的代码文件
- `src/Texmacs/Window/tm_window.cpp` — `get_menu_widget`:菜单展开、缓存、判等
- `src/Plugins/Qt/qt_tm_widget.cpp` — `replaceButtons` / `same_actions` / `replaceActions`
- `src/Plugins/Qt/qt_ui_element.cpp` — `get_qactionlist`:QAction 创建
- `src/Edit/Interface/edit_interface.cpp` — `update_menus` 触发时机
- `TeXmacs/tests/1145.scm` — GUI 诊断驱动脚本

## 3 如何测试

### 3.1 确定性测试(单元测试)
```
xmake b stem
```

### 3.2 非确定性测试(文档验证)
```
xmake b stem
MOGAN_TEST_GUI=1 xmake r 1145 # 观察 [1145] 诊断日志
xmake r 1145 # headless:menu-expand 判等诊断
```

## 4 如何提交

提交前执行以下最少步骤:

```bash
gf fmt --changed-since=main
```

## 5 What
1. 建立菜单更新链路性能诊断:`get_menu_widget` / `replaceButtons` /
`get_qactionlist` 插桩(`[1145]` 前缀日志)+ `TeXmacs/tests/1145.scm`
GUI 驱动测试(`MOGAN_TEST_GUI=1 xmake r 1145`)。
2. 实测定位瓶颈(见 7.2):按钮重建不是瓶颈,每次 update_menus 的 10 次
scheme `menu-expand`(~75-110ms)才是。
3. **修复 focus icons 缓存永远 MISS**:`get_menu_widget` 的缓存判等改用
归一化 key(新增 scheme 函数 `menu-cache-normalize`,剔除 `invisible`
项载荷),重复进入数学模式时 which=2 从每次 MISS + make_menu_widget
~47ms 变为 CACHE_HIT 零重建。

## 6 Why
mode 工具栏每次更新都通过 `replaceButtons` 全量重建按钮,是性能差的主要原因。

## 7 How

### 7.1 现状链路(调查记录)

1. `edit_interface_rep::apply_changes`:无待处理变更且 idle ≥ 1/60s 时调
`update_menus()`(edit_interface.cpp:918-921),即每次输入/光标移动后
空闲即触发。
2. `update_menus` 依次对主菜单、4 个图标栏、通知栏调 `menu_icons` /
`menu_main`(edit_interface.cpp:819-870)。
3. `tm_window_rep::get_menu_widget`(tm_window.cpp:403):
- 每次都执行 scheme 侧 `menu-expand` 全量展开菜单;
- 展开结果 `xmenu` 与 `menu_current[which]` 用 `equal?` 判等,相同则
早退(不重建);
- 不同则查 `menu_cache`(按 xmenu 缓存 widget),命中复用 widget,
未命中 `make_menu_widget` 全量新建 widget 树。
4. Qt 侧 `SLOT_MODE_ICONS` → `mode_icons_widget->get_qactionlist()`:
每个 widget 实例首次调用时创建全部 QAction(有 `cachedActionList`)。
5. `replaceButtons`:`same_actions` 指针判等,相同早退;不同则
hide → 全部 removeAction → 全部 addAction → 重设按钮样式 → show。

(结论待插桩数据补充)

### 7.2 插桩实测结论(2026-07-19,GUI 驱动 TeXmacs/tests/1145.scm)

插桩点:`get_menu_widget`(EQUAL/CACHE_HIT/MISS 计数 + menu-expand /
make_menu_widget 计时)、`replaceButtons`(SAME/REPLACE 计数 + 计时)、
`get_qactionlist`(计时)。

**每次 update_menus(任何编辑后 idle ≥66ms 触发一次)的实测分解:**

| 场景 | total | menu-expand×10 | make_menu_widget | replaceButtons |
|---|---|---|---|---|
| 纯文本输入(每步) | 77-102ms | ~75-95ms | 0 | 0 |
| 进入数学模式 | 166-173ms(首次315ms) | ~105ms | ~41ms | 1-4ms |
| 数学模式内输入 | 110-154ms | ~110-152ms | 0 | 0 |
| 退出数学模式 | 81-91ms | ~75ms | 0(CACHE_HIT) | 0-2ms |

**核心结论:**

1. **按钮重建不是瓶颈**:`replaceButtons` 全量 REPLACE 每次仅 0-2ms;
`same_actions` 早退 + `menu_current` equal? 判等 + `menu_cache` 三层
机制已经避免了绝大多数重建。纯文本输入时 mode 工具栏展开结果稳定,
全程 EQUAL、零 REPLACE。
2. **真正的瓶颈是每次 update_menus 都重新跑 10 次 scheme `menu-expand`
(~75-110ms)**,即使结果与上次完全相等——判等发生在展开之后,展开
本身的开销省不掉。大头是 which=0(主图标 ~20ms)、which=1(模式
图标 ~22ms)、which=2(焦点图标 ~11ms)。
3. **次要浪费**:数学上下文下 which=2(focus icons)展开结果不稳定
(每次 MISS),每轮进出数学都 make_menu_widget ~41ms;文本上下文
下展开稳定(CACHE_HIT)。
4. which=4(tab 页)已有签名机制避免重建,但签名检查在 expand 之后,
可先算签名、不变则跳过 expand(每次省 ~5ms)。

### 7.3 focus icons 缓存 MISS 根因与修复(2026-07-19)

**根因**:`gui-make-push-focus`(menu-define.scm:58)给每个 `push-focus`
菜单项注入 `(invisible (tree->path pushed-tree))`——焦点树的**文档路径**。
该载荷不参与 widget 构建(`menu-convert` 对 `invisible` 一律返回空表),
只被 `get_menu_widget` 的展开结果判等/缓存 key 使用。路径随光标位置变化
(如数学模式里 `(invisible (2 0 2))` → `(2 0 3)`),导致展开结果永远
`equal?` 失败,`menu_cache` 永远 MISS,每次进数学模式都全量
`make_menu_widget`(~41ms)+ 重建 QAction(~6ms)。

**修复**:缓存 key 归一化——新增 scheme 函数 `menu-cache-normalize`
(menu-widget.scm,置于 `cache-menu?` 旁),递归把 `(invisible 载荷)`
替换为 `(invisible)`;`get_menu_widget` 用归一化后的 `xkey` 做
`menu_current` 判等与 `menu_cache` 索引。`umenu`(构建 widget 用的未展开
菜单)不受影响,行为零变化。

**效果**(TeXmacs/tests/1145.scm 实测):

| 场景 | 修复前 | 修复后 |
|---|---|---|
| 重复进入数学模式(第 2 次起) | 166-173ms(which=2 MISS + make ~41ms + qact ~6ms) | 121-156ms(which=2 CACHE_HIT,零重建) |
| 首次进入数学模式(冷缓存) | ~315ms | ~350ms(不变,一次性) |

### 7.4 遗留问题(后续任务)

1. 每次 update_menus 仍全量跑 10 次 `menu-expand`(~75-110ms),即使
结果全 EQUAL。需要前置签名/降频机制,收益最大但有一定正确性风险。
2. which=4 签名检查可前移到 expand 之前。
3. 首次进入某上下文的冷缓存 MISS 无法避免(可做预 warmup)。
18 changes: 17 additions & 1 deletion src/Plugins/Qt/qt_tm_widget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ bool in_presentation_mode ();
#include "qt_menu.hpp"
#include "qt_simple_widget.hpp"
#include "qt_window_widget.hpp"
#include "tm_debug.hpp"
#include "tm_server.hpp"
#include "tm_sys_utils.hpp"
#include "tm_url.hpp"
Expand Down Expand Up @@ -179,11 +180,25 @@ same_actions (QWidget* dest, QList<QAction*>* src) {
return true;
}

// [1145] 临时诊断:统计 replaceButtons 的 same(早退)与
// replace(全量重建)次数
static int rb_diag_same= 0, rb_diag_replace= 0;

static void
replaceButtons (QToolBar* dest, QList<QAction*>* src) {
if (src == NULL || dest == NULL)
TM_FAILED ("replaceButtons expects valid objects");
if (same_actions (dest, src)) return;
if (same_actions (dest, src)) {
rb_diag_same++;
std_bench << "[1145] replaceButtons SAME (same=" << rb_diag_same
<< " replace=" << rb_diag_replace << ")\n";
return;
}
rb_diag_replace++;
std_bench << "[1145] replaceButtons REPLACE n=" << src->count ()
<< " (same=" << rb_diag_same << " replace=" << rb_diag_replace
<< ")\n";
bench_start ("replaceButtons");
dest->setUpdatesEnabled (false);
bool visible= dest->isVisible ();
if (visible) dest->hide (); // TRICK: to avoid flicker of the dest widget
Expand All @@ -198,6 +213,7 @@ replaceButtons (QToolBar* dest, QList<QAction*>* src) {
}
if (visible) dest->show (); // TRICK: see above
dest->setUpdatesEnabled (true);
bench_end ("replaceButtons");
}

void
Expand Down
4 changes: 4 additions & 0 deletions src/Plugins/Qt/qt_ui_element.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "message.hpp"
#include "promise.hpp"
#include "scheme.hpp"
#include "tm_debug.hpp"
#include "widget.hpp"

#include "QTMGuiHelper.hpp"
Expand Down Expand Up @@ -265,6 +266,8 @@ QList<QAction*>*
qt_ui_element_rep::get_qactionlist () {
if (cachedActionList) return cachedActionList;

// [1145] 临时诊断:QAction 全量创建耗时
bench_start ("get_qactionlist");
QList<QAction*>* list= new QList<QAction*> ();

switch (type) {
Expand All @@ -285,6 +288,7 @@ qt_ui_element_rep::get_qactionlist () {
break;
}
cachedActionList= list;
bench_end ("get_qactionlist");
return list;
}

Expand Down
Loading
Loading