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`
|
Runtime observer and never rewrites functions. `etaf-performance-summary`
|
||||||
computes operation p50/p95/max statistics on demand, while
|
computes operation p50/p95/max statistics on demand, while
|
||||||
`etaf-performance-operation-stage-summary` groups one operation's flat stages
|
`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
|
Use `etaf-performance-call-operation` or
|
||||||
`etaf-performance-with-operation` to trace an arbitrary operation that has no
|
`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,不修改任何函数。
|
限制;关闭 mode 只会解除当前 Runtime 的 observer,不修改任何函数。
|
||||||
`etaf-performance-summary` 会按需计算 operation 的 p50/p95/max,
|
`etaf-performance-summary` 会按需计算 operation 的 p50/p95/max,
|
||||||
`etaf-performance-operation-stage-summary` 则按 provider category 汇总单次
|
`etaf-performance-operation-stage-summary` 则按 provider category 汇总单次
|
||||||
operation 的 flat 阶段。
|
operation 的 flat 阶段。`etaf-performance-records` 返回 operation/stage 的防御性
|
||||||
|
快照,调用方修改返回值不会改写已保留的历史。
|
||||||
|
|
||||||
没有内置公共边界的任意操作,可以使用
|
没有内置公共边界的任意操作,可以使用
|
||||||
`etaf-performance-call-operation` 或 `etaf-performance-with-operation`
|
`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
|
`etaf-performance-summary` calculates grouped p50/p95/max statistics only when
|
||||||
requested. `etaf-performance-operation-stage-summary` groups the flat stages
|
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
|
Use `etaf-performance-call-operation` or `etaf-performance-with-operation` for
|
||||||
application work that does not enter through a built-in public boundary. Both
|
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-summary` 只在请求时计算分组后的 p50/p95/max;
|
||||||
`etaf-performance-operation-stage-summary` 会按 provider category 汇总某条记录
|
`etaf-performance-operation-stage-summary` 会按 provider category 汇总某条记录
|
||||||
中的 flat 阶段。
|
中的 flat 阶段。`etaf-performance-records` 返回 operation/stage 的防御性快照,
|
||||||
|
调用方修改返回值不会影响保留的历史。
|
||||||
|
|
||||||
没有经过内置公共边界的应用操作可以使用
|
没有经过内置公共边界的应用操作可以使用
|
||||||
`etaf-performance-call-operation`/`etaf-performance-with-operation`;它们直接委托
|
`etaf-performance-call-operation`/`etaf-performance-with-operation`;它们直接委托
|
||||||
|
|||||||
@ -55,10 +55,68 @@
|
|||||||
:provider :stage :status :duration-ms)
|
:provider :stage :status :duration-ms)
|
||||||
"Flat report keys represented by typed recorder fields.")
|
"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 ()
|
(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
|
(if etaf-performance--record-ring
|
||||||
(ring-elements etaf-performance--record-ring)
|
(mapcar #'etaf-performance--copy-operation
|
||||||
|
(ring-elements etaf-performance--record-ring))
|
||||||
nil))
|
nil))
|
||||||
|
|
||||||
(defun etaf-performance--percentile (samples percentile)
|
(defun etaf-performance--percentile (samples percentile)
|
||||||
@ -79,7 +137,9 @@
|
|||||||
(let ((key (pop report))
|
(let ((key (pop report))
|
||||||
(value (pop report)))
|
(value (pop report)))
|
||||||
(unless (memq key etaf-performance--report-context-keys)
|
(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))
|
metadata))
|
||||||
|
|
||||||
(defun etaf-performance--stage-from-report (report)
|
(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)
|
:stage (etaf-performance-stage-name stage)
|
||||||
:duration-ms (etaf-performance-stage-duration stage)
|
:duration-ms (etaf-performance-stage-duration stage)
|
||||||
:status (etaf-performance-stage-status 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)
|
(defun etaf-performance--operation-report-data (operation)
|
||||||
"Return portable report data for 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)
|
:status (etaf-performance-operation-status operation)
|
||||||
:gc-count (etaf-performance-operation-gc-count operation)
|
:gc-count (etaf-performance-operation-gc-count operation)
|
||||||
:gc-duration-ms (etaf-performance-operation-gc-elapsed 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
|
:stage-summary
|
||||||
(etaf-performance-operation-stage-summary operation)
|
(etaf-performance-operation-stage-summary operation)
|
||||||
:stages
|
:stages
|
||||||
|
|||||||
@ -854,6 +854,11 @@ Component owner to absorb them."
|
|||||||
(make-hash-table :test #'eql :weakness 'value)
|
(make-hash-table :test #'eql :weakness 'value)
|
||||||
"Mount epoch -> live Runtime weak-value registry.")
|
"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)
|
(defun etaf--runtime-enqueue-effect (runtime effect-id)
|
||||||
"Append EFFECT-ID once to RUNTIME's stable FIFO work queue."
|
"Append EFFECT-ID once to RUNTIME's stable FIFO work queue."
|
||||||
(unless (gethash effect-id (etaf-runtime-dirty-effect-ids runtime))
|
(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
|
:observer observer
|
||||||
:root-dirty-p t)))
|
:root-dirty-p t)))
|
||||||
(puthash buffer runtime etaf--runtime-table)
|
(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
|
(let ((route (etaf-runtime-route-create
|
||||||
:runtime-id (etaf-runtime-mount-epoch runtime)
|
:runtime-id (etaf-runtime-mount-epoch runtime)
|
||||||
:mount-epoch (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))
|
(etaf-events-enable-input buffer))
|
||||||
buffer)
|
buffer)
|
||||||
((error quit)
|
((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 buffer etaf--runtime-table)
|
||||||
(remhash (etaf-runtime-mount-epoch runtime)
|
(remhash (etaf-runtime-mount-epoch runtime)
|
||||||
etaf--runtime-route-registry)
|
etaf--runtime-route-registry)
|
||||||
@ -5316,6 +5327,9 @@ before the first Ebox publication."
|
|||||||
(defun etaf--runtime-unmount-now (runtime)
|
(defun etaf--runtime-unmount-now (runtime)
|
||||||
"Unmount RUNTIME and dispose its Component scopes now."
|
"Unmount RUNTIME and dispose its Component scopes now."
|
||||||
(let ((runtime (etaf-runtime-require-mounted runtime)))
|
(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)
|
(setf (etaf-runtime-mounted-p runtime) nil)
|
||||||
(when (fboundp 'etaf-events-disable-input)
|
(when (fboundp 'etaf-events-disable-input)
|
||||||
(etaf-events-disable-input (etaf-runtime-buffer runtime)))
|
(etaf-events-disable-input (etaf-runtime-buffer runtime)))
|
||||||
|
|||||||
@ -123,6 +123,23 @@
|
|||||||
(when-let* ((buffer (get-buffer buffer-name)))
|
(when-let* ((buffer (get-buffer buffer-name)))
|
||||||
(kill-buffer buffer))))))
|
(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 ()
|
(ert-deftest etaf-performance-groups-flat-reports-in-sequence-order ()
|
||||||
"One ETAF final report retains one ordered, categorized operation."
|
"One ETAF final report retains one ordered, categorized operation."
|
||||||
(etaf-performance-test--isolated
|
(etaf-performance-test--isolated
|
||||||
@ -191,6 +208,34 @@
|
|||||||
(should (> (etaf-performance-operation-id (car records))
|
(should (> (etaf-performance-operation-id (car records))
|
||||||
(etaf-performance-operation-id (cadr 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 ()
|
(ert-deftest etaf-performance-summary-uses-flat-duration ()
|
||||||
"Summaries expose percentiles and overlapping provider durations honestly."
|
"Summaries expose percentiles and overlapping provider durations honestly."
|
||||||
(etaf-performance-test--isolated
|
(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)))
|
(when-let* ((buffer (get-buffer buffer-name)))
|
||||||
(kill-buffer buffer)))))
|
(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 ()
|
(ert-deftest etaf-pvec-put-many-preserves-values-and-shares-untouched-branches ()
|
||||||
"Batch persistent-vector updates match sequential puts and share untouched paths."
|
"Batch persistent-vector updates match sequential puts and share untouched paths."
|
||||||
(let* ((untouched-id (* 31 (expt 2 30)))
|
(let* ((untouched-id (* 31 (expt 2 30)))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user