实现了 debounce/throttle 的 timeout 包要加入 Emacs 了

最近时不时看 Emacs-devel 的时候会看到关于一个叫做 timeout 的包加入到 GNU ELPA 的讨论,最近似乎变成了直接加入 Emacs 本身了: karthink/timeout: Throttle or debounce elisp code,七月和八月的讨论大致以这两个链接开始:

作者也写了一篇 blog 来说明用法: Cool your heels, Emacs | Karthinks

:seedling:,开个帖子在这里睡觉先,明天仔细研究下。

7 个赞

这也许不是个好主意。让Elisp里的线程服从某种调度机制,并不会让它表现得像在Elisp层面被调度,或者说,我们仍然无法在任意位置强制打断当前代码的运行,如果不在代码中聪明的书写checkpoint,仍然可能陷入死循环。在使用多线程的时候,最大的问题在于我们往往不知道线程是在哪里卡住的,而这一点是无法通过策略避免的。

感觉这个包的作用更像是idle,通过空闲调度去抖动。

4 个赞

是的,这和多线程关系不大。我也主要是想看看这些消抖/节流操作能用在什么地方。

我不认为这个包是一个好主意。

首先这个包本身就是 timer 的一个简单的包装。去看一下这个包的代码就知道,是很简单的代码。不涉及到任何 C 代码层次的更改。 throttle 和 debounce 本身就是异步编程的基操。任何面向 UI UX 的操作都免不了这个。况且本身实现一个 throttle 和 debounce 机制也用不了几行代码,完全没有必要引入一个新的语法糖。更何况什么时候需要 throttle 什么时候需要 debounce 不同的包都会有自己的逻辑来判断,自己写要灵活的多。大量的 emacs 包,company,eglot,copilot 等都自己实现了。

其次我不喜欢这个包的设计模式。这个包引入了两个新的 advice,throttle! 和 debounce! 通过 advice 强制的把一个函数进行 thrrottle 和 debounce。

我认为这个设计是不好的。throttle 和 debounce 不是那种简单的 conditional 的 advice,是会从根本上改变一个函数行为的 advice。如果一个函数需要被 throttle 或者 debounce,那么原作者在实现这个函数的时候就应该去内置好 throttle 和 debounce。

在 advice 层面上去 throttle 和 debounce 的功能如果能够进入 core,我认为是一种变相鼓励 hack 和隐式改变函数行为的设计。如果有人给你报 bug 说你的包的某个功能不 work,结果 debug 到最后发现不知道是在哪个地方把你的函数给 advice 了一个 throttle 和 debounce,这样不好吧。

3 个赞

感觉取名为 timeout 有点泛了,一眼看还以为和 with-timeout 甚至多线程有什么关系。

可能能配合 after-change-functions 实现区域改动检测,进一步实现 Org节点 修改检测。

我在浏览 emacs-devel 讨论时也看到了不少类似的观点。不过还是让我先把作者的博客内容做个总结,之后再结合其他的讨论继续展开 :rofl:

博客的链接已经在前面给出,这里就不重复了。

首先,作者表示这个包并不是要解决正确与否的问题,而是性能问题。说到底 Emacs 基本上就是个单线程程序,后台的计时器执行都有可能会造成 Emacs 的卡顿。作者将导致 Emacs 卡顿的操作分为两类,分别是「推操作」和「拉操作」(当然,归根结底 Emacs 中发生的一切最终都由用户驱动。这里的区分仅仅在于与用户的“贴近程度”。):

  • 推操作 指的是由于用户输入直接触发的缓慢 Elisp 代码。之所以这样命名,是因为 Emacs 的控制权会因为输入动作而被“推”走。例子有:
    • 一个 markdown 缓冲区的 HTML 预览在每次按键后都会更新。
    • 当一些代码被粘贴进缓冲区时向 LSP 服务器发送数据,或者当光标移动到一个新函数时更新 header-line。
  • 拉操作 指的是与用户输入无关而运行的缓慢 elisp 代码,尽管可能会被用户输入中断。之所以这样命名,是因为无论是否有用户输入,这类代码都会“拉走” Emacs 的控制权。例子有:
    • 任何不依赖用户输入的空闲定时器 (idle-timer),比如 RefTeX 对 LaTeX 缓冲区的周期性扫描。

根据作者的说法,一个包开发者在想到类似的问题时有两条路可以走,一是尽量保守谨慎地使用某些绝对不会出现问题的默认值,或者允许用户调整 timer 的时延;二是尽可能地考虑复杂而全面的情况,即考虑到各种可能出现卡顿的场景并处理,但这很少有人能够做到。换句话说,能想到这个问题的包作者大多选择了比较保守的做法。

类似地,用户在面对包带来的卡顿时也有两种方式处理。一是联系包作者解决,但通常来说性能问题相比正确性问题更加晦涩和难以发现,一般需要不少的 Elisp 开发使用经验,而且如果你能够发现问题你大概率也能解决它;二是少用第三方扩展包来避免 Emacs 过于臃肿和缓慢。

为了解决这些问题,作者提出了 timeout 这个包,往 Emacs 插件生态这依托上又抹了一把泥巴(作者希望用户在不与包作者沟通的情况下解决问题)。对于频繁执行「拉操作」的函数,可以限制它在执行一次后的指定时间内无法执行,这被叫做 Throttle (有节流的意思);对于由用户频繁动作引发执行的「推操作」函数,可以让动作延后到最后一次用户动作之后的一小段时间再开始执行,这被叫做 Debounce (去抖动,或者叫消抖)。作者在博客中为 Throttle 和 Debounce 分别举了一个例子,感兴趣的同学可以去看看。

从原理上来说,throttle 和 debounce 的实现非常简单,就是一般的装饰器,不过使用了 Elisp 的 advice 机制。整个 timeout 包所有的代码都是围绕下面两段代码展开的:

Throttle

;; FUNC is the function to be throttled, called with arguments ARGS
;; TIMEOUT is the throttle duration

;; If the timer is running
(if (and throttle-timer (timerp throttle-timer))
    ;; do nothing, return nil
    nil
  ;; else run and start the timer
  (prog1 (apply func args)
    (setq throttle-timer
          (run-with-timer
           timeout nil
           (lambda ()
             ;; clear the timer
             (cancel-timer throttle-timer)
             (setq throttle-timer nil))))))

Debounce

;; FUNC is the function to be throttled, called with arguments ARGS
;; DELAY is the debounce period

;; If the timer is running
(if (timerp debounce-timer)
    ;; Do not run the function, reset the timer instead
    (timer-set-idle-time debounce-timer delay)
  ;; start the timer over
  (setq debounce-timer
        (run-with-idle-timer
         delay nil
         (lambda ()
           (setq debounce-timer nil)
           ;; Timer ran, finally run the function
           (apply func args)))))

节流装饰器会在执行一次函数后运行一个 timer,在 timer 结束之前不允许执行该函数;消抖装饰器会在 timer 结束后执行函数,在 timer 生存期间新的调用会重置 timer 的倒计时。

4 个赞

有点像事件总线的防堵塞设计。

一般来说,对顶层执行的函数(比如用户命令或者计时器回调)进行节流/消抖相对简单,因为它们不需要向 caller 返回什么有用的信息。比较麻烦的是会被其他函数调用的函数,你可以注意到在上一个帖子给出的代码中,如果调用了函数,节流器会在倒计时未结束时直接返回 nil,而消抖器则是返回 timer-set-idle-time 的值。

如果某个被节流/消抖的函数不能正常地返回合理的值,还真可能导致未知的后果。

当然上面的只是示例代码,现在 timeout 已经加入 Emacs 主线了: Add new library ‘timeout’ · emacs-mirror/emacs@d2532a4。在最新的代码中是这样做的。

(defun timeout--throttle-advice (&optional timeout)
  "Return a function that throttles its argument function.

TIMEOUT defaults to 1 second.

When FUNC does not run because of the throttle, the result from the
previous successful call is returned.

This is intended for use as function advice."
  (let ((throttle-timer)
        (timeout (or timeout 1.0))
        (result))
    (lambda (orig-fn &rest args)
      "Throttle calls to this function."
      (prog1 result
        (unless (and throttle-timer (timerp throttle-timer))
          (setq result (apply orig-fn args))
          (setq throttle-timer
                (run-with-timer
                 timeout nil
                 (lambda ()
                   (cancel-timer throttle-timer)
                   (setq throttle-timer nil)))))))))

(defun timeout--debounce-advice (&optional delay default)
  "Return a function that debounces its argument function.

DELAY defaults to 0.50 seconds.  The function returns immediately with
value DEFAULT when called the first time.  On future invocations, the
result from the previous call is returned.

This is intended for use as function advice."
  (let ((debounce-timer nil)
        (delay (or delay 0.50)))
    (lambda (orig-fn &rest args)
      "Debounce calls to this function."
      (prog1 default
        (if (timerp debounce-timer)
            (timer-set-idle-time debounce-timer delay)
          (setq debounce-timer
                (run-with-idle-timer
                 delay nil
                 (lambda (buf)
                   (cancel-timer debounce-timer)
                   (setq debounce-timer nil)
                   (setq default
                         (if (buffer-live-p buf)
                             (with-current-buffer buf
                               (apply orig-fn args))
                           (apply orig-fn args))))
                 (current-buffer))))))))

目前节流操作会返回上一次函数正常返回的结果,而消抖操作不会,但是根据参数列表来看我们可以添加一个默认返回值。作者对此的解释是:

This is unimportant for top-level interactive commands, but might at least help avoid type errors if this is a pure function, or if we’re monkey-patching something deep inside an elisp library.

The debounce version is similar, but there’s nowhere to return the result to this time. func may be called from anywhere in the code but runs (eventually, via the timer) at top-level, so it doesn’t make sense to store its return value.

是的,作者也强调了遇到类似问题时首先应该在包中查找有没有相关选项。

Of course, if packages with over-eager timers allow for customizable eagerness, then the simpler solution is to tweak the frequency directly. Packages that implement “pull operations” are generally conscientious about this. If something is running far more often than it should, searching for the variable(s) <package-name>-*-delay or <package-name>-*-timer is a good start to fixing it. Further, these actions typically run on idle timers, which is like a built-in debounce and unlikely to block Emacs in a perceptible way.

But there are many exceptions. What we’d like, in the absence of a provided dial for tuning this, is an elisp function or macro that can easily – and generically – throttle any other elisp function.

不太清楚,如果不是有上面帖子提到 UI 相关我都不知道这和 UI 有关。随便搜了一下,JS 里面也有类似的东西。 The art of Smooth UX : Debouncing and Throttling for a more performant UI - DEV Community

关于这个包本身和作者的博客差不多总结道这里就结束了,等会看看邮件列表讨论去。

顺带一提,timeout 包的作者是 gptel 的作者。

3 个赞

在 2025 年 7 月 31 日,timeout 的作者 Karthik Chikmagalur 在 emacs-devel 中发出了把这个包添加到 ELPA 的请求。并在当天得到了 John Wiegley 的强烈推荐

I’d like to give my definite thumbs up for this timeout library. I’m using it also now to debounce some functions related to the Org-agenda. It’s a great idea, and applicable in many scenarios.

在第二天 Moakt 就提出了反对意见,他首先是肯定了 timeout 包的这种做法解决了一些现实问题,但是他认为不应该通过 function advice 来实现(毕竟 Elisp 手册就是这样写的),而是应该主动提交补丁来解决问题来让每个人都能受益。Karthik 表示同意,但是同时他也表示如果某些包已经不再活跃那么添加补丁不太可能,使用一两个 advice 比维护一个 fork 版本要容易很多。

Eli Zaretskii 认为既然功能如此基础,这个包应该可能被加入到 Emacs 中。 John Wiegley 表示同意,Moakt 也表达了同意意见,他认为这个包的代码还需要一些改进,比如添加测试之类的(Karthik 表示不知道怎么测)。后续的邮件讨论大多围绕改进 timeout 的实现展开。

8 月 2 日是一个讨论的小高峰,Philip Kaludercic 认为 既然这个包保持到 Emacs 24.4 的兼容性,那么它同时也可以加入到 GNU ELPA 来让这个包在先前版本的 Emacs 中可用。同时他也对是使用基于 advice 的方法还是在 C 语言层面来实现提出了疑问。Eli 不反对添加 timeout 到 GNU ELPA,他也认同尽量避免使用 advice,也许能够使用 add-function 来替换。

对于是否使用 C 来实现 timeout 的功能,Stefan Monnier 的回答是否,同时他也不认为使用 advice 有什么问题,与其说 timeout 是 advice-based 的,不如说它只是提供了结合 advice 和 debounce/throttle 结合起来的工具,用户完全可以自行决定是否安装 debounce/throttle 装饰器。Karthik 对这一认识表示赞同,并表示 timeout 实际上提供了 advice 和非 advice 的 API,前者用于直接改变函数的行为,后者会返回被包装后的函数来供开发者使用。

8 月 2 日和 3 日的余下讨论是减少 timeout 中的代码重复,但从最终结果来看似乎没有什么成果,这里就略过了。8 月 7 日的讨论中有一条很有意思的信息,John Wiegley 提到 Emacs 27.1 发布前曾移除了已经实现的 debounce 宏: Use run-with-idle-timer instead of debounce for responsive image scal… · emacs-mirror/emacs@d64ea18。从 git 历史来看,这一 macro 于 2019 年 11 月24 日 加入,于 2019 年 12 月 1 日移除,两次 commit 作者都是 Juri Linkov,之所以撤销可能是 run-with-idle-timer 已经够用了。从实现上来看和 timeout 的做法没有太大区别:

(defmacro debounce (secs function)
  "Call FUNCTION after SECS seconds have elapsed.
Postpone FUNCTION call until after SECS seconds have elapsed since the
last time it was invoked.  On consecutive calls within the interval of
SECS seconds, cancel all previous calls that occur rapidly in quick succession,
and execute only the last call.  This improves performance of event processing."
  (declare (indent 1) (debug t))
  (let ((timer-sym (make-symbol "timer")))
    `(let (,timer-sym)
       (lambda (&rest args)
         (when (timerp ,timer-sym)
           (cancel-timer ,timer-sym))
         (setq ,timer-sym
               (run-with-timer
                ,secs nil (lambda ()
                            (apply ,function args))))))))
6 个赞

下一次讨论来到了 8 月 19 日,主要讨论的问题是是否将 timeout 加入 GNU ELPA 和 Emacs 主线,以及接下来如何维护的问题。8 月 20 日 Karthik 与 Thierry Volpiatto 对具体的实现进行了一些讨论: msg00610, msg00612, msg00616, msg00665, msg00666。其他的一些讨论分布在 22 日和 23 日,主要涉及命名风格等问题。后续的讨论也大差不差。

当然,在 8 月末和 9 月初,Moakt 强烈地表达了反对意见,并质问 Philip 为什么一直在忽略他的意见(逐渐暴躁),这里我懒得总结了,直接让 Gemini 翻译得了:

Re: [ELPA] New package: timeout

发件人: Moakt Temporary Email @ 2025-08-31 1:36 UTC (permalink / raw) 致: emacs-devel

你好,Karthik, Philip,

请允许我重申一些我之前问过或评论过,而你们可能遗漏了的几点。

关于“advice”函数:

我希望我们不要“正式”支持(在 Core 和 (Non)GNU ELPA 中)这类解决方案,因为它们恰好与手册中明确所说的相悖。

相反,我们只应该支持本地的修复/解决方案。

其他作者可能会希望或被鼓励,使用同样基于函数 advice 的逻辑来修复其他问题。

如果第三方代码因为“某些原因”无法被修改,这不应该是 Emacs 的责任,也不应成为我们容许那些本可以也应该以正确方式实现的临时解决方案的借口。

或者,我在这里是否遗漏了什么?

关于“非 advice”函数:

如果我理解得没错,如果你认为它们需要被重构/修正,我强烈建议你添加一些最基本的测试,以覆盖它们不同方面的用法。(无论如何,测试总是很重要的。)

我还想建议从函数名中移除“timeout”。

我很难想象会看到像 (timeout-throttled-func ...) 这样的代码来节流一个函数,而依我之见,它应该简单地是 (throttled-func ...) 或类似的东西。

我猜你之所以想出“timeout”这个名字,是为了能够将它们归类到一个单独的包中,对吗?但你为什么需要将它们归类呢?对我来说,它们是两个不同的功能,因此也应该是两个不同的包(无论是放在核心还是 ELPA 中)。

你觉得以下命名中的一个怎么样? (当然,函数名也要相应地调整)

  • throttle.eldebounce.el
  • throttle-func.eldebounce-func.el
  • throttle-function.eldebounce-function.el
  • func-throttle.elfunc-debounce.el
  • function-throttle.elfunction-debounce.el
  • 等等。

我认为这些名字能更好地表达包的内容,也更易于查找和发现。而“timeout”是一个非常通用的名字,可能暗示许多不同的事物,即使有人知道它的作用,我仍然觉得它令人困惑,因为很难通过包名来记住它的内容。

话虽如此,我有一个关于将它们添加到 ELPA 的问题:

我们真的需要也将这两个函数添加到 ELPA 吗?或者我们应该等到有包作者提出请求再说?(而这可能根本不会发生?) Philip,这类函数是否可以通过“compat”基础设施为包作者们反向移植?

谢谢,

(写着写着 emacs-devel 挂了,只能用镜像了)

https://yhetil.org/emacs-devel/028mokUsHy9DEIwKIQNrMIw1NNV6TmlIT7J9mo9bDFg@localhost.localdomain/

发件人: Moakt Temporary Email @ 2025-09-01 14:53 UTC (permalink / raw) 致: emacs-devel

你好,Juri,

我也想建议从函数名中移除“timeout”。 我很难想象会看到像 (timeout-throttled-func ...) 这样的代码来节流一个函数,而依我之见,它应该简单地是 (throttled-func ...) 或类似的东西。

同意。举个例子,在 d64ea182fb6 这个提交中,那个返回 lambda 而不使用 advice 的宏就只被命名为 debounce

debouncethrottle 对我来说也很好。

(或者我们可以为所有包装函数——那些为了增加额外功能而包装其他函数的函数——想出一个统一的命名惯例。)

在名字中加上“timeout”,除了过于冗长且没有带来任何额外的有用信息之外,还非常容易引起混淆。

其他人怎么看?

Philip,Karthik?

也请你们加入讨论,并对我之前的邮件提供反馈:Re: [ELPA] New package: timeout

感谢你们的时间。

谢谢,

https://yhetil.org/emacs-devel/[email protected]/raw

发件人: Moakt Temporary Email @ 2025-09-01 18:08 UTC (permalink / raw)

致: emacs-devel

你好,Philip,

好的,那在将 timeout.el 添加到核心之前,我会进行以下修改:

我给你发了一封邮件。

你似乎遗漏了我发到 emacs-devel 邮件列表的那些邮件。 但如果是这样的话,那你就会错过所有你没有被抄送的讨论(子讨论),即使你在某个时间点是这些讨论的参与者。

看起来你也遗漏了那些你被抄送的邮件,比如 Juri 发的那封?Re: [ELPA] New package: timeout

我还给你发过其他邮件,在其他线程里,你也没有回复。如果你找不到,告诉我一声,我会重新发给你。

谢谢,

https://yhetil.org/emacs-devel/[email protected]/raw

发件人: Moakt Temporary Email @ 2025-09-02 12:40 UTC (permalink / raw)

致: emacs-devel

再次你好,Philip,

好的,我已经把这个库推送到 master 分支了。感谢你的贡献!

我不明白。

你无视了所有人——你无视了我的邮件,无视了 Juri 的邮件,也没有给其他人时间来分享他们的意见。

甚至 Eli 都没来得及对我的邮件做出评论。

我给你收件箱发了多封邮件,还在 IRC 上发了其他消息。

请你把我们的意见考虑进去好吗?

(顺便说一句,我还在等你的答复)

即使你没有看到我们的任何消息,在做出任何更改之后,你至少也应该在推送包之前,给大家足够的时间来做出贡献。

我之前也注意到,你在没有明显理由的情况下,总是急于添加一个包,忽略了其他人可能做出的贡献。

这并不影响我对你时间和努力的感激。

谢谢,

最后,Sean Whitton 解释 了为什么 Moakt 没有受到回复的可能原因,这大概是因为他的回复总是打开新的 thread(我猜也许是忘了回复全部?)

并 非 忘 记

Re: [ELPA] New package: timeout

From: Philip Kaludercic
Subject: Re: [ELPA] New package: timeout
Date: Tue, 02 Sep 2025 15:09:10 +0000

Eli Zaretskii [email protected] writes:

Date: Tue, 2 Sep 2025 12:40:54 +0000*

From: Moakt Temporary Email [email protected]

Even Eli didn’t have the time to comment on my messages.

Actually, Eli had time to comment to everything he wanted to comment.

Right, and I was going by Eli’s request to add the package as it already was a few weeks back. If there are any minor changes, anyone is free to change the file in emacs.git or submit a patch, but I was not that interested in participating in bike-shedding discussions w.r.t. naming. Sorry if that came of the wrong way.

timeout 已于 2025 年 9 月 2 号加入 Emacs 主线,不过我也不太清楚之后会不会有些变化,我会在这个帖子里面更新最新进展。

2 个赞

去邮件列表里表达我的反对意见了。

但是邮件列表这种沟通形式有一个很普遍的问题,就是邮件很容易被(有意或无意的)忽略掉。毕竟它没有一个论坛一样的把所有的帖子都追踪到一起的一个形式。

2 个赞

对“拉操作”,这里我补充一个简单的例子和使用 timeout 的缓解方法。

在 Emacs 30 中,Emacs 的 tab-line 新添加了一个选项 tab-line-tabs-buffer-group-function,可以使用项目来实现对 buffer 的分类,下面是我的 tab-line 配置:

;;@@TAB-LINE Windows 顶端显示可方便切换相关 buffer
;; (global-tab-line-mode t)
(setopt tab-line-close-button-show 'non-selected)
;; 使用项目作为 tab line 分组
;; 当前实现的效率不佳
(setopt tab-line-tabs-function 'tab-line-tabs-buffer-groups)
(setopt tab-line-tabs-buffer-group-function
        #'tab-line-tabs-buffer-group-by-project)

我目前不使用 tab-line 的很大原因是 global-tab-line-modetab-line-tabs-buffer-group-by-project 在 Windows 上的效率太低,它内部使用的 project-current 太慢,通过 benchmark-run 测试,单次 project-current 调用开销在 1ms 左右:

(defun tab-line-tabs-buffer-group-by-project (&optional buffer)
  "Group tab buffers by project name."
  (with-current-buffer buffer
    (if-let* ((project (project-current)))
        (project-name project)
      "No project")))

由于 tab-line 的实现机制问题,每当 Emacs 界面需要重新绘制时,它会重新计算所有 buffer 归属的 group,这也就导致 tab-line-tabs-buffer-group-by-project 在 Emacs 每次重绘界面时都需要被调用,而触发 Emacs 重绘实在太过容易和频繁:普通的光标移动命令都能触发。即便在 Emacs 中打开几十个 buffer,你也能够感到明显的操作不跟手。

通过 timeout ,这一问题看似能够在一定程度上得到缓解:

(timeout-throttle 'tab-line-tabs-buffer-group-by-project)

但问题没这么简单,这实际上阻止了 tab-line 从每个 buffer 获取正确的分组信息,因为一次调用后指定的节流时间内的其余调用会直接返回第一次调用的返回结果,从而导致不正确的分组。如果你对 tab-line 的代码进行一些分析,你会发现我们实际上需要节流的是 tab-line-format

(timeout-throttle 'tab-line-format)

但是话又说回来了,这也只是一种“权宜之计”,从稳定的卡顿变成了时不时卡你一下。要用好 timeout 还是需要对性能问题的原因有一定的了解。(也许更好的做法是添加带实际缓存的 advice)。

:innocent:

通过 timeout ,这一问题看似能够在一定程度上得到缓解:

但问题没这么简单,这实际上阻止了 tab-line 从每个 buffer 获取正确的分组信息,因为一次调用后指定的节流时间内的其余调用会直接返回第一次调用的返回结果,从而导致不正确的分组。如果你对 tab-line 的代码进行一些分析,你会发现我们实际上需要节流的是 tab-line-format

你的这个例子很好,我想要考虑要不要发到邮件列表里。

这个也是我为什么不认可这个包的原因。throttle 和 debounce 是用于异步编程里的。你现在是把一个本来写的时候是假设同步的逻辑写的代码给强制 delay 了,那么从逻辑上来说,这个代码就变成 不正确 的了。实现一个正确的 throttle 和 debounce,是不可能只改这个函数本身,而不去改调用方的逻辑的,肯定要改动调用方的逻辑,把这个调用方的调用要么改成异步的,要么修改为正确的更新和查找缓存的逻辑。

1 个赞

同意。从糊泥巴的角度来说这个包还不错,但真正要解决问题还得先把问题弄清楚(至少是对于我上面的例子,我认为更好的方法是用一个哈希表记录 buffer 对应的 project,因为 buffer 对应的文件基本上与 project 绑定,一般不会变动)

退一步来说,想弄明白在哪里糊泥巴大概不是初级用户能做到的。

虽然有点好笑,不过从 throttle 和 debounce 出发确实也能带来点新思路,我对这个包的评价是有点好吃的鸡肋 :joy:

1 个赞

谢谢您 人肉gpt

2 个赞
  • 这玩意没必要单独弄个包吧? 至少 timeout 这个名字误导性太强了, package 的作者们总是没什么取名的品位…
  • debounce/throttle 是 UI 编程的常规操作了, JavaScript 里随处可见. 但是 JavaScript 是天生异步的, 也就是说大家在书写时默许自己挂在 UI 组件上的 callback 不会立即执行. ELisp 是吗? 我感觉这两个函数会对代码的正确性有影响, 主要是破坏了 package 作者的假设.
  • debounce/throttle 其实也有用, 属于不怎么常用的基础设施. 想玩的时候不需要自己亲手编写也挺好的. 指望提高多少性能就算了..
2 个赞

(从 9 月初到现在 (2025-09-15),本贴基本算完结了,最近的一次讨论发生在 9 月 12 日。)

正如楼上 @milan-glacier 提到的那样,他前往 emacs-devel 发表了反对意见,这些邮件主要集中在 9 月 2 日,具体内容大体可以用这个帖子中的相关发言概括。感兴趣的同学可以看看 9 月 2 日的相关讨论。这里我就不展开了,而是简单说下 Milan-glacier 提出的改进 pr

Re: [ELPA] New package: timeout

我认为动态节流(dynamic throttling)和防抖动(debouncing)是通用库的基本特性。至少,timeout-* 函数除了接受一个固定的数值外,还应该接受一个符号。这样,该包就可以在运行时读取这个符号的值,从而让用户能够设置缓冲区本地(buffer-local)的值,或者通过模式钩子(mode hooks)来自定义延迟。

这是 Emacs 中一种常见且有效的模式。例如,company-idle-delay 就是一个用于控制 Company 防抖动行为的变量。因为它是一个符号,它的值可以动态地改变;它甚至支持被求值为一个在运行时求值的函数。采用类似的方法将显著增强 timeout-* 包的灵活性。

这说得通。至于你说的“除了固定数值外,还接受一个符号”,我的理解是 THROTTLE/DELAY 参数可以以一个数值一个符号的形式提供,而不是说 timeout-throttle(及类似函数)应该接受一个额外的参数。

你愿意为这个功能提供一个补丁吗?这个补丁可以针对 ELPA 包或 Emacs 本身,如果是后者,我可以自己将它应用到 ELPA 包上。

Karthik

从内容上来说,这一 pr 主要是增加了时延的种类,由单一的数值变为可接受数字,函数或符号值,这主要由以下函数来实现:

(defsubst timeout--eval-value (value)
  "Eval a VALUE.
If value is a function (either lambda or a callable symbol), eval the
function (with no argument) and return the result.  Else if value is a
symbol, return its value.  Else return itself."
  (cond ((numberp value) value)
        ((functionp value) (funcall value))
        ((and (symbolp value) (boundp value)) (symbol-value value))
        (t (error "Invalid value %s" value))))

从优先级上来说,数值 > 函数(函数对象或 function-cell 为函数对象的符号) > 符号(value-cell 的值)。通过在调用 debounce/throttle 系列函数时指定符号或函数,我们可以通过函数/符号的值来控制 debounce/throttle 行为的时延。这相比原版更加灵活。

目前还有另一 pr 未被合并: timeout: Name advice functions. by milanglacier,这一 pr 主张在添加 advice 时调用 defalias 为 advice 自动命名来使其更加容易分辨。karthink 不太赞成这种做法。

目前,通过拉取最新的 master 分支,或通过 elpa 下载 timeout,就可以试试这个包了。

以上。

2 个赞

从糊泥巴的角度,是不是这个包和几个函数改名大家就不会有这么强的反对声音了?毕竟是提供了一条缓解的途径,虽然不彻底 :joy:

主要问题在于这个包不被认为应该直接加入 到 Emacs。实际上原作者只是提交到了 GNU ELPA,但是 Eli 认为这样一个通用的包应该被直接加入 Emacs。

反对者主要的顾虑可能是“Advice 机制应由用户在完全自己负责的情况下使用而不是由包来直接使用”,以及这可能会“鼓励”用户滥用这一机制增大调试和排错难度。

嗯,这确实是个原则性问题。通过了也就意味着鼓励用户也这么用。不知道Eli究竟是个什么想法,我还没时间慢慢爬楼去看emacs-devel

1 个赞