fix: close observation lifecycle and snapshot ownership
This commit is contained in:
parent
7e5f55c286
commit
e9323ef700
@ -73,7 +73,8 @@ 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.
|
||||
by provider category. `etaf-performance-records` returns defensive operation
|
||||
and stage snapshots; caller mutation cannot rewrite retained history.
|
||||
|
||||
Use `etaf-performance-call-operation` or
|
||||
`etaf-performance-with-operation` to trace an arbitrary operation that has no
|
||||
|
||||
@ -68,7 +68,8 @@ p50/p95/max、每次 operation、GC 增量和有序 provider 阶段;面板 hea
|
||||
限制;关闭 mode 只会解除当前 Runtime 的 observer,不修改任何函数。
|
||||
`etaf-performance-summary` 会按需计算 operation 的 p50/p95/max,
|
||||
`etaf-performance-operation-stage-summary` 则按 provider category 汇总单次
|
||||
operation 的 flat 阶段。
|
||||
operation 的 flat 阶段。`etaf-performance-records` 返回 operation/stage 的防御性
|
||||
快照,调用方修改返回值不会改写已保留的历史。
|
||||
|
||||
没有内置公共边界的任意操作,可以使用
|
||||
`etaf-performance-call-operation` 或 `etaf-performance-with-operation`
|
||||
|
||||
@ -635,7 +635,9 @@ 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` groups the flat stages
|
||||
of one recorded operation by provider category.
|
||||
of one recorded operation by provider category. `etaf-performance-records`
|
||||
returns defensive operation and stage snapshots; caller mutation cannot alter
|
||||
retained history.
|
||||
|
||||
Use `etaf-performance-call-operation` or `etaf-performance-with-operation` for
|
||||
application work that does not enter through a built-in public boundary. Both
|
||||
|
||||
@ -625,7 +625,8 @@ Ebox、TP、Data、Resource 与 SQLite 可以贡献按 sequence 排列的 flat p
|
||||
|
||||
`etaf-performance-summary` 只在请求时计算分组后的 p50/p95/max;
|
||||
`etaf-performance-operation-stage-summary` 会按 provider category 汇总某条记录
|
||||
中的 flat 阶段。
|
||||
中的 flat 阶段。`etaf-performance-records` 返回 operation/stage 的防御性快照,
|
||||
调用方修改返回值不会影响保留的历史。
|
||||
|
||||
没有经过内置公共边界的应用操作可以使用
|
||||
`etaf-performance-call-operation`/`etaf-performance-with-operation`;它们直接委托
|
||||
|
||||
@ -55,10 +55,68 @@
|
||||
:provider :stage :status :duration-ms)
|
||||
"Flat report keys represented by typed recorder fields.")
|
||||
|
||||
(defun etaf-performance--copy-value (value)
|
||||
"Return a defensive recorder snapshot of VALUE."
|
||||
(cond
|
||||
((stringp value) (copy-sequence value))
|
||||
((consp value)
|
||||
(cons (etaf-performance--copy-value (car value))
|
||||
(etaf-performance--copy-value (cdr value))))
|
||||
((vectorp value)
|
||||
(apply #'vector
|
||||
(mapcar #'etaf-performance--copy-value (append value nil))))
|
||||
((hash-table-p value)
|
||||
(let ((copy (copy-hash-table value)))
|
||||
(clrhash copy)
|
||||
(maphash (lambda (key item)
|
||||
(puthash (etaf-performance--copy-value key)
|
||||
(etaf-performance--copy-value item)
|
||||
copy))
|
||||
value)
|
||||
copy))
|
||||
(t value)))
|
||||
|
||||
(defun etaf-performance--copy-stage (stage)
|
||||
"Return an isolated snapshot of recorder STAGE."
|
||||
(etaf-performance--stage-create
|
||||
:sequence (etaf-performance-stage-sequence stage)
|
||||
:provider (etaf-performance-stage-provider stage)
|
||||
:category (etaf-performance-stage-category stage)
|
||||
:name (etaf-performance-stage-name stage)
|
||||
:duration (etaf-performance-stage-duration stage)
|
||||
:status (etaf-performance-stage-status stage)
|
||||
:metadata
|
||||
(etaf-performance--copy-value (etaf-performance-stage-metadata stage))))
|
||||
|
||||
(defun etaf-performance--copy-operation (operation)
|
||||
"Return an isolated snapshot of recorder OPERATION."
|
||||
(etaf-performance--operation-create
|
||||
:id (etaf-performance-operation-id operation)
|
||||
:kind (etaf-performance-operation-kind operation)
|
||||
:label (etaf-performance--copy-value
|
||||
(etaf-performance-operation-label operation))
|
||||
:runtime-id (etaf-performance-operation-runtime-id operation)
|
||||
:buffer-name (etaf-performance--copy-value
|
||||
(etaf-performance-operation-buffer-name operation))
|
||||
:generation-before
|
||||
(etaf-performance-operation-generation-before operation)
|
||||
:generation-after
|
||||
(etaf-performance-operation-generation-after operation)
|
||||
:elapsed (etaf-performance-operation-elapsed operation)
|
||||
:status (etaf-performance-operation-status operation)
|
||||
:gc-count (etaf-performance-operation-gc-count operation)
|
||||
:gc-elapsed (etaf-performance-operation-gc-elapsed operation)
|
||||
:metadata
|
||||
(etaf-performance--copy-value
|
||||
(etaf-performance-operation-metadata operation))
|
||||
:stages (mapcar #'etaf-performance--copy-stage
|
||||
(etaf-performance-operation-stages operation))))
|
||||
|
||||
(defun etaf-performance-records ()
|
||||
"Return a newest-first copy of completed performance records."
|
||||
"Return newest-first immutable snapshots of completed records."
|
||||
(if etaf-performance--record-ring
|
||||
(ring-elements etaf-performance--record-ring)
|
||||
(mapcar #'etaf-performance--copy-operation
|
||||
(ring-elements etaf-performance--record-ring))
|
||||
nil))
|
||||
|
||||
(defun etaf-performance--percentile (samples percentile)
|
||||
@ -79,7 +137,9 @@
|
||||
(let ((key (pop report))
|
||||
(value (pop report)))
|
||||
(unless (memq key etaf-performance--report-context-keys)
|
||||
(setq metadata (append metadata (list key (copy-tree value)))))))
|
||||
(setq metadata
|
||||
(append metadata
|
||||
(list key (etaf-performance--copy-value value)))))))
|
||||
metadata))
|
||||
|
||||
(defun etaf-performance--stage-from-report (report)
|
||||
@ -319,7 +379,9 @@ If another observer replaced the recorder sink, leave that observer intact."
|
||||
:stage (etaf-performance-stage-name stage)
|
||||
:duration-ms (etaf-performance-stage-duration stage)
|
||||
:status (etaf-performance-stage-status stage)
|
||||
:metadata (copy-tree (etaf-performance-stage-metadata stage))))
|
||||
:metadata
|
||||
(etaf-performance--copy-value
|
||||
(etaf-performance-stage-metadata stage))))
|
||||
|
||||
(defun etaf-performance--operation-report-data (operation)
|
||||
"Return portable report data for OPERATION."
|
||||
@ -336,7 +398,9 @@ If another observer replaced the recorder sink, leave that observer intact."
|
||||
:status (etaf-performance-operation-status operation)
|
||||
:gc-count (etaf-performance-operation-gc-count operation)
|
||||
:gc-duration-ms (etaf-performance-operation-gc-elapsed operation)
|
||||
:metadata (copy-tree (etaf-performance-operation-metadata operation))
|
||||
:metadata
|
||||
(etaf-performance--copy-value
|
||||
(etaf-performance-operation-metadata operation))
|
||||
:stage-summary
|
||||
(etaf-performance-operation-stage-summary operation)
|
||||
:stages
|
||||
|
||||
@ -854,6 +854,11 @@ Component owner to absorb them."
|
||||
(make-hash-table :test #'eql :weakness 'value)
|
||||
"Mount epoch -> live Runtime weak-value registry.")
|
||||
|
||||
(defun etaf--runtime-kill-buffer ()
|
||||
"Unmount the ETAF Runtime owned by the current buffer before it dies."
|
||||
(when-let* ((runtime (gethash (current-buffer) etaf--runtime-table)))
|
||||
(etaf-runtime-unmount runtime)))
|
||||
|
||||
(defun etaf--runtime-enqueue-effect (runtime effect-id)
|
||||
"Append EFFECT-ID once to RUNTIME's stable FIFO work queue."
|
||||
(unless (gethash effect-id (etaf-runtime-dirty-effect-ids runtime))
|
||||
@ -5243,6 +5248,8 @@ backend anchor proof failed; ordinary root turns keep their artifact reuse."
|
||||
:observer observer
|
||||
:root-dirty-p t)))
|
||||
(puthash buffer runtime etaf--runtime-table)
|
||||
(with-current-buffer buffer
|
||||
(add-hook 'kill-buffer-hook #'etaf--runtime-kill-buffer nil t))
|
||||
(let ((route (etaf-runtime-route-create
|
||||
:runtime-id (etaf-runtime-mount-epoch runtime)
|
||||
:mount-epoch (etaf-runtime-mount-epoch runtime)
|
||||
@ -5268,6 +5275,10 @@ backend anchor proof failed; ordinary root turns keep their artifact reuse."
|
||||
(etaf-events-enable-input buffer))
|
||||
buffer)
|
||||
((error quit)
|
||||
(when (buffer-live-p buffer)
|
||||
(with-current-buffer buffer
|
||||
(remove-hook 'kill-buffer-hook
|
||||
#'etaf--runtime-kill-buffer t)))
|
||||
(remhash buffer etaf--runtime-table)
|
||||
(remhash (etaf-runtime-mount-epoch runtime)
|
||||
etaf--runtime-route-registry)
|
||||
@ -5316,6 +5327,9 @@ before the first Ebox publication."
|
||||
(defun etaf--runtime-unmount-now (runtime)
|
||||
"Unmount RUNTIME and dispose its Component scopes now."
|
||||
(let ((runtime (etaf-runtime-require-mounted runtime)))
|
||||
(when (buffer-live-p (etaf-runtime-buffer runtime))
|
||||
(with-current-buffer (etaf-runtime-buffer runtime)
|
||||
(remove-hook 'kill-buffer-hook #'etaf--runtime-kill-buffer t)))
|
||||
(setf (etaf-runtime-mounted-p runtime) nil)
|
||||
(when (fboundp 'etaf-events-disable-input)
|
||||
(etaf-events-disable-input (etaf-runtime-buffer runtime)))
|
||||
|
||||
@ -123,6 +123,23 @@
|
||||
(when-let* ((buffer (get-buffer buffer-name)))
|
||||
(kill-buffer buffer))))))
|
||||
|
||||
(ert-deftest etaf-performance-killed-buffer-releases-runtime-attachment ()
|
||||
"Direct buffer death unmounts Runtime and releases recorder ownership."
|
||||
(etaf-performance-test--isolated
|
||||
(let ((buffer-name (generate-new-buffer-name " *etaf-performance-kill*"))
|
||||
runtime buffer)
|
||||
(etaf-mount buffer-name (etaf-view (box "Kill")))
|
||||
(setq buffer (get-buffer buffer-name)
|
||||
runtime (etaf-runtime-for-buffer buffer))
|
||||
(etaf-performance-start runtime)
|
||||
(should (gethash runtime etaf-performance--attachments))
|
||||
(kill-buffer buffer)
|
||||
(should-not (buffer-live-p buffer))
|
||||
(should-not (etaf-runtime-mounted-p runtime))
|
||||
(should-not (gethash runtime etaf-performance--attachments))
|
||||
(should-error (etaf-performance-start runtime)
|
||||
:type 'etaf-runtime-error))))
|
||||
|
||||
(ert-deftest etaf-performance-groups-flat-reports-in-sequence-order ()
|
||||
"One ETAF final report retains one ordered, categorized operation."
|
||||
(etaf-performance-test--isolated
|
||||
@ -191,6 +208,34 @@
|
||||
(should (> (etaf-performance-operation-id (car records))
|
||||
(etaf-performance-operation-id (cadr records)))))))))
|
||||
|
||||
(ert-deftest etaf-performance-records-return-deep-snapshots ()
|
||||
"Caller mutation cannot alter retained operations or stages."
|
||||
(etaf-performance-test--isolated
|
||||
(etaf-performance-test--with-runtime (runtime buffer-name)
|
||||
(etaf-performance-start runtime)
|
||||
(etaf-performance-call-operation
|
||||
runtime 'snapshot "stable label"
|
||||
(lambda ()
|
||||
(etaf-observer-emit
|
||||
'(:provider data :stage load :duration-ms 1.0
|
||||
:detail ("stable")))
|
||||
'result))
|
||||
(let* ((operation (car (etaf-performance-records)))
|
||||
(stage (car (etaf-performance-operation-stages operation)))
|
||||
(detail (car (plist-get
|
||||
(etaf-performance-stage-metadata stage) :detail))))
|
||||
(setf (etaf-performance-operation-label operation) "changed"
|
||||
(etaf-performance-stage-name stage) 'changed)
|
||||
(aset detail 0 ?X))
|
||||
(let* ((operation (car (etaf-performance-records)))
|
||||
(stage (car (etaf-performance-operation-stages operation))))
|
||||
(should (equal "stable label"
|
||||
(etaf-performance-operation-label operation)))
|
||||
(should (eq 'load (etaf-performance-stage-name stage)))
|
||||
(should (equal '("stable")
|
||||
(plist-get (etaf-performance-stage-metadata stage)
|
||||
:detail)))))))
|
||||
|
||||
(ert-deftest etaf-performance-summary-uses-flat-duration ()
|
||||
"Summaries expose percentiles and overlapping provider durations honestly."
|
||||
(etaf-performance-test--isolated
|
||||
|
||||
@ -2534,6 +2534,21 @@ Event composition is a Runtime contract, not a UI-library helper contract."
|
||||
(when-let* ((buffer (get-buffer buffer-name)))
|
||||
(kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest etaf-runtime-killed-buffer-unmounts-owned-scope ()
|
||||
"Killing a mounted buffer follows the ordinary Runtime unmount path."
|
||||
(let* ((buffer-name (generate-new-buffer-name " *etaf-kill-runtime-test*"))
|
||||
runtime scope buffer)
|
||||
(etaf-mount buffer-name (etaf-view (box "Kill Runtime")))
|
||||
(setq buffer (get-buffer buffer-name)
|
||||
runtime (etaf-runtime-for-buffer buffer)
|
||||
scope (etaf-runtime-scope runtime))
|
||||
(should (etaf-effect-scope-active-p scope))
|
||||
(kill-buffer buffer)
|
||||
(should-not (buffer-live-p buffer))
|
||||
(should-not (etaf-runtime-mounted-p runtime))
|
||||
(should-not (etaf-effect-scope-active-p scope))
|
||||
(should-not (gethash buffer etaf--runtime-table))))
|
||||
|
||||
(ert-deftest etaf-pvec-put-many-preserves-values-and-shares-untouched-branches ()
|
||||
"Batch persistent-vector updates match sequential puts and share untouched paths."
|
||||
(let* ((untouched-id (* 31 (expt 2 30)))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user