give the cache a chance to prepare all the components in multiple

threads
This commit is contained in:
bancika
2020-04-08 13:32:46 +02:00
parent ae7f2625ed
commit fd8640ba3d
2 changed files with 188 additions and 109 deletions
@@ -31,6 +31,7 @@ import org.diylc.common.ComponentType;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.Project;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.utils.Pair;
/**
@@ -51,6 +52,39 @@ public class DrawingCache {
public static DrawingCache Instance = new DrawingCache();
/**
* Prepares cached renderings of those components from the specified map that have {@link ComponentDescriptor#enableCache()} set to true.
*
* @param components
* @param g2d
* @param outlineMode
* @param project
* @param zoom
*/
@SuppressWarnings("unchecked")
public void bulkPrepare(Map<IDIYComponent<?>, ComponentState> components, G2DWrapper g2d, boolean outlineMode, Project project, double zoom) {
// run all renders in parallel to utilize all CPU cores
components.entrySet().stream().parallel().forEach(x -> {
ComponentType type = ComponentProcessor.getInstance()
.extractComponentTypeFrom((Class<? extends IDIYComponent<?>>) x.getKey().getClass());
if (type.getEnableCache()) {
renderAndCache(x.getKey(), g2d, x.getValue(), outlineMode, project, zoom);
}
});
}
/**
* Draws a component onto the specified {@link G2DWrapper}. If the component has {@link ComponentDescriptor#enableCache()} set to true,
* the drawing will come from the cache. Otherwise, it will be drawn as usual.
*
* @param component
* @param g2d
* @param componentState
* @param outlineMode
* @param project
* @param zoom
*/
@SuppressWarnings("unchecked")
public void draw(IDIYComponent<?> component, G2DWrapper g2d, ComponentState componentState,
boolean outlineMode, Project project, double zoom) {
@@ -61,86 +95,7 @@ public class DrawingCache {
// if we need to apply caching
Point firstPoint = component.getControlPoint(0);
// cache hit!
CacheValue value = null;
if (imageCache.containsKey(component))
value = imageCache.get(component);
// only honor the cache if the component hasn't changed in the meantime
if (value == null || !value.getComponent().equalsTo(component)
|| value.getState() != componentState || value.getZoom() != zoom) {
// figure out size and placement of buffer image
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 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
if (Math.abs(1.0 - zoom) > 1e-4) {
cg2d.scale(zoom, zoom);
}
// translate to make the top-left corner of the component match (0, 0)
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();
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();
// 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) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
if (wrapper.getCurrentArea() == null || wrapper.getCurrentArea().isEmpty()) {
LOG.warn("Empty outline area detected for " + component.getName());
}
// add to the cache
value = new CacheValue(component, image, wrapper.getCurrentArea(), wrapper.getContinuityPositiveAreas(),
wrapper.getContinuityNegativeAreas(), componentState, zoom, dx, dy);
imageCache.put(component, value);
}
CacheValue value = renderAndCache(component, g2d, componentState, outlineMode, project, zoom);
long componentStart = System.nanoTime();
@@ -196,6 +151,95 @@ public class DrawingCache {
imageCache.clear();
}
private CacheValue renderAndCache(IDIYComponent<?> component, G2DWrapper g2d, ComponentState componentState,
boolean outlineMode, Project project, double zoom) {
// if we need to apply caching
Point firstPoint = component.getControlPoint(0);
// cache hit!
CacheValue value = null;
if (imageCache.containsKey(component))
value = imageCache.get(component);
// only honor the cache if the component hasn't changed in the meantime
if (value == null || !value.getComponent().equalsTo(component)
|| value.getState() != componentState || value.getZoom() != zoom) {
// figure out size and placement of buffer image
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 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
if (Math.abs(1.0 - zoom) > 1e-4) {
cg2d.scale(zoom, zoom);
}
// translate to make the top-left corner of the component match (0, 0)
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();
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();
// 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) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
if (wrapper.getCurrentArea() == null || wrapper.getCurrentArea().isEmpty()) {
LOG.warn("Empty outline area detected for " + component.getName());
}
// add to the cache
value = new CacheValue(component, image, wrapper.getCurrentArea(), wrapper.getContinuityPositiveAreas(),
wrapper.getContinuityNegativeAreas(), componentState, zoom, dx, dy);
imageCache.put(component, value);
}
return value;
}
private class CacheValue {
IDIYComponent<?> component;
BufferedImage image;
@@ -255,12 +255,9 @@ public class DrawingManager {
// g2dWrapper.resetTx();
// componentAreaMap.clear();
// put components and their states in a map
Map<IDIYComponent<?>, ComponentState> componentStateMap = new HashMap<IDIYComponent<?>, ComponentState>();
for (IDIYComponent<?> component : project.getComponents()) {
// Do not draw the component if it's filtered out.
if (filter != null && !filter.testComponent(component)) {
continue;
}
ComponentState state = ComponentState.NORMAL;
if (drawOptions.contains(DrawOption.SELECTION) && selectedComponents.contains(component)) {
if (dragInProgress) {
@@ -269,6 +266,23 @@ public class DrawingManager {
state = ComponentState.SELECTED;
}
}
componentStateMap.put(component, state);
}
boolean outlineMode = drawOptions.contains(DrawOption.OUTLINE_MODE);
// give the cache a chance to prepare all the components in multiple threads
if (drawOptions.contains(DrawOption.ENABLE_CACHING)) {
DrawingCache.Instance.bulkPrepare(componentStateMap, g2dWrapper, outlineMode, project, zoom);
}
// componentAreaMap.clear();
for (IDIYComponent<?> component : project.getComponents()) {
// Do not draw the component if it's filtered out.
if (filter != null && !filter.testComponent(component)) {
continue;
}
ComponentState state = componentStateMap.get(component);
// Do not track the area if component is not invalidated and was
// drawn in the same state.
boolean trackArea = lastDrawnStateMap.get(component) != state;
@@ -289,7 +303,7 @@ public class DrawingManager {
if (drawOptions.contains(DrawOption.ENABLE_CACHING)) // go through the DrawingCache
DrawingCache.Instance.draw(component, g2dWrapper, state, drawOptions.contains(DrawOption.OUTLINE_MODE), project, zoom);
else // go stragiht to the wrapper
component.draw(g2dWrapper, state, drawOptions.contains(DrawOption.OUTLINE_MODE), project, g2dWrapper);
component.draw(g2dWrapper, state, outlineMode, project, g2dWrapper);
if (g2dWrapper.isTrackingContinuityArea()) {
LOG.info("Component " + component.getName() + " of type " + component.getClass().getName() + " did not stop tracking continuity area.");
@@ -652,32 +666,53 @@ public class DrawingManager {
List<Area> newAreas = new ArrayList<Area>();
boolean[] consumed = new boolean[areas.size()];
for (int i = 0; i < areas.size(); i++) {
Area a1 = areas.get(i);
Rectangle2D bounds1 = a1.getBounds2D();
Map<Area, Integer> candidates = new HashMap<Area, Integer>();
for (int j = i + 1; j < areas.size(); j++) {
if (consumed[j])
continue;
Area a1 = areas.get(i);
// if (consumed[j])
// continue;
Area a2 = areas.get(j);
Area intersection = null;
if (a1.getBounds2D().intersects(a2.getBounds())) {
intersection = new Area(a1);
intersection.intersect(a2);
}
// if the two areas intersect, make a union and consume the second area
if (intersection != null && !intersection.isEmpty()) {
a1.add(a2);
consumed[j] = true;
} else { // maybe there's a connection between them
for (Connection p : connections) { // use getBounds to optimize the computation, don't get into complex math if not needed
if ((a1.getBounds().contains(p.getP1()) && a2.getBounds().contains(p.getP2()) && a1.contains(p.getP1()) && a2.contains(p.getP2())) ||
(a1.getBounds().contains(p.getP2()) && a2.getBounds().contains(p.getP1())) && a1.contains(p.getP2()) && a2.contains(p.getP1())) {
a1.add(a2);
consumed[j] = true;
break;
}
Rectangle2D bounds2 = a2.getBounds2D();
// maybe there's a direct connection between them, try that first because it's faster than area overlap
for (Connection p : connections) { // use getBounds to optimize the computation, don't get into complex math if not needed
if ((bounds1.contains(p.getP1()) && bounds2.contains(p.getP2()) && a1.contains(p.getP1()) && a2.contains(p.getP2())) ||
(bounds1.contains(p.getP2()) && bounds2.contains(p.getP1())) && a1.contains(p.getP2()) && a2.contains(p.getP1())) {
a1.add(a2);
consumed[j] = true;
break;
}
}
if (bounds1.intersects(bounds2)) {
candidates.put(a2, j);
}
}
Set<Area> mergeQueue = new HashSet<Area>();
// run a parallel for-each to determine which areas overlap with a1 and add them to a queue to be added to a1 later
if (candidates.size() > 0) {
candidates.entrySet().parallelStream().forEach(x -> {
Area intersection = new Area(a1);
intersection.intersect(x.getKey());
if (intersection != null && !intersection.isEmpty()) {
synchronized (mergeQueue) {
mergeQueue.add(x.getKey());
}
consumed[x.getValue()] = true;
}
});
}
// if make a union of all intersecting areas
for(Area a2 : mergeQueue)
a1.add(a2);
}
for (int i = 0; i < areas.size(); i++)
if (!consumed[i])
newAreas.add(areas.get(i));