diff --git a/source/net/yacy/visualization/CircleTool.java b/source/net/yacy/visualization/CircleTool.java index 87bbfbef5..def9c6d51 100644 --- a/source/net/yacy/visualization/CircleTool.java +++ b/source/net/yacy/visualization/CircleTool.java @@ -24,17 +24,28 @@ import java.util.ArrayList; 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 class CircleTool { } 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 class CircleTool { 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); diff --git a/test/java/net/yacy/visualization/CircleToolTest.java b/test/java/net/yacy/visualization/CircleToolTest.java new file mode 100644 index 000000000..14fda8c23 --- /dev/null +++ b/test/java/net/yacy/visualization/CircleToolTest.java @@ -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(); + } + } + +}