Skip to content
Permalink
Browse files

Fixed concurrency issue on cache used for circles rendering

Without synchronization lock, concurrent rendering of images including
circles could lead to glitches as reported in issue #248
  • Loading branch information...
luccioman committed Nov 10, 2018
1 parent c347e7d commit 9daeea823b85c2a447afca73c4d35efe2d969cf1
Showing with 167 additions and 12 deletions.
  1. +48 −12 source/net/yacy/visualization/CircleTool.java
  2. +119 −0 test/java/net/yacy/visualization/CircleToolTest.java
@@ -24,17 +24,28 @@
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;


public class CircleTool {

private static List<int[]> circles = new ArrayList<>();
/** Cache of calculated circles */
private static final List<int[]> CIRCLES_CACHE = new ArrayList<>();

/** Synchronization lock for cache access */
private static final ReentrantLock CIRCLES_CACHE_LOCK = new ReentrantLock();

public static void clearcache() {
circles.clear();
CIRCLES_CACHE_LOCK.lock();
try {
CIRCLES_CACHE.clear();
} finally {
CIRCLES_CACHE_LOCK.unlock();
}
}

private static int[] getCircleCoords(final short radius) {
private static int[] getCircleCoords(final short radius, final List<int[]> circles) {
if (radius - 1 < circles.size()) return circles.get(radius - 1);

// read some lines from known circles
@@ -90,10 +101,23 @@ public static void clearcache() {
}

public static void circle(final RasterPlotter matrix, final int xc, final int yc, final int radius, final int intensity) {
if (radius == 0) {
//matrix.plot(xc, yc, 100);
} else {
final int[] c = getCircleCoords((short) radius);
if (radius != 0) {
final int[] c;
try {
if (CIRCLES_CACHE_LOCK.tryLock(1, TimeUnit.SECONDS)) {
try {
c = getCircleCoords((short) radius, CIRCLES_CACHE);
} finally {
CIRCLES_CACHE_LOCK.unlock();
}
} else {
/* Cache is too busy : let's calculate without it */
c = getCircleCoords((short) radius, new ArrayList<>());
}
} catch (final InterruptedException e) {
Thread.currentThread().interrupt(); // preserve thread interrupted state
return;
}
short x, y;
short limit = (short) c.length;
int co;
@@ -116,11 +140,23 @@ public static void circle(final RasterPlotter matrix, final int xc, final int yc
while (fromArc < 0 ) fromArc +=360;
while ( toArc > 360) toArc -=360;
while ( toArc < 0 ) toArc +=360;
if (radius == 0) {
//matrix.plot(xc, yc, 100);
} else {
int[] c = getCircleCoords((short) radius);
if (c == null) c = getCircleCoords((short) radius);
if (radius != 0) {
final int[] c;
try {
if (CIRCLES_CACHE_LOCK.tryLock(1, TimeUnit.SECONDS)) {
try {
c = getCircleCoords((short) radius, CIRCLES_CACHE);
} finally {
CIRCLES_CACHE_LOCK.unlock();
}
} else {
/* Cache is too busy : let's calculate without it */
c = getCircleCoords((short) radius, new ArrayList<>());
}
} catch (final InterruptedException e) {
Thread.currentThread().interrupt(); // preserve thread interrupted state
return;
}
final short q = (short) c.length;
final short q2 = (short) (q * 2);
final short q3 = (short) (q * 3);
@@ -0,0 +1,119 @@
// CircleToolTest.java
// Copyright 2018 by luccioman; https://github.com/luccioman
//
// This is a part of YaCy, a peer-to-peer based web search engine
//
// LICENSE
//
// This program 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 2 of the License, or
// (at your option) any later version.
//
// This program 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 this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

package net.yacy.visualization;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.bouncycastle.util.Arrays;
import org.junit.Assert;
import org.junit.Test;

import net.yacy.peers.graphics.EncodedImage;

/**
* Unit tests for the {@link CircleTool} class.
*/
public class CircleToolTest {

/**
* Check circle function consistency when run on multiple concurrent threads.
*
* @throws Exception when an unexpected error occurred
*/
@Test
public void testCircleConcurrentConsistency() throws Exception {
final int concurrency = 20;
final int side = 600;
final String ext = "png";

final Callable<byte[]> drawTask = () -> {
final RasterPlotter raster = new RasterPlotter(side, side, RasterPlotter.DrawMode.MODE_SUB, "FFFFFF");
for (int radius = side / 4; radius < side / 2; radius++) {
raster.setColor(RasterPlotter.GREEN);
CircleTool.circle(raster, side / 2, side / 2, radius, 100);
raster.setColor(RasterPlotter.RED);
CircleTool.circle(raster, side / 2, side / 2, radius, 0, 45);
}

final EncodedImage image = new EncodedImage(raster, ext, true);
return image.getImage().getBytes();
};

/* Generate a reference image without concurrency */
CircleTool.clearcache();
final byte[] refImageBytes = drawTask.call();

/* Write the reference image to the file system to enable manual visual check */
final Path outputPath = Paths.get(System.getProperty("java.io.tmpdir", ""), "CircleToolTest." + ext);
try {
Files.write(outputPath, refImageBytes);
System.out.println("Wrote CircleTool.circle() test image to file " + outputPath.toAbsolutePath());
} catch (final IOException e) {
/*
* Even if output file writing failed we do not make the test fail as this is
* not the purpose of the test
*/
e.printStackTrace();
}

/*
* Generate the same image multiple times in concurrent threads without initial
* cache
*/
CircleTool.clearcache();
final ExecutorService executor = Executors.newFixedThreadPool(concurrency);
final ArrayList<Future<byte[]>> futures = new ArrayList<>();
for (int i = 0; i < concurrency; i++) {
futures.add(executor.submit(drawTask));
}
try {
for (final Future<byte[]> future : futures) {
/* Check that all concurrently generated images are equal to the reference */
final byte[] imageBytes = future.get();
if (!Arrays.areEqual(refImageBytes, imageBytes)) {
/* Write the image in error to file system to enable manual visual check */
final Path errOutputPath = Paths.get(System.getProperty("java.io.tmpdir", ""),
"CircleToolTestError." + ext);
try {
Files.write(errOutputPath, imageBytes);
System.out.println(
"Wrote CircleTool.circle() error image to file " + errOutputPath.toAbsolutePath());
} catch (final IOException e) {
e.printStackTrace();
}
Assert.fail();
}
}
} finally {
executor.shutdown();
}
}

}

0 comments on commit 9daeea8

Please sign in to comment.
You can’t perform that action at this time.