Skip to content

Commit d65e3b6

Browse files
committed
WIP: Formalize a single cache folder plus UX fixes for stream mode
1 parent b7efe51 commit d65e3b6

8 files changed

Lines changed: 168 additions & 6 deletions

File tree

‎src/main/java/sc/fiji/snt/BookmarkManager.java‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,14 +1148,22 @@ private JToolBar assembleToolbar() {
11481148
prevButton.addActionListener(e -> {
11491149
final int row = table.getSelectedRow();
11501150
final int target = (row <= 0) ? table.getRowCount() - 1 : row - 1;
1151-
if (target >= 0) { table.setRowSelectionInterval(target, target); flyTo(target); }
1151+
if (target >= 0) {
1152+
table.setRowSelectionInterval(target, target);
1153+
table.scrollRectToVisible(table.getCellRect(target, 0, true));
1154+
flyTo(target);
1155+
}
11521156
});
11531157
final JButton nextButton = new JButton(IconFactory.menuIcon(IconFactory.GLYPH.PREVIOUS));
11541158
nextButton.setToolTipText("Fly to next marker");
11551159
nextButton.addActionListener(e -> {
11561160
final int row = table.getSelectedRow();
11571161
final int target = (row < 0 || row >= table.getRowCount() - 1) ? 0 : row + 1;
1158-
if (target < table.getRowCount()) { table.setRowSelectionInterval(target, target); flyTo(target); }
1162+
if (target < table.getRowCount()) {
1163+
table.setRowSelectionInterval(target, target);
1164+
table.scrollRectToVisible(table.getCellRect(target, 0, true));
1165+
flyTo(target);
1166+
}
11591167
});
11601168
final JButton helpButton = GuiUtils.Buttons.help(null);
11611169
helpButton.addActionListener(e -> bigViewerMarkerHelp(viewer));

‎src/main/java/sc/fiji/snt/SNT.java‎

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,9 @@ public String toString() {
354354
protected boolean doSearchOnSecondaryData;
355355
@SuppressWarnings("rawtypes")
356356
protected RandomAccessibleInterval secondaryData;
357+
// Tracks the disk cache backing secondaryData when it was built via Lazy's "lazy" strategy.
358+
// Deleted explicitly in flushSecondaryData()
359+
private java.nio.file.Path secondaryDataCacheDir;
357360
protected File secondaryImageFile = null;
358361
private final ImageStatistics statsSecondary = new ImageStatistics();
359362
protected boolean tubularGeodesicsTracingEnabled = false;
@@ -3239,20 +3242,32 @@ public <T extends RealType<T>> void startHessian(final String image, final Strin
32393242
throw new IllegalArgumentException("Unknown filter: " + filter);
32403243
}
32413244
final RandomAccessibleInterval<FloatType> filtered;
3245+
// Tracked separately from secondaryDataCacheDir (only overwritten below, after the old cache is
3246+
// flushed) so that flushSecondaryData() still sees and removes the PREVIOUS lazy cache, not this new one
3247+
final java.nio.file.Path newSecondaryDataCacheDir;
32423248
if (strategy.equalsIgnoreCase("lazy")) {
3249+
try {
3250+
newSecondaryDataCacheDir = java.nio.file.Files.createTempDirectory(
3251+
SNTUtils.getCacheDir().toPath(), "secondary-lazy-");
3252+
} catch (final IOException e) {
3253+
throw new RuntimeException("Failed to create temp directory for secondary image disk cache", e);
3254+
}
32433255
filtered = Lazy.process(
32443256
data,
32453257
data,
32463258
new int[]{32, 32, 32},
32473259
new FloatType(),
3248-
op);
3260+
op,
3261+
newSecondaryDataCacheDir);
32493262
} else if (strategy.equalsIgnoreCase("preprocess")) {
3263+
newSecondaryDataCacheDir = null;
32503264
filtered = opService.create().img(data, new FloatType());
32513265
op.compute(data, filtered);
32523266
} else {
32533267
throw new IllegalArgumentException("Unknown strategy: " + strategy);
32543268
}
3255-
flushSecondaryData();
3269+
flushSecondaryData(); // deletes the previous secondaryDataCacheDir, if any
3270+
secondaryDataCacheDir = newSecondaryDataCacheDir;
32563271
loadSecondaryImage(filtered, false);
32573272
setSecondaryImageMinMax(min, max);
32583273
doSearchOnSecondaryData = true;
@@ -4604,6 +4619,21 @@ public void flushSecondaryData() {
46044619
if (img.getCache() != null)
46054620
img.getCache().invalidateAll();
46064621
}
4622+
if (secondaryDataCacheDir != null) {
4623+
// NB: shutdown()/invalidateAll() above stop I/O and drop the in-memory cache map, but neither
4624+
// removes the on-disk cache files -- without this, they would otherwise only be cleaned up by
4625+
// the JVM-exit shutdown hook registered in Lazy (see Lazy#createImg(..., Path)), so every prior
4626+
// "lazy" secondary image computed within the same session would keep occupying disk space
4627+
try {
4628+
org.apache.commons.io.FileUtils.deleteDirectory(secondaryDataCacheDir.toFile());
4629+
SNTUtils.log("Deleted secondary image disk cache: " + secondaryDataCacheDir);
4630+
} catch (final IOException e) {
4631+
SNTUtils.log("Could not delete secondary image disk cache (will be removed at JVM exit): "
4632+
+ e.getMessage());
4633+
} finally {
4634+
secondaryDataCacheDir = null;
4635+
}
4636+
}
46074637
secondaryData = null;
46084638
setSecondaryImage(null);
46094639
if (getUI() != null) {

‎src/main/java/sc/fiji/snt/SNTUI.java‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3253,9 +3253,11 @@ private JPanel secondaryDataPanel() {
32533253
mi1.addActionListener(e -> runSecondaryLayerWizard(true));
32543254
final JMenuItem mi2 = GuiUtils.MenuItems.fromOpenImage();
32553255
mi2.addActionListener(e -> loadSecondaryImage(true));
3256+
mi2.setEnabled(!plugin.isStreamMode());
32563257
commandFinder.register(mi2, "Main tab", "Interactive tracing (II Layer)");
32573258
final JMenuItem mi3 = GuiUtils.MenuItems.fromFileImage();
32583259
mi3.addActionListener(e -> loadSecondaryImage(false));
3260+
mi2.setEnabled(!plugin.isStreamMode()); // ChooseDatasetCmd is not yet ready for OME-ZARR/N5 etc. disable for now
32593261
commandFinder.register(mi3, "Main tab", "Interactive tracing (II Layer)");
32603262
final JMenuItem mi4 = new JMenuItem("Flush Current Layer...", IconFactory.menuIcon(IconFactory.GLYPH.TOILET));
32613263
registerInCommandFinder(mi4, "Flush Secondary Layer", "Main tab", "Interactive tracing");
@@ -4167,6 +4169,7 @@ private JMenu showFolderMenu(final ScriptInstaller installer) {
41674169
final String[] labels = {"Current Workspace", "Change Workspace...", "-", //
41684170
"Backup(s)", "Sessions", "-", //
41694171
"Current TRACES File", "Image Being Traced", "Last Accessed Folder", "Secondary Layer Image", "-", //
4172+
"Cache", "-", //
41704173
"Fiji Scripts"};
41714174
Arrays.stream(labels).forEach(label -> {
41724175
if ("-".equals(label)) {
@@ -4211,6 +4214,7 @@ private JMenu showFolderMenu(final ScriptInstaller installer) {
42114214
f = plugin.getFilteredImageFile();
42124215
proceed = !noSecondaryDataAvailableError();
42134216
}
4217+
case "Cache" -> f = SNTUtils.getCacheDir();
42144218
case "Fiji Scripts" -> f = installer.getScriptsDir();
42154219
default -> {
42164220
}

‎src/main/java/sc/fiji/snt/SNTUtils.java‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,53 @@ public static String stripExtension(final String filename) {
223223
return (lastDot > 0) ? filename.substring(0, lastDot) : filename;
224224
}
225225

226+
/**
227+
* Returns SNT's own scratch/cache directory, used by disk-backed operations
228+
* (e.g. {@link sc.fiji.snt.tracing.auto.gwdt.DiskBackedStorageBackend}, {@link sc.fiji.snt.filter.Lazy}).
229+
* Unlike the workspace directory (see {@code SNTPrefs#getWorkspaceDir()}),
230+
* this directory holds only disposable, regenerable scratch data -- never anything a user created --
231+
* so it lives under the OS temp root.
232+
* <p>
233+
* Individual operations create their own uniquely-named subdirectory here and clean up after
234+
* themselves once done. This parent directory itself is created lazily and left in place across
235+
* sessions, so that (1) it is always at the same, discoverable path and (2) leftovers from a
236+
* crashed session (which skipped its own cleanup) remain visible and removable.
237+
*
238+
* @return SNT's cache directory (created if it did not already exist), or, if that path could
239+
* not be created/written to (e.g. permissions, a network-mounted or read-only temp
240+
* location, a stray file already occupying that path), a fallback directory under the
241+
* user's home folder. Callers relying on this directory for disk-backed caching should
242+
* still be prepared for {@code IOException}s down the line (e.g. if the disk is full).
243+
*/
244+
public static File getCacheDir() {
245+
final File primary = new File(System.getProperty("java.io.tmpdir"), "SNT-cache");
246+
if (isUsableDir(primary)) return primary;
247+
final File fallback = new File(System.getProperty("user.home"), ".SNT-cache");
248+
if (isUsableDir(fallback)) {
249+
warn("SNT's temp-based cache directory (" + primary.getAbsolutePath()
250+
+ ") is not usable; falling back to " + fallback.getAbsolutePath());
251+
return fallback;
252+
}
253+
warn("Neither SNT's temp-based cache directory (" + primary.getAbsolutePath()
254+
+ ") nor its fallback (" + fallback.getAbsolutePath()
255+
+ ") could be created or written to. Disk-backed operations relying on this directory "
256+
+ "(e.g. auto-tracing, secondary/filtered-image caching) will likely fail.");
257+
return primary;
258+
}
259+
260+
/**
261+
* @param dir candidate directory (created if it does not already exist)
262+
* @return true if {@code dir} exists (or was created) as a writable directory
263+
*/
264+
private static boolean isUsableDir(final File dir) {
265+
try {
266+
if (!dir.exists() && !dir.mkdirs() && !dir.exists()) return false;
267+
return dir.isDirectory() && dir.canWrite();
268+
} catch (final SecurityException e) {
269+
return false;
270+
}
271+
}
272+
226273
public static File getUniquelySuffixedTifFile(final File referenceFile) {
227274
if (referenceFile != null && !referenceFile.isDirectory() && !referenceFile.getName().endsWith(".tif")) {
228275
return getUniquelySuffixedFile(new File(referenceFile.getAbsolutePath() + ".tif"));

‎src/main/java/sc/fiji/snt/filter/Lazy.java‎

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@
3131
import net.imglib2.cache.img.optional.CacheOptions;
3232
import net.imglib2.type.NativeType;
3333
import net.imglib2.util.Intervals;
34+
import sc.fiji.snt.SNTUtils;
3435

36+
import java.nio.file.Path;
3537
import java.util.function.Consumer;
3638

3739
/**
@@ -51,13 +53,40 @@ private Lazy() { }
5153
final int[] blockSize,
5254
final T type,
5355
final CellLoader<T> loader)
56+
{
57+
return createImg(targetInterval, blockSize, type, loader, SNTUtils.getCacheDir().toPath());
58+
}
59+
60+
/**
61+
* As {@link #createImg(Interval, int[], NativeType, CellLoader)}, but the disk cache is created
62+
* directly under {@code cacheDir} rather than SNT's shared {@link SNTUtils#getCacheDir()}. Use this
63+
* overload when the caller needs to track and explicitly remove this specific cache later (e.g. it
64+
* backs a long-lived image that may be replaced several times within a session, so its disk usage
65+
* would otherwise accumulate until the JVM exits).
66+
*/
67+
public static <T extends NativeType<T>> CachedCellImg<T, ?> createImg(
68+
final Interval targetInterval,
69+
final int[] blockSize,
70+
final T type,
71+
final CellLoader<T> loader,
72+
final Path cacheDir)
5473
{
5574
return new DiskCachedCellImgFactory<>(
5675
type,
5776
DiskCachedCellImgOptions.options()
5877
.cellDimensions(blockSize)
5978
.cacheType(CacheOptions.CacheType.SOFTREF)
60-
.initializeCellsAsDirty(true))
79+
.initializeCellsAsDirty(true)
80+
// Nested under SNTUtils#getCacheDir() (or a caller-supplied subdirectory of it)
81+
// rather than left at this library's own default temp location, so this cache
82+
// stays discoverable at a fixed, SNT-owned path alongside DiskBackedStorageBackend's
83+
.tempDirectory(cacheDir)
84+
// Explicit rather than relying on the library's own default: unless a caller
85+
// tracks and removes cacheDir itself (see the overload above), this cache is
86+
// never disposed of explicitly elsewhere in SNT (unlike DiskBackedStorageBackend,
87+
// whose dispose() removes its own temp dir immediately after each run), so a
88+
// JVM-exit shutdown hook is this cache's only fallback cleanup path
89+
.deleteCacheDirectoryOnExit(true))
6190
.create(Intervals.dimensionsAsLongArray(targetInterval), loader);
6291
}
6392

@@ -109,6 +138,29 @@ private Lazy() { }
109138
op));
110139
}
111140

141+
/**
142+
* As {@link #process(RandomAccessibleInterval, Interval, int[], NativeType, UnaryComputerOp)}, but the
143+
* disk cache is created under the caller-supplied {@code cacheDir} -- see
144+
* {@link #createImg(Interval, int[], NativeType, CellLoader, Path)} for when to use this.
145+
*/
146+
public static <I, O extends NativeType<O>> CachedCellImg<O, ?> process(
147+
final RandomAccessibleInterval<I> source,
148+
final Interval sourceInterval,
149+
final int[] blockSize,
150+
final O type,
151+
final UnaryComputerOp<RandomAccessibleInterval<I>, RandomAccessibleInterval<O>> op,
152+
final Path cacheDir) {
153+
154+
return createImg(
155+
sourceInterval,
156+
blockSize,
157+
type,
158+
new UnaryComputerOpCellLoader<>(
159+
source,
160+
op),
161+
cacheDir);
162+
}
163+
112164
/**
113165
* Create a {@link DiskCachedCellImg} with a cell generator
114166
* {@link UnaryComputerOp} provided by an {@link OpService}.

‎src/main/java/sc/fiji/snt/gui/CostPalette.java‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import sc.fiji.snt.util.PointInCanvas;
4141
import sc.fiji.snt.util.PointInImage;
4242

43+
import javax.swing.*;
4344
import java.awt.*;
4445
import java.awt.event.WindowEvent;
4546
import java.util.ArrayList;
@@ -144,6 +145,15 @@ public void run() {
144145
computeBounds();
145146
runProbes();
146147
buildAndDisplay();
148+
if (paletteWindow == null) { // image was not displayed
149+
if (snt.getUI()!=null) {
150+
snt.getUI().error("The Cost Function Wizard could not be created. " +
151+
"Make sure probe segment (last traced path or path currently selected in the Manager) " +
152+
" is within image bounds.");
153+
} else {
154+
SNTUtils.log("Cost-function wizard failed: probe segments out-of-bounds!?");
155+
}
156+
}
147157
} catch (final Throwable t) {
148158
t.printStackTrace();
149159
SNTUtils.log("Cost-function wizard failed: " + t.getMessage());
@@ -266,6 +276,9 @@ private void buildAndDisplay() {
266276
final Roi existingRoi = image.getRoi();
267277
image.setRoi(x_min, y_min, croppedWidth, croppedHeight);
268278
paletteImage = image.crop((z_min + 1) + "-" + (z_max + 1));
279+
if (paletteImage.getWidth() < 2 || paletteImage.getHeight() < 2) {
280+
return;
281+
}
269282
image.setRoi(existingRoi);
270283
paletteImage.setTitle("Cost Function Sel. Wizard");
271284

‎src/main/java/sc/fiji/snt/io/WekaModelLoader.java‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import net.imglib2.util.Intervals;
3333
import org.scijava.ItemVisibility;
3434
import org.scijava.command.Command;
35+
import org.scijava.module.MutableModuleItem;
3536
import org.scijava.plugin.Parameter;
3637
import org.scijava.plugin.Plugin;
3738
import sc.fiji.labkit.ui.segmentation.SegmentationTool;
@@ -87,6 +88,13 @@ private void init() {
8788
init(true);
8889
redirectErrorMsgsState = IJ.redirectingErrorMessages();
8990
IJ.redirectErrorMessages(true); // required to handle TWS errors
91+
if (snt.isStreamMode()) {
92+
// TWS requires a traditional ImagePlus not available in stream mode
93+
final MutableModuleItem<String> engineItem = getInfo().getMutableInput("engineChoice", String.class);
94+
engineItem.setChoices(List.of("Labkit", "Labkit w/ GPU acceleration"));
95+
resolveInput("display");
96+
display = false;
97+
}
9098
}
9199

92100
@Override

‎src/main/java/sc/fiji/snt/tracing/auto/gwdt/DiskBackedStorageBackend.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ public DiskBackedStorageBackend(long[] dims, int cellSize, int cacheSize) {
110110
this.cellSize = cellSize > 0 ? cellSize : DEFAULT_CELL_SIZE;
111111
this.cacheSize = cacheSize > 0 ? cacheSize : DEFAULT_CACHE_SIZE;
112112
try {
113-
this.tempDir = Files.createTempDirectory("gwdt-cache-").toFile();
113+
this.tempDir = Files.createTempDirectory(SNTUtils.getCacheDir().toPath(), "gwdt-cache-").toFile();
114114
tempDir.deleteOnExit();
115115
SNTUtils.log("Disk-backed storage using temp dir: " + tempDir.getAbsolutePath());
116116
} catch (IOException e) {

0 commit comments

Comments
 (0)