From 6bb50e7dfa55965e635ab6d0c26a062933d9d8a7 Mon Sep 17 00:00:00 2001 From: Kenneth VanderLinde Date: Thu, 30 Nov 2023 14:43:58 -0800 Subject: [PATCH 1/5] Remove unused parts of CodeTimer --- src/main/java/net/rptools/lib/CodeTimer.java | 35 ++------------------ 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/src/main/java/net/rptools/lib/CodeTimer.java b/src/main/java/net/rptools/lib/CodeTimer.java index bbcb28bd07..8078b69d4d 100644 --- a/src/main/java/net/rptools/lib/CodeTimer.java +++ b/src/main/java/net/rptools/lib/CodeTimer.java @@ -14,30 +14,21 @@ */ package net.rptools.lib; -import java.text.DecimalFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class CodeTimer { - private final Map timeMap = new HashMap(); - private final Map orderMap = new HashMap(); + private final Map timeMap = new HashMap<>(); + private final Map orderMap = new HashMap<>(); private final String name; - private final long created = System.currentTimeMillis(); private boolean enabled; private int threshold = 1; - private final DecimalFormat df = new DecimalFormat(); - - public CodeTimer() { - this(""); - } - public CodeTimer(String n) { name = n; enabled = true; - df.setMinimumIntegerDigits(5); } public boolean isEnabled() { @@ -79,26 +70,6 @@ public void stop(String id) { timeMap.get(id).stop(); } - public long getElapsed(String id) { - if (!enabled) { - return 0; - } - if (!orderMap.containsKey(id)) { - throw new IllegalArgumentException("Could not find orderMap id: " + id); - } - if (!timeMap.containsKey(id)) { - throw new IllegalArgumentException("Could not find timer id: " + id); - } - return timeMap.get(id).getElapsed(); - } - - public void reset(String id) { - if (!orderMap.containsKey(id)) { - throw new IllegalArgumentException("Could not find orderMap id: " + id); - } - timeMap.remove(id); - } - public void clear() { orderMap.clear(); timeMap.clear(); @@ -123,8 +94,6 @@ public String toString() { continue; } builder.append(String.format(" %3d. %6d ms %s\n", orderMap.get(id), elapsed, id)); - // builder.append("\t").append(orderMap.get(id)).append(". ").append(id).append(": - // ").append(timer.getElapsed()).append(" ms\n"); } return builder.toString(); } From ac2c3cbb0bda0acdb77ee1f97e68fd670d29ebed Mon Sep 17 00:00:00 2001 From: Kenneth VanderLinde Date: Thu, 30 Nov 2023 16:21:22 -0800 Subject: [PATCH 2/5] Reduce HashMap<> overhead in CodeTimer The `orderMap` field was removed as it was only responsible for maintaining the insertion order of timer IDs. This is now accomplished more cheaply by changing `timeMap` to `LinkedHashMap<>`. This saves some extra hash table reads and writes, and also means we don't need to explicitly sort IDs during reporting. There were a couple places where we did redundant key lookups on `timeMap`, to handle cases where the key may or may not already exist. These were modified to instead do a single key lookup, via `computeIfAbsent()` and `get()`. --- src/main/java/net/rptools/lib/CodeTimer.java | 37 ++++++++------------ 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/src/main/java/net/rptools/lib/CodeTimer.java b/src/main/java/net/rptools/lib/CodeTimer.java index 8078b69d4d..a5c2252536 100644 --- a/src/main/java/net/rptools/lib/CodeTimer.java +++ b/src/main/java/net/rptools/lib/CodeTimer.java @@ -15,13 +15,12 @@ package net.rptools.lib; import java.util.ArrayList; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; public class CodeTimer { - private final Map timeMap = new HashMap<>(); - private final Map orderMap = new HashMap<>(); + private final Map timeMap = new LinkedHashMap<>(); private final String name; private boolean enabled; private int threshold = 1; @@ -47,13 +46,7 @@ public void start(String id) { if (!enabled) { return; } - int count = orderMap.size(); - orderMap.putIfAbsent(id, count); - Timer timer = timeMap.get(id); - if (timer == null) { - timer = new Timer(); - timeMap.put(id, timer); - } + Timer timer = timeMap.computeIfAbsent(id, key -> new Timer()); timer.start(); } @@ -61,17 +54,15 @@ public void stop(String id) { if (!enabled) { return; } - if (!orderMap.containsKey(id)) { - throw new IllegalArgumentException("Could not find orderMap id: " + id); - } - if (!timeMap.containsKey(id)) { + + Timer timer = timeMap.get(id); + if (timer == null) { throw new IllegalArgumentException("Could not find timer id: " + id); } - timeMap.get(id).stop(); + timer.stop(); } public void clear() { - orderMap.clear(); timeMap.clear(); } @@ -83,17 +74,19 @@ public String toString() { .append("Timer ") .append(name) .append(" (") - .append(orderMap.size()) + .append(timeMap.size()) .append(" elements)\n"); - List idSet = new ArrayList(timeMap.keySet()); - idSet.sort((arg0, arg1) -> orderMap.get(arg0) - orderMap.get(arg1)); - for (String id : idSet) { - long elapsed = timeMap.get(id).getElapsed(); + var i = -1; + for (var entry : timeMap.entrySet()) { + ++i; + + var id = entry.getKey(); + long elapsed = entry.getValue().getElapsed(); if (elapsed < threshold) { continue; } - builder.append(String.format(" %3d. %6d ms %s\n", orderMap.get(id), elapsed, id)); + builder.append(String.format(" %3d. %6d ms %s\n", i, elapsed, id)); } return builder.toString(); } From b06cc0c7e8794d9e851dcd859cd039e2d46714c1 Mon Sep 17 00:00:00 2001 From: Kenneth VanderLinde Date: Thu, 30 Nov 2023 15:52:50 -0800 Subject: [PATCH 3/5] Add support for parameterized timer IDs This is inspired by loggers, though based on `String.format()`. It allows using timers with dynamic names without needing to clutter calling code with `CodeTimer.isEnabled()` checks to avoid the overhead when not needed. The few places that used `CodeTimer.isEnabled()` for this purpose have been updated to use the parameterized form, and `isEnabled()` is now only used to check if timings should be reported. --- src/main/java/net/rptools/lib/CodeTimer.java | 13 +++++++++---- .../maptool/client/swing/ImagePanel.java | 8 ++------ .../client/ui/zone/renderer/ZoneRenderer.java | 18 ++++-------------- 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/src/main/java/net/rptools/lib/CodeTimer.java b/src/main/java/net/rptools/lib/CodeTimer.java index a5c2252536..f49eac0a04 100644 --- a/src/main/java/net/rptools/lib/CodeTimer.java +++ b/src/main/java/net/rptools/lib/CodeTimer.java @@ -14,9 +14,7 @@ */ package net.rptools.lib; -import java.util.ArrayList; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; public class CodeTimer { @@ -42,18 +40,25 @@ public void setEnabled(boolean enabled) { this.enabled = enabled; } - public void start(String id) { + public void start(String id, Object... parameters) { if (!enabled) { return; } + if (parameters.length > 0) { + id = String.format(id, parameters); + } + Timer timer = timeMap.computeIfAbsent(id, key -> new Timer()); timer.start(); } - public void stop(String id) { + public void stop(String id, Object... parameters) { if (!enabled) { return; } + if (parameters.length > 0) { + id = String.format(id, parameters); + } Timer timer = timeMap.get(id); if (timer == null) { diff --git a/src/main/java/net/rptools/maptool/client/swing/ImagePanel.java b/src/main/java/net/rptools/maptool/client/swing/ImagePanel.java index b745603727..7e4b8aa8ad 100644 --- a/src/main/java/net/rptools/maptool/client/swing/ImagePanel.java +++ b/src/main/java/net/rptools/maptool/client/swing/ImagePanel.java @@ -229,11 +229,7 @@ protected void paintComponent(Graphics gfx) { int itemHeight = getItemHeight(); int numToProcess = model.getImageCount(); - String timerField = null; - if (timer.isEnabled()) { - timerField = "time to process " + numToProcess + " images"; - timer.start(timerField); - } + timer.start("time to process %d images", numToProcess); for (int i = 0; i < numToProcess; i++) { int row = i / itemsPerRow; int column = i % itemsPerRow; @@ -328,8 +324,8 @@ protected void paintComponent(Graphics gfx) { } } g.setFont(savedFont); + timer.stop("time to process %d images", numToProcess); if (timer.isEnabled()) { - timer.stop(timerField); System.out.println(timer); } } diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java index f2ab8d1baa..87b68ab356 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java @@ -1211,15 +1211,9 @@ public void renderZone(Graphics2D g2d, PlayerView view) { } timer.start("overlays"); for (ZoneOverlay overlay : overlayList) { - String msg = null; - if (timer.isEnabled()) { - msg = "overlays:" + overlay.getClass().getSimpleName(); - timer.start(msg); - } + timer.start("overlays: %s", overlay.getClass().getSimpleName()); overlay.paintOverlay(this, g2d); - if (timer.isEnabled()) { - timer.stop(msg); - } + timer.stop("overlays: %s", overlay.getClass().getSimpleName()); } timer.stop("overlays"); @@ -1736,13 +1730,9 @@ private void renderFog(Graphics2D g, PlayerView view) { Area visibleArea = zoneView.getVisibleArea(view); timer.stop("renderFog-visibleArea"); - String msg = null; - if (timer.isEnabled()) { - msg = "renderFog-combined(" + (view.isUsingTokenView() ? view.getTokens().size() : 0) + ")"; - } - timer.start(msg); + timer.start("renderFog-combined(%d)", view.isUsingTokenView() ? view.getTokens().size() : 0); Area combined = zoneView.getExposedArea(view); - timer.stop(msg); + timer.stop("renderFog-combined(%d)", view.isUsingTokenView() ? view.getTokens().size() : 0); timer.start("renderFogArea"); buffG.fill(combined); From ca9753eb814842d80ef7e1c810ceb34db5ca0101 Mon Sep 17 00:00:00 2001 From: Kenneth VanderLinde Date: Thu, 30 Nov 2023 14:52:23 -0800 Subject: [PATCH 4/5] Implement a CodeTimer stack Each thread has a stack of `CodeTimer`, with the top of the stack being available anywhere via a call to `CodeTimer.get()`. When a different timer is needed for a section of code, `CodeTimer.using()` will push a new timer onto the stack, and pop it off the stack when the timed code completes, restoring the previous timer. `CodeTimer.using()` also takes care of enabling the new timer based on `AppState.isCollectProfilingData()`, and reporting the results at the end if the timer is enabled. This saves some repeated logic and adds a bit of consistency in how timers are used. --- src/main/java/net/rptools/lib/CodeTimer.java | 41 ++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/main/java/net/rptools/lib/CodeTimer.java b/src/main/java/net/rptools/lib/CodeTimer.java index f49eac0a04..00cfe35cd7 100644 --- a/src/main/java/net/rptools/lib/CodeTimer.java +++ b/src/main/java/net/rptools/lib/CodeTimer.java @@ -14,10 +14,51 @@ */ package net.rptools.lib; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import net.rptools.maptool.client.AppState; +import net.rptools.maptool.client.MapTool; public class CodeTimer { + private static final ThreadLocal ROOT_TIMER = + ThreadLocal.withInitial(() -> new CodeTimer("")); + private static final ThreadLocal> timerStack = + ThreadLocal.withInitial(ArrayList::new); + + @FunctionalInterface + public interface TimedSection { + void call(CodeTimer timer) throws Ex; + } + + public static void using(String name, TimedSection callback) + throws Ex { + var stack = timerStack.get(); + + var timer = new CodeTimer(name); + timer.setEnabled(AppState.isCollectProfilingData()); + + stack.addLast(timer); + try { + callback.call(timer); + } finally { + final var lastTimer = stack.removeLast(); + assert lastTimer == timer : "Timer stack is corrupted"; + + if (timer.isEnabled()) { + String results = timer.toString(); + MapTool.getProfilingNoteFrame().addText(results); + } + timer.clear(); + } + } + + public static CodeTimer get() { + final var stack = timerStack.get(); + return stack.isEmpty() ? ROOT_TIMER.get() : stack.getLast(); + } + private final Map timeMap = new LinkedHashMap<>(); private final String name; private boolean enabled; From 07f58667c91bc7479c9917094829671d149cea96 Mon Sep 17 00:00:00 2001 From: Kenneth VanderLinde Date: Thu, 30 Nov 2023 15:05:13 -0800 Subject: [PATCH 5/5] Use CodeTimer.using() instead of creating new CodeTimers everywhere All affected components followed the same basic pattern, with some variations: 1. Create the timer. 2. Enable / disable the timer, usually based on AppState. 3. Set a minimum threshold. 4. At the end, report to the profiling frame. Now (1), (2) and (4) are done automatically and consistently in `CodeTimer.using()`, with (2) and (3) also doable by the caller to customize the timer as needed. --- .../java/net/rptools/lib/io/PackedFile.java | 222 ++++++----- .../maptool/client/swing/ImagePanel.java | 237 ++++++------ .../maptool/client/tool/PointerTool.java | 65 ++-- .../macrobuttons/panels/SelectionPanel.java | 83 ++--- .../maptool/client/ui/zone/FogUtil.java | 144 +++---- .../ui/zone/PartitionedDrawableRenderer.java | 210 ++++++----- .../ui/zone/renderer/TokenLocation.java | 13 +- .../client/ui/zone/renderer/ZoneRenderer.java | 350 +++++++++--------- .../rptools/maptool/util/PersistenceUtil.java | 226 ++++++----- 9 files changed, 770 insertions(+), 780 deletions(-) diff --git a/src/main/java/net/rptools/lib/io/PackedFile.java b/src/main/java/net/rptools/lib/io/PackedFile.java index c622ad07dc..8d33a4c80d 100644 --- a/src/main/java/net/rptools/lib/io/PackedFile.java +++ b/src/main/java/net/rptools/lib/io/PackedFile.java @@ -52,8 +52,6 @@ import net.rptools.lib.CodeTimer; import net.rptools.lib.FileUtil; import net.rptools.lib.ModelVersionManager; -import net.rptools.maptool.client.AppState; -import net.rptools.maptool.client.MapTool; import net.rptools.maptool.model.Asset; import net.rptools.maptool.model.AssetManager; import net.rptools.maptool.model.GUID; @@ -286,128 +284,118 @@ public boolean isDirty() { } public void save() throws IOException { - CodeTimer saveTimer; - if (!dirty) { return; } - saveTimer = new CodeTimer("PackedFile.save"); - saveTimer.setEnabled(AppState.isCollectProfilingData()); - - // Create the new file - File newFile = new File(tmpDir, new GUID() + ".pak"); - ZipOutputStream zout = - new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(newFile))); - zout.setLevel(Deflater.BEST_COMPRESSION); // fast compression - try { - saveTimer.start(CONTENT_FILE); - if (hasFile(CONTENT_FILE)) { - saveEntry(zout, CONTENT_FILE); - } - saveTimer.stop(CONTENT_FILE); - saveTimer.start(PROPERTY_FILE); - if (getPropertyMap().isEmpty()) { - removeFile(PROPERTY_FILE); - } else { - zout.putNextEntry(new ZipEntry(PROPERTY_FILE)); - xstream.toXML(getPropertyMap(), zout); - zout.closeEntry(); - } - saveTimer.stop(PROPERTY_FILE); - - // Now put each file - saveTimer.start("addFiles"); - addedFileSet.remove(CONTENT_FILE); - for (String path : addedFileSet) { - saveEntry(zout, path); - } - saveTimer.stop("addFiles"); - - // Copy the rest of the zip entries over - saveTimer.start("copyFiles"); - if (file.exists()) { - Enumeration entries = zFile.entries(); - while (entries.hasMoreElements()) { - ZipEntry entry = entries.nextElement(); - if (!entry.isDirectory() - && !addedFileSet.contains(entry.getName()) - && !removedFileSet.contains(entry.getName()) - && !CONTENT_FILE.equals(entry.getName()) - && !PROPERTY_FILE.equals(entry.getName())) { - // if (entry.getName().endsWith(".png") || - // entry.getName().endsWith(".gif") || - // entry.getName().endsWith(".jpeg")) - // zout.setLevel(Deflater.NO_COMPRESSION); // none needed for images as they are already - // compressed - // else - // zout.setLevel(Deflater.BEST_COMPRESSION); // fast compression - zout.putNextEntry(entry); - try (InputStream is = getFileAsInputStream(entry.getName())) { - // When copying, always use an InputStream - IOUtils.copy(is, zout); + CodeTimer.using( + "PackedFile.save", + saveTimer -> { + // Create the new file + File newFile = new File(tmpDir, new GUID() + ".pak"); + ZipOutputStream zout = + new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(newFile))); + zout.setLevel(Deflater.BEST_COMPRESSION); // fast compression + + try { + saveTimer.start(CONTENT_FILE); + if (hasFile(CONTENT_FILE)) { + saveEntry(zout, CONTENT_FILE); } - zout.closeEntry(); - } else if (entry.isDirectory()) { - zout.putNextEntry(entry); - zout.closeEntry(); - } - } - } - try { - if (zFile != null) zFile.close(); - } catch (IOException e) { - // ignore close exception - } - zFile = null; - saveTimer.stop("copyFiles"); - - saveTimer.start("close"); - IOUtils.closeQuietly(zout); - zout = null; - saveTimer.stop("close"); - - // Backup the original - saveTimer.start("backup"); - File backupFile = new File(tmpDir, new GUID() + ".mv"); - if (file.exists()) { - backupFile.delete(); // Always delete the old backup file first; renameTo() is very - // platform-dependent - if (!file.renameTo(backupFile)) { - saveTimer.start("backup file"); - FileUtil.copyFile(file, backupFile); - file.delete(); - saveTimer.stop("backup file"); - } - } - saveTimer.stop("backup"); - - saveTimer.start("finalize"); - // Finalize - if (!newFile.renameTo(file)) { - saveTimer.start("backup newFile"); - FileUtil.copyFile(newFile, file); - saveTimer.stop("backup newFile"); - } - if (backupFile.exists()) backupFile.delete(); - saveTimer.stop("finalize"); + saveTimer.stop(CONTENT_FILE); - dirty = false; - } finally { - saveTimer.start("cleanup"); - try { - if (zFile != null) zFile.close(); - } catch (IOException e) { - // ignore close exception - } - if (newFile.exists()) newFile.delete(); - IOUtils.closeQuietly(zout); - saveTimer.stop("cleanup"); + saveTimer.start(PROPERTY_FILE); + if (getPropertyMap().isEmpty()) { + removeFile(PROPERTY_FILE); + } else { + zout.putNextEntry(new ZipEntry(PROPERTY_FILE)); + xstream.toXML(getPropertyMap(), zout); + zout.closeEntry(); + } + saveTimer.stop(PROPERTY_FILE); - if (saveTimer.isEnabled()) { - MapTool.getProfilingNoteFrame().addText(saveTimer.toString()); - } - } + // Now put each file + saveTimer.start("addFiles"); + addedFileSet.remove(CONTENT_FILE); + for (String path : addedFileSet) { + saveEntry(zout, path); + } + saveTimer.stop("addFiles"); + + // Copy the rest of the zip entries over + saveTimer.start("copyFiles"); + if (file.exists()) { + Enumeration entries = zFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + if (!entry.isDirectory() + && !addedFileSet.contains(entry.getName()) + && !removedFileSet.contains(entry.getName()) + && !CONTENT_FILE.equals(entry.getName()) + && !PROPERTY_FILE.equals(entry.getName())) { + zout.putNextEntry(entry); + try (InputStream is = getFileAsInputStream(entry.getName())) { + // When copying, always use an InputStream + IOUtils.copy(is, zout); + } + zout.closeEntry(); + } else if (entry.isDirectory()) { + zout.putNextEntry(entry); + zout.closeEntry(); + } + } + } + try { + if (zFile != null) zFile.close(); + } catch (IOException e) { + // ignore close exception + } + zFile = null; + saveTimer.stop("copyFiles"); + + saveTimer.start("close"); + IOUtils.closeQuietly(zout); + zout = null; + saveTimer.stop("close"); + + // Backup the original + saveTimer.start("backup"); + File backupFile = new File(tmpDir, new GUID() + ".mv"); + if (file.exists()) { + backupFile.delete(); // Always delete the old backup file first; renameTo() is very + // platform-dependent + if (!file.renameTo(backupFile)) { + saveTimer.start("backup file"); + FileUtil.copyFile(file, backupFile); + file.delete(); + saveTimer.stop("backup file"); + } + } + saveTimer.stop("backup"); + + saveTimer.start("finalize"); + // Finalize + if (!newFile.renameTo(file)) { + saveTimer.start("backup newFile"); + FileUtil.copyFile(newFile, file); + saveTimer.stop("backup newFile"); + } + if (backupFile.exists()) backupFile.delete(); + saveTimer.stop("finalize"); + + dirty = false; + } finally { + saveTimer.start("cleanup"); + try { + if (zFile != null) zFile.close(); + } catch (IOException e) { + // ignore close exception + } + if (newFile.exists()) newFile.delete(); + IOUtils.closeQuietly(zout); + saveTimer.stop("cleanup"); + } + }); } private void saveEntry(ZipOutputStream zout, String path) throws IOException { diff --git a/src/main/java/net/rptools/maptool/client/swing/ImagePanel.java b/src/main/java/net/rptools/maptool/client/swing/ImagePanel.java index 7e4b8aa8ad..bb7b191c38 100644 --- a/src/main/java/net/rptools/maptool/client/swing/ImagePanel.java +++ b/src/main/java/net/rptools/maptool/client/swing/ImagePanel.java @@ -205,129 +205,132 @@ public boolean isOpaque() { @Override protected void paintComponent(Graphics gfx) { - var g = (Graphics2D) gfx; - CodeTimer timer = new CodeTimer("ImagePanel.paintComponent"); - timer.setEnabled(false); // Change this to turn on perf data to System.out + CodeTimer.using( + "ImagePanel.paintComponent", + timer -> { + timer.setEnabled(false); // Change this to turn on perf data - Rectangle clipBounds = g.getClipBounds(); - Dimension size = getSize(); - var savedFont = g.getFont(); - g.setFont(UIManager.getFont("Label.font")); - FontMetrics fm = g.getFontMetrics(); - fontHeight = fm.getHeight(); + var g = (Graphics2D) gfx; - g.setColor(getBackground()); - g.fillRect(0, 0, size.width, size.height); + Rectangle clipBounds = g.getClipBounds(); + Dimension size = getSize(); + var savedFont = g.getFont(); + g.setFont(UIManager.getFont("Label.font")); + FontMetrics fm = g.getFontMetrics(); + fontHeight = fm.getHeight(); - if (model == null) { - return; - } - imageBoundsMap.clear(); - - int itemsPerRow = calculateItemsPerRow(); - int itemWidth = getItemWidth(); - int itemHeight = getItemHeight(); - - int numToProcess = model.getImageCount(); - timer.start("time to process %d images", numToProcess); - for (int i = 0; i < numToProcess; i++) { - int row = i / itemsPerRow; - int column = i % itemsPerRow; - - int x = gridPadding.width + column * itemWidth; - int y = gridPadding.height + row * itemHeight; - - Image image; - - Rectangle bounds = new Rectangle(x, y, gridSize, gridSize); - imageBoundsMap.put( - new Rectangle(x, y, itemWidth - gridPadding.width, itemHeight - gridPadding.height), i); + g.setColor(getBackground()); + g.fillRect(0, 0, size.width, size.height); - // Background - Paint paint = model.getBackground(i); - if (paint != null) { - g.setPaint(paint); - g.fillRect(x - 2, y - 2, gridSize + 4, gridSize + 4); // bleed out a little - } - if (bounds.intersects(clipBounds)) { - image = model.getImage(i); - if (image != null) { - Dimension dim = constrainSize(image, gridSize); - var savedRenderingHints = g.getRenderingHints(); - if (dim.width < image.getWidth(null) || dim.height < image.getHeight(null)) { - AppPreferences.getRenderQuality().setShrinkRenderingHints(g); - } else if (dim.width > image.getWidth(null) || dim.height > image.getHeight(null)) { - AppPreferences.getRenderQuality().setRenderingHints(g); + if (model == null) { + return; } - g.drawImage( - image, - x + (gridSize - dim.width) / 2, - y + (gridSize - dim.height) / 2, - dim.width, - dim.height, - this); - - // Image border - g.setRenderingHints(savedRenderingHints); - if (showImageBorder) { - g.setColor(Color.black); - g.drawRect(bounds.x, bounds.y, bounds.width, bounds.height); + imageBoundsMap.clear(); + + int itemsPerRow = calculateItemsPerRow(); + int itemWidth = getItemWidth(); + int itemHeight = getItemHeight(); + + int numToProcess = model.getImageCount(); + String timerField = null; + timer.start("time to process %d images", numToProcess); + for (int i = 0; i < numToProcess; i++) { + int row = i / itemsPerRow; + int column = i % itemsPerRow; + + int x = gridPadding.width + column * itemWidth; + int y = gridPadding.height + row * itemHeight; + + Image image; + + Rectangle bounds = new Rectangle(x, y, gridSize, gridSize); + imageBoundsMap.put( + new Rectangle(x, y, itemWidth - gridPadding.width, itemHeight - gridPadding.height), + i); + + // Background + Paint paint = model.getBackground(i); + if (paint != null) { + g.setPaint(paint); + g.fillRect(x - 2, y - 2, gridSize + 4, gridSize + 4); // bleed out a little + } + if (bounds.intersects(clipBounds)) { + image = model.getImage(i); + if (image != null) { + Dimension dim = constrainSize(image, gridSize); + var savedRenderingHints = g.getRenderingHints(); + if (dim.width < image.getWidth(null) || dim.height < image.getHeight(null)) { + AppPreferences.getRenderQuality().setShrinkRenderingHints(g); + } else if (dim.width > image.getWidth(null) || dim.height > image.getHeight(null)) { + AppPreferences.getRenderQuality().setRenderingHints(g); + } + g.drawImage( + image, + x + (gridSize - dim.width) / 2, + y + (gridSize - dim.height) / 2, + dim.width, + dim.height, + this); + + // Image border + g.setRenderingHints(savedRenderingHints); + if (showImageBorder) { + g.setColor(Color.black); + g.drawRect(bounds.x, bounds.y, bounds.width, bounds.height); + } + } + } + // Selected + if (selectedIDList.contains(model.getID(i))) { + // TODO: Let the user pick the border + RessourceManager.getBorder(Borders.RED) + .paintAround(g, bounds.x, bounds.y, bounds.width, bounds.height); + } + // Decorations + Image[] decorations = model.getDecorations(i); + if (decorations != null) { + int offx = x; + int offy = y + gridSize; + int rowHeight = 0; + for (Image decoration : decorations) { + g.drawImage(decoration, offx, offy - decoration.getHeight(null), this); + + rowHeight = Math.max(rowHeight, decoration.getHeight(null)); + offx += decoration.getWidth(null); + if (offx > gridSize) { + offx = x; + offy -= rowHeight + 2; + rowHeight = 0; + } + } + } + // Caption + if (showCaptions) { + String caption = model.getCaption(i); + if (caption != null) { + boolean nameTooLong = false; + int strWidth = fm.stringWidth(caption); + if (strWidth > bounds.width) { + var avgCharWidth = (double) strWidth / caption.length(); + var fittableChars = (int) (bounds.width / avgCharWidth); + caption = String.format("%s...", caption.substring(0, fittableChars - 2)); + strWidth = fm.stringWidth(caption); + } + int cx = x + (gridSize - strWidth) / 2; + int cy = y + gridSize + fm.getHeight(); + + g.setColor(getForeground()); + var savedRenderingHints = g.getRenderingHints(); + g.setRenderingHint( + RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + g.drawString(caption, cx, cy); + g.setRenderingHints(savedRenderingHints); + } + } } - } - } - // Selected - if (selectedIDList.contains(model.getID(i))) { - // TODO: Let the user pick the border - RessourceManager.getBorder(Borders.RED) - .paintAround(g, bounds.x, bounds.y, bounds.width, bounds.height); - } - // Decorations - Image[] decorations = model.getDecorations(i); - if (decorations != null) { - int offx = x; - int offy = y + gridSize; - int rowHeight = 0; - for (Image decoration : decorations) { - g.drawImage(decoration, offx, offy - decoration.getHeight(null), this); - - rowHeight = Math.max(rowHeight, decoration.getHeight(null)); - offx += decoration.getWidth(null); - if (offx > gridSize) { - offx = x; - offy -= rowHeight + 2; - rowHeight = 0; - } - } - } - // Caption - if (showCaptions) { - String caption = model.getCaption(i); - if (caption != null) { - boolean nameTooLong = false; - int strWidth = fm.stringWidth(caption); - if (strWidth > bounds.width) { - var avgCharWidth = (double) strWidth / caption.length(); - var fittableChars = (int) (bounds.width / avgCharWidth); - caption = String.format("%s...", caption.substring(0, fittableChars - 2)); - strWidth = fm.stringWidth(caption); - } - int cx = x + (gridSize - strWidth) / 2; - int cy = y + gridSize + fm.getHeight(); - - g.setColor(getForeground()); - var savedRenderingHints = g.getRenderingHints(); - g.setRenderingHint( - RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); - g.drawString(caption, cx, cy); - g.setRenderingHints(savedRenderingHints); - } - } - } - g.setFont(savedFont); - timer.stop("time to process %d images", numToProcess); - if (timer.isEnabled()) { - System.out.println(timer); - } + g.setFont(savedFont); + timer.stop("time to process %d images", numToProcess); + }); } /** diff --git a/src/main/java/net/rptools/maptool/client/tool/PointerTool.java b/src/main/java/net/rptools/maptool/client/tool/PointerTool.java index ac0e2f47f3..091741f7ec 100644 --- a/src/main/java/net/rptools/maptool/client/tool/PointerTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/PointerTool.java @@ -1756,39 +1756,38 @@ && new StatSheetManager().isLegacyStatSheet(tokenUnderMouse.getStatSheet()) LinkedList lineLayouts = new LinkedList(); if (AppPreferences.getShowStatSheet() && new StatSheetManager().isLegacyStatSheet(tokenUnderMouse.getStatSheet())) { - CodeTimer timer = new CodeTimer("statSheet"); - timer.setEnabled(AppState.isCollectProfilingData()); - timer.setThreshold(5); - timer.start("allProps"); - for (TokenProperty property : - MapTool.getCampaign().getTokenPropertyList(tokenUnderMouse.getPropertyType())) { - if (property.isShowOnStatSheet()) { - if (property.isGMOnly() && !MapTool.getPlayer().isGM()) { - continue; - } - if (property.isOwnerOnly() && !AppUtil.playerOwns(tokenUnderMouse)) { - continue; - } - timer.start(property.getName()); - MapToolVariableResolver resolver = new MapToolVariableResolver(tokenUnderMouse); - resolver.initialize(); - resolver.setAutoPrompt(false); - Object propertyValue = - tokenUnderMouse.getEvaluatedProperty(resolver, property.getName()); - resolver.flush(); - if (propertyValue != null && propertyValue.toString().length() > 0) { - String propName = property.getShortName(); - if (StringUtils.isEmpty(propName)) propName = property.getName(); - propertyMap.put(propName, propertyValue.toString()); - } - timer.stop(property.getName()); - } - } - timer.stop("allProps"); - if (timer.isEnabled()) { - String results = timer.toString(); - MapTool.getProfilingNoteFrame().addText(results); - } + CodeTimer.using( + "statSheet", + timer -> { + timer.setThreshold(5); + + timer.start("allProps"); + for (TokenProperty property : + MapTool.getCampaign().getTokenPropertyList(tokenUnderMouse.getPropertyType())) { + if (property.isShowOnStatSheet()) { + if (property.isGMOnly() && !MapTool.getPlayer().isGM()) { + continue; + } + if (property.isOwnerOnly() && !AppUtil.playerOwns(tokenUnderMouse)) { + continue; + } + timer.start(property.getName()); + MapToolVariableResolver resolver = new MapToolVariableResolver(tokenUnderMouse); + resolver.initialize(); + resolver.setAutoPrompt(false); + Object propertyValue = + tokenUnderMouse.getEvaluatedProperty(resolver, property.getName()); + resolver.flush(); + if (propertyValue != null && propertyValue.toString().length() > 0) { + String propName = property.getShortName(); + if (StringUtils.isEmpty(propName)) propName = property.getName(); + propertyMap.put(propName, propertyValue.toString()); + } + timer.stop(property.getName()); + } + } + timer.stop("allProps"); + }); } if (tokenUnderMouse.getPortraitImage() != null || !propertyMap.isEmpty()) { Font font = AppStyle.labelFont; diff --git a/src/main/java/net/rptools/maptool/client/ui/macrobuttons/panels/SelectionPanel.java b/src/main/java/net/rptools/maptool/client/ui/macrobuttons/panels/SelectionPanel.java index 853e858278..00f4d26245 100644 --- a/src/main/java/net/rptools/maptool/client/ui/macrobuttons/panels/SelectionPanel.java +++ b/src/main/java/net/rptools/maptool/client/ui/macrobuttons/panels/SelectionPanel.java @@ -19,7 +19,6 @@ import java.util.*; import javax.swing.SwingUtilities; import net.rptools.lib.CodeTimer; -import net.rptools.maptool.client.AppState; import net.rptools.maptool.client.AppUtil; import net.rptools.maptool.client.MapTool; import net.rptools.maptool.client.ui.MapToolFrame; @@ -43,7 +42,6 @@ public class SelectionPanel extends AbstractMacroPanel { private static final Logger log = LogManager.getLogger(SelectionPanel.class); private List commonMacros = new ArrayList(); - private CodeTimer timer; public SelectionPanel() { // TODO: refactoring reminder @@ -68,49 +66,48 @@ public void init() { } public void init(List selectedTokenList) { - boolean panelVisible = true; - - if (MapTool.getFrame() != null) { - DockableFrame selectionPanel = MapTool.getFrame().getDockingManager().getFrame("SELECTION"); - if (selectionPanel != null) - panelVisible = - (selectionPanel.isVisible() && !selectionPanel.isAutohide()) - || selectionPanel.isAutohideShowing(); - } // Set up a code timer to get some performance data - timer = new CodeTimer("selectionpanel"); - timer.setEnabled(AppState.isCollectProfilingData()); - timer.setThreshold(10); - - timer.start("painting"); - - // paint panel only when it's visible or active - if (panelVisible) { - - // draw common group only when there is more than one token selected - if (selectedTokenList.size() > 1) { - populateCommonButtons(selectedTokenList); - addArea(commonMacros, I18N.getText("component.areaGroup.macro.commonMacros")); - // add(new ButtonGroup(selectedTokenList, commonMacros, this)); - } - for (Token token : selectedTokenList) { - if (!AppUtil.playerOwns(token)) { - continue; - } - addArea(token.getId()); - } - if (selectedTokenList.size() == 1 && AppUtil.playerOwns(selectedTokenList.get(0))) { - // if only one token selected, show its image as tab icon - MapTool.getFrame() - .getFrame(MTFrame.SELECTION) - .setFrameIcon(selectedTokenList.get(0).getIcon(16, 16)); - } - } - timer.stop("painting"); + CodeTimer.using( + "selectionpanel", + timer -> { + timer.setThreshold(10); + + boolean panelVisible = true; + if (MapTool.getFrame() != null) { + DockableFrame selectionPanel = + MapTool.getFrame().getDockingManager().getFrame("SELECTION"); + if (selectionPanel != null) + panelVisible = + (selectionPanel.isVisible() && !selectionPanel.isAutohide()) + || selectionPanel.isAutohideShowing(); + } - if (timer.isEnabled()) { - MapTool.getProfilingNoteFrame().addText(timer.toString()); - } + timer.start("painting"); + + // paint panel only when it's visible or active + if (panelVisible) { + + // draw common group only when there is more than one token selected + if (selectedTokenList.size() > 1) { + populateCommonButtons(selectedTokenList); + addArea(commonMacros, I18N.getText("component.areaGroup.macro.commonMacros")); + // add(new ButtonGroup(selectedTokenList, commonMacros, this)); + } + for (Token token : selectedTokenList) { + if (!AppUtil.playerOwns(token)) { + continue; + } + addArea(token.getId()); + } + if (selectedTokenList.size() == 1 && AppUtil.playerOwns(selectedTokenList.get(0))) { + // if only one token selected, show its image as tab icon + MapTool.getFrame() + .getFrame(MTFrame.SELECTION) + .setFrameIcon(selectedTokenList.get(0).getIcon(16, 16)); + } + } + timer.stop("painting"); + }); } @Subscribe diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/FogUtil.java b/src/main/java/net/rptools/maptool/client/ui/zone/FogUtil.java index 214163066a..4f33fc5574 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/FogUtil.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/FogUtil.java @@ -509,82 +509,82 @@ public static void restoreFoW(final ZoneRenderer renderer) { } public static void exposeLastPath(final ZoneRenderer renderer, final Set tokenSet) { - CodeTimer timer = new CodeTimer("exposeLastPath"); - - final Zone zone = renderer.getZone(); - final Grid grid = zone.getGrid(); - GridCapabilities caps = grid.getCapabilities(); - - if (!caps.isPathingSupported() || !caps.isSnapToGridSupported()) { - return; - } - - final Set filteredToks = new HashSet(2); - - for (final GUID tokenGUID : tokenSet) { - final Token token = zone.getToken(tokenGUID); - timer.start("exposeLastPath-" + token.getName()); - - Path lastPath = token.getLastPath(); - - if (lastPath == null) return; - - Map fullMeta = zone.getExposedAreaMetaData(); - GUID exposedGUID = token.getExposedAreaGUID(); - final ExposedAreaMetaData meta = - fullMeta.computeIfAbsent(exposedGUID, guid -> new ExposedAreaMetaData()); - - final Token tokenClone = new Token(token); - final ZoneView zoneView = renderer.getZoneView(); - Area visionArea = new Area(); - - // Lee: get path according to zone's way point exposure toggle... - List processPath = - zone.getWaypointExposureToggle() ? lastPath.getWayPointList() : lastPath.getCellPath(); - - int stepCount = processPath.size(); - log.debug("Path size = " + stepCount); - - Consumer revealAt = - zp -> { - tokenClone.setX(zp.x); - tokenClone.setY(zp.y); + CodeTimer.using( + "exposeLastPath", + timer -> { + final Zone zone = renderer.getZone(); + final Grid grid = zone.getGrid(); + GridCapabilities caps = grid.getCapabilities(); + + if (!caps.isPathingSupported() || !caps.isSnapToGridSupported()) { + return; + } - Area currVisionArea = zoneView.getVisibleArea(tokenClone, renderer.getPlayerView()); - if (currVisionArea != null) { - visionArea.add(currVisionArea); - meta.addToExposedAreaHistory(currVisionArea); + final Set filteredToks = new HashSet(2); + + for (final GUID tokenGUID : tokenSet) { + final Token token = zone.getToken(tokenGUID); + timer.start("exposeLastPath-" + token.getName()); + + Path lastPath = token.getLastPath(); + + if (lastPath == null) return; + + Map fullMeta = zone.getExposedAreaMetaData(); + GUID exposedGUID = token.getExposedAreaGUID(); + final ExposedAreaMetaData meta = + fullMeta.computeIfAbsent(exposedGUID, guid -> new ExposedAreaMetaData()); + + final Token tokenClone = new Token(token); + final ZoneView zoneView = renderer.getZoneView(); + Area visionArea = new Area(); + + // Lee: get path according to zone's way point exposure toggle... + List processPath = + zone.getWaypointExposureToggle() + ? lastPath.getWayPointList() + : lastPath.getCellPath(); + + int stepCount = processPath.size(); + log.debug("Path size = " + stepCount); + + Consumer revealAt = + zp -> { + tokenClone.setX(zp.x); + tokenClone.setY(zp.y); + + Area currVisionArea = + zoneView.getVisibleArea(tokenClone, renderer.getPlayerView()); + if (currVisionArea != null) { + visionArea.add(currVisionArea); + meta.addToExposedAreaHistory(currVisionArea); + } + + zoneView.flush(tokenClone); + }; + if (token.isSnapToGrid()) { + // For each cell point along the path, reveal FoW. + for (final AbstractPoint cell : processPath) { + assert cell instanceof CellPoint; + revealAt.accept(grid.convert((CellPoint) cell)); + } + } else { + // Only reveal the final position. + final AbstractPoint finalCell = processPath.get(processPath.size() - 1); + assert finalCell instanceof ZonePoint; + revealAt.accept((ZonePoint) finalCell); } - zoneView.flush(tokenClone); - }; - if (token.isSnapToGrid()) { - // For each cell point along the path, reveal FoW. - for (final AbstractPoint cell : processPath) { - assert cell instanceof CellPoint; - revealAt.accept(grid.convert((CellPoint) cell)); - } - } else { - // Only reveal the final position. - final AbstractPoint finalCell = processPath.get(processPath.size() - 1); - assert finalCell instanceof ZonePoint; - revealAt.accept((ZonePoint) finalCell); - } - - timer.stop("exposeLastPath-" + token.getName()); - renderer.flush(tokenClone); + timer.stop("exposeLastPath-" + token.getName()); + renderer.flush(tokenClone); - filteredToks.clear(); - filteredToks.add(token.getId()); - zone.putToken(token); - MapTool.serverCommand().exposeFoW(zone.getId(), visionArea, filteredToks); - MapTool.serverCommand().updateExposedAreaMeta(zone.getId(), exposedGUID, meta); - } - - String results = timer.toString(); - MapTool.getProfilingNoteFrame().addText(results); - // System.out.println(results); - timer.clear(); + filteredToks.clear(); + filteredToks.add(token.getId()); + zone.putToken(token); + MapTool.serverCommand().exposeFoW(zone.getId(), visionArea, filteredToks); + MapTool.serverCommand().updateExposedAreaMeta(zone.getId(), exposedGUID, meta); + } + }); } /** diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/PartitionedDrawableRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/PartitionedDrawableRenderer.java index 248d9fddba..72b33859b3 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/PartitionedDrawableRenderer.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/PartitionedDrawableRenderer.java @@ -56,8 +56,6 @@ public class PartitionedDrawableRenderer implements DrawableRenderer { private boolean dirty = false; - private CodeTimer timer; - public void flush() { int unusedSize = unusedChunkList.size(); for (Tuple tuple : chunkList) { @@ -78,115 +76,113 @@ public void setDirty() { public void renderDrawables( Graphics g, List drawableList, Rectangle viewport, double scale) { - timer = new CodeTimer("Renderer"); - timer.setThreshold(10); - timer.setEnabled(false); - - // NOTHING TO DO - if (drawableList == null || drawableList.isEmpty()) { - if (dirty) flush(); - return; - } - // View changed ? - if (dirty || lastScale != scale) { - flush(); - } - if (lastViewport == null - || viewport.width != lastViewport.width - || viewport.height != lastViewport.height) { - horizontalChunkCount = (int) Math.ceil(viewport.width / (double) CHUNK_SIZE) + 1; - verticalChunkCount = (int) Math.ceil(viewport.height / (double) CHUNK_SIZE) + 1; - - maxChunks = (horizontalChunkCount * verticalChunkCount * 2); - } - // Compute grid - int gridx = (int) Math.floor(-viewport.x / (double) CHUNK_SIZE); - int gridy = (int) Math.floor(-viewport.y / (double) CHUNK_SIZE); - - // OK, weirdest hack ever. Basically, when the viewport.x is exactly divisible by the chunk - // size, the gridx decrements - // too early, creating a visual jump in the drawables. I don't know the exact cause, but this - // seems to account for it - // note that it only happens in the negative space. Weird. - gridx += (viewport.x > CHUNK_SIZE && (viewport.x % CHUNK_SIZE == 0) ? -1 : 0); - gridy += (viewport.y > CHUNK_SIZE && (viewport.y % CHUNK_SIZE == 0) ? -1 : 0); - - for (int row = 0; row < verticalChunkCount; row++) { - for (int col = 0; col < horizontalChunkCount; col++) { - int cellX = gridx + col; - int cellY = gridy + row; - - String key = getKey(cellX, cellY); - if (noImageSet.contains(key)) { - continue; - } - Tuple chunk = findChunk(chunkList, key); - if (chunk == null) { - chunk = new Tuple(key, createChunk(drawableList, cellX, cellY, scale)); - - if (chunk.image == null) { - noImageSet.add(key); - continue; + CodeTimer.using( + "Renderer", + timer -> { + timer.setThreshold(10); + timer.setEnabled(false); + + // NOTHING TO DO + if (drawableList == null || drawableList.isEmpty()) { + if (dirty) flush(); + return; } - } - // Most recently used is at the front - chunkList.add(0, chunk); - - // Trim to the right size - if (chunkList.size() > maxChunks) { - int chunkSize = chunkList.size(); - // chunkList.subList(maxChunks, chunkSize).clear(); - while (chunkSize > maxChunks) { - chunkList.remove(--chunkSize); + // View changed ? + if (dirty || lastScale != scale) { + flush(); } - } - int x = - col * CHUNK_SIZE - - ((CHUNK_SIZE - viewport.x)) % CHUNK_SIZE - - (gridx < -1 ? CHUNK_SIZE : 0); - int y = - row * CHUNK_SIZE - - ((CHUNK_SIZE - viewport.y)) % CHUNK_SIZE - - (gridy < -1 ? CHUNK_SIZE : 0); - - timer.start("render:DrawImage"); - g.drawImage(chunk.image, x, y, null); - timer.stop("render:DrawImage"); - - // DEBUG: Show partition boundaries - if (DeveloperOptions.Toggle.ShowPartitionDrawableBoundaries.isEnabled()) { - if (!messageLogged) { - messageLogged = true; - log.debug( - "DEBUG logging of " - + this.getClass().getSimpleName() - + " causes colored rectangles and message strings."); + if (lastViewport == null + || viewport.width != lastViewport.width + || viewport.height != lastViewport.height) { + horizontalChunkCount = (int) Math.ceil(viewport.width / (double) CHUNK_SIZE) + 1; + verticalChunkCount = (int) Math.ceil(viewport.height / (double) CHUNK_SIZE) + 1; + + maxChunks = (horizontalChunkCount * verticalChunkCount * 2); } - if (col % 2 == 0) { - if (row % 2 == 0) { - g.setColor(Color.white); - } else { - g.setColor(Color.green); - } - } else { - if (row % 2 == 0) { - g.setColor(Color.green); - } else { - g.setColor(Color.white); + // Compute grid + int gridx = (int) Math.floor(-viewport.x / (double) CHUNK_SIZE); + int gridy = (int) Math.floor(-viewport.y / (double) CHUNK_SIZE); + + // OK, weirdest hack ever. Basically, when the viewport.x is exactly divisible by the + // chunk size, the gridx decrements too early, creating a visual jump in the drawables. I + // don't know the exact cause, but this seems to account for it + // note that it only happens in the negative space. Weird. + gridx += (viewport.x > CHUNK_SIZE && (viewport.x % CHUNK_SIZE == 0) ? -1 : 0); + gridy += (viewport.y > CHUNK_SIZE && (viewport.y % CHUNK_SIZE == 0) ? -1 : 0); + + for (int row = 0; row < verticalChunkCount; row++) { + for (int col = 0; col < horizontalChunkCount; col++) { + int cellX = gridx + col; + int cellY = gridy + row; + + String key = getKey(cellX, cellY); + if (noImageSet.contains(key)) { + continue; + } + Tuple chunk = findChunk(chunkList, key); + if (chunk == null) { + chunk = new Tuple(key, createChunk(drawableList, cellX, cellY, scale)); + + if (chunk.image == null) { + noImageSet.add(key); + continue; + } + } + // Most recently used is at the front + chunkList.add(0, chunk); + + // Trim to the right size + if (chunkList.size() > maxChunks) { + int chunkSize = chunkList.size(); + // chunkList.subList(maxChunks, chunkSize).clear(); + while (chunkSize > maxChunks) { + chunkList.remove(--chunkSize); + } + } + int x = + col * CHUNK_SIZE + - ((CHUNK_SIZE - viewport.x)) % CHUNK_SIZE + - (gridx < -1 ? CHUNK_SIZE : 0); + int y = + row * CHUNK_SIZE + - ((CHUNK_SIZE - viewport.y)) % CHUNK_SIZE + - (gridy < -1 ? CHUNK_SIZE : 0); + + timer.start("render:DrawImage"); + g.drawImage(chunk.image, x, y, null); + timer.stop("render:DrawImage"); + + // DEBUG: Show partition boundaries + if (DeveloperOptions.Toggle.ShowPartitionDrawableBoundaries.isEnabled()) { + if (!messageLogged) { + messageLogged = true; + log.debug( + "DEBUG logging of " + + this.getClass().getSimpleName() + + " causes colored rectangles and message strings."); + } + if (col % 2 == 0) { + if (row % 2 == 0) { + g.setColor(Color.white); + } else { + g.setColor(Color.green); + } + } else { + if (row % 2 == 0) { + g.setColor(Color.green); + } else { + g.setColor(Color.white); + } + } + g.drawRect(x, y, CHUNK_SIZE - 1, CHUNK_SIZE - 1); + g.drawString(key, x + CHUNK_SIZE / 2, y + CHUNK_SIZE / 2); + } } } - g.drawRect(x, y, CHUNK_SIZE - 1, CHUNK_SIZE - 1); - g.drawString(key, x + CHUNK_SIZE / 2, y + CHUNK_SIZE / 2); - } - } - } - // REMEMBER - lastViewport = viewport; - lastScale = scale; - - if (timer.isEnabled()) { - // System.out.println(timer); - } + // REMEMBER + lastViewport = viewport; + lastScale = scale; + }); } /** @@ -210,6 +206,8 @@ private Tuple findChunk(List list, String key) { private BufferedImage createChunk( List drawableList, int gridx, int gridy, double scale) { + final var timer = CodeTimer.get(); + int x = gridx * CHUNK_SIZE; int y = gridy * CHUNK_SIZE; diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/TokenLocation.java b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/TokenLocation.java index 7a1a33dfe2..1f95f76940 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/TokenLocation.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/TokenLocation.java @@ -17,6 +17,7 @@ import java.awt.*; import java.awt.geom.Area; import java.awt.geom.Rectangle2D; +import net.rptools.lib.CodeTimer; import net.rptools.maptool.model.Token; class TokenLocation { @@ -81,12 +82,12 @@ public boolean maybeOnscreen(Rectangle viewport) { offsetX = renderer.getViewOffsetX(); offsetY = renderer.getViewOffsetY(); - renderer.timer.start("maybeOnsceen"); - if (!boundsCache.intersects(viewport)) { - renderer.timer.stop("maybeOnsceen"); - return false; + final var timer = CodeTimer.get(); + timer.start("maybeOnsceen"); + try { + return boundsCache.intersects(viewport); + } finally { + timer.stop("maybeOnsceen"); } - renderer.timer.stop("maybeOnsceen"); - return true; } } diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java index 87b68ab356..71cba559db 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java @@ -144,7 +144,6 @@ public class ZoneRenderer extends JComponent private final List itemRenderList = new LinkedList(); private PlayerView lastView; private Set visibleTokenSet = new HashSet<>(); - CodeTimer timer; private boolean autoResizeStamp = false; @@ -385,7 +384,6 @@ public void commitMoveSelectionSet(GUID keyTokenId) { removeMoveSelectionSet(keyTokenId); MapTool.serverCommand().stopTokenMove(getZone().getId(), keyTokenId); Token keyToken = zone.getToken(keyTokenId); - boolean topologyTokenMoved = false; // If any token has topology we need to reset FoW /* * Lee: if the lead token is snapped-to-grid and has not moved, every follower should return to where they were. Flag set at PointerTool and StampTool's stopTokenDrag() Handling the rest here. @@ -403,148 +401,146 @@ public void commitMoveSelectionSet(GUID keyTokenId) { // Lee: check only matters for snap-to-grid if (stg) { - CodeTimer moveTimer = new CodeTimer("ZoneRenderer.commitMoveSelectionSet"); - moveTimer.setEnabled(AppState.isCollectProfilingData()); - moveTimer.setThreshold(1); + CodeTimer.using( + "ZoneRenderer.commitMoveSelectionSet", + moveTimer -> { + moveTimer.setThreshold(1); - moveTimer.start("setup"); + moveTimer.start("setup"); - // Lee: the 1st of evils. changing it to handle proper computation - // for a key token's snapped state - AbstractPoint originPoint, tokenCell; - if (keyToken.isSnapToGrid()) { - originPoint = zone.getGrid().convert(new ZonePoint(keyToken.getX(), keyToken.getY())); - } else { - originPoint = new ZonePoint(keyToken.getX(), keyToken.getY()); - } + boolean topologyTokenMoved = false; // If any token has topology we need to reset FoW - Path path = - set.getWalker() != null ? set.getWalker().getPath() : set.gridlessPath; - // Jamz: add final path render here? + // Lee: the 1st of evils. changing it to handle proper computation + // for a key token's snapped state + AbstractPoint originPoint, tokenCell; + if (keyToken.isSnapToGrid()) { + originPoint = zone.getGrid().convert(new ZonePoint(keyToken.getX(), keyToken.getY())); + } else { + originPoint = new ZonePoint(keyToken.getX(), keyToken.getY()); + } - List filteredTokens = new ArrayList(); - moveTimer.stop("setup"); + Path path = + set.getWalker() != null ? set.getWalker().getPath() : set.gridlessPath; + // Jamz: add final path render here? - int offsetX, offsetY; + List filteredTokens = new ArrayList(); + moveTimer.stop("setup"); - moveTimer.start("eachtoken"); - for (GUID tokenGUID : selectionSet) { - Token token = zone.getToken(tokenGUID); - // If the token has been deleted, the GUID will still be in the - // set but getToken() will return null. - if (token == null) { - continue; - } + int offsetX, offsetY; - // Lee: get offsets based on key token's snapped state - if (token.isSnapToGrid()) { - tokenCell = zone.getGrid().convert(new ZonePoint(token.getX(), token.getY())); - } else { - tokenCell = new ZonePoint(token.getX(), token.getY()); - } + moveTimer.start("eachtoken"); + for (GUID tokenGUID : selectionSet) { + Token token = zone.getToken(tokenGUID); + // If the token has been deleted, the GUID will still be in the + // set but getToken() will return null. + if (token == null) { + continue; + } - int cellOffX, cellOffY; - if (token.isSnapToGrid() == keyToken.isSnapToGrid()) { - cellOffX = originPoint.x - tokenCell.x; - cellOffY = originPoint.y - tokenCell.y; - } else { - cellOffX = cellOffY = 0; // not used unless both are of same SnapToGrid - } + // Lee: get offsets based on key token's snapped state + if (token.isSnapToGrid()) { + tokenCell = zone.getGrid().convert(new ZonePoint(token.getX(), token.getY())); + } else { + tokenCell = new ZonePoint(token.getX(), token.getY()); + } - if (token.isSnapToGrid() - && (!AppPreferences.getTokensSnapWhileDragging() || !keyToken.isSnapToGrid())) { - // convert to Cellpoint and back to ensure token ends up at correct X and Y - CellPoint cellEnd = - zone.getGrid() - .convert( - new ZonePoint( - token.getX() + set.getOffsetX(), token.getY() + set.getOffsetY())); - ZonePoint pointEnd = cellEnd.convertToZonePoint(zone.getGrid()); - offsetX = pointEnd.x - token.getX(); - offsetY = pointEnd.y - token.getY(); - } else { - offsetX = set.getOffsetX(); - offsetY = set.getOffsetY(); - } + int cellOffX, cellOffY; + if (token.isSnapToGrid() == keyToken.isSnapToGrid()) { + cellOffX = originPoint.x - tokenCell.x; + cellOffY = originPoint.y - tokenCell.y; + } else { + cellOffX = cellOffY = 0; // not used unless both are of same SnapToGrid + } - /* - * Lee: the problem now is to keep the precise coordinate computations for unsnapped tokens following a snapped key token. The derived path in the following section contains rounded - * down values because the integer cell values were passed. If these were double in nature, the precision would be kept, but that would be too difficult to change at this stage... - */ + if (token.isSnapToGrid() + && (!AppPreferences.getTokensSnapWhileDragging() || !keyToken.isSnapToGrid())) { + // convert to Cellpoint and back to ensure token ends up at correct X and Y + CellPoint cellEnd = + zone.getGrid() + .convert( + new ZonePoint( + token.getX() + set.getOffsetX(), token.getY() + set.getOffsetY())); + ZonePoint pointEnd = cellEnd.convertToZonePoint(zone.getGrid()); + offsetX = pointEnd.x - token.getX(); + offsetY = pointEnd.y - token.getY(); + } else { + offsetX = set.getOffsetX(); + offsetY = set.getOffsetY(); + } - token.applyMove(set, path, offsetX, offsetY, keyToken, cellOffX, cellOffY); + /* + * Lee: the problem now is to keep the precise coordinate computations for unsnapped tokens following a snapped key token. The derived path in the following section contains rounded + * down values because the integer cell values were passed. If these were double in nature, the precision would be kept, but that would be too difficult to change at this stage... + */ - // Lee: setting originPoint to landing point - token.setOriginPoint(new ZonePoint(token.getX(), token.getY())); + token.applyMove(set, path, offsetX, offsetY, keyToken, cellOffX, cellOffY); - flush(token); - MapTool.serverCommand().putToken(zone.getId(), token); + // Lee: setting originPoint to landing point + token.setOriginPoint(new ZonePoint(token.getX(), token.getY())); - // No longer need this version - // Lee: redundant flush() already did this above - // replacementImageMap.remove(token); + flush(token); + MapTool.serverCommand().putToken(zone.getId(), token); - // Only add certain tokens to the list to process in the move - // Macro function(s). - if (token.getLayer().supportsWalker() && token.isVisible()) { - filteredTokens.add(tokenGUID); - } + // No longer need this version + // Lee: redundant flush() already did this above + // replacementImageMap.remove(token); - if (token.hasAnyTopology()) { - topologyTokenMoved = true; - } + // Only add certain tokens to the list to process in the move + // Macro function(s). + if (token.getLayer().supportsWalker() && token.isVisible()) { + filteredTokens.add(tokenGUID); + } - // renderPath((Graphics2D) this.getGraphics(), path, token.getFootprint(zone.getGrid())); - } - moveTimer.stop("eachtoken"); - - moveTimer.start("onTokenMove"); - if (!filteredTokens.isEmpty()) { - // give onTokenMove a chance to reject each token's movement. - // pass in all the tokens at once so it doesn't have to re-scan for handlers for each token - List tokensToCheck = - filteredTokens.stream().map(zone::getToken).collect(Collectors.toList()); - List tokensDenied = - TokenMoveFunctions.callForIndividualTokenMoveVetoes(path, tokensToCheck); - for (Token token : tokensDenied) { - denyMovement(token); - } - } - moveTimer.stop("onTokenMove"); - - moveTimer.start("onMultipleTokensMove"); - // Multiple tokens, the list of tokens and call - // onMultipleTokensMove() macro function. - if (filteredTokens.size() > 1) { - // now determine if the macro returned false and if so - // revert each token's move to the last path. - boolean moveDenied = TokenMoveFunctions.callForMultiTokenMoveVeto(filteredTokens); - if (moveDenied) { - for (GUID tokenGUID : filteredTokens) { - Token token = zone.getToken(tokenGUID); - denyMovement(token); - } - } - } - moveTimer.stop("onMultipleTokensMove"); + if (token.hasAnyTopology()) { + topologyTokenMoved = true; + } + } + moveTimer.stop("eachtoken"); + + moveTimer.start("onTokenMove"); + if (!filteredTokens.isEmpty()) { + // give onTokenMove a chance to reject each token's movement. + // pass in all the tokens at once so it doesn't have to re-scan for handlers for each + // token + List tokensToCheck = + filteredTokens.stream().map(zone::getToken).collect(Collectors.toList()); + List tokensDenied = + TokenMoveFunctions.callForIndividualTokenMoveVetoes(path, tokensToCheck); + for (Token token : tokensDenied) { + denyMovement(token); + } + } + moveTimer.stop("onTokenMove"); + + moveTimer.start("onMultipleTokensMove"); + // Multiple tokens, the list of tokens and call + // onMultipleTokensMove() macro function. + if (filteredTokens.size() > 1) { + // now determine if the macro returned false and if so + // revert each token's move to the last path. + boolean moveDenied = TokenMoveFunctions.callForMultiTokenMoveVeto(filteredTokens); + if (moveDenied) { + for (GUID tokenGUID : filteredTokens) { + Token token = zone.getToken(tokenGUID); + denyMovement(token); + } + } + } + moveTimer.stop("onMultipleTokensMove"); - moveTimer.start("updateTokenTree"); - MapTool.getFrame().updateTokenTree(); - moveTimer.stop("updateTokenTree"); + moveTimer.start("updateTokenTree"); + MapTool.getFrame().updateTokenTree(); + moveTimer.stop("updateTokenTree"); - if (moveTimer.isEnabled()) { - MapTool.getProfilingNoteFrame().addText(moveTimer.toString()); - moveTimer.clear(); - } + if (topologyTokenMoved) { + zone.tokenTopologyChanged(); + } + }); } else { for (GUID tokenGUID : selectionSet) { denyMovement(zone.getToken(tokenGUID)); } } - - if (topologyTokenMoved) { - zone.tokenTopologyChanged(); - } } /** @@ -784,57 +780,53 @@ public BufferedImage getMiniImage(int size) { @Override public void paintComponent(Graphics g) { - if (timer == null) { - timer = new CodeTimer("ZoneRenderer.renderZone"); - } - timer.setEnabled(AppState.isCollectProfilingData()); - timer.clear(); - timer.setThreshold(10); - timer.start("paintComponent"); - - Graphics2D g2d = (Graphics2D) g; - - timer.start("paintComponent:allocateBuffer"); - tempBufferPool.setWidth(getSize().width); - tempBufferPool.setHeight(getSize().height); - tempBufferPool.setConfiguration(g2d.getDeviceConfiguration()); - timer.stop("paintComponent:allocateBuffer"); - - try (final var bufferHandle = tempBufferPool.acquire()) { - final var buffer = bufferHandle.get(); - final var bufferG2d = buffer.createGraphics(); - // Keep the clip so we don't render more than we have to. - bufferG2d.setClip(g2d.getClip()); - - timer.start("paintComponent:createView"); - PlayerView pl = getPlayerView(); - timer.stop("paintComponent:createView"); - - renderZone(bufferG2d, pl); - int noteVPos = 20; - if (MapTool.getFrame().areFullScreenToolsShown()) noteVPos += 40; - - if (!AppPreferences.getMapVisibilityWarning() && (!zone.isVisible() && pl.isGMView())) { - GraphicsUtil.drawBoxedString( - bufferG2d, I18N.getText("zone.map_not_visible"), getSize().width / 2, noteVPos); - noteVPos += 20; - } - if (AppState.isShowAsPlayer()) { - GraphicsUtil.drawBoxedString( - bufferG2d, I18N.getText("zone.player_view"), getSize().width / 2, noteVPos); - } + CodeTimer.using( + "ZoneRenderer.renderZone", + timer -> { + timer.setThreshold(10); + + timer.start("paintComponent"); + + Graphics2D g2d = (Graphics2D) g; + + timer.start("paintComponent:allocateBuffer"); + tempBufferPool.setWidth(getSize().width); + tempBufferPool.setHeight(getSize().height); + tempBufferPool.setConfiguration(g2d.getDeviceConfiguration()); + timer.stop("paintComponent:allocateBuffer"); + + try (final var bufferHandle = tempBufferPool.acquire()) { + final var buffer = bufferHandle.get(); + final var bufferG2d = buffer.createGraphics(); + // Keep the clip so we don't render more than we have to. + bufferG2d.setClip(g2d.getClip()); + + timer.start("paintComponent:createView"); + PlayerView pl = getPlayerView(); + timer.stop("paintComponent:createView"); + + renderZone(bufferG2d, pl); + int noteVPos = 20; + if (MapTool.getFrame().areFullScreenToolsShown()) noteVPos += 40; + + if (!AppPreferences.getMapVisibilityWarning() && (!zone.isVisible() && pl.isGMView())) { + GraphicsUtil.drawBoxedString( + bufferG2d, I18N.getText("zone.map_not_visible"), getSize().width / 2, noteVPos); + noteVPos += 20; + } + if (AppState.isShowAsPlayer()) { + GraphicsUtil.drawBoxedString( + bufferG2d, I18N.getText("zone.player_view"), getSize().width / 2, noteVPos); + } - timer.start("paintComponent:renderBuffer"); - bufferG2d.dispose(); - g2d.drawImage(buffer, null, 0, 0); - timer.stop("paintComponent:renderBuffer"); - } + timer.start("paintComponent:renderBuffer"); + bufferG2d.dispose(); + g2d.drawImage(buffer, null, 0, 0); + timer.stop("paintComponent:renderBuffer"); + } - timer.stop("paintComponent"); - if (timer.isEnabled()) { - MapTool.getProfilingNoteFrame().addText(timer.toString()); - timer.clear(); - } + timer.stop("paintComponent"); + }); } public PlayerView getPlayerView() { @@ -913,6 +905,8 @@ private boolean shouldRenderLayer(Layer layer, PlayerView view) { * @param view PlayerView object that describes whether the view is a Player or GM view */ public void renderZone(Graphics2D g2d, PlayerView view) { + final var timer = CodeTimer.get(); + timer.start("setup"); // store previous rendering settings RenderingHints oldRenderingHints = g2d.getRenderingHints(); @@ -1271,6 +1265,8 @@ private void renderRenderables(Graphics2D g) { * @param view the player view */ private void renderLights(Graphics2D g, PlayerView view) { + final var timer = CodeTimer.get(); + // Collect and organize lights timer.start("renderLights:getLights"); final var drawableLights = zoneView.getDrawableLights(view); @@ -1320,6 +1316,8 @@ private void renderLights(Graphics2D g, PlayerView view) { * @param view the player view. */ private void renderAuras(Graphics2D g, PlayerView view) { + final var timer = CodeTimer.get(); + // Setup timer.start("renderAuras:getAuras"); final var drawableAuras = zoneView.getDrawableAuras(); @@ -1341,6 +1339,8 @@ private void renderLumensOverlay( PlayerView view, @Nullable ZoneRendererConstants.LightOverlayClipStyle clipStyle, float overlayOpacity) { + final var timer = CodeTimer.get(); + g = (Graphics2D) g.create(); final var disjointLumensLevels = zoneView.getDisjointObscuredLumensLevels(view); @@ -1453,6 +1453,8 @@ private void renderLightOverlay( @Nullable ZoneRendererConstants.LightOverlayClipStyle clipStyle, Collection lights, Paint backgroundFill) { + final var timer = CodeTimer.get(); + if (lights.isEmpty()) { // No point spending resources accomplishing nothing. return; @@ -1519,6 +1521,8 @@ private void renderLightOverlay( * @param view The player view. */ private void renderPlayerDarkness(Graphics2D g, PlayerView view) { + final var timer = CodeTimer.get(); + if (view.isGMView()) { // GMs see the darkness rendered as lights, not as blackness. return; @@ -1647,6 +1651,8 @@ private void renderHaloArea(Graphics2D g, Area visible) { } private void renderLabels(Graphics2D g, PlayerView view) { + final var timer = CodeTimer.get(); + timer.start("labels-1"); labelLocationList.clear(); for (Label label : zone.getLabels()) { @@ -1686,6 +1692,8 @@ private void renderLabels(Graphics2D g, PlayerView view) { } private void renderFog(Graphics2D g, PlayerView view) { + final var timer = CodeTimer.get(); + Dimension size = getSize(); Area fogClip = new Area(new Rectangle(0, 0, size.width, size.height)); @@ -2231,6 +2239,8 @@ protected void showBlockedMoves(Graphics2D g, PlayerView view, Set @SuppressWarnings("unchecked") public void renderPath( Graphics2D g, Path path, TokenFootprint footprint) { + final var timer = CodeTimer.get(); + if (path == null) { return; } @@ -2728,6 +2738,8 @@ protected void renderTokens(Graphics2D g, List tokenList, PlayerView view protected void renderTokens( Graphics2D g, List tokenList, PlayerView view, boolean figuresOnly) { + final var timer = CodeTimer.get(); + Graphics2D clippedG = g; boolean isGMView = view.isGMView(); // speed things up diff --git a/src/main/java/net/rptools/maptool/util/PersistenceUtil.java b/src/main/java/net/rptools/maptool/util/PersistenceUtil.java index cdf0a539fe..2731c4ca43 100644 --- a/src/main/java/net/rptools/maptool/util/PersistenceUtil.java +++ b/src/main/java/net/rptools/maptool/util/PersistenceUtil.java @@ -44,7 +44,6 @@ import net.rptools.lib.io.PackedFile; import net.rptools.maptool.client.AppConstants; import net.rptools.maptool.client.AppPreferences; -import net.rptools.maptool.client.AppState; import net.rptools.maptool.client.AppUtil; import net.rptools.maptool.client.MapTool; import net.rptools.maptool.client.swing.SwingUtil; @@ -272,126 +271,119 @@ private static String fixupZoneName(String n) { } public static void saveCampaign(Campaign campaign, File campaignFile) throws IOException { - CodeTimer saveTimer; // FJE Previously this was 'private static' -- why? - saveTimer = new CodeTimer("CampaignSave"); - saveTimer.setThreshold(5); - // Don't bother keeping track if it won't be displayed... - saveTimer.setEnabled(AppState.isCollectProfilingData()); - - // Strategy: save the file to a tmp location so that if there's a failure the original file - // won't be touched. Then once we're finished, replace the old with the new. - File tmpDir = AppUtil.getTmpDir(); - File tmpFile = new File(tmpDir.getAbsolutePath(), campaignFile.getName()); - if (tmpFile.exists()) tmpFile.delete(); - - PackedFile pakFile = null; - try { - pakFile = new PackedFile(tmpFile); - // Configure the meta file (this is for legacy support) - PersistedCampaign persistedCampaign = new PersistedCampaign(); - - persistedCampaign.campaign = campaign; - - // Keep track of the current view - ZoneRenderer currentZoneRenderer = MapTool.getFrame().getCurrentZoneRenderer(); - if (currentZoneRenderer != null) { - persistedCampaign.currentZoneId = currentZoneRenderer.getZone().getId(); - persistedCampaign.currentView = currentZoneRenderer.getZoneScale(); - } - // Save all assets in active use (consolidate duplicates between maps) - saveTimer.start("Collect all assets"); - Set allAssetIds = campaign.getAllAssetIds(); - for (MD5Key key : allAssetIds) { - // Put in a placeholder; all we really care about is the MD5Key for now... - persistedCampaign.assetMap.put(key, null); - } - saveTimer.stop("Collect all assets"); - - // And store the asset elsewhere - saveTimer.start("Save assets"); - saveAssets(allAssetIds, pakFile); - saveTimer.stop("Save assets"); - - // Store the Drop In Libraries. - saveTimer.start("Save Drop In Libraries"); - saveAddOnLibraries(pakFile); - saveTimer.stop("Save Drop In Libraries"); - - // Store the Game Data - saveTimer.start("Save Game Data"); - saveGameData(pakFile); - saveTimer.stop("Save Game Data"); - - try { - saveTimer.start("Set content"); - - pakFile.setContent(persistedCampaign); - pakFile.setProperty(PROP_CAMPAIGN_VERSION, CAMPAIGN_VERSION); - pakFile.setProperty(PROP_VERSION, MapTool.getVersion()); - - saveTimer.stop("Set content"); - saveTimer.start("Save"); - pakFile.save(); - saveTimer.stop("Save"); - } catch (OutOfMemoryError oom) { - /* - * This error is normally because the heap space has been exceeded while trying to save the campaign. Since MapTool caches the images used by the current Zone, and since the - * VersionManager must keep the XML for objects in memory in order to apply transforms to them, the memory usage can spike very high during the save() operation. A common solution is - * to switch to an empty map and perform the save from there; this causes MapTool to unload any images that it may have had cached and this can frequently free up enough memory for the - * save() to work. We'll tell the user all this right here and then fail the save and they can try again. - */ - saveTimer.start("OOM Close"); - pakFile.close(); // Have to close the tmpFile first on some OSes - pakFile = null; - tmpFile.delete(); // Delete the temporary file - saveTimer.stop("OOM Close"); - if (saveTimer.isEnabled()) { - MapTool.getProfilingNoteFrame().addText(saveTimer.toString()); - } - MapTool.showError("msg.error.failedSaveCampaignOOM"); - return; - } - } finally { - saveTimer.start("Close"); - try { - if (pakFile != null) pakFile.close(); - } catch (Exception e) { - } - saveTimer.stop("Close"); - pakFile = null; - } - - /* - * Copy to the new location. Not the fastest solution in the world if renameTo() fails, but worth the safety net it provides. Jamz: So, renameTo() is causing more issues than it is worth. It - * has a tendency to lock a file under Google Drive/Drop box causing the save to fail. Removed the for final save location... - */ - saveTimer.start("Backup"); - File bakFile = new File(tmpDir.getAbsolutePath(), campaignFile.getName() + ".bak"); - - bakFile.delete(); // Delete the last backup file... + CodeTimer.using( + "CampaignSave", + saveTimer -> { + saveTimer.setThreshold(5); + + // Strategy: save the file to a tmp location so that if there's a failure the original + // file won't be touched. Then once we're finished, replace the old with the new. + File tmpDir = AppUtil.getTmpDir(); + File tmpFile = new File(tmpDir.getAbsolutePath(), campaignFile.getName()); + if (tmpFile.exists()) tmpFile.delete(); + + PackedFile pakFile = null; + try { + pakFile = new PackedFile(tmpFile); + // Configure the meta file (this is for legacy support) + PersistedCampaign persistedCampaign = new PersistedCampaign(); + + persistedCampaign.campaign = campaign; + + // Keep track of the current view + ZoneRenderer currentZoneRenderer = MapTool.getFrame().getCurrentZoneRenderer(); + if (currentZoneRenderer != null) { + persistedCampaign.currentZoneId = currentZoneRenderer.getZone().getId(); + persistedCampaign.currentView = currentZoneRenderer.getZoneScale(); + } + // Save all assets in active use (consolidate duplicates between maps) + saveTimer.start("Collect all assets"); + Set allAssetIds = campaign.getAllAssetIds(); + for (MD5Key key : allAssetIds) { + // Put in a placeholder; all we really care about is the MD5Key for now... + persistedCampaign.assetMap.put(key, null); + } + saveTimer.stop("Collect all assets"); + + // And store the asset elsewhere + saveTimer.start("Save assets"); + saveAssets(allAssetIds, pakFile); + saveTimer.stop("Save assets"); + + // Store the Drop In Libraries. + saveTimer.start("Save Drop In Libraries"); + saveAddOnLibraries(pakFile); + saveTimer.stop("Save Drop In Libraries"); + + // Store the Game Data + saveTimer.start("Save Game Data"); + saveGameData(pakFile); + saveTimer.stop("Save Game Data"); + + try { + saveTimer.start("Set content"); + + pakFile.setContent(persistedCampaign); + pakFile.setProperty(PROP_CAMPAIGN_VERSION, CAMPAIGN_VERSION); + pakFile.setProperty(PROP_VERSION, MapTool.getVersion()); + + saveTimer.stop("Set content"); + saveTimer.start("Save"); + pakFile.save(); + saveTimer.stop("Save"); + } catch (OutOfMemoryError oom) { + /* + * This error is normally because the heap space has been exceeded while trying to save the campaign. Since MapTool caches the images used by the current Zone, and since the + * VersionManager must keep the XML for objects in memory in order to apply transforms to them, the memory usage can spike very high during the save() operation. A common solution is + * to switch to an empty map and perform the save from there; this causes MapTool to unload any images that it may have had cached and this can frequently free up enough memory for the + * save() to work. We'll tell the user all this right here and then fail the save and they can try again. + */ + saveTimer.start("OOM Close"); + pakFile.close(); // Have to close the tmpFile first on some OSes + pakFile = null; + tmpFile.delete(); // Delete the temporary file + saveTimer.stop("OOM Close"); + MapTool.showError("msg.error.failedSaveCampaignOOM"); + return; + } + } finally { + saveTimer.start("Close"); + try { + if (pakFile != null) pakFile.close(); + } catch (Exception e) { + } + saveTimer.stop("Close"); + pakFile = null; + } - if (campaignFile.exists()) { - saveTimer.start("Backup campaignFile"); - FileUtil.copyFile(campaignFile, bakFile); - // campaignFile.delete(); - saveTimer.stop("Backup campaignFile"); - } + /* + * Copy to the new location. Not the fastest solution in the world if renameTo() fails, but worth the safety net it provides. Jamz: So, renameTo() is causing more issues than it is worth. It + * has a tendency to lock a file under Google Drive/Drop box causing the save to fail. Removed the for final save location... + */ + saveTimer.start("Backup"); + File bakFile = new File(tmpDir.getAbsolutePath(), campaignFile.getName() + ".bak"); - saveTimer.start("Backup tmpFile"); - FileUtil.copyFile(tmpFile, campaignFile); - tmpFile.delete(); - saveTimer.stop("Backup tmpFile"); - if (bakFile.exists()) bakFile.delete(); - saveTimer.stop("Backup"); + bakFile.delete(); // Delete the last backup file... - // Save the campaign thumbnail - saveTimer.start("Thumbnail"); - saveCampaignThumbnail(campaignFile.getName()); - saveTimer.stop("Thumbnail"); + if (campaignFile.exists()) { + saveTimer.start("Backup campaignFile"); + FileUtil.copyFile(campaignFile, bakFile); + // campaignFile.delete(); + saveTimer.stop("Backup campaignFile"); + } - if (saveTimer.isEnabled()) { - MapTool.getProfilingNoteFrame().addText(saveTimer.toString()); - } + saveTimer.start("Backup tmpFile"); + FileUtil.copyFile(tmpFile, campaignFile); + tmpFile.delete(); + saveTimer.stop("Backup tmpFile"); + if (bakFile.exists()) bakFile.delete(); + saveTimer.stop("Backup"); + + // Save the campaign thumbnail + saveTimer.start("Thumbnail"); + saveCampaignThumbnail(campaignFile.getName()); + saveTimer.stop("Thumbnail"); + }); } /*