refactor: consume public performance reports without advice

This commit is contained in:
Kinneyzhang 2026-08-28 00:52:54 +08:00
parent bcb7254350
commit d63cf1a813
6 changed files with 644 additions and 1148 deletions

View File

@ -42,12 +42,15 @@ Define a Component:
## Performance records
ETAF includes an opt-in, application-neutral timing recorder. It recognizes
public Event, Action, mount, flush, focus, Data, Resource, and viewport
operations, then correlates coarse ETAF, Ebox, TP, and SQLite stages when those
packages are loaded.
ETAF provides an independent, opt-in, application-neutral timing recorder. It
consumes public Runtime observer reports without advice or private cross-package
probes. Runtime operations such as Event, Action, mount, flush, and unmount are
recorded automatically; Ebox, TP, Data, Resource, and SQLite provider stages
inside the same operation are correlated by sequence.
```elisp
(require 'etaf-performance)
;; In a buffer with a mounted ETAF Runtime:
(etaf-performance-mode 1)
;; Use any mounted ETAF application normally.
(etaf-performance-show)
@ -58,30 +61,24 @@ reproducing the operations, press `c` in the panel (or run
`M-x etaf-performance-copy-report`) to copy a complete report. Press `w` (or
run `M-x etaf-performance-export`) to save the same report as an `.eld` file.
The portable report includes the Emacs/display environment, power source,
low-power mode, system load, grouped p50/p95/max, individual operations, GC
deltas, and nested stages. The panel header exposes the same environment
context so a machine-wide slowdown is not mistaken for one package hotspot.
low-power mode, native-JIT state, system load, grouped p50/p95/max, individual
operations, GC deltas, and ordered provider stages. The panel header exposes
the same environment context so a machine-wide slowdown is not mistaken for
one package hotspot.
The `*ETAF Performance*` panel shows operation IDs and parent operations,
generation changes, total latency, GC deltas, and nested stage inclusive/self
time. Records are bounded by `etaf-performance-max-records`; disabling the
mode removes all recorder advice. `etaf-performance-summary` computes
operation p50/p95/max statistics on demand, while
`etaf-performance-operation-stage-summary` ranks one operation's package
categories by exclusive time.
The default registry deliberately uses coarse package boundaries to keep the
measured application representative. A package or application can add a
temporary detail probe without coupling itself to an example:
```elisp
(etaf-performance-register-stage
'my-package-expensive-step 'application 'expensive-step)
```
The `*ETAF Performance*` panel shows operation IDs, generation changes, total
latency, GC deltas, and each flat provider stage in sequence. Provider stages
may overlap, so they are not presented as exclusive/self time. Records are
bounded by `etaf-performance-max-records`; disabling the mode only detaches the
Runtime observer and never rewrites functions. `etaf-performance-summary`
computes operation p50/p95/max statistics on demand, while
`etaf-performance-operation-stage-summary` groups one operation's flat stages
by provider category.
Use `etaf-performance-call-operation` or
`etaf-performance-with-operation` to trace an arbitrary operation that has no
built-in public boundary.
built-in public boundary. Both delegate to the same Runtime operation boundary;
they do not create a second timer.
## Executable examples

View File

@ -42,12 +42,14 @@ Component(props, Scope) → View → Renderer → Ebox Node → Emacs buffer
## 性能记录面板
ETAF 内置了一个按需启用、与具体应用无关的性能记录器。它自动识别公共
Event、Action、mount、flush、focus、Data、Resource 和 viewport 操作;当
Ebox、TP、SQLite 等包已加载时,还会把这些包的粗粒度阶段关联到同一个
operation
ETAF 提供一个独立、按需加载、与具体应用无关的性能记录器。它消费 Runtime
公共 observer 报告,不使用 advice 或私有跨包探针。Event、Action、mount、flush
和 unmount 等 Runtime operation 会自动记录;同一 operation 内的 Ebox、TP、Data、
Resource 和 SQLite 等 provider 阶段按 sequence 关联到同一条记录
```elisp
(require 'etaf-performance)
;; 在已挂载 ETAF Runtime 的 buffer 中:
(etaf-performance-mode 1)
;; 正常操作任意已挂载的 ETAF 应用。
(etaf-performance-show)
@ -56,28 +58,21 @@ operation。
交互采集时先执行 `M-x etaf-performance-clear`,复现一组操作后,在面板按
`c`(或执行 `M-x etaf-performance-copy-report`)即可把完整报告复制到剪贴板;
`w`(或执行 `M-x etaf-performance-export`)可保存为 `.eld` 文件。报告包含
Emacs/显示环境、电源来源、低功耗模式、系统负载、分组 p50/p95/max、每次
operation、GC 增量和嵌套阶段;面板 header 也显示同一环境信息,避免把整机降频
误判成某个包的热点。
Emacs/显示环境、电源来源、低功耗模式、native JIT 状态、系统负载、分组
p50/p95/max、每次 operation、GC 增量和有序 provider 阶段;面板 header 也显示
同一环境信息,避免把整机降频误判成某个包的热点。
`*ETAF Performance*` 面板会显示 operation ID 与父 operation、generation
变化、总耗时、GC 增量,以及嵌套阶段的 inclusive/self 耗时。记录数量由
`etaf-performance-max-records` 限制;关闭 mode 会移除全部记录 advice。
`*ETAF Performance*` 面板会显示 operation ID、generation 变化、总耗时、GC
增量,以及 flat provider 阶段的顺序和各自耗时。provider 阶段可能重叠,因此
不会伪装成 exclusive/self 时间。记录数量由 `etaf-performance-max-records`
限制;关闭 mode 只会解除当前 Runtime 的 observer不修改任何函数。
`etaf-performance-summary` 会按需计算 operation 的 p50/p95/max
`etaf-performance-operation-stage-summary` 则按 exclusive 耗时排列单次
operation 的包级阶段。
默认注册表刻意只记录包级粗边界,避免分析工具明显改变被测应用。任意包或
应用可以临时增加更细的探针,不需要绑定某个示例:
```elisp
(etaf-performance-register-stage
'my-package-expensive-step 'application 'expensive-step)
```
`etaf-performance-operation-stage-summary` 则按 provider category 汇总单次
operation 的 flat 阶段。
没有内置公共边界的任意操作,可以使用
`etaf-performance-call-operation``etaf-performance-with-operation`
包裹。
包裹;它们直接委托同一个 Runtime operation 边界,不建立第二套计时器。
## 可执行示例

View File

@ -612,6 +612,8 @@ Enable the generic recorder around any application workload, then open its
ordinary `tabulated-list-mode` panel:
```elisp
(require 'etaf-performance)
;; In a buffer with a mounted ETAF Runtime:
(etaf-performance-mode 1)
(etaf-performance-show)
```
@ -620,23 +622,26 @@ Run `M-x etaf-performance-clear` before the measured reproduction. Afterwards,
press `c` in the panel, or run `M-x etaf-performance-copy-report`, to copy the
environment, summary, operation, GC, and stage data. Press `w`, or run
`M-x etaf-performance-export`, to save the same portable report as an `.eld`
file. The report and panel header include power source, low-power mode, and
system load alongside the ordinary Emacs/display environment.
file. The report and panel header include power source, low-power mode,
native-JIT state, and system load alongside the ordinary Emacs/display
environment.
The recorder creates bounded operation records for public interaction,
lifecycle, Data, Resource, and viewport boundaries. Loaded Ebox, TP, and
SQLite functions contribute nested coarse stages without depending on ETAF.
Each stage reports inclusive and exclusive milliseconds; failures and quits
are recorded and then re-signaled unchanged.
The recorder only consumes public Runtime observer reports and installs no
advice. Runtime Event, Action, mount, flush, and unmount boundaries create
bounded operation records. Ebox, TP, Data, Resource, and SQLite may contribute
flat provider stages, ordered by sequence, inside the same operation. Stages
may overlap and are therefore not reported as exclusive/self time; failures and
quits are recorded and then re-signaled unchanged.
`etaf-performance-summary` calculates grouped p50/p95/max statistics only when
requested. `etaf-performance-operation-stage-summary` ranks the package
categories of one recorded operation by exclusive time.
requested. `etaf-performance-operation-stage-summary` groups the flat stages
of one recorded operation by provider category.
Use `etaf-performance-register-stage` for a temporary package-specific detail
probe. Use `etaf-performance-with-operation` for application work that does
not enter through a built-in public boundary. Disable the mode when the
capture is complete; all installed advice is removed.
Use `etaf-performance-call-operation` or `etaf-performance-with-operation` for
application work that does not enter through a built-in public boundary. Both
delegate to the same Runtime operation boundary. Disable the mode when the
capture is complete; this only detaches the Runtime observer and does not
rewrite any function.
## 14. Public API map
@ -655,7 +660,7 @@ capture is complete; all installed advice is removed.
| Resource | `etaf-resource`, `etaf-resource-result`, `etaf-error-boundary-run` | Own loader state and cleanup |
| Official UI | `require 'etaf-ui`, `etaf-button`, `etaf-checkbox`, `etaf-number-input`, `etaf-label`, `etaf-panel`, `etaf-data-grid` | Use ready-made Components |
| Playground | `etaf-playground-open`, `etaf-playground-open-ui`, `etaf-playground-close`, `ebox-playground-open`, `ebox-playground-close` | Explore the corresponding layer interactively |
| Performance | `etaf-performance-mode`, `etaf-performance-show`, `etaf-performance-copy-report`, `etaf-performance-export`, `etaf-performance-environment-data`, `etaf-performance-records`, `etaf-performance-summary`, `etaf-performance-operation-stage-summary`, `etaf-performance-with-operation`, `etaf-performance-register-stage` | Attribute generic operation latency across loaded packages and share reports |
| Performance | `etaf-performance-start`, `etaf-performance-stop`, `etaf-performance-mode`, `etaf-performance-show`, `etaf-performance-copy-report`, `etaf-performance-export`, `etaf-performance-environment-data`, `etaf-performance-records`, `etaf-performance-summary`, `etaf-performance-operation-stage-summary`, `etaf-performance-call-operation`, `etaf-performance-with-operation` | Attribute generic operation latency through public observers and share reports |
Most applications need only `etaf-view`, `etaf-mount`, `etaf-define-component`, `etaf-ref`, and event callbacks at first. The remaining APIs are additive capabilities, not prerequisites for understanding the core grammar.

View File

@ -605,6 +605,8 @@ Loader 错误会保存在 `etaf-resource-error`cleanup/type 错误保持可
对任意应用负载启用通用记录器,然后打开普通的 `tabulated-list-mode` 面板:
```elisp
(require 'etaf-performance)
;; 在已挂载 ETAF Runtime 的 buffer 中:
(etaf-performance-mode 1)
(etaf-performance-show)
```
@ -612,21 +614,23 @@ Loader 错误会保存在 `etaf-resource-error`cleanup/type 错误保持可
正式复现前执行 `M-x etaf-performance-clear`。复现后在面板按 `c`,或者执行
`M-x etaf-performance-copy-report`可把环境、汇总、operation、GC 和阶段数据
完整复制到剪贴板;按 `w` 或执行 `M-x etaf-performance-export` 可导出 `.eld`
文件。报告和面板 header 还会显示电源来源、低功耗模式与系统负载。这两种报告
都可以直接发送给分析者。
文件。报告和面板 header 还会显示电源来源、低功耗模式、native JIT 状态与系统
负载。这两种报告都可以直接发送给分析者。
记录器会为公共交互、lifecycle、Data、Resource 和 viewport 边界创建有界的
operation 记录。已加载的 Ebox、TP 与 SQLite 可以贡献嵌套的粗粒度阶段,
但不需要反向依赖 ETAF。每个阶段同时报告 inclusive 与 exclusive 毫秒;
error 和 quit 会先被记录,再保持原条件继续抛出。
记录器只消费 Runtime 的公共 observer 报告,不安装 advice。Runtime 的 Event、
Action、mount、flush 和 unmount 会创建有界 operation 记录;同一 operation 内,
Ebox、TP、Data、Resource 与 SQLite 可以贡献按 sequence 排列的 flat provider
阶段。阶段可能重叠,因此不声称 exclusive/self 时间error 和 quit 会先被记录,
再保持原条件继续抛出。
`etaf-performance-summary` 只在请求时计算分组后的 p50/p95/max
`etaf-performance-operation-stage-summary` 会按 exclusive 时间排列某条记录
中的包级阶段。
`etaf-performance-operation-stage-summary` 会按 provider category 汇总某条记录
中的 flat 阶段。
使用 `etaf-performance-register-stage` 临时加入包级细节探针。没有经过内置
公共边界的应用操作可以使用 `etaf-performance-with-operation`。采集结束后
关闭 mode所有已安装 advice 都会被移除。
没有经过内置公共边界的应用操作可以使用
`etaf-performance-call-operation`/`etaf-performance-with-operation`;它们直接委托
同一个 Runtime operation 边界。采集结束后关闭 mode只解除当前 Runtime 的
observer不修改任何函数。
## 14. 公共 API 速查
@ -645,7 +649,7 @@ error 和 quit 会先被记录,再保持原条件继续抛出。
| Resource | `etaf-resource`、`etaf-resource-result`、`etaf-error-boundary-run` | 管理 loader 状态和 cleanup |
| 官方 UI | `require 'etaf-ui`、`etaf-button`、`etaf-checkbox`、`etaf-number-input`、`etaf-label`、`etaf-panel`、`etaf-data-grid` | 使用现成 Component |
| Playground | `etaf-playground-open`、`etaf-playground-open-ui`、`etaf-playground-close`、`ebox-playground-open`、`ebox-playground-close` | 探索对应层 |
| 性能分析 | `etaf-performance-mode`、`etaf-performance-show`、`etaf-performance-copy-report`、`etaf-performance-export`、`etaf-performance-environment-data`、`etaf-performance-records`、`etaf-performance-summary`、`etaf-performance-operation-stage-summary`、`etaf-performance-with-operation`、`etaf-performance-register-stage` | 跨已加载包定位通用 operation 的耗时并分享报告 |
| 性能分析 | `etaf-performance-start`、`etaf-performance-stop`、`etaf-performance-mode`、`etaf-performance-show`、`etaf-performance-copy-report`、`etaf-performance-export`、`etaf-performance-environment-data`、`etaf-performance-records`、`etaf-performance-summary`、`etaf-performance-operation-stage-summary`、`etaf-performance-call-operation`、`etaf-performance-with-operation` | 通过公共 observer 定位通用 operation 的耗时并分享报告 |
大多数应用一开始只需要 `etaf-view`、`etaf-mount`、`etaf-define-component`、`etaf-ref` 和事件 callback。其余 API 都是可选能力,不是理解核心语法的前置条件。

File diff suppressed because it is too large Load Diff

View File

@ -6,445 +6,299 @@
(require 'ert)
(require 'etaf)
(defconst etaf-performance-test--facade-loaded-recorder-p
(featurep 'etaf-performance)
"Whether loading the ETAF facade eagerly loaded the optional recorder.")
(require 'etaf-performance)
(ert-deftest etaf-performance-is-not-an-etaf-facade-dependency ()
"The optional recorder must be loaded explicitly."
(should-not etaf-performance-test--facade-loaded-recorder-p))
(defmacro etaf-performance-test--isolated (&rest body)
"Run BODY with isolated performance recorder state."
"Run BODY with isolated recorder state."
(declare (indent 0) (debug t))
`(unwind-protect
(progn
(etaf-performance-mode -1)
(etaf-performance-clear)
,@body)
(etaf-performance-mode -1)
(etaf-performance-clear)))
`(let ((etaf-performance--record-ring nil)
(etaf-performance--pending (make-hash-table :test #'equal))
(etaf-performance--attachments (make-hash-table :test #'eq)))
,@body))
(ert-deftest etaf-performance-records-success-result-and-error ()
"Recording preserves successful results and original error data."
(etaf-performance-test--isolated
(etaf-performance-mode 1)
(should (equal 42
(etaf-performance--call-operation
(lambda () 42) 'test-success 'test nil)))
(let ((condition
(condition-case error-data
(etaf-performance--call-operation
(lambda ()
(etaf-performance--call-stage
(lambda ()
(signal 'args-out-of-range '(source 1 2)))
'failing-stage 'test 'failure nil))
'test-error 'test nil)
(args-out-of-range error-data))))
(should (equal condition '(args-out-of-range source 1 2))))
(let ((records (etaf-performance-records)))
(should (= 2 (length records)))
(should (eq 'error
(etaf-performance-operation-status (car records))))
(should (eq 'error
(etaf-performance-stage-status
(car (etaf-performance-operation-stages
(car records))))))
(should (eq 'success
(etaf-performance-operation-status (cadr records)))))))
(cl-defmacro etaf-performance-test--with-runtime
((runtime buffer-name) &rest body)
"Mount RUNTIME in BUFFER-NAME, evaluate BODY, then clean up."
(declare (indent 1) (debug ((symbolp symbolp) body)))
`(let ((,buffer-name (generate-new-buffer-name " *etaf-performance-test*"))
,runtime)
(unwind-protect
(progn
(etaf-mount ,buffer-name (etaf-view (box "Performance")))
(setq ,runtime (etaf-runtime-for-buffer ,buffer-name))
,@body)
(when ,runtime
(etaf-performance-stop ,runtime)
(when (etaf-runtime-mounted-p ,runtime)
(etaf-unmount ,runtime)))
(when-let* ((buffer (get-buffer ,buffer-name)))
(kill-buffer buffer)))))
(ert-deftest etaf-performance-records-preserve-quit ()
"Recording marks and re-signals a quit without changing its data."
(ert-deftest etaf-performance-start-stop-own-exact-runtime-sink ()
"Attach idempotently and remove only this recorder's exact sink."
(etaf-performance-test--isolated
(etaf-performance-mode 1)
(let ((condition
(condition-case quit-data
(etaf-performance--call-operation
(lambda () (signal 'quit '(performance-test)))
'test-quit 'test nil)
(quit quit-data))))
(should (equal condition '(quit performance-test)))
(should (eq 'quit
(etaf-performance-operation-status
(car (etaf-performance-records))))))))
(etaf-performance-test--with-runtime (runtime buffer-name)
(should (eq runtime (etaf-performance-start runtime)))
(let* ((attachment
(gethash runtime etaf-performance--attachments))
(sink (etaf-performance--attachment-sink attachment)))
(should (functionp sink))
(should (etaf-runtime-compare-and-set-observer runtime sink sink))
(should (eq runtime (etaf-performance-start runtime)))
(should (eq runtime (etaf-performance-stop runtime)))
(should (etaf-runtime-compare-and-set-observer runtime nil nil))))))
(ert-deftest etaf-performance-public-operation-wrapper-is-generic ()
"The public operation API records arbitrary code and preserves its result."
(ert-deftest etaf-performance-start-rejects-foreign-observer ()
"Never replace an observer owned by another consumer."
(etaf-performance-test--isolated
(etaf-performance-mode 1)
(should
(equal '(generic result)
(etaf-performance-with-operation ('command "generic fixture")
'(generic result))))
(let ((operation (car (etaf-performance-records))))
(should (eq 'command
(etaf-performance-operation-kind operation)))
(should (equal "generic fixture"
(etaf-performance-operation-label operation))))))
(etaf-performance-test--with-runtime (runtime buffer-name)
(let ((foreign #'ignore))
(etaf-runtime-set-observer runtime foreign)
(should-error (etaf-performance-start runtime))
(should (etaf-runtime-compare-and-set-observer
runtime foreign foreign))
(etaf-runtime-set-observer runtime nil)))))
(ert-deftest etaf-performance-stage-registration-is-load-order-safe ()
"Third-party stages may register before their function is defined."
(ert-deftest etaf-performance-stop-preserves-replacement-observer ()
"A later foreign replacement is not detached by recorder stop."
(etaf-performance-test--isolated
(let ((function (make-symbol "etaf-performance-generic-stage")))
(etaf-performance-test--with-runtime (runtime buffer-name)
(etaf-performance-start runtime)
(let ((foreign #'ignore))
(etaf-runtime-set-observer runtime foreign)
(etaf-performance-stop runtime)
(should (etaf-runtime-compare-and-set-observer
runtime foreign foreign))
(etaf-runtime-set-observer runtime nil)))))
(ert-deftest etaf-performance-mode-is-current-buffer-convenience ()
"The local mode attaches only the Runtime in its current buffer."
(etaf-performance-test--isolated
(etaf-performance-test--with-runtime (runtime buffer-name)
(with-current-buffer buffer-name
(etaf-performance-mode 1)
(should etaf-performance-mode)
(should (eq runtime etaf-performance--mode-runtime))
(let* ((attachment
(gethash runtime etaf-performance--attachments))
(sink (etaf-performance--attachment-sink attachment)))
(should (functionp sink))
(should (etaf-runtime-compare-and-set-observer
runtime sink sink)))
(etaf-performance-mode -1)
(should-not etaf-performance-mode)
(should (etaf-runtime-compare-and-set-observer runtime nil nil))))))
(ert-deftest etaf-performance-mode-rejects-non-runtime-buffer ()
"The buffer-local convenience requires a mounted Runtime."
(with-temp-buffer
(should-error (etaf-performance-mode 1))
(should-not etaf-performance-mode)))
(ert-deftest etaf-performance-unmount-records-and-releases-attachment ()
"Record unmount before the Runtime detaches this recorder."
(etaf-performance-test--isolated
(let ((buffer-name (generate-new-buffer-name " *etaf-performance-unmount*"))
runtime)
(unwind-protect
(progn
(etaf-performance-register-stage function 'extension 'prepare)
(etaf-performance-mode 1)
(should-not (assq function etaf-performance--installed-advices))
(fset function (lambda () 'extension-result))
(etaf-performance--install-loaded-advices)
(should (assq function etaf-performance--installed-advices))
(should (eq 'extension-result
(etaf-performance-call-operation
'extension "registered stage"
(lambda () (funcall function)))))
(let ((stage
(car (etaf-performance-operation-stages
(car (etaf-performance-records))))))
(should (eq function
(etaf-performance-stage-function stage)))
(should (eq 'extension
(etaf-performance-stage-category stage)))))
(etaf-performance-unregister-stage function)
(when (fboundp function)
(fmakunbound function))))))
(ert-deftest etaf-performance-default-registry-keeps-per-node-probes-opt-in ()
"High-frequency per-node probes must not distort default operation timing."
(dolist (function '(ebox-incremental--candidate-local-changed-keys
ebox-tree-node-local-source-signature))
(should-not (assq function etaf-performance--stage-functions))))
(ert-deftest etaf-performance-stage-advice-survives-package-reload ()
"Reattach the tracked stage closure after its function is redefined."
(etaf-performance-test--isolated
(let ((function (make-symbol "etaf-performance-reloaded-stage")))
(unwind-protect
(progn
(fset function (lambda () 'first))
(etaf-performance-register-stage function 'extension 'reload)
(etaf-performance-mode 1)
(let ((entry (assq function
etaf-performance--installed-advices)))
(should (advice-member-p (cdr entry) function))
(fset function (lambda () 'second))
(should-not (advice-member-p (cdr entry) function))
(etaf-performance--install-loaded-advices)
(should (advice-member-p (cdr entry) function)))
(etaf-performance-clear)
(should (eq 'second
(etaf-performance-call-operation
'reload "package reload"
(lambda () (funcall function)))))
(should (= 1
(length
(etaf-performance-operation-stages
(car (etaf-performance-records)))))))
(etaf-performance-unregister-stage function)
(when (fboundp function) (fmakunbound function))))))
(ert-deftest etaf-performance-stage-registration-rejects-operation-collision ()
"A detail stage must not replace a public operation boundary advice."
(etaf-performance-test--isolated
(etaf-performance-mode 1)
(let* ((function 'etaf-data-load)
(entry (assq function etaf-performance--installed-advices)))
(should entry)
(should-error
(etaf-performance-register-stage function 'data 'nested-load))
(should-error (etaf-performance-unregister-stage function))
(should (eq entry (assq function etaf-performance--installed-advices)))
(should (advice-member-p (cdr entry) function)))))
(ert-deftest etaf-performance-public-data-resource-and-viewport-boundaries ()
"Public cross-package operations are boundaries, never duplicate stages."
(etaf-performance-test--isolated
(let ((functions '(etaf-data-load etaf-data-source-load-page
etaf-data-mutate etaf-resource-load
ebox-surface-update-buffer-viewport
ebox-rerender-buffer-with-context)))
(dolist (function functions)
(should (assq function etaf-performance--operation-functions))
(should-not (assq function etaf-performance--stage-functions)))
(etaf-performance-mode 1)
(dolist (function functions)
(when (fboundp function)
(should (assq function etaf-performance--installed-advices))))
(let* ((source (etaf-data-memory-source
'((:id 1 :name "one")) :id-key :id))
(controller (etaf-data-controller source))
(resource (etaf-resource (lambda () 'loaded) :immediate nil)))
(unwind-protect
(progn
(etaf-data-load controller)
(etaf-data-source-load-page source nil 1 20)
(etaf-data-mutate controller 'insert '(:id 2 :name "two"))
(etaf-resource-load resource)
(let ((kinds
(mapcar #'etaf-performance-operation-kind
(etaf-performance-records))))
(should (memq 'data-load kinds))
(should (memq 'data-prepare kinds))
(should (memq 'data-mutate kinds))
(should (memq 'resource-load kinds))))
(etaf-data-stop controller)
(etaf-resource-dispose resource))))))
(ert-deftest etaf-performance-records-nested-stage-exclusive-time ()
"Nested stages retain depth and subtract child inclusive time."
(etaf-performance-test--isolated
(etaf-performance-mode 1)
(etaf-performance--call-operation
(lambda ()
(etaf-performance--call-stage
(lambda ()
(sleep-for 0.002)
(etaf-performance--call-stage
(lambda () (sleep-for 0.002) 'inner-result)
'inner-stage 'inner 'work nil)
(sleep-for 0.002))
'outer-stage 'outer 'work nil))
'nested-operation 'test nil)
(let* ((operation (car (etaf-performance-records)))
(stages (etaf-performance-operation-stages operation))
(outer (car stages))
(inner (cadr stages)))
(should (= 2 (length stages)))
(should (= 0 (etaf-performance-stage-depth outer)))
(should (= 1 (etaf-performance-stage-depth inner)))
(should (>= (etaf-performance-stage-inclusive outer)
(etaf-performance-stage-inclusive inner)))
(should (< (abs (- (etaf-performance-stage-exclusive outer)
(- (etaf-performance-stage-inclusive outer)
(etaf-performance-stage-inclusive inner))))
0.001)))))
(ert-deftest etaf-performance-records-are-bounded ()
"Only the configured number of newest operations is retained."
(etaf-performance-test--isolated
(let ((etaf-performance-max-records 2))
(etaf-performance-mode 1)
(dotimes (index 4)
(etaf-performance--call-operation
(lambda () index) 'bounded 'test nil))
(let* ((records (etaf-performance-records))
(ids (mapcar #'etaf-performance-operation-id records)))
(should (= 2 (length records)))
(should (= 1 (- (car ids) (cadr ids))))))))
(ert-deftest etaf-performance-clear-keeps-correlation-ids-monotonic ()
"Clearing history inside an operation must not reuse its ID."
(etaf-performance-test--isolated
(etaf-performance-mode 1)
(etaf-performance-call-operation
'outer "clear inside" (lambda () (etaf-performance-clear)))
(etaf-performance-call-operation 'next "after clear" #'ignore)
(let ((ids (mapcar #'etaf-performance-operation-id
(etaf-performance-records))))
(should (= 2 (length (delete-dups (copy-sequence ids)))))
(should (= 1 (- (car ids) (cadr ids)))))))
(ert-deftest etaf-performance-summaries-aggregate-off-the-hot-path ()
"Summarize operation percentiles and non-overlapping category self time."
(etaf-performance-test--isolated
(etaf-performance-mode 1)
(dolist (delay '(0.001 0.002 0.003))
(etaf-performance-call-operation
'summary "generic"
(lambda ()
(etaf-performance--call-stage
(lambda () (sleep-for delay)) 'summary-stage 'application 'work nil))))
(let* ((records (etaf-performance-records))
(summary (car (etaf-performance-summary records)))
(stage-summary
(car (etaf-performance-operation-stage-summary (car records)))))
(should (= 3 (plist-get summary :count)))
(should (<= (plist-get summary :min-ms)
(plist-get summary :p50-ms)
(plist-get summary :p95-ms)
(plist-get summary :max-ms)))
(should (eq 'application (plist-get stage-summary :category)))
(should (= 1 (plist-get stage-summary :count)))
(should (<= (plist-get stage-summary :exclusive-ms)
(plist-get stage-summary :inclusive-ms))))))
(ert-deftest etaf-performance-records-real-event-and-runtime-stages ()
"A real mounted event creates an event operation and runtime stages."
(etaf-performance-test--isolated
(let ((buffer-name " *etaf-performance-event-test*")
(calls 0)
runtime-holder)
(unwind-protect
(progn
(etaf-performance-mode 1)
(etaf-mount
buffer-name
(etaf-view
(text :ref 'button
:on-press (lambda ()
(cl-incf calls)
(etaf-runtime-flush runtime-holder))
"Press")))
(etaf-performance-clear)
(let ((runtime (etaf-runtime-for-buffer buffer-name)))
(setq runtime-holder runtime)
(etaf-dispatch-event runtime 'button 'press)
(should (= calls 1))
(let ((operation
(cl-find 'event (etaf-performance-records)
:key #'etaf-performance-operation-kind)))
(should (eq 'event
(etaf-performance-operation-kind operation)))
(should (= (etaf-runtime-mount-epoch runtime)
(etaf-performance-operation-runtime-id operation)))
(should (equal buffer-name
(etaf-performance-operation-buffer-name
operation)))
(should (numberp
(etaf-performance-operation-generation-before
operation)))
(should
(cl-some
(lambda (record)
(cl-some
(lambda (stage)
(memq (etaf-performance-stage-category stage)
'(runtime reactive ebox tp)))
(etaf-performance-operation-stages record)))
(etaf-performance-records))))))
(when-let* ((runtime (etaf-runtime-for-buffer buffer-name)))
(etaf-unmount runtime))
(etaf-mount buffer-name (etaf-view (box "Unmount")))
(setq runtime (etaf-runtime-for-buffer buffer-name))
(etaf-performance-start runtime)
(etaf-unmount runtime)
(should-not (gethash runtime etaf-performance--attachments))
(let ((operation (car (etaf-performance-records))))
(should (eq 'unmount
(etaf-performance-operation-kind operation)))
(should (eq 'success
(etaf-performance-operation-status operation))))
(should (eq runtime (etaf-performance-stop runtime))))
(when-let* ((buffer (get-buffer buffer-name)))
(kill-buffer buffer))))))
(ert-deftest etaf-performance-panel-has-operation-and-stage-entries ()
"The tabulated panel expands one operation into its stage rows."
(ert-deftest etaf-performance-groups-flat-reports-in-sequence-order ()
"One ETAF final report retains one ordered, categorized operation."
(etaf-performance-test--isolated
(etaf-performance-mode 1)
(etaf-performance--call-operation
(lambda ()
(etaf-performance--call-stage
(lambda () 'ok) 'panel-stage 'panel 'detail nil))
'panel-operation 'test nil)
(let ((entries (etaf-performance--tabulated-entries)))
(should (= 2 (length entries)))
(should (equal "operation" (aref (cadr (car entries)) 4)))
(should (equal "stage" (aref (cadr (cadr entries)) 2))))
(with-temp-buffer
(etaf-performance-panel-mode)
(tabulated-list-print t)
(should (string-match-p "panel-operation" (buffer-string)))
(should (string-match-p "panel-stage" (buffer-string))))))
(etaf-performance-test--with-runtime (runtime buffer-name)
(etaf-performance-start runtime)
(should
(equal
'result
(etaf-performance-call-operation
runtime 'command "flat fixture"
(lambda ()
(etaf-observer-emit
'(:provider data :stage load :duration-ms 2.0 :rows 4))
(etaf-observer-emit
'(:provider ebox :stage commit :duration-ms 3.0 :patches 2))
'result))))
(let* ((records (etaf-performance-records))
(operation (car records))
(stages (etaf-performance-operation-stages operation)))
(should (= 1 (length records)))
(should (eq 'command (etaf-performance-operation-kind operation)))
(should (equal "flat fixture"
(etaf-performance-operation-label operation)))
(should (equal '(1 2 3)
(mapcar #'etaf-performance-stage-sequence stages)))
(should (equal '(data ebox etaf)
(mapcar #'etaf-performance-stage-provider stages)))
(should (equal '(data ebox runtime)
(mapcar #'etaf-performance-stage-category stages)))
(should (eq 'runtime-operation
(etaf-performance-stage-name (car (last stages)))))
(should (= 4 (plist-get
(etaf-performance-stage-metadata (car stages))
:rows)))))))
(ert-deftest etaf-performance-report-is-portable-and-copyable ()
"The shareable report contains environment, operation, and stage data."
(ert-deftest etaf-performance-operation-wrapper-preserves-errors ()
"The public wrapper delegates result, timing, and failure to Runtime."
(etaf-performance-test--isolated
(etaf-performance-mode 1)
(etaf-performance-call-operation
'report "copy fixture"
(lambda ()
(etaf-performance--call-stage
(lambda () 'ok) 'report-stage 'fixture 'work nil)))
(let* ((data (etaf-performance-report-data))
(operations (plist-get data :operations))
(operation (car operations))
(stage (car (plist-get operation :stages)))
(kill-ring nil)
(kill-ring-yank-pointer nil)
(report (etaf-performance-copy-report)))
(should (= etaf-performance-report-format-version
(plist-get data :format-version)))
(should (equal emacs-version
(plist-get (plist-get data :environment)
:emacs-version)))
(should (equal "copy fixture" (plist-get operation :label)))
(should (eq 'report-stage (plist-get stage :function)))
(should (equal report (current-kill 0 t)))
(should (string-match-p "ETAF performance report" report)))))
(etaf-performance-test--with-runtime (runtime buffer-name)
(etaf-performance-start runtime)
(let ((condition
(condition-case data
(etaf-performance-with-operation
(runtime 'command "failure")
(signal 'args-out-of-range '(source 1 2)))
(args-out-of-range data))))
(should (equal condition '(args-out-of-range source 1 2))))
(let ((operation (car (etaf-performance-records))))
(should (eq 'error (etaf-performance-operation-status operation)))
(should (eq 'error
(etaf-performance-stage-status
(car (last
(etaf-performance-operation-stages operation))))))))))
(ert-deftest etaf-performance-record-ring-is-bounded ()
"Retain only the configured number of newest operations."
(etaf-performance-test--isolated
(let ((etaf-performance-max-records 2))
(etaf-performance-test--with-runtime (runtime buffer-name)
(etaf-performance-start runtime)
(dotimes (index 4)
(etaf-performance-call-operation
runtime 'bounded (format "operation %d" index) #'ignore))
(let ((records (etaf-performance-records)))
(should (= 2 (length records)))
(should (> (etaf-performance-operation-id (car records))
(etaf-performance-operation-id (cadr records)))))))))
(ert-deftest etaf-performance-summary-uses-flat-duration ()
"Summaries expose percentiles and overlapping provider durations honestly."
(etaf-performance-test--isolated
(etaf-performance-test--with-runtime (runtime buffer-name)
(etaf-performance-start runtime)
(dotimes (_ 3)
(etaf-performance-call-operation
runtime 'summary "fixture"
(lambda ()
(etaf-observer-emit
'(:provider tp :stage publish :duration-ms 2.5)))))
(let* ((operation (car (etaf-performance-records)))
(summary (car (etaf-performance-summary)))
(stage-summary
(etaf-performance-operation-stage-summary operation))
(tp-summary
(cl-find 'tp stage-summary :key
(lambda (entry) (plist-get entry :category)))))
(should (= 3 (plist-get summary :count)))
(should (<= (plist-get summary :min-ms)
(plist-get summary :p50-ms)
(plist-get summary :p95-ms)
(plist-get summary :max-ms)))
(should (= 1 (plist-get tp-summary :count)))
(should (= 2.5 (plist-get tp-summary :duration-ms)))
(should-not (plist-member tp-summary :exclusive-ms))))))
(ert-deftest etaf-performance-report-panel-copy-and-export-remain-usable ()
"Portable reports, the panel, copy, and export consume flat records."
(etaf-performance-test--isolated
(etaf-performance-test--with-runtime (runtime buffer-name)
(etaf-performance-start runtime)
(etaf-performance-call-operation
runtime 'report "portable fixture"
(lambda ()
(etaf-observer-emit
'(:provider tp :stage publish :duration-ms 1.25))))
(let* ((data (etaf-performance-report-data))
(operation (car (plist-get data :operations)))
(stage (car (plist-get operation :stages)))
(kill-ring nil)
(kill-ring-yank-pointer nil)
(copied (etaf-performance-copy-report))
(file (make-temp-file "etaf-performance-" nil ".eld")))
(unwind-protect
(progn
(delete-file file)
(should (= 2 (plist-get data :format-version)))
(should (equal "portable fixture"
(plist-get operation :label)))
(should (eq 'tp (plist-get stage :provider)))
(should (equal copied (current-kill 0 t)))
(should (equal file (etaf-performance-export file)))
(with-temp-buffer
(insert-file-contents file)
(should (string-match-p "portable fixture"
(buffer-string)))))
(when (file-exists-p file)
(delete-file file))))
(let ((entries (etaf-performance--tabulated-entries)))
(should (= 3 (length entries))))
(with-temp-buffer
(etaf-performance-panel-mode)
(tabulated-list-print t)
(should (string-match-p "portable fixture" (buffer-string)))
(should (string-match-p "publish" (buffer-string)))))))
(ert-deftest etaf-performance-environment-normalizes-darwin-power-state ()
"Reports distinguish AC, battery low-power, and unknown power states."
(should
(equal
'(:source battery :low-power-mode on)
(etaf-performance--parse-darwin-power-state
"Now drawing from 'Battery Power'\n"
"Battery Power:\n lowpowermode 1\nAC Power:\n lowpowermode 0\n")))
(equal '(:source battery :low-power-mode on)
(etaf-performance--parse-darwin-power-state
"Now drawing from 'Battery Power'\n"
"Battery Power:\n lowpowermode 1\nAC Power:\n lowpowermode 0\n")))
(should
(equal
'(:source ac :low-power-mode off)
(etaf-performance--parse-darwin-power-state
"Now drawing from 'AC Power'\n"
"Battery Power:\n lowpowermode 1\nAC Power:\n lowpowermode 0\n")))
(equal '(:source ac :low-power-mode off)
(etaf-performance--parse-darwin-power-state
"Now drawing from 'AC Power'\n"
"Battery Power:\n lowpowermode 1\nAC Power:\n lowpowermode 0\n")))
(should
(equal
'(:source unknown :low-power-mode unknown)
(etaf-performance--parse-darwin-power-state "" ""))))
(equal '(:source unknown :low-power-mode unknown)
(etaf-performance--parse-darwin-power-state "" ""))))
(ert-deftest etaf-performance-panel-exposes-power-and-load-context ()
"The visible panel explains system-wide latency shifts."
(let ((line
(etaf-performance--panel-environment-line
'(:emacs-version "31.1"
:power-state (:source battery :low-power-mode on)
:load-average (2.5 2.0 1.5)))))
(should (string-match-p "Emacs 31.1" line))
(should (string-match-p "power battery/low:on" line))
(should (string-match-p "load 2.50" line))))
(ert-deftest etaf-performance-environment-reports-native-jit-state ()
"Expose background JIT state beside absolute latency records."
(let ((native-comp-jit-compilation t))
(should (eq t (plist-get (etaf-performance-environment-data)
:native-comp-jit-compilation))))
(let ((native-comp-jit-compilation nil))
(should-not (plist-get (etaf-performance-environment-data)
:native-comp-jit-compilation))))
(ert-deftest etaf-performance-export-writes-readable-report ()
"The interactive export payload is a readable plain-data report."
(etaf-performance-test--isolated
(etaf-performance-mode 1)
(etaf-performance-call-operation 'export "file fixture" #'ignore)
(let ((file (concat (make-temp-name
(expand-file-name "etaf-performance-test-"
temporary-file-directory))
".eld")))
(unwind-protect
(progn
(should (equal file (etaf-performance-export file)))
(with-temp-buffer
(insert-file-contents file)
(should (string-match-p "ETAF performance report"
(buffer-string)))
(should (string-match-p "file fixture" (buffer-string)))))
(when (file-exists-p file)
(delete-file file))))))
(ert-deftest etaf-performance-captures-public-ebox-report-metadata ()
"Attach compact public Ebox report facts to the active operation."
(etaf-performance-test--isolated
(etaf-performance-mode 1)
(etaf-performance--call-operation
(lambda ()
(etaf-performance--call-stage
(lambda ()
'(:strategy mixed-owner-reflow
:projection-kind mixed-owner-reflow
:dirty-count 8 :owner-ids (1 2 3) :patch-count 3
:tp-scope-count 3 :tp-text-operations 12
:tp-property-operations 111 :tp-full-root nil
:tp-scope-fallback nil))
'ebox-commit 'ebox 'commit nil))
'report-operation 'test nil)
(let* ((operation (car (etaf-performance-records)))
(report (plist-get
(etaf-performance-operation-metadata operation) :ebox)))
(should (eq 'mixed-owner-reflow (plist-get report :strategy)))
(should (= 3 (plist-get report :owner-count)))
(should (= 111 (plist-get report :tp-property-operations)))
(should (string-match-p "mixed-owner-reflow"
(etaf-performance--format-report operation))))))
(ert-deftest etaf-performance-mode-uninstalls-all-advices ()
"Disabling recording removes operation, stage, and after-load hooks."
(etaf-performance-test--isolated
(etaf-performance-mode 1)
(let ((installed (copy-sequence etaf-performance--installed-advices)))
(should installed)
(dolist (entry installed)
(should (advice-member-p (cdr entry) (car entry))))
(etaf-performance-mode -1)
(should-not etaf-performance--installed-advices)
(should-not (memq #'etaf-performance--install-loaded-advices
after-load-functions))
(dolist (entry installed)
(should-not (advice-member-p (cdr entry) (car entry)))))))
(ert-deftest etaf-performance-source-has-no-advice-or-private-provider-registry ()
"Recorder source depends only on the public Runtime observation boundary."
(let ((source
(with-temp-buffer
(insert-file-contents
(expand-file-name
"etaf-performance.el"
(file-name-directory (locate-library "etaf-performance"))))
(buffer-string))))
(dolist (pattern '("advice-add" "advice-remove" "after-load-functions"
"operation-functions" "stage-functions"
"etaf-runtime-mount-epoch"
"etaf-runtime-observer runtime"
"etaf-runtime-mounted-p"
"ebox--" "tp--" "etaf-sqlite--"))
(should-not (string-match-p (regexp-quote pattern) source)))))
(provide 'etaf-performance-tests)