generalize caching

This commit is contained in:
bancika
2020-04-06 21:47:07 +02:00
parent 7b3326a5cd
commit e29bdf5f62
17 changed files with 305 additions and 51 deletions
@@ -251,5 +251,10 @@ public abstract class AbstractComponent<T> implements IDIYComponent<T> {
@Override
public boolean canPointMoveFreely(int pointIndex) {
return true;
}
@Override
public Rectangle2D getCachingBounds() {
return null;
}
}
@@ -25,6 +25,7 @@ import java.awt.Point;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import org.apache.log4j.Logger;
import org.diylc.appframework.miscutils.ConfigurationManager;
@@ -360,6 +361,28 @@ public abstract class AbstractCurvedComponent<T> extends AbstractTransparentComp
return null;
}
}
@Override
public Rectangle2D getCachingBounds() {
int minX = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int minY = Integer.MAX_VALUE;
int maxY = Integer.MIN_VALUE;
int margin = 50; // hard coded, replace with something meaningful
for (int i = 0; i < getControlPointCount(); i++) {
Point p = getControlPoint(i);
if (p.x < minX)
minX = p.x;
if (p.x > maxX)
maxX = p.x;
if (p.y < minY)
minY = p.y;
if (p.y > maxY)
maxY = p.y;
}
return new Rectangle2D.Double(minX - margin, minY - margin, maxX - minX + 2 * margin, maxY - minY + 2 * margin);
}
public enum PointCount {
TWO(2), THREE(3), FOUR(4), FIVE(5), SEVEN(7);
@@ -22,7 +22,8 @@
package org.diylc.core;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Point;
import java.awt.geom.Rectangle2D;
import java.io.Serializable;
import org.diylc.core.annotations.ComponentDescriptor;
@@ -189,5 +190,11 @@ public interface IDIYComponent<T> extends Serializable, Cloneable {
* @param other
* @return
*/
boolean equalsTo(IDIYComponent<?> other);
boolean equalsTo(IDIYComponent<?> other);
/**
* Used only if {@link ComponentDescriptor#enableCache()} is set to true.
* @return
*/
Rectangle2D getCachingBounds();
}
@@ -0,0 +1,44 @@
/*
*
* DIY Layout Creator (DIYLC). Copyright (c) 2009-2018 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with DIYLC. If not, see
* <http://www.gnu.org/licenses/>.
*/
package org.diylc.presenter;
import java.util.concurrent.TimeUnit;
public class Counter {
private int count;
private long nanotime;
public int getCount() {
return count;
}
public long getNanoTime() {
return nanotime;
}
public void add(long nanotime) {
count++;
this.nanotime += nanotime;
}
@Override
public String toString() {
return String.format("%dx %dms", count, TimeUnit.MILLISECONDS.convert(nanotime, TimeUnit.NANOSECONDS));
}
}
@@ -19,15 +19,19 @@ package org.diylc.presenter;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.geom.Area;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.log4j.Logger;
import org.diylc.common.ComponentType;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.Project;
import org.diylc.utils.Pair;
/**
* A layer that injects itself between the provided {@link G2DWrapper} instance and the underlying
@@ -39,8 +43,9 @@ import org.diylc.core.Project;
*/
public class DrawingCache {
private Map<IDIYComponent<?>, CacheValue> imageCache =
new HashMap<IDIYComponent<?>, CacheValue>();
private Map<IDIYComponent<?>, CacheValue> imageCache = new HashMap<IDIYComponent<?>, CacheValue>();
private Map<String, Pair<Counter, Counter>> renderStatsByType = new HashMap<String, Pair<Counter, Counter>>();
private static final Logger LOG = Logger.getLogger(DrawingCache.class);
@@ -55,7 +60,6 @@ public class DrawingCache {
if (type.getEnableCache()) {
// if we need to apply caching
Point firstPoint = component.getControlPoint(0);
Point lastPoint = component.getControlPoint(component.getControlPointCount() - 1);
// cache hit!
CacheValue value = null;
@@ -66,14 +70,22 @@ public class DrawingCache {
if (value == null || !value.getComponent().equalsTo(component)
|| value.getState() != componentState || value.getZoom() != zoom) {
LOG.trace("Rendering " + component.getName() + " for cache.");
int width = (int) (Math.abs(firstPoint.x - lastPoint.x) * zoom) + 2;
int height = (int) (Math.abs(firstPoint.y - lastPoint.y) * zoom) + 2;
Rectangle2D rect = component.getCachingBounds();
int width = (int)Math.round(rect.getWidth() * zoom);
int height = (int)Math.round(rect.getHeight() * zoom);
// calculate the position of the first point relative to the caching bounds
int dx = (int) (firstPoint.x - rect.getX());
int dy = (int) (firstPoint.y - rect.getY());
// create image
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
// create graphics
Graphics2D cg2d = image.createGraphics();
// copy over rendering settings
// copy over rendering settings so our cached image has the same features
cg2d.setRenderingHints(g2d.getRenderingHints());
// set clip bounds to the whole image
cg2d.setClip(0, 0, width, height);
// apply zoom
@@ -81,24 +93,41 @@ public class DrawingCache {
cg2d.scale(zoom, zoom);
}
// translate to make the top-left corner of the component match (0, 0)
cg2d.translate(-firstPoint.x, -firstPoint.y);
cg2d.translate(-firstPoint.x + dx, -firstPoint.y + dy);
// wrap the graphics so we can track
G2DWrapper wrapper = new G2DWrapper(cg2d, zoom);
// initialize wrapper
wrapper.startedDrawingComponent();
if (!trackArea) {
wrapper.stopTracking();
}
long componentStart = System.nanoTime();
// now draw the component to the buffer image
component.draw(wrapper, componentState, outlineMode, project, wrapper);
long componentEnd = System.nanoTime();
Pair<Counter, Counter> stats;
String key = component.getClass().getCanonicalName().replace("org.diylc.components.", "");
if (renderStatsByType.containsKey(key)) {
stats = renderStatsByType.get(key);
} else {
stats = new Pair<Counter, Counter>(new Counter(), new Counter());
renderStatsByType.put(key, stats);
}
stats.getFirst().add(componentEnd - componentStart);
// finalize wrapper
wrapper.finishedDrawingComponent();
cg2d.dispose();
// File outputfile = new File("d:\\image_" + component.getName() + ".png");
// output cached drawings to separate files
// File outputfile = new File("d:\\tmp\\image_" + component.getName() + ".png");
// try {
// ImageIO.write(image, "png", outputfile);
// } catch (IOException e) {
@@ -106,10 +135,13 @@ public class DrawingCache {
// e.printStackTrace();
// }
value = new CacheValue(component, image, wrapper, componentState, zoom);
// add to the cache
value = new CacheValue(component, image, wrapper.getCurrentArea(), wrapper.getContinuityPositiveAreas(),
wrapper.getContinuityNegativeAreas(), componentState, zoom, dx, dy);
imageCache.put(component, value);
}
long componentStart = System.nanoTime();
// temporarily scale the graphic to compensate for the zoom, since we are rendering an image with the zoom appled
if (Math.abs(1.0 - zoom) > 1e-4) {
@@ -117,21 +149,45 @@ public class DrawingCache {
}
try {
// draw cached image
g2d.getCanvasGraphics().drawImage(value.getImage(), (int) (firstPoint.x * zoom),
(int) (firstPoint.y * zoom), null);
g2d.getCanvasGraphics().drawImage(value.getImage(), (int) ((firstPoint.x - value.getDx()) * zoom),
(int) ((firstPoint.y - value.getDy())* zoom), null);
} finally {
if (Math.abs(1.0 - zoom) > 1e-4) {
g2d.getCanvasGraphics().scale(zoom, zoom);
}
}
long componentEnd = System.nanoTime();
Pair<Counter, Counter> stats;
String key = component.getClass().getCanonicalName().replace("org.diylc.components.", "");
if (renderStatsByType.containsKey(key)) {
stats = renderStatsByType.get(key);
} else {
stats = new Pair<Counter, Counter>(new Counter(), new Counter());
renderStatsByType.put(key, stats);
}
stats.getSecond().add(componentEnd - componentStart);
// copy over tracking area from the cache
g2d.merge(value.getWrapper());
g2d.merge(value.getCurrentArea(), value.getContinuityPositiveAreas(), value.getContinuityNegativeAreas());
} else {
// no caching, just draw as usual
component.draw(g2d, componentState, outlineMode, project, g2d);
}
}
public void logStats() {
int totalSizeMB = imageCache.values().stream().map(x -> x.image.getData().getDataBuffer().getSize()).reduce(0, Integer::sum) * 4 / 1024 / 1024; // 4 bytes per pixel
LOG.debug(String.format("Render cache contains %d elements, approx size is %d MB", imageCache.size(), totalSizeMB));
String mapAsString = renderStatsByType.entrySet().stream().sorted((e1, e2) -> -Long.compare(e1.getValue().getFirst().getNanoTime() + e1.getValue().getSecond().getNanoTime(),
e2.getValue().getFirst().getNanoTime() + e2.getValue().getSecond().getNanoTime()))
.map(e -> e.getKey() + ": render: " + e.getValue().getFirst() + ", paste: " + e.getValue().getSecond())
.collect(Collectors.joining("; ", "{", "}"));
LOG.debug("Cache render distribution stats: " + mapAsString);
}
public void clear() {
imageCache.clear();
@@ -140,12 +196,17 @@ public class DrawingCache {
private class CacheValue {
IDIYComponent<?> component;
BufferedImage image;
G2DWrapper wrapper;
Area currentArea;
Map<String, Area> continuityPositiveAreas;
Map<String, Area> continuityNegativeAreas;
ComponentState state;
double zoom;
int dx;
int dy;
public CacheValue(IDIYComponent<?> component, BufferedImage image, G2DWrapper wrapper,
ComponentState state, double zoom) {
public CacheValue(IDIYComponent<?> component, BufferedImage image, Area currentArea,
Map<String, Area> continuityPositiveAreas, Map<String, Area> continuityNegativeAreas,
ComponentState state, double zoom, int dx, int dy) {
super();
// take a copy of the component, so we can check if it changed in the meantime
try {
@@ -153,9 +214,13 @@ public class DrawingCache {
} catch (CloneNotSupportedException e) {
}
this.image = image;
this.wrapper = wrapper;
this.currentArea = currentArea;
this.continuityPositiveAreas = continuityPositiveAreas;
this.continuityNegativeAreas = continuityNegativeAreas;
this.state = state;
this.zoom = zoom;
this.dx = dx;
this.dy = dy;
}
public IDIYComponent<?> getComponent() {
@@ -166,8 +231,16 @@ public class DrawingCache {
return image;
}
public G2DWrapper getWrapper() {
return wrapper;
public Area getCurrentArea() {
return currentArea;
}
public Map<String, Area> getContinuityPositiveAreas() {
return continuityPositiveAreas;
}
public Map<String, Area> getContinuityNegativeAreas() {
return continuityNegativeAreas;
}
public ComponentState getState() {
@@ -177,6 +250,14 @@ public class DrawingCache {
public double getZoom() {
return zoom;
}
public int getDx() {
return dx;
}
public int getDy() {
return dy;
}
@Override
public String toString() {
@@ -58,7 +58,6 @@ import org.diylc.core.Project;
import org.diylc.core.Theme;
import org.diylc.core.VisibilityPolicy;
import org.diylc.utils.Constants;
import org.diylc.utils.Pair;
/**
* Utility that deals with painting {@link Project} on the {@link Graphics2D} and keeps areas taken
@@ -109,7 +108,7 @@ public class DrawingManager {
private boolean debugContinuityAreas;
// rendering stats
private Map<String, Pair<Integer, Long>> renderStatsByType = new HashMap<String, Pair<Integer, Long>>();
private Map<String, Counter> renderStatsByType = new HashMap<String, Counter>();
private long lastStatsReportedTime = System.currentTimeMillis();
private long statReportFrequencyMs = 1000 * 60;
@@ -305,26 +304,15 @@ public class DrawingManager {
} finally {
// record render stats
long componentEnd = System.nanoTime();
Pair<Integer, Long> stats = null;
String key = component.getClass().getCanonicalName();
Counter stats = null;
String key = component.getClass().getCanonicalName().replace("org.diylc.components.", "");
if (renderStatsByType.containsKey(key)) {
stats = renderStatsByType.get(key);
stats.setFirst(stats.getFirst() + 1);
stats.setSecond(stats.getSecond() + (componentEnd - componentStart));
stats = renderStatsByType.get(key);
} else {
stats = new Pair<Integer, Long>(1, componentEnd - componentStart);
stats = new Counter();
renderStatsByType.put(key, stats);
}
// log render time stats periodically
if (System.currentTimeMillis() - lastStatsReportedTime > statReportFrequencyMs) {
lastStatsReportedTime = System.currentTimeMillis();
String mapAsString = renderStatsByType.entrySet().stream().sorted((e1, e2) -> -e1.getValue().getSecond().compareTo(e2.getValue().getSecond()))
.map(e -> e.getKey() + "=" + e.getValue().getFirst() + "x " +
TimeUnit.MILLISECONDS.convert( e.getValue().getSecond(), TimeUnit.NANOSECONDS) + "ms ")
.collect(Collectors.joining(", ", "{", "}"));
LOG.debug("Render stats: " + mapAsString);
}
stats.add(componentEnd - componentStart);
}
ComponentArea area = g2dWrapper.finishedDrawingComponent();
if (trackArea && area != null && !area.getOutlineArea().isEmpty()) {
@@ -469,9 +457,23 @@ public class DrawingManager {
long ms = TimeUnit.MILLISECONDS.convert(end - start, TimeUnit.NANOSECONDS);
LOG.trace(String.format("Render time = %d ms", ms));
logStats();
return failedComponents;
}
public void logStats() {
// log render time stats periodically
if (System.currentTimeMillis() - lastStatsReportedTime > statReportFrequencyMs) {
lastStatsReportedTime = System.currentTimeMillis();
String mapAsString = renderStatsByType.entrySet().stream().sorted((e1, e2) -> -Long.compare(e1.getValue().getNanoTime(), e2.getValue().getNanoTime()))
.map(e -> e.toString())
.collect(Collectors.joining("; ", "{", "}"));
LOG.debug("Render stats: " + mapAsString);
DrawingCache.Instance.logStats();
}
}
public double getZoomLevel() {
return zoomLevel;
@@ -162,10 +162,26 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
return new ComponentArea(currentArea, continuityPositiveAreas.values(), continuityNegativeAreas.values());
}
public void merge(G2DWrapper wrapper) {
currentArea = wrapper.currentArea;
continuityPositiveAreas = wrapper.continuityPositiveAreas;
continuityNegativeAreas = wrapper.continuityNegativeAreas;
// start - used for caching
public Area getCurrentArea() {
return currentArea;
}
public Map<String, Area> getContinuityPositiveAreas() {
return continuityPositiveAreas;
}
public Map<String, Area> getContinuityNegativeAreas() {
return continuityNegativeAreas;
}
// end - used for caching
public void merge(Area currentArea, Map<String, Area> continuityPositiveAreas, Map<String, Area> continuityNegativeAreas) {
this.currentArea = currentArea;
this.continuityPositiveAreas = continuityPositiveAreas;
this.continuityNegativeAreas = continuityNegativeAreas;
}
@Override