-
Notifications
You must be signed in to change notification settings - Fork 0
correctness: Add capacity and eviction bounds check to FrameCache #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
9dfb90f
01cb553
21750b0
457d773
37f35bc
567abd5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -27,13 +27,13 @@ | |||||||||||||||||||||||||||||||||||||||||
| private final Map<Integer, BufferedImage> cache; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** Lock for thread-safe access to cache state */ | ||||||||||||||||||||||||||||||||||||||||||
| private final ReentrantReadWriteLock lock; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** Total frames loaded across all cache accesses */ | ||||||||||||||||||||||||||||||||||||||||||
| private long totalLoads; | ||||||||||||||||||||||||||||||||||||||||||
| private final java.util.concurrent.atomic.LongAdder totalLoads; | ||||||||||||||||||||||||||||||||||||||||||
|
Check notice on line 33 in src/main/java/org/opensourcephysics/cabrillo/tracker/video/FrameCache.java
|
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** Total frame cache hits */ | ||||||||||||||||||||||||||||||||||||||||||
| private long totalHits; | ||||||||||||||||||||||||||||||||||||||||||
| private final java.util.concurrent.atomic.LongAdder totalHits; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Creates a FrameCache with default maximum capacity of 100 frames. | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -63,8 +63,8 @@ | |||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
| this.lock = new ReentrantReadWriteLock(); | ||||||||||||||||||||||||||||||||||||||||||
| this.totalLoads = 0; | ||||||||||||||||||||||||||||||||||||||||||
| this.totalHits = 0; | ||||||||||||||||||||||||||||||||||||||||||
| this.totalLoads = new java.util.concurrent.atomic.LongAdder(); | ||||||||||||||||||||||||||||||||||||||||||
| this.totalHits = new java.util.concurrent.atomic.LongAdder(); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -74,26 +74,26 @@ | |||||||||||||||||||||||||||||||||||||||||
| * This method does not load a new frame - call loadFrame() for that.</p> | ||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||
| * @param frameIndex the frame index to retrieve | ||||||||||||||||||||||||||||||||||||||||||
| * @return the cached Bufferedlmage, or null if not found or index is invalid | ||||||||||||||||||||||||||||||||||||||||||
| * @return the cached BufferedImage, or null if not found | ||||||||||||||||||||||||||||||||||||||||||
| * @throws IllegalStateException if frameIndex is invalid | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| public BufferedImage get(int frameIndex) { | ||||||||||||||||||||||||||||||||||||||||||
| if (frameIndex < 0) { | ||||||||||||||||||||||||||||||||||||||||||
| throw new IllegalStateException("frameIndex cannot be negative: " + frameIndex); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| lock.writeLock().lock(); | ||||||||||||||||||||||||||||||||||||||||||
| lock.readLock().lock(); | ||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||
| BufferedImage frame = cache.get(frameIndex); | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+85
to
87
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||||||||||||||||||||||||||||||||||||||||||
| if (frame != null) { | ||||||||||||||||||||||||||||||||||||||||||
| totalHits++; | ||||||||||||||||||||||||||||||||||||||||||
| totalHits.increment(); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| return frame; | ||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||
| lock.writeLock().unlock(); | ||||||||||||||||||||||||||||||||||||||||||
| lock.readLock().unlock(); | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+85
to
+93
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 ReadLock used for structurally-modifying LinkedHashMap.get() with accessOrder=true The
Suggested change
Was this helpful? React with 👍 or 👎 to provide feedback. |
||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
|
Check failure on line 96 in src/main/java/org/opensourcephysics/cabrillo/tracker/video/FrameCache.java
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Loads and stores a frame in the cache. | ||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -109,19 +109,18 @@ | |||||||||||||||||||||||||||||||||||||||||
| throw new IllegalArgumentException("frameIndex cannot be negative: " + frameIndex); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| if (frame == null) { | ||||||||||||||||||||||||||||||||||||||||||
| throw new IllegalArgumentException("frame cannot be null"); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| totalLoads++; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| lock.writeLock().lock(); | ||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||
| // Only update if not already present (hit means no load needed) | ||||||||||||||||||||||||||||||||||||||||||
| if (!cache.containsKey(frameIndex)) { | ||||||||||||||||||||||||||||||||||||||||||
| totalLoads.increment(); | ||||||||||||||||||||||||||||||||||||||||||
| cache.put(frameIndex, frame); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
117
to
121
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚩 Semantic change: totalLoads now counts only actual inserts, not all put() calls Previously, Was this helpful? React with 👍 or 👎 to provide feedback. |
||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 [warning] Race condition in The PR moves
However, this is acceptable for approximate statistics. The more subtle issue: This is not a critical bug but a design limitation: the metrics are best-effort, not transactionally consistent with cache state. The PR doesn't worsen this but doesn't fix the fundamental inconsistency either. Consider documenting that metrics are approximate. |
||||||||||||||||||||||||||||||||||||||||||
| lock.writeLock().unlock(); | ||||||||||||||||||||||||||||||||||||||||||
|
Check notice on line 123 in src/main/java/org/opensourcephysics/cabrillo/tracker/video/FrameCache.java
|
||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -178,45 +177,54 @@ | |||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Returns the total number of frame cache loads. | ||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||
| * @return total loads count | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 [warning] Unnecessary complexity: metric getters use The PR description claims that The current code (after PR) correctly uses |
||||||||||||||||||||||||||||||||||||||||||
| public long getTotalLoads() { | ||||||||||||||||||||||||||||||||||||||||||
| return totalLoads; | ||||||||||||||||||||||||||||||||||||||||||
| return totalLoads.sum(); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Returns the total number of cache hits. | ||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||
| * @return total hits count | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| public long getTotalHits() { | ||||||||||||||||||||||||||||||||||||||||||
| return totalHits; | ||||||||||||||||||||||||||||||||||||||||||
| return totalHits.sum(); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Returns the cache hit ratio (hits / total loads). | ||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||
| * @return hit ratio as a double between 0 and 1, or 0.0 if no loads have occurred | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| public double getHitRatio() { | ||||||||||||||||||||||||||||||||||||||||||
| if (totalLoads == 0) { | ||||||||||||||||||||||||||||||||||||||||||
| long loads = totalLoads.sum(); | ||||||||||||||||||||||||||||||||||||||||||
| if (loads == 0) { | ||||||||||||||||||||||||||||||||||||||||||
| return 0.0; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| return (double) totalHits / totalLoads; | ||||||||||||||||||||||||||||||||||||||||||
| return (double) totalHits.sum() / loads; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
|
Check warning on line 208 in src/main/java/org/opensourcephysics/cabrillo/tracker/video/FrameCache.java
|
||||||||||||||||||||||||||||||||||||||||||
| * Returns a summary string showing cache statistics. | ||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||
| * @return statistics string | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| @Override | ||||||||||||||||||||||||||||||||||||||||||
| public String toString() { | ||||||||||||||||||||||||||||||||||||||||||
| return String.format("FrameCache[size=%d/%d, hits=%d, loads=%d, ratio=%.2f%%]", | ||||||||||||||||||||||||||||||||||||||||||
| size(), maxSize, totalHits, totalLoads, getHitRatio() * 100); | ||||||||||||||||||||||||||||||||||||||||||
| lock.readLock().lock(); | ||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||
| long loads = totalLoads.sum(); | ||||||||||||||||||||||||||||||||||||||||||
| long hits = totalHits.sum(); | ||||||||||||||||||||||||||||||||||||||||||
| double ratio = loads == 0 ? 0.0 : (double) hits / loads; | ||||||||||||||||||||||||||||||||||||||||||
| return String.format("FrameCache[size=%d/%d, hits=%d, loads=%d, ratio=%.2f%%]", | ||||||||||||||||||||||||||||||||||||||||||
| cache.size(), maxSize, hits, loads, ratio * 100); | ||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||
| lock.readLock().unlock(); | ||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 [critical] Deadlock risk: The
More critically, if any code path ever calls The real bug: if However, the more serious issue: the PR adds complexity and lock contention for no benefit. Suggested fix: Remove the read lock from Suggested fix:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 [suggestion] Even if the deadlock concern were addressed, taking a read lock in The Remove the lock and rely on Suggested fix:
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
|
Check warning on line 227 in src/main/java/org/opensourcephysics/cabrillo/tracker/video/FrameCache.java
|
||||||||||||||||||||||||||||||||||||||||||
| * Simple test of the FrameCache implementation. | ||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||
| * @param args command line arguments (unused) | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 Info: LongAdder migration is partially redundant given existing lock coverage
The
totalHitscounter is incremented atFrameCache.java:89insideget(), andtotalLoadsis incremented atFrameCache.java:119insideput()under the write lock. If the bug inget()is fixed (restoring the write lock), both counters would always be modified under the write lock, makingLongAdderunnecessary — plainlongfields with volatile or lock-protected access would suffice.LongAdderadds overhead (memory for striped cells) without benefit when all mutations are already serialized by the write lock. This isn't a bug but is a design inconsistency worth noting.Was this helpful? React with 👍 or 👎 to provide feedback.