debug(gallery): add temporary diagnostic logging for the missing-thumbnail bug

Still reproduces after the previous fix (6abf6b2). Adding targeted,
clearly-marked [DIAG] logging around the actual suspect points, since
guessing a fourth fix blind isn't warranted on a bug this central:

- GalleryRowCell.renderThumbnail/setImageContent: cache hit/miss,
  which thread the async decode callback actually lands on, whether
  the card is still the one cardsByFile currently holds for that file
  (the staleness guard already there), and the card's own bounds/
  visibility/opacity right after content is set.
- ThumbnailGalleryPane.relayout/pulseRowsLayout: file/row counts per
  relayout, realised-cell count per retry pulse, and a full dump of
  every currently-realised photo-card's painted bounds once retries
  are exhausted — plus the same dump on the first mouse move after a
  relayout, to diff the before/after state around whatever a mouse
  move actually changes.

To remove once the real cause is found: every line is tagged [DIAG]
or "TEMPORARY DIAGNOSTIC".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016okXfGz39FtQawFWDYpQ5C
This commit is contained in:
2026-09-23 19:35:23 -04:00
co-authored by Claude Sonnet 5
parent 6abf6b2d0d
commit fd8d3bbf6c
2 changed files with 51 additions and 5 deletions
@@ -42,6 +42,8 @@ import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2AL;
import org.kordamp.ikonli.material2.Material2MZ;
import org.kordamp.ikonli.material2.Material2OutlinedAL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDate;
import java.util.*;
@@ -66,6 +68,9 @@ import java.util.function.Supplier;
*/
final class GalleryRowCell extends ListCell<GalleryRow> {
// TEMPORARY DIAGNOSTIC — tracking a missing-thumbnail-until-mouse-move bug, remove once fixed.
private static final Logger log = LoggerFactory.getLogger(GalleryRowCell.class);
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
private static final PseudoClass CHECKED = PseudoClass.getPseudoClass("checked");
@@ -755,26 +760,34 @@ final class GalleryRowCell extends ListCell<GalleryRow> {
private void renderThumbnail(StackPane card, MediaFile file, EThumbnailQuality quality, double width, double height) {
Image cached = imageCache.getIfPresent(file.hash(), quality);
if (cached != null) {
setImageContent(card, imageView(cached, width, height, checkedProperty.apply(file).get()));
log.info("[DIAG] renderThumbnail file={} cacheHit=true thread={}", file.id(), Thread.currentThread().getName());
setImageContent(card, imageView(cached, width, height, checkedProperty.apply(file).get()), file, "sync-hit");
return;
}
setImageContent(card, pendingThumbnail(width, height, checkedProperty.apply(file).get()));
log.info("[DIAG] renderThumbnail file={} cacheHit=false thread={}", file.id(), Thread.currentThread().getName());
setImageContent(card, pendingThumbnail(width, height, checkedProperty.apply(file).get()), file, "placeholder");
imageCache.request(file.hash(), quality, image -> {
if (cardsByFile.get(file) == card) {
boolean matches = cardsByFile.get(file) == card;
log.info("[DIAG] renderThumbnail ASYNC file={} matches={} thread={} onFxThread={} cardInScene={} cardVisible={} cardManaged={}",
file.id(), matches, Thread.currentThread().getName(), javafx.application.Platform.isFxApplicationThread(),
card.getScene() != null, card.isVisible(), card.isManaged());
if (matches) {
// Re-read rather than close over the value above: file could have been checked/unchecked
// in the time it took this decode to come back.
setImageContent(card, imageView(image, width, height, checkedProperty.apply(file).get()));
setImageContent(card, imageView(image, width, height, checkedProperty.apply(file).get()), file, "async-delivered");
}
});
}
private static void setImageContent(StackPane card, Node content) {
private static void setImageContent(StackPane card, Node content, MediaFile file, String origin) {
ObservableList<Node> children = card.getChildren();
if (children.isEmpty()) {
children.add(content);
} else {
children.set(0, content);
}
log.info("[DIAG] setImageContent file={} origin={} cardBoundsInParent={} cardWidth={} cardHeight={} cardVisible={} cardOpacity={}",
file.id(), origin, card.getBoundsInParent(), card.getWidth(), card.getHeight(), card.isVisible(), card.getOpacity());
}
/**
@@ -35,6 +35,8 @@ import org.icroco.pholio.ui.library.MediaLibraryState;
import org.icroco.pholio.ui.util.EasingFX;
import org.icroco.pholio.ui.view.gallery.filter.IMediaFileFilter;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDate;
import java.time.YearMonth;
@@ -86,6 +88,10 @@ import static java.util.Objects.requireNonNull;
*/
public class ThumbnailGalleryPane extends StackPane implements Disposable {
// TEMPORARY DIAGNOSTIC — tracking a missing-thumbnail-until-mouse-move bug, remove once fixed.
private static final Logger log = LoggerFactory.getLogger(ThumbnailGalleryPane.class);
private boolean diagLogNextMouseMove;
/**
* Gap between adjacent same-day thumbnail <em>cards</em>, in pixels — both across a row
* ({@link GalleryRowCell}'s {@code photosBox}) and between rows ({@link GalleryRow#chunk}'s row-width
@@ -388,6 +394,16 @@ public class ThumbnailGalleryPane extends StackPane implements Disposable {
// on the same pulse the cells it moved are still laying out, so reading them synchronously here would
// see last frame's positions — runLater lands after that layout pass completes.
addEventFilter(ScrollEvent.SCROLL, this::onScroll);
// TEMPORARY DIAGNOSTIC — logs the very first mouse move after a relayout, to compare card paint
// state right before/after whatever it is a mouse move does that "fixes" missing thumbnails. Remove
// once fixed.
addEventFilter(MouseEvent.MOUSE_MOVED, event -> {
if (diagLogNextMouseMove) {
diagLogNextMouseMove = false;
log.info("[DIAG] MOUSE_MOVED at sceneX={} sceneY={} — dumping card paint state", event.getSceneX(), event.getSceneY());
dumpCardPaintState();
}
});
// A click or trackpad tap mid-glide is the same "stop it now" signal as touching the pad to scroll
// again — a filter, not a handler, so it stops the glide even when the press lands on a thumbnail
// card underneath rather than bare pane background.
@@ -515,6 +531,8 @@ public class ThumbnailGalleryPane extends StackPane implements Disposable {
// elsewhere in this class. A single extra pulse was enough for the "clearing a filter" case
// (09c58ab); the folder-tree case has since shown the same symptom survive that one retry, so this
// now keeps forcing a fresh pass for a few more pulses instead of giving up after just one.
log.info("[DIAG] relayout files={} rows={}", sorted.size(), grouped.size());
diagLogNextMouseMove = true;
pulseRowsLayout(RELAYOUT_SETTLE_PULSES);
// Only when height/quality actually changed since the last relayout: a change small enough to
// leave the column count (and so every GalleryRow's own grouping) unchanged produces a
@@ -1333,13 +1351,28 @@ public class ThumbnailGalleryPane extends StackPane implements Disposable {
*/
private void pulseRowsLayout(int attemptsLeft) {
rows.requestLayout();
int cellCount = 0;
for (Node node : rows.lookupAll(".list-cell")) {
if (node instanceof ListCell<?> cell) {
cell.requestLayout();
cellCount++;
}
}
log.info("[DIAG] pulseRowsLayout attemptsLeft={} realizedCells={}", attemptsLeft, cellCount);
if (attemptsLeft > 0) {
Platform.runLater(() -> pulseRowsLayout(attemptsLeft - 1));
} else {
Platform.runLater(this::dumpCardPaintState);
}
}
/** TEMPORARY DIAGNOSTIC — logs every currently realised photo-card's actual painted bounds, once pulseRowsLayout's retries are exhausted. Remove once fixed. */
private void dumpCardPaintState() {
int i = 0;
for (Node node : rows.lookupAll(".photo-card")) {
javafx.geometry.Bounds b = node.getBoundsInParent();
log.info("[DIAG] dumpCardPaintState #{} boundsInParent={} visible={} opacity={} managed={}",
i++, b, node.isVisible(), node.getOpacity(), node.isManaged());
}
}