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,6 +95,67 @@ public class DrawingCache {
// if we need to apply caching
Point firstPoint = component.getControlPoint(0);
CacheValue value = renderAndCache(component, g2d, componentState, outlineMode, project, zoom);
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) {
g2d.getCanvasGraphics().scale(1 / zoom, 1 / zoom);
}
try {
// draw cached image
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.getCurrentArea(), value.getContinuityPositiveAreas(), value.getContinuityNegativeAreas());
} else {
// no caching, just draw as usual
component.draw(g2d, componentState, outlineMode, project, g2d);
}
g2d.stopTracking();
g2d.stopTrackingContinuityArea();
}
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();
}
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))
@@ -142,58 +237,7 @@ public class DrawingCache {
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) {
g2d.getCanvasGraphics().scale(1 / zoom, 1 / zoom);
}
try {
// draw cached image
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.getCurrentArea(), value.getContinuityPositiveAreas(), value.getContinuityNegativeAreas());
} else {
// no caching, just draw as usual
component.draw(g2d, componentState, outlineMode, project, g2d);
}
g2d.stopTracking();
g2d.stopTrackingContinuityArea();
}
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();
return value;
}
private class CacheValue {
@@ -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++) {
for (int j = i + 1; j < areas.size(); j++) {
if (consumed[j])
continue;
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 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
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 ((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())) {
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));