Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions desktop/capture_desktop/widgets_analysis_plots.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,9 @@ def __init__(self, parent: QWidget | None = None) -> None:
def clear(self) -> None:
self._figure_export.clear()
while self._tabs.count() > 1:
page = self._tabs.widget(1)
self._tabs.removeTab(1)
page.deleteLater()
self._sync.clear()

def load_job_dir(self, job_dir: Path) -> None:
Expand Down
7 changes: 7 additions & 0 deletions docs/design/research/PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -942,3 +942,10 @@ Source pins, methods, limits and exact receipts are in
docs/evidence/nested-figure-gallery-401c5d17da79/README.md.
Full hosted Windows acceptance and current owner integration remain separate.



### 2026-10-08 — independently received native gallery page lifetime (f5c5ccd6)

The actual 11cc main + PR71 8747 + corrected PR80 837fa composition retained removed image pages across saved-result opens: three-image views left 3, 6, 9 and 12 native pages, and a package change retained 15. This receiver's narrow two-line FigureGallery.clear correction schedules the removed page for Qt deletion while preserving Sync and export invalidation. The same real synthetic-MCAP/analysis-QThread/history/gallery/export-QThread journey passes 18/18; page counts remain 3 throughout reopening and fall to zero at package change. The focused native Qt regression changes from one failure to one pass. Root exact-byte and independent semantic reviews accept the bounded change.

Evidence is retained under docs/receiving/gallery-page-lifetime-f5c5ccd6/. The companion targets estate-401c5d17da79's existing PR80 branch; that owner retains the later a2c source-picker/parameter-comparison composition and proposed-main gate. These pin-bound native results make no later-source, Windows full-suite, installed-service, hardware or deployment claim.
2 changes: 2 additions & 0 deletions docs/receiving/gallery-page-lifetime-f5c5ccd6/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.tar.gz -text
*.log -text
114 changes: 114 additions & 0 deletions docs/receiving/gallery-page-lifetime-f5c5ccd6/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Release old native image pages when saved analysis is reopened

This companion contribution targets the existing PR80 owner branch
`fix/nested-figure-gallery-401c5d17da79`, pinned at
`837fa1fbf2c5ad471cbb3924c212fc08d0e0f0ed`.
The product change is exactly two added lines in `FigureGallery.clear`:
retain the page before removing its tab, then schedule that page for Qt deletion.
The retained Sync page, export invalidation, and existing sync-plot cleanup are unchanged.

## Demonstrated user-facing defect

The current received history/gallery composition completed a real range-scoped
analysis job from synthetic protobuf/MCAP input. That job produced three nested
numeric PNGs. Opening the saved result repeatedly correctly showed three current
tabs, but Qt still owned the removed image pages and their pixmaps.

After explicit Qt DeferredDelete processing, the original code retained
**3, 6, 9, 12** image pages across the first view and three reopens; nine replaced
pages remained valid. Switching packages left **15** image pages alive.
With this repair, the same receiver observed **3, 3, 3, 3**, no replaced live
pages, and **zero** image pages after switching packages.

This is an inherited gallery lifetime defect exposed by repeated saved-history
opens. It is not attributed to either contribution's author and is not a
diagnosis of the separate CI suite deadline.

## Exact source and ownership

| Input | Commit |
| --- | --- |
| Canonical main selected for this receiving | `11cc78297d8d214407aea693548af4de855d61da` |
| Saved-history PR71 diagnostic successor | `8747cb95bfb2ac91aa4f1ff604ad677f3597fa12` |
| Nested-gallery PR80, including received PR85 correction | `837fa1fbf2c5ad471cbb3924c212fc08d0e0f0ed` |
| Actual native combined baseline | `9d90abb574f7fbadc85f323e73b4b268acc0f5b3` |
| Combined baseline tree | `7eee55fbe0b28011edf3aa5bb33f9ad409ca444e` |
| Actual native combined repair | `a5e6e7ed4052a71c22c3d385d562f4de1263a7aa` |
| Combined repair tree | `fa4d573334b06ce2879dfb6542de7489780e2c52` |
| Narrow native source/test child of PR80 | `123d39ee1ff38b9f622a778635fbb1ef60b438cd` |

The combined baseline was built through real merge parents. Its 1,175 leaves
preserve the exact current-main, history, and gallery input union. The only
content conflict was the progress document, resolved as the complete current
prefix followed by both owners' exact additions. Original and diagnostic source
union receipts are retained in the raw evidence.

Owner `estate-401c5d17da79` retains feature integration and the proposed-main gate.
This worker owns only the independent receiving and narrow page-lifetime repair.
Current main later advanced to `a2c89957c2cfe6fd5347d4ac317982449d3baa3a`,
adding the source picker and saved-parameter comparison. The owner is already
composing those changes. The 11cc-based results here do not qualify that later
Analysis-screen/inspector composition. PR71 also advanced to `c2e1d9c1`,
removing its diagnostic fixture and changing its suite deadline; it has no
product runtime delta from the history input tested here.

## Qualification

| Evidence | Original | Repaired |
| --- | --- | --- |
| Same native 18-check cross-feature receiver | 16 pass, 2 lifetime failures | 18 pass, 0 failures |
| Focused maintained native Qt regression | fails with 6 pages where 3 are expected | 1 pass |
| Root exact-byte source review | original gallery SHA256 `002327b8e6303412269c0ca8c4a105d9e8eeb96491eec0836e39ce199e2c18fd` | only the two specified lines added |
| Independent semantic review | confirms Qt retains removed pages | accepts deferred page deletion and unchanged Sync/export boundaries |

The native receiver uses Python 3.12.15, Qt/PySide6 6.12, real synthetic MCAP,
the actual analysis QThread and actual export QThread. It checks range
provenance, saved-result association, native PNG decoding, exact exported PNG
bytes, read-only parameters, unchanged retained/raw inputs, history/export
dialog invalidation, page lifetime, package switching, and source immutability.
It uses offscreen Qt Widgets. The purpose-limited environment excludes unused
WebEngine/PDF/Addons; it is not a full hosted Windows or hardware environment.

An initial receiver attempt completed the real analysis job, then stopped on
a receiver-only snake_case versus saved camelCase parameter-key mistake.
Its source and non-accepting setup-error receipt are retained separately.
The corrected receiver was then frozen and used unchanged for both the
16/18 original and 18/18 repaired runs. No prior 14-test result is relabeled.

## Evidence layout and reproduction

- `receiving-summary.json` is a compact projection of the original complete
receipts, with their byte hashes and all checks.
- `receive_history_gallery.py` is the exact corrected receiver.
- `raw-evidence.tar.gz` contains the complete original records, logs, source
manifests, synthetic fixture/job outputs, PNG frames, exported ZIPs, and the
initial receiver setup failure. `raw-evidence-manifest.json` binds every member.
- `root-gallery-source-review.json` and
`coordination-gallery-semantic-review.json` are copied unchanged from the
two independent reviewers.
- Focused pytest logs, process receipts, and JUnit are retained separately.

Run the maintained focused regression from this branch with the normal
repository dependencies:

```sh
QT_QPA_PLATFORM=offscreen python -m pytest tests/ui/test_analysis_gallery_lifetime.py -q
```

The combined receiver must run against the recorded combined source, because
the companion PR80 branch intentionally does not introduce PR71 history:

```sh
python receive_history_gallery.py --source /path/to/combined-source --out /path/to/new-evidence --variant unique-run
```

The native source bundle is retained at
`/home/jacob/hamon-capture-composition-f5c5ccd6-proof/current-history-gallery-source.bundle`.
Its SHA256 is `6afb8d675b1d9549ba8cf5a74627c363d5a5cabb39e5ec36ac3717fb989a47ac`;
the verified prerequisites are `a2fd6c58ccc97c7262b97570a85d8aae8dfb978c`
and `9c44354cb101c76beff79265de0040b6839d249f`.
It retains the native combined repair ref at `a5e6e7ed`.

The later owner composition, its hosted gate, main integration, and installed
release remain separate actions. This contribution changes no runtime service,
capture state, dependency manifest, or CI workflow.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"command": [
"/home/jacob/hamon-capture-composition-f5c5ccd6-tools/venv/bin/python",
"-B",
"-m",
"pytest",
"tests/ui/test_analysis_gallery_lifetime.py",
"-q",
"-ra",
"--junitxml=/home/jacob/hamon-capture-composition-f5c5ccd6-proof/baseline-lifetime-unit.xml"
],
"exit": 1,
"elapsed_seconds": 9.940228607971221
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
F [100%]
=================================== FAILURES ===================================
__________ test_reloaded_gallery_releases_replaced_native_image_pages __________

qapp = <PySide6.QtWidgets.QApplication(0x306d58c0) at 0x71d77afe7e40>
tmp_path = PosixPath('/tmp/pytest-of-jacob/pytest-262/test_reloaded_gallery_releases0')

def test_reloaded_gallery_releases_replaced_native_image_pages(qapp, tmp_path: Path):
from capture_desktop.widgets_analysis_plots import FigureGallery
from PySide6.QtCore import QCoreApplication, QEvent
from PySide6.QtGui import QColor, QImage
from PySide6.QtWidgets import QLabel, QScrollArea
from shiboken6 import isValid

for number in range(3):
path = tmp_path / "figures" / "nested" / f"channel_{number}.png"
path.parent.mkdir(parents=True, exist_ok=True)
image = QImage(16, 12, QImage.Format.Format_RGB32)
image.fill(QColor("#2855cc"))
assert image.save(str(path), "PNG")

def drain():
qapp.processEvents()
QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete)
qapp.processEvents()

def pages(gallery):
return [
area for area in gallery._tabs.findChildren(QScrollArea)
if isinstance(area.widget(), QLabel)
]

gallery = FigureGallery()
previous = []
try:
for _ in range(4):
gallery.load_job_dir(tmp_path)
drain()
assert gallery._tabs.count() == 4
> assert len(pages(gallery)) == 3
E assert 6 == 3
E + where 6 = len([<PySide6.QtWidgets.QScrollArea(0x310f72c0) at 0x71d75e766200>, <PySide6.QtWidgets.QScrollArea(0x310c6ec0) at 0x71d75e...e6.QtWidgets.QScrollArea(0x3106f120) at 0x71d75e9909c0>, <PySide6.QtWidgets.QScrollArea(0x310dd8d0) at 0x71d75e765940>])
E + where [<PySide6.QtWidgets.QScrollArea(0x310f72c0) at 0x71d75e766200>, <PySide6.QtWidgets.QScrollArea(0x310c6ec0) at 0x71d75e...e6.QtWidgets.QScrollArea(0x3106f120) at 0x71d75e9909c0>, <PySide6.QtWidgets.QScrollArea(0x310dd8d0) at 0x71d75e765940>] = <function test_reloaded_gallery_releases_replaced_native_image_pages.<locals>.pages at 0x71d77c781300>(<capture_desktop.widgets_analysis_plots.FigureGallery(0x30f7f350) at 0x71d75ea70040>)

tests/ui/test_analysis_gallery_lifetime.py:43: AssertionError
=========================== short test summary info ============================
FAILED tests/ui/test_analysis_gallery_lifetime.py::test_reloaded_gallery_releases_replaced_native_image_pages
1 failed in 6.91s
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?><testsuites name="pytest tests"><testsuite name="pytest" errors="0" failures="1" skipped="0" tests="1" time="6.922" timestamp="2026-10-08T19:37:08.005490+00:00" hostname="hamon-thinkpad"><testcase classname="tests.ui.test_analysis_gallery_lifetime" name="test_reloaded_gallery_releases_replaced_native_image_pages" time="5.435"><failure message="assert 6 == 3&#10; + where 6 = len([&lt;PySide6.QtWidgets.QScrollArea(0x310f72c0) at 0x71d75e766200&gt;, &lt;PySide6.QtWidgets.QScrollArea(0x310c6ec0) at 0x71d75e...e6.QtWidgets.QScrollArea(0x3106f120) at 0x71d75e9909c0&gt;, &lt;PySide6.QtWidgets.QScrollArea(0x310dd8d0) at 0x71d75e765940&gt;])&#10; + where [&lt;PySide6.QtWidgets.QScrollArea(0x310f72c0) at 0x71d75e766200&gt;, &lt;PySide6.QtWidgets.QScrollArea(0x310c6ec0) at 0x71d75e...e6.QtWidgets.QScrollArea(0x3106f120) at 0x71d75e9909c0&gt;, &lt;PySide6.QtWidgets.QScrollArea(0x310dd8d0) at 0x71d75e765940&gt;] = &lt;function test_reloaded_gallery_releases_replaced_native_image_pages.&lt;locals&gt;.pages at 0x71d77c781300&gt;(&lt;capture_desktop.widgets_analysis_plots.FigureGallery(0x30f7f350) at 0x71d75ea70040&gt;)">qapp = &lt;PySide6.QtWidgets.QApplication(0x306d58c0) at 0x71d77afe7e40&gt;
tmp_path = PosixPath('/tmp/pytest-of-jacob/pytest-262/test_reloaded_gallery_releases0')

def test_reloaded_gallery_releases_replaced_native_image_pages(qapp, tmp_path: Path):
from capture_desktop.widgets_analysis_plots import FigureGallery
from PySide6.QtCore import QCoreApplication, QEvent
from PySide6.QtGui import QColor, QImage
from PySide6.QtWidgets import QLabel, QScrollArea
from shiboken6 import isValid

for number in range(3):
path = tmp_path / "figures" / "nested" / f"channel_{number}.png"
path.parent.mkdir(parents=True, exist_ok=True)
image = QImage(16, 12, QImage.Format.Format_RGB32)
image.fill(QColor("#2855cc"))
assert image.save(str(path), "PNG")

def drain():
qapp.processEvents()
QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete)
qapp.processEvents()

def pages(gallery):
return [
area for area in gallery._tabs.findChildren(QScrollArea)
if isinstance(area.widget(), QLabel)
]

gallery = FigureGallery()
previous = []
try:
for _ in range(4):
gallery.load_job_dir(tmp_path)
drain()
assert gallery._tabs.count() == 4
&gt; assert len(pages(gallery)) == 3
E assert 6 == 3
E + where 6 = len([&lt;PySide6.QtWidgets.QScrollArea(0x310f72c0) at 0x71d75e766200&gt;, &lt;PySide6.QtWidgets.QScrollArea(0x310c6ec0) at 0x71d75e...e6.QtWidgets.QScrollArea(0x3106f120) at 0x71d75e9909c0&gt;, &lt;PySide6.QtWidgets.QScrollArea(0x310dd8d0) at 0x71d75e765940&gt;])
E + where [&lt;PySide6.QtWidgets.QScrollArea(0x310f72c0) at 0x71d75e766200&gt;, &lt;PySide6.QtWidgets.QScrollArea(0x310c6ec0) at 0x71d75e...e6.QtWidgets.QScrollArea(0x3106f120) at 0x71d75e9909c0&gt;, &lt;PySide6.QtWidgets.QScrollArea(0x310dd8d0) at 0x71d75e765940&gt;] = &lt;function test_reloaded_gallery_releases_replaced_native_image_pages.&lt;locals&gt;.pages at 0x71d77c781300&gt;(&lt;capture_desktop.widgets_analysis_plots.FigureGallery(0x30f7f350) at 0x71d75ea70040&gt;)

tests/ui/test_analysis_gallery_lifetime.py:43: AssertionError</failure></testcase></testsuite></testsuites>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"command": [
"/home/jacob/hamon-capture-composition-f5c5ccd6-tools/venv/bin/python",
"-B",
"-m",
"pytest",
"tests/ui/test_analysis_gallery_lifetime.py",
"-q",
"-ra",
"--junitxml=/home/jacob/hamon-capture-composition-f5c5ccd6-proof/candidate-lifetime-unit.xml"
],
"exit": 0,
"elapsed_seconds": 20.281431191018783
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
. [100%]
1 passed in 14.42s
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?><testsuites name="pytest tests"><testsuite name="pytest" errors="0" failures="0" skipped="0" tests="1" time="14.426" timestamp="2026-10-08T19:47:32.533907+00:00" hostname="hamon-thinkpad"><testcase classname="tests.ui.test_analysis_gallery_lifetime" name="test_reloaded_gallery_releases_replaced_native_image_pages" time="12.190" /></testsuite></testsuites>
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"reviewer": "hamon-f5c5ccd6-20261008/coordination",
"reviewed_at": "2026-10-08T19:52:05Z",
"decision": "accepted",
"scope": "Semantic review of FigureGallery.clear page lifetime and focused regression-test behavior; no source edits or test reruns by this reviewer.",
"baseline_commit": "9d90abb574f7fbadc85f323e73b4b268acc0f5b3",
"candidate_commit": "a5e6e7ed4052a71c22c3d385d562f4de1263a7aa",
"candidate_tree": "fa4d573334b06ce2879dfb6542de7489780e2c52",
"candidate_module": "desktop/capture_desktop/widgets_analysis_plots.py",
"candidate_sha256": "846239118bff9c6ad014a7736309e4f7a0d43ae2158de13d81dfd3b8de75404f",
"direct_review": {
"source": "Read original complete gallery/history/export lifecycle source and reviewed the exact two-line candidate diff supplied by author and confirmed by root.",
"test": "Directly read all 56 lines of tests/ui/test_analysis_gallery_lifetime.py through native file access.",
"baseline_receipt": "Directly read baseline-current-union-v2/receipt.json: 16/18 passed; page counts [3,6,9,12], nine replaced pages still valid after DeferredDelete, fifteen retained after package switch.",
"conclusions": [
"The count > 1 loop guarantees widget(1); capturing it before removal identifies the correct page.",
"deleteLater releases each removed image page through GUI event processing; the persistent Sync page at index zero is retained.",
"Export source invalidation remains before the loop and Sync cleanup remains after it; analysis/export workers and source-file behavior are unchanged.",
"The focused test checks native object lifetime with shiboken6.isValid after DeferredDelete, bounded descendant counts across four loads, exact Sync identity, and zero image pages after clear."
]
},
"byte_equality_proof": {
"reviewer": "hamon-f5c5ccd6-20261008/root",
"path": "/home/jacob/hamon-capture-composition-f5c5ccd6-proof/root-gallery-source-review.json",
"sha256": "72142d89fb07b47d27803568afd358b1d2a23a33c89dfde8f25ed879d4509fc2",
"attribution": "Root's whole-module exact-byte comparison; not claimed as this reviewer's independent equality proof."
},
"native_acceptance": {
"reviewer": "hamon-f5c5ccd6-20261008/thinkpad_commons",
"attribution": "Author-reported subsequent unchanged 18-case receiver: 18/18, no setup error, counts [3,3,3,3], zero stale pages after reopen and package switch; focused test one pass versus baseline one fail. No rerun by coordination."
},
"ownership": {
"source_owner": "estate-401c5d17da79",
"coordination": "https://github.com/Jacob-Met/hamon/issues/221",
"existing_intake": "/srv/hamon-estate/coord/handoff-e46e74cdc4f4-to-401c5d17da79-cs80-windows-links.json",
"established_acknowledgment": "https://github.com/Jacob-Met/CaptureSuite/pull/85#issuecomment-6065926844",
"state": "Semantic acceptance for existing-owner source incorporation; no current-owner adoption or deployment inferred."
},
"qualification": "The retained-page defect is inherited from the inspected main/history/gallery clear implementation. No causal link to the historical CI timeout is established.",
"qt_contract": "https://doc.qt.io/qt-6/qtabwidget.html#removeTab"
}
Loading
Loading