This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 426b57744b5e2a026e3970f1ce5a964b29ea08b5 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Thu Sep 15 17:42:18 2022 +0200 Improve caching in `ConcatenatedGridCoverage` by using soft references instead than strong references, and by making possible to share the same cache between derived coverages (e.g. converted coverages). --- .../org/apache/sis/internal/util/Numerics.java | 10 +- .../org/apache/sis/internal/util/NumericsTest.java | 5 +- .../aggregate/ConcatenatedGridCoverage.java | 182 ++++++++++++++------- .../aggregate/ConcatenatedGridResource.java | 33 ++-- 4 files changed, 151 insertions(+), 79 deletions(-) diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java index fce0e2b2fc..673ec3ec4a 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java +++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java @@ -149,13 +149,11 @@ public final class Numerics extends Static { /** * Maximal integer value which is convertible to {@code float} type without lost of precision digits. - * - * @since 1.1 */ public static final int MAX_INTEGER_CONVERTIBLE_TO_FLOAT = 1 << (SIGNIFICAND_SIZE_OF_FLOAT + 1); /** - * Right shift to apply for a result equivalent to a division by {@Long#SIZE} (ignoring negative numbers). + * Right shift to apply for a result equivalent to a division by {@value Long#SIZE} (ignoring negative numbers). * The value is {@value} so that the following relationship hold: 2⁶ = {@value Long#SIZE}. * * <h4>Usage</h4> @@ -167,6 +165,12 @@ public final class Numerics extends Static { */ public static final int LONG_SHIFT = 6; + /** + * Right shift to apply for a result equivalent to a division by {@value Integer#SIZE} (ignoring negative numbers). + * This is for the same purpose as {@link #LONG_SHIFT} but applied to 32 bits integer. + */ + public static final int INT_SHIFT = 5; + /** * Do not allow instantiation of this class. */ diff --git a/core/sis-utility/src/test/java/org/apache/sis/internal/util/NumericsTest.java b/core/sis-utility/src/test/java/org/apache/sis/internal/util/NumericsTest.java index de5c9a953a..db3dedbec3 100644 --- a/core/sis-utility/src/test/java/org/apache/sis/internal/util/NumericsTest.java +++ b/core/sis-utility/src/test/java/org/apache/sis/internal/util/NumericsTest.java @@ -34,17 +34,18 @@ import static org.junit.Assert.*; * Tests the {@link Numerics} class. * * @author Martin Desruisseaux (Geomatys) - * @version 1.2 + * @version 1.3 * @since 0.3 * @module */ @SuppressWarnings("UnnecessaryBoxing") public final strictfp class NumericsTest extends TestCase { /** - * Verifies the value of {@link Numerics#LONG_SHIFT}. + * Verifies the value of {@link Numerics#LONG_SHIFT} and {@link Numerics#INT_SHIFT}. */ @Test public void verifyMaxDimension() { + assertEquals(Integer.SIZE, 1 << Numerics.INT_SHIFT); assertEquals(Long.SIZE, 1 << Numerics.LONG_SHIFT); for (int i=350; i<400; i += 17) { assertEquals(i / Long.SIZE, i >> Numerics.LONG_SHIFT); diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/ConcatenatedGridCoverage.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/ConcatenatedGridCoverage.java index 163fd34d41..31f799efcd 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/ConcatenatedGridCoverage.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/ConcatenatedGridCoverage.java @@ -28,6 +28,8 @@ import org.apache.sis.coverage.grid.DisjointExtentException; import org.apache.sis.storage.GridCoverageResource; import org.apache.sis.storage.DataStoreException; import org.apache.sis.internal.storage.Resources; +import org.apache.sis.internal.util.Numerics; +import org.apache.sis.util.collection.Cache; // Branch-dependent imports import org.opengis.coverage.CannotEvaluateException; @@ -45,47 +47,108 @@ import org.opengis.referencing.operation.TransformException; */ final class ConcatenatedGridCoverage extends GridCoverage { /** - * The slices of this coverage, in the same order than {@link #coordinatesOfSlices}. - * Each slice is not necessarily 1 cell tick; larger slices are accepted. - * The length of this array shall be at least 2. - * - * <p>Some elements in the array may be {@code null} if the coverage are lazily loaded.</p> + * The object for identifying indices in the {@link #slices} array. */ - private final GridCoverage[] slices; + private final GridSliceLocator locator; /** - * The resource from which load the coverages in the {@link #slices} array, or {@code null} if none. - * This is non-null only if the {@linkplain #slices} are lazily loaded. + * Index of the first slice in {@link #locator}. */ - private final GridCoverageResource[] resources; + private final int startAt; /** - * The domain to request when reading a coverage from the resource. - * This is non-null only if the {@linkplain #slices} are lazily loaded. + * The class in charge of loading and caching grid coverages. + * The same loader may be shared by many {@link ConcatenatedGridCoverage} instances. + * Loaders are immutable (except for the cache) and thread-safe. */ - private final GridGeometry request; + private static final class Loader { + /** + * Whether loading of grid coverages should be deferred to rendering time. + * This is a bit set packed as {@code long} values. A bit value of 1 means + * that the coverages at the corresponding index should be loaded from the + * {@linkplain #slices slices} at same index only when first needed. + * + * @see ConcatenatedGridResource#deferredLoading + * @see #isDeferred(int) + */ + final int[] deferred; - /** - * The sample dimensions to request when loading slices from the {@linkplain #resources}. - * This is non-null only if the {@linkplain #slices} are lazily loaded. - */ - private final int[] ranges; + /** + * The domain to request when reading a coverage from the resource. + */ + private final GridGeometry domain; + + /** + * The sample dimensions to request when loading slices from the {@linkplain #resources}. + */ + private final int[] ranges; + + /** + * Cache of {@link GridCoverage} instances. Keys are index in the {@link #slices} array. + */ + private final Cache<Integer,GridCoverage> coverages; + + /** + * Creates a new loader. + * + * @param deferred whether loading of grid coverages should be deferred to rendering time. + * @param domain grid geometry to request when loading data. + * @param ranges bands to request when loading coverages. + */ + Loader(final int[] deferred, final GridGeometry domain, final int[] ranges) { + this.deferred = deferred; + this.domain = domain; + this.ranges = ranges; + coverages = new Cache<>(15, 2, true); // Keep 2 slices by strong reference (for interpolations). + } + + /** + * Returns the coverage if available in the cache, or load it immediately otherwise. + * This method shall be invoked only when {@code isDeferred(key) == true}. + * This method can be invoked from any thread. + * + * @param key index of the {@link GridCoverageResource} in the {@link #slices} array. + * @param source value of {@code slices[key]}, used only if data need to be loaded. + * @return the coverage at the given index. + * @throws NullPointerException if no {@link GridCoverageResource} are expected to exist. + * @throws DataStoreException if an error occurred while loading data from the resource. + */ + final GridCoverage getOrLoad(final Integer key, final GridCoverageResource source) throws DataStoreException { + GridCoverage coverage = coverages.peek(key); + if (coverage == null) { + final Cache.Handler<GridCoverage> handler = coverages.lock(key); + try { + coverage = handler.peek(); + if (coverage == null) { + coverage = source.read(domain, ranges); + } + } finally { + handler.putAndUnlock(coverage); + } + } + return coverage; + } + } /** - * Whether this grid coverage should be considered as converted. - * This is used only if the {@linkplain #slices} are lazily loaded. + * The object in charge of loading and caching grid coverages, or {@code null} if none. + * The same loader may be shared by many {@link ConcatenatedGridCoverage} instances. */ - private final boolean isConverted; + private final Loader loader; /** - * The object for identifying indices in the {@link #slices} array. + * The slices of this coverage, in the same order than {@link GridSliceLocator#sliceLows}. + * Array elements shall be instances of {@link GridCoverage} or {@link GridCoverageResource}. + * Each slice is not necessarily 1 cell tick; larger slices are accepted. + * The length of this array shall be at least 2. Shall be read-only. */ - private final GridSliceLocator locator; + private final Object[] slices; /** - * Index of the first slice in {@link #locator}. + * Whether this grid coverage should be considered as converted. + * This is used only if the {@linkplain #slices} are lazily loaded. */ - private final int startAt; + private final boolean isConverted; /** * Algorithm to apply when more than one grid coverage can be found at the same grid index. @@ -96,24 +159,21 @@ final class ConcatenatedGridCoverage extends GridCoverage { /** * Creates a new aggregated coverage. * - * @param source the concatenated resource which is creating this coverage. - * @param domain domain of the coverage to create. - * @param request grid geometry to request when loading data. Used only if {@code resources} is non-null. - * @param slices grid coverages for each slice. May contain {@code null} elements is lazy loading is applied. - * @param resources resources from which to load grid coverages, or {@code null} if none. - * @param startAt index of the first slice in {@link #locator}. - * @param ranges bands to request when loading coverages. Used only if {@code resources} is non-null. + * @param source the concatenated resource which is creating this coverage. + * @param domain domain of the coverage to create. + * @param slices each slice as instances of {@link GridCoverage} or {@link GridCoverageResource}. + * @param startAt index of the first slice in {@link #locator}. + * @param deferred whether loading of grid coverages should be deferred to rendering time, or {@code null} if none. + * @param request grid geometry to request when loading data. Used only if {@code slices} are lazily loaded. + * @param ranges bands to request when loading coverages. Used only if {@code slices} are lazily loaded. */ - ConcatenatedGridCoverage(final ConcatenatedGridResource source, final GridGeometry domain, final GridGeometry request, - final GridCoverage[] slices, final GridCoverageResource[] resources, final int startAt, - final int[] ranges) + ConcatenatedGridCoverage(final ConcatenatedGridResource source, final GridGeometry domain, final Object[] slices, + final int startAt, final int[] deferred, final GridGeometry request, final int[] ranges) { super(domain, source.getSampleDimensions()); + loader = (deferred != null) ? new Loader(deferred, request, ranges) : null; this.slices = slices; - this.resources = resources; this.startAt = startAt; - this.request = request; - this.ranges = ranges; this.isConverted = source.isConverted; this.locator = source.locator; this.strategy = source.strategy; @@ -123,20 +183,27 @@ final class ConcatenatedGridCoverage extends GridCoverage { * Creates a new aggregated coverage for the result of a conversion from/to packed values. * This constructor assumes that all slices use the same sample dimensions. */ - private ConcatenatedGridCoverage(final ConcatenatedGridCoverage source, final GridCoverage[] slices, - final List<SampleDimension> sampleDimensions, final boolean converted) + private ConcatenatedGridCoverage(final ConcatenatedGridCoverage source, final Object[] slices, + final List<SampleDimension> sampleDimensions, final boolean converted) { super(source.getGridGeometry(), sampleDimensions); this.slices = slices; - this.resources = source.resources; + this.loader = source.loader; this.startAt = source.startAt; - this.request = source.request; - this.ranges = source.ranges; this.locator = source.locator; this.strategy = source.strategy; this.isConverted = converted; } + /** + * Returns {@code true} if the loading the coverage at the given index is deferred. + * If {@code true}, then {@code slices[i]} shall be an instance of {@link GridCoverageResource}. + * If {@code false}, then {@code slices[i]} shall be an instance of {@link GridCoverage}. + */ + private boolean isDeferred(final int i) { + return (loader == null) || (loader.deferred[i >>> Numerics.INT_SHIFT] & (1 << i)) != 0; + } + /** * Returns a grid coverage that contains real values or sample values, * depending if {@code converted} is {@code true} or {@code false} respectively. @@ -149,13 +216,13 @@ final class ConcatenatedGridCoverage extends GridCoverage { @Override protected GridCoverage createConvertedValues(final boolean converted) { boolean changed = false; - int template = -1; // Index of a grid coverage to use as a template. - final GridCoverage[] c = new GridCoverage[slices.length]; + GridCoverage template = null; // Arbitrary instance to use as a template for sample dimensions. + final Object[] c = slices.clone(); for (int i=0; i<c.length; i++) { - final GridCoverage source = slices[i]; - if (source != null) { + if (!isDeferred(i)) { + final GridCoverage source = (GridCoverage) c[i]; // Should never fail. changed |= (c[i] = source.forConvertedValues(converted)) != source; - template = i; + template = source; } else { changed |= (converted != isConverted); } @@ -164,8 +231,8 @@ final class ConcatenatedGridCoverage extends GridCoverage { return this; } final List<SampleDimension> sampleDimensions; - if (template >= 0) { - sampleDimensions = c[template].getSampleDimensions(); + if (template !=null) { + sampleDimensions = template.getSampleDimensions(); } else { sampleDimensions = new ArrayList<>(getSampleDimensions()); sampleDimensions.replaceAll((b) -> b.forConvertedValues(converted)); @@ -218,8 +285,9 @@ final class ConcatenatedGridCoverage extends GridCoverage { try { for (int i=0; i<count; i++) { final int j = lower + i; - final GridCoverage slice = slices[j]; - geometries[i] = (slice != null) ? slice.getGridGeometry() : resources[j].getGridGeometry(); + final Object slice = slices[j]; + geometries[i] = isDeferred(j) ? ((GridCoverageResource) slice).getGridGeometry() + : ((GridCoverage) slice).getGridGeometry(); } lower += strategy.apply(new GridGeometry(getGridGeometry(), extent, null), geometries); } catch (DataStoreException | TransformException e) { @@ -230,13 +298,15 @@ final class ConcatenatedGridCoverage extends GridCoverage { * Argument have been validated and slice has been located. * If the coverage has not already been loaded, load it now. */ - GridCoverage slice = slices[lower]; - if (slice == null) try { - slice = resources[lower].read(request, ranges).forConvertedValues(isConverted); - slices[lower] = slice; + final GridCoverage coverage; + final Object slice = slices[lower]; + if (!isDeferred(lower)) { + coverage = (GridCoverage) slice; // Should never fail. + } else try { + coverage = loader.getOrLoad(lower, (GridCoverageResource) slice).forConvertedValues(isConverted); } catch (DataStoreException e) { throw new CannotEvaluateException(Resources.format(Resources.Keys.CanNotReadSlice_1, lower + startAt), e); } - return slice.render(locator.toSliceExtent(extent, lower)); + return coverage.render(locator.toSliceExtent(extent, lower)); } } diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/ConcatenatedGridResource.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/ConcatenatedGridResource.java index b2ada74626..903bc8f4a5 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/ConcatenatedGridResource.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/ConcatenatedGridResource.java @@ -80,7 +80,7 @@ final class ConcatenatedGridResource extends AbstractGridCoverageResource implem final boolean isConverted; /** - * The slices of this resource, in the same order than {@link #coordinatesOfSlices}. + * The slices of this resource, in the same order than {@link GridSliceLocator#sliceLows}. * Each slice is not necessarily 1 cell tick; larger slices are accepted. */ private final GridCoverageResource[] slices; @@ -142,7 +142,7 @@ final class ConcatenatedGridResource extends AbstractGridCoverageResource implem * @param listeners listeners of the parent resource, or {@code null} if none. * @param domain value to be returned by {@link #getGridGeometry()}. * @param ranges value to be returned by {@link #getSampleDimensions()}. - * @param slices the slices of this resource, in the same order than {@code coordinatesOfSlices}. + * @param slices the slices of this resource, in the same order than {@link GridSliceLocator#sliceLows}. */ ConcatenatedGridResource(final String name, final StoreListeners listeners, @@ -357,10 +357,10 @@ final class ConcatenatedGridResource extends AbstractGridCoverageResource implem * Create arrays with only the requested range, without keeping reference to this concatenated resource, * for allowing garbage-collection of resources outside that range. */ - final int count = upper - lower; - final GridGeometry[] geometries = new GridGeometry[count]; - final GridCoverage[] coverages = new GridCoverage[count]; - GridCoverageResource[] resources = null; // Created when first needed. + final int count = upper - lower; + final Object[] coverages = new Object[count]; + final GridGeometry[] geometries = new GridGeometry[count]; + int[] deferred = null; int bitx = lower >>> Numerics.LONG_SHIFT; long mask = 1L << lower; // No need for (lower & 63) because high bits are ignored. if (count <= 1) mask = 0; // Trick for forcing coverage loading. @@ -375,11 +375,12 @@ final class ConcatenatedGridResource extends AbstractGridCoverageResource implem coverages [i] = coverage; geometries[i] = coverage.getGridGeometry(); } else { - if (resources == null) { - resources = new GridCoverageResource[count]; - } - resources [i] = slice; + coverages [i] = slice; geometries[i] = slice.getGridGeometry(); + if (deferred == null) { + deferred = new int[Numerics.ceilDiv(count, Integer.SIZE)]; + } + deferred[i >>> Numerics.INT_SHIFT] |= (1 << i); } if ((mask <<= 1) == 0) { mask=1; @@ -387,17 +388,13 @@ final class ConcatenatedGridResource extends AbstractGridCoverageResource implem } } /* - * If it was not necessary to keep references to any resource, clear references to information - * which were needed only for loading coverages from resources. Then create create the coverage. + * Following cast should never fail because the `mask = 0` trick ensures that we loaded the coverage. + * Otherwise (if more than one slice), create a concatenation of all slices. */ if (count == 1) { - return coverages[0]; - } - if (resources == null) { - domain = null; - ranges = null; + return (GridCoverage) coverages[0]; } final GridGeometry union = locator.union(gridGeometry, Arrays.asList(geometries), GridGeometry::getExtent); - return new ConcatenatedGridCoverage(this, union, domain, coverages, resources, lower, ranges); + return new ConcatenatedGridCoverage(this, union, coverages, lower, deferred, domain, ranges); } }