惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Help Net Security
Help Net Security
S
Schneier on Security
Security Latest
Security Latest
C
Cyber Attacks, Cyber Crime and Cyber Security
博客园_首页
K
Kaspersky official blog
B
Blog
雷峰网
雷峰网
MyScale Blog
MyScale Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
人人都是产品经理
人人都是产品经理
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
T
Threatpost
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Latest news
Latest news
J
Java Code Geeks
W
WeLiveSecurity
GbyAI
GbyAI
V
Vulnerabilities – Threatpost
S
Secure Thoughts
IT之家
IT之家
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 叶小钗
Engineering at Meta
Engineering at Meta
S
Security @ Cisco Blogs
罗磊的独立博客
M
MIT News - Artificial intelligence
Blog — PlanetScale
Blog — PlanetScale
Stack Overflow Blog
Stack Overflow Blog
月光博客
月光博客
P
Proofpoint News Feed
博客园 - 聂微东
云风的 BLOG
云风的 BLOG
N
Netflix TechBlog - Medium
D
DataBreaches.Net
S
SegmentFault 最新的问题
V
Visual Studio Blog
V
V2EX
The Register - Security
The Register - Security
N
News | PayPal Newsroom
aimingoo的专栏
aimingoo的专栏
C
Cybersecurity and Infrastructure Security Agency CISA
N
News and Events Feed by Topic
Hacker News: Ask HN
Hacker News: Ask HN
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Google DeepMind News
Google DeepMind News
Webroot Blog
Webroot Blog
NISL@THU
NISL@THU
D
Darknet – Hacking Tools, Hacker News & Cyber Security

暗无天日

读:AI Agent 安全日志——从可见性与隐私的两难说起 - 暗无天日 AI写作的语言指纹——如何让文字不那么像机器 - 暗无天日 读:50 条 Claude Code 技巧——一个工程经理的六个月使用心得 读:AI 辅助开发为什么让 E2E 测试更有价值 - 暗无天日 读:在Emacs中使用Claude Code(Spacemacs适配版) - 暗无天日 Claude Code 背后的工程哲学——读 Agent Harness Engineering 读:Agent Harness Engineering——AI 智能体不只是模型,还有套件 - 暗无天日 browser-harness:让 AI 直接接管你的浏览器 - 暗无天日 读:Security-First CI/CD —— DevSecOps 自动化实践指南 TIL: 数字小键盘的小数点陷阱与行内算术求值 - 暗无天日 读:Immutability 不是万能药,它是一种权衡 - 暗无天日 Conducty:给 Claude Code 加上项目记忆和并行执行能力 - 暗无天日 读 — GitHub Trending 里的 Claude Code 技能包 读 — Prompt Caching 省钱指南 TIL: Emacs 中那些跟鼠标配合的冷门快捷键 - 暗无天日 读:Anvil——把 Emacs 变成 AI 的工具服务器 读:Emacs 代码折叠终极指南 - 暗无天日 读:Clojure 搭车客指南 - 暗无天日 git推送失败后恢复仓库损坏的完整记录 - 暗无天日 多智能体系统的两个有效模式——以及对 Claude Code 用户的启示 - 暗无天日 用 Org Babel 写 Literate 博文:扩展执行 + 定制导出 proced:Emacs 内置的进程查看器 - 暗无天日 从 proced 定制中学到的 Elisp 模式 读:让 Emacs proced 在 macOS 上显示 CPU 和内存 异步编程的函数着色税 - 暗无天日 链式调用的代价:JavaScript 和 Clojure 的共同教训 - 暗无天日 hyperfine:命令行基准测试工具 - 暗无天日 管道中的变量去哪了?——子 shell 作用域陷阱 - 暗无天日 开源包装器的信任陷阱:四个危险信号 - 暗无天日 程序员愿意为 AI 写文档,却不愿为同事写 - 暗无天日 mktemp: Shell 脚本中临时文件的安全陷阱与最佳实践 - 暗无天日 WSL9x —— 在 Windows 9x 里跑 Linux 内核 6.19 用 ox.el 做你想做的事 —— org-export 高级编程指南 读:Hot-wiring the Lisp Machine —— 用纯 Elisp 构建零依赖的 Org 静态站点生成器 Elisp 性能优化的六个实战教训 - 暗无天日 fcitx5 下 Emacs 无法切换输入法的排查 - 暗无天日 ERT 测试交互命令的三种方式 - 暗无天日 SEM Assistant: 当 Elisp 守护进程遇上 LLM 用 dmsg 给 Elisp 加上结构化调试日志 用 org-habit 追踪非每日习惯 - 暗无天日 Clojure X-Men:当编程语言特性变成超能力 - 暗无天日 TIL: 用 diff-hl 在 fringe 中显示 git 变更 读:llm-test —— 用 LLM agent 驱动 Emacs 测试 TIL: AI 时代的橡皮鸭调试 - 暗无天日 fcitx 启动后键盘输入卡顿的排查 - 暗无天日 TIL: 早期网页的图片热区导航 - 暗无天日 读 Seeing the Whole System 用 Emacs 自动生成每周链接推荐 - 暗无天日 读:ASCII control characters in my terminal 读 What to learn - 暗无天日 Lisp 的括号之痛——一个愚人节玩笑揭开的老伤疤 - 暗无天日 一本书该"线性读"还是"并行读" - 暗无天日 读 How to Monetize a Blog:一篇伪装成变现指南的讽刺文 Python Mock 第三方依赖的四种策略 - 暗无天日 Emacs Lisp 热重载实用指南 - 暗无天日 Prot 的 Emacs 配置哲学 - 暗无天日 TIL: 从直播对谈中学到的三个 Emacs 技巧 - 暗无天日 TIL: 自动使用项目虚拟环境的 Python - 暗无天日 TIL: 让 Help buffer 自动获得焦点 一条命令让本地开发用上 HTTPS —— slim 工具介绍 用 fsck 检查和修复 Linux 文件系统 排查Linux进程"卡死"实战:从strace到gdb全流程 - 暗无天日 PostgreSQL 索引:从基础到你可能不知道的高级用法 - 暗无天日 用 .pdbrc 自定义 Python 调试器 ANSI 转义码的标准化现状 - 暗无天日 终端程序的潜规则 - 暗无天日 PARA Org-mode 测试配置 - 暗无天日 AI越强越辣鸡?控制论说这是必然的 - 暗无天日 AI 越强越需要你盯着——反馈循环实操指南 - 暗无天日 你的AI代理正在偷你的密钥——四种你没想到的泄露通道 - 暗无天日 LLM 在 DevOps 中的三种角色 - 暗无天日 写作风格的反建议 - 暗无天日 反驳本质复杂性——Dan Luu 论为什么《没有银弹》错了 - 暗无天日 文件充满了危险——Dan Luu 谈文件系统的可靠性陷阱 - 暗无天日 AI 时代的 PARA 方法:用 Org-mode 和 AI 打造个人知识管理系统 Linux 数据去重学习笔记 - 暗无天日 创建跨平台 ZIP 文件的隐藏陷阱:Extra Field - 暗无天日 X11 Forwarding 排障指南 - 暗无天日 IP欺骗端口扫描:当别人冒充你去扫描别人 - 暗无天日 Linux 输入栈全景解析:从硬件按键到屏幕响应 - 暗无天日 Unix 系统中那些被埋没的配置开关——以 FontConfig 为例 - 暗无天日 在Linux上限制儿童使用电脑 - 暗无天日 GIF不仅仅是一种图片格式——用GIF流做些奇怪的事 - 暗无天日 Leiningen 学习笔记:Clojure 项目构建与管理从入门到实战配置 - 暗无天日 Google SRE Book 读书笔记 - 暗无天日 yes 管道 head 发生了什么 - 暗无天日 为什么 nohup 在 crontab 中不起作用 Bash中的Indirection与Nameref - 暗无天日 Linux PAM 简介 - 暗无天日 从Linux ISO文件启动计算机 - 暗无天日 用 Bash 打造一个Screen Locker 用GitHub Actions自动构建EGO博客 - 暗无天日 blocking I/O 的作用 - 暗无天日 mobileog 手机端同步提示Error:2 No such file 的解决方法 回收 WSL2 VHDX 文件占用空间 使用 org-mode columnview 生成任务列表 - 暗无天日 Emacs 作为 MPD 客户端 - 暗无天日 移动文件路径却不破坏org file link的方法 - 暗无天日 如何合理的导出help link 成HTML - 暗无天日 笑话理解之Biology - 暗无天日
org-mobile-push 卡顿排查实战:从黑盒到字节码反编译 - 暗无天日
2026-05-10 · via 暗无天日

故障现象

我用 Docker 跑了一个 mobileorg-sync 服务,实现手机端 Org-mode(MobileOrg)和电脑端的自动同步。同步流程大概是:

  1. git pull 拉取最新org数据
  2. org-mobile-pull 应用手机端的修改
  3. org-mobile-push 生成 agenda 视图推送到手机
  4. git commit && git push 提交并推送org数据

某天发现,同步脚本卡住了——日志显示 Creating agendas... 后就再也没有输出。等了 16 分钟进程还在跑,CPU 占用 99%。

整个排查过程花了不少功夫,也踩了很多 Elisp advice(函数建议)的坑。下面一步步复盘。

第一步:缩小范围——到底是哪个步骤慢?

添加计时日志

首先需要搞清楚,是 org-mobile-pull 慢,还是 org-mobile-push 慢,还是 git 操作慢?

我先定义一个日志函数,把带时间戳的消息追加写入文件。这样即使进程卡死,已经写入的日志也不会丢失:

(defun mobileorg-sync-log (fmt &rest args)
  "带时间戳写入日志到文件,同时输出到 stdout。"
  (let ((msg (format "[%s] %s"
               (format-time-string "%Y-%m-%d %H:%M:%S.%3N")
               (apply #'format fmt args))))
    (message "PERF: %s" msg)        (with-temp-buffer
      (insert msg "\n")
      (append-to-file (point-min) (point-max) "/tmp/mobileorg-sync-perf.log"))))

然后定义一个计时宏 mobileorg-sync--timed ,用它把 mobileorg-sync 函数中的每个步骤包裹起来:

(defmacro mobileorg-sync--timed (label &rest body)
  "执行 BODY 并记录耗时,LABEL 为步骤名。"
  `(let ((start (float-time)))
     (mobileorg-sync-log ">>> BEGIN %s" ,label)
     (prog1 (progn ,@body)
       (mobileorg-sync-log "<<< END %s (%.3fs)" ,label (- (float-time) start)))))

(defun mobileorg-sync ()
  (interactive)
  (let ((default-directory org-directory))
    (mobileorg-sync--timed "git pull"        (shell-command "git pull"))
    (mobileorg-sync--timed "org-mobile-pull"  (org-mobile-pull))
    (mobileorg-sync--timed "save-some-buffers" (save-some-buffers 4))
    (mobileorg-sync--timed "org-mobile-push"  (org-mobile-push))
    ...))

第一轮结果

部署后运行,日志输出如下:

[08:20:46.982] >>> BEGIN org-mobile-pull
[08:20:47.305] <<< END org-mobile-pull (0.323s)
[08:20:47.310] >>> BEGIN org-mobile-push
(等了 16 分钟,没有后续输出)

org-mobile-pull 只用了 0.3 秒,而 org-mobile-push 卡了 16 分钟以上。问题锁定在 org-mobile-push

第二步:搞清楚 org-mobile-push 内部调了什么

org-mobile-push 是 Org-mode 内置函数,但我的容器里只装了编译后的 .elc 字节码文件,没有 .el 源码。=find-function= 也找不到源文件。

怎么办?Emacs 的字节码文件虽然不能直接阅读,但可以提取编译时生成的 常量表 。每个编译后的函数都有一张常量表,里面按序存放了函数内部引用的所有符号和字面量。

(let* ((def (indirect-function 'org-mobile-push))
       (consts (aref def 2)))     (dotimes (i (length consts))
    (message "[%d] %S" i (aref consts i))))

输出如下(省略了不关键的部分):

[17] org-mobile-check-setup
[18] org-mobile-prepare-file-lists
[19] message
[20] "Creating agendas..."
[24] org-mobile-create-sumo-agenda
[25] "Creating agendas...done"
[26] org-save-all-org-buffers
[28] org-mobile-copy-agenda-files
[30] org-mobile-create-index-file
[32] org-mobile-write-checksums

于是 org-mobile-push 的完整执行流程一目了然:

  1. org-mobile-check-setup
  2. org-mobile-prepare-file-lists
  3. message "Creating agendas..."
  4. org-mobile-create-sumo-agenda
  5. org-save-all-org-buffers
  6. org-mobile-copy-agenda-files
  7. org-mobile-create-index-file
  8. org-mobile-write-checksums

其中第 4 步 org-mobile-create-sumo-agenda 看名字就知道是"生成汇总 agenda"的关键步骤。用同样的常量表方法继续挖它一层:

(let* ((def (indirect-function 'org-mobile-create-sumo-agenda))
       (consts (aref def 2)))
  (dotimes (i (length consts))
    (message "[%d] %S" i (aref consts i))))
[3]  org-agenda-custom-commands
[4]  org-mobile-creating-agendas
[6]  "agendas.org"
[7]  org-mobile-sumo-agenda-command
[13] org-store-agenda-views

可以看到它调用了 org-store-agenda-views ,并引用了 org-agenda-custom-commands 。所以这就是生成所有自定义 agenda 视图的地方。

这是一个非常有用的小技巧:即使没有源码,也能通过常量表了解函数的执行逻辑。

第三步:用 advice 注入计时——踩坑与解决

知道了 org-mobile-push 内部调用了哪些函数,下一步就是用 Emacs 的 advice 机制,在每个子函数执行前后插入计时代码。

advice 类似于其他语言中的装饰器(decorator)或 AOP,可以在不修改原函数的情况下,在函数执行前后插入自定义逻辑。比如 :before advice 在原函数执行前运行,=:after= 在执行后运行,=:around= 可以完全包裹原函数。

坑1:=:around= advice 中 orig-fn 不是符号

我最初尝试用 :around advice 包裹 org-mobile-check-setup 等函数:

(advice-add 'org-mobile-check-setup :around
            (lambda (orig-fn &rest args)
              (let ((fn-name (symbol-name orig-fn)))                  (mobileorg-sync-log ">> %s START" fn-name)
                (apply orig-fn args))))

运行时报错:

Wrong type argument: symbolp, #[0 "À Â" [org-mobile-files-alist ...] 2]

原因是 :around advice 的第一个参数 orig-fn 接收的是 /原始函数对象/,可能是字节码对象(=#[0 ...]=),而不是函数的符号名。对字节码对象调用 symbol-name 当然会报错。

坑2:改用 let 捕获函数名后闭包失效

为了绕开坑1,我在 advice 外面用 let 提前捕获函数名,让 lambda 通过闭包引用它:

(dolist (fn '(org-mobile-check-setup org-mobile-prepare-file-lists ...))
  (let ((fname (symbol-name fn)))
    (advice-add fn :around
                (lambda (orig-fn &rest args)
                  (mobileorg-sync-log ">> %s START" fname)                    (apply orig-fn args)))))

运行时报错:

Symbol's value as variable is void: fname

这说明 fname 并没有被 lambda 闭包捕获。这个脚本使用 emacs --quick --script 方式运行,文件头通过 # -*- lexical-binding: t; -*- 声明了词法作用域。但由于脚本第一行是 shell 多行注释伪装成 Elisp 的技巧(=\":\"; exec emacs --quick --script \"$0\"),可能影响了 Emacs 对 file-local variables 的解析,导致词法绑定没有真正生效。于是 lambda 中的 =fname 变成了自由变量,运行时找不到绑定就报错了。

解决方案:用 defmacro 在编译期展开

既然闭包不可靠,我就用宏(macro)来彻底规避这个问题。宏在 编译期 就会把函数名字面量直接写进生成的代码,完全不依赖运行时的闭包捕获:

(defmacro mobileorg--def-perf-advice (fn)
  (let ((before-fn (intern (format "mobileorg--perf-before-%s" fn)))
        (after-fn  (intern (format "mobileorg--perf-after-%s" fn))))
    `(progn
       (defun ,before-fn (&rest _)
         (mobileorg--perf-log (format ">> %s START" ,(symbol-name fn)))
         (puthash ',fn (float-time) mobileorg--perf-start-times))
       (defun ,after-fn (&rest _)
         (mobileorg--perf-log
          (format "<< %s END (%.3fs)" ,(symbol-name fn)
                  (- (float-time) (gethash ',fn mobileorg--perf-start-times)))))
                                   (advice-add ',fn :before #',before-fn)
       (advice-add ',fn :after  #',after-fn))))

(dolist (fn '(org-mobile-check-setup
              org-mobile-prepare-file-lists
              org-mobile-create-sumo-agenda
              org-store-agenda-views
              org-mobile-write-agenda-for-mobile
              org-agenda-list org-tags-view org-todo-list))
  (eval `(mobileorg--def-perf-advice ,fn)))

这个方案同时解决了两个坑:

  • :before / :after 而非 :around ,彻底避开了 orig-fn 类型问题
  • defmacro 在宏展开时嵌入字面量,彻底避开了闭包捕获问题

第四步:精确定位——哪个 agenda 命令卡死了?

用修复后的 advice 重新运行,终于拿到了详细的函数级日志:

[09:20:05.514] >> org-mobile-create-sumo-agenda START
[09:20:05.514] >> org-store-agenda-views START
[09:20:09.246] >> org-agenda-list START
[09:20:14.305] << org-agenda-list END (5.058s)
[09:20:14.305] >> org-todo-list START
[09:20:14.442] << org-todo-list END (0.136s)
[09:20:14.442] >> org-todo-list START
[09:20:14.582] << org-todo-list END (0.139s)
[09:20:14.582] >> org-tags-view START
[09:20:17.853] << org-tags-view END (3.270s)
[09:20:17.853] >> org-tags-view START
[09:20:21.183] << org-tags-view END (3.330s)
[09:20:21.184] >> org-tags-view START
[09:20:24.777] << org-tags-view END (3.593s)
[09:20:24.778] >> org-tags-view START
(卡住,之后再无输出)

org-store-agenda-views 会遍历 org-agenda-custom-commands 中的每一条命令,逐一生成 agenda 视图。每条命令对应一次底层函数调用(=org-agenda-list= 、=org-todo-list= 或 org-tags-view )。

我的 org-agenda-custom-commands 配置是这样的:

(setq org-agenda-custom-commands
      '(("n" "Next"   todo      "NEXT"  (...))                ("t" "Today"  todo      "TODAY" (...))                 ("t" "Today"  (lambda ...) "")                         ("s" "超市"   tags      "超市"  (...))                 ("m" "会议"   tags      "会议"  (...))                 ("o" "OFFICE" tags      "WORK"  (...))                 ("h" "Habits" tags-todo "STYLE=\"habit\"" (...))         ))

对照日志,可以把每次函数调用和具体命令一一对应起来:

"n" NEXT       → org-todo-list  (0.14s) ✓
"t" TODAY      → org-todo-list  (0.14s) ✓
"t" (lambda)   → org-agenda-list (5.1s) ✓
"s" 超市       → org-tags-view  (3.3s)
"m" 会议       → org-tags-view  (3.3s)
"o" WORK       → org-tags-view  (3.6s)
"h" Habits     → org-tags-view  (卡死!) ✗

罪魁祸首就是 ("h" "Habits" tags-todo "STYLE\"habit\"" ...)= 这条命令。

第五步:为什么 Habits 命令会卡死?

这条命令的类型是 tags-todo ,匹配条件是 STYLE"habit"= 。

这里要区分两个概念:

  • /Tags/(标签)写在标题行末尾,比如 * TODO 买菜 :生活: 。搜索标签只需要扫描标题行,速度很快。
  • Properties/(属性)写在 :PROPERTIES: drawer 中,比如 :STYLE: habit: 。搜索属性需要 /打开每个 headline 的属性 drawer 来解析,开销大得多。

my 配置中的 org-agenda-file-regexp 匹配了 _archive 文件,所以 agenda 范围包含了 office.org_archive (5272 个 headline、1.9MB)。=tags-todo "STYLE=\"habit\""= 会在 所有 headline 上逐个解析属性 drawer,试图匹配 STYLE 属性。这就是灾难性的性能表现。

第六步:验证与修复

先做一个快速验证——去掉 Habits 命令,只保留 todo 类命令,看 org-mobile-push 多快:

real  0m12.001s

12 秒!再加上 tags 命令(不含 Habits):

real  0m33.782s

34 秒。而加上 Habits 命令就是 16 分钟以上。差距一目了然。

最终修改很简单——从 org-agenda-custom-commands 中移除 Habits 命令即可。修改后完整同步时间从 16+ 分钟降到 24 秒

总结

这次排查用到几个关键技巧:

  1. **计时日志定位大范围**:用宏在 sync 的每个步骤前后记录时间戳,先确定是 push 而不是 pull 慢。
  2. **字节码常量表反推执行流程**:没有源码时,用 (aref (indirect-function 'fn) 2) 提取常量表,就能看清函数调用了哪些子函数。
  3. **advice 注入计时代码**:Emacs 的 advice 机制类似于装饰器,可以在不修改原函数的情况下插入逻辑。但要注意 :around advice 中 orig-fn 可能是字节码对象而非符号,以及闭包在特定加载方式下可能失效的问题。用 defmacro 生成命名函数配合 :before / :after 是最稳妥的方案。
  4. **对比实验验证假设**:分别测试"只有 todo 命令"和"todo + tags"的耗时,精确定位到 tags-todo "STYLE\"habit\""= 是卡死的原因。

最后的教训是:=tags-todo= 搭配属性匹配(如 STYLE"habit"= )需要对每个 headline 解析属性 drawer,在大量 headline 的场景下性能极差。如果需要在移动端查看 Habits,可以考虑改用 tag(比如 :habit: )来代替属性匹配,这样只需要扫描标题行,速度会快得多。