diff options
3 files changed, 272 insertions, 19 deletions
diff --git a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java index 40f5111f8a..6edb73b4df 100644 --- a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java +++ b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java @@ -19,6 +19,7 @@ package com.vaadin.client.data; import java.util.HashMap; import java.util.List; +import com.google.gwt.core.client.Duration; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.vaadin.client.Profiler; @@ -40,7 +41,13 @@ import com.vaadin.shared.ui.grid.Range; */ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> { - private boolean requestPending = false; + /** + * Records the start of the previously requested range. This is used when + * tracking request timings to distinguish between explicit responses and + * arbitrary updates pushed from the server. + */ + private int lastRequestStart = -1; + private double pendingRequestTime; private boolean coverageCheckPending = false; @@ -52,7 +59,9 @@ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> { private DataChangeHandler dataChangeHandler; - private int estimatedSize; + private Range estimatedAvailableRange = Range.between(0, 0); + + private CacheStrategy cacheStrategy = new CacheStrategy.DefaultCacheStrategy(); private final ScheduledCommand coverageChecker = new ScheduledCommand() { @Override @@ -70,7 +79,7 @@ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> { */ protected void setEstimatedSize(int estimatedSize) { // TODO update dataChangeHandler if size changes - this.estimatedSize = estimatedSize; + estimatedAvailableRange = Range.withLength(0, estimatedSize); } private void ensureCoverageCheck() { @@ -92,14 +101,16 @@ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> { } private void checkCacheCoverage() { - if (requestPending) { - // Anyone clearing requestPending should run this method again + if (lastRequestStart != -1) { + // Anyone clearing lastRequestStart should run this method again return; } Profiler.enter("AbstractRemoteDataSource.checkCacheCoverage"); - if (!requestedAvailability.intersects(cached) || cached.isEmpty()) { + Range minCacheRange = getMinCacheRange(); + + if (!minCacheRange.intersects(cached) || cached.isEmpty()) { /* * Simple case: no overlap between cached data and needed data. * Clear the cache and request new data @@ -107,22 +118,24 @@ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> { rowCache.clear(); cached = Range.between(0, 0); - handleMissingRows(requestedAvailability); + handleMissingRows(getMaxCacheRange()); } else { discardStaleCacheEntries(); // Might need more rows -> request them - Range[] availabilityPartition = requestedAvailability - .partitionWith(cached); - handleMissingRows(availabilityPartition[0]); - handleMissingRows(availabilityPartition[2]); + if (!minCacheRange.isSubsetOf(cached)) { + Range[] missingCachePartition = getMaxCacheRange() + .partitionWith(cached); + handleMissingRows(missingCachePartition[0]); + handleMissingRows(missingCachePartition[2]); + } } Profiler.leave("AbstractRemoteDataSource.checkCacheCoverage"); } private void discardStaleCacheEntries() { - Range[] cacheParition = cached.partitionWith(requestedAvailability); + Range[] cacheParition = cached.partitionWith(getMaxCacheRange()); dropFromCache(cacheParition[0]); cached = cacheParition[1]; dropFromCache(cacheParition[2]); @@ -138,7 +151,8 @@ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> { if (range.isEmpty()) { return; } - requestPending = true; + lastRequestStart = range.getStart(); + pendingRequestTime = Duration.currentTimeMillis(); requestRows(range.getStart(), range.length()); } @@ -156,7 +170,7 @@ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> { @Override public int getEstimatedSize() { - return estimatedSize; + return estimatedAvailableRange.length(); } @Override @@ -183,13 +197,21 @@ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> { * a list of rows, starting from <code>firstRowIndex</code> */ protected void setRowData(int firstRowIndex, List<T> rowData) { - requestPending = false; Profiler.enter("AbstractRemoteDataSource.setRowData"); Range received = Range.withLength(firstRowIndex, rowData.size()); - Range[] partition = received.partitionWith(requestedAvailability); + if (firstRowIndex == lastRequestStart) { + // Provide timing information if we know when we asked for this data + cacheStrategy.onDataArrive(Duration.currentTimeMillis() + - pendingRequestTime, received.length()); + } + lastRequestStart = -1; + + Range maxCacheRange = getMaxCacheRange(); + + Range[] partition = received.partitionWith(maxCacheRange); Range newUsefulData = partition[1]; if (!newUsefulData.isEmpty()) { @@ -268,7 +290,7 @@ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> { .length()); cached = remainsBefore.combineWith(transposedRemainsAfter); } - estimatedSize -= count; + setEstimatedSize(getEstimatedSize() - count); dataChangeHandler.dataRemoved(firstRowIndex, count); checkCacheCoverage(); @@ -314,7 +336,7 @@ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> { } } - estimatedSize += count; + setEstimatedSize(getEstimatedSize() + count); dataChangeHandler.dataAdded(firstRowIndex, count); checkCacheCoverage(); @@ -329,4 +351,44 @@ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> { public Range getCachedRange() { return cached; } + + /** + * Sets the cache strategy that is used to determine how much data is + * fetched and cached. + * <p> + * The new strategy is immediately used to evaluate whether currently cached + * rows should be discarded or new rows should be fetched. + * + * @param cacheStrategy + * a cache strategy implementation, not <code>null</code> + */ + public void setCacheStrategy(CacheStrategy cacheStrategy) { + if (cacheStrategy == null) { + throw new IllegalArgumentException(); + } + + if (this.cacheStrategy != cacheStrategy) { + this.cacheStrategy = cacheStrategy; + + checkCacheCoverage(); + } + } + + private Range getMinCacheRange() { + Range minCacheRange = cacheStrategy.getMinCacheRange( + requestedAvailability, cached, estimatedAvailableRange); + + assert minCacheRange.isSubsetOf(estimatedAvailableRange); + + return minCacheRange; + } + + private Range getMaxCacheRange() { + Range maxCacheRange = cacheStrategy.getMaxCacheRange( + requestedAvailability, cached, estimatedAvailableRange); + + assert maxCacheRange.isSubsetOf(estimatedAvailableRange); + + return maxCacheRange; + } } diff --git a/client/src/com/vaadin/client/data/CacheStrategy.java b/client/src/com/vaadin/client/data/CacheStrategy.java new file mode 100644 index 0000000000..79ce537314 --- /dev/null +++ b/client/src/com/vaadin/client/data/CacheStrategy.java @@ -0,0 +1,183 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client.data; + +import com.vaadin.shared.ui.grid.Range; + +/** + * Determines what data an {@link AbstractRemoteDataSource} should fetch and + * keep cached. + * + * @since 7.4 + * @author Vaadin Ltd + */ +public interface CacheStrategy { + /** + * A helper class for creating a simple symmetric cache strategy that uses + * the same logic for both rows before and after the currently cached range. + * <p> + * This simple approach rules out more advanced heuristics that would take + * the current scrolling direction or past scrolling behavior into account. + */ + public static abstract class AbstractBasicSymmetricalCacheStrategy + implements CacheStrategy { + + @Override + public void onDataArrive(double roundTripTime, int rowCount) { + // NOP + } + + @Override + public Range getMinCacheRange(Range displayedRange, Range cachedRange, + Range estimatedAvailableRange) { + int cacheSize = getMinimumCacheSize(displayedRange.length()); + + return displayedRange.expand(cacheSize, cacheSize).restrictTo( + estimatedAvailableRange); + } + + @Override + public Range getMaxCacheRange(Range displayedRange, Range cachedRange, + Range estimatedAvailableRange) { + int cacheSize = getMaximumCacheSize(displayedRange.length()); + + return displayedRange.expand(cacheSize, cacheSize).restrictTo( + estimatedAvailableRange); + } + + /** + * Gets the maximum number of extra items to cache in one direction. + * + * @param pageSize + * the current number of items used at once + * @return maximum of items to cache + */ + public abstract int getMaximumCacheSize(int pageSize); + + /** + * Gets the the minimum number of extra items to cache in one direction. + * + * @param pageSize + * the current number of items used at once + * @return minimum number of items to cache + */ + public abstract int getMinimumCacheSize(int pageSize); + } + + /** + * The default cache strategy used by {@link AbstractRemoteDataSource}, + * using multiples of the page size for determining the minimum and maximum + * number of items to keep in the cache. By default, at least three times + * the page size both before and after the currently used range are kept in + * the cache and items are discarded if there's yet another page size worth + * of items cached in either direction. + */ + public static class DefaultCacheStrategy extends + AbstractBasicSymmetricalCacheStrategy { + private final int minimumRatio; + private final int maximumRatio; + + /** + * Creates a DefaultCacheStrategy keeping between 3 and 4 pages worth of + * data cached both before and after the active range. + */ + public DefaultCacheStrategy() { + this(3, 4); + } + + /** + * Creates a DefaultCacheStrategy with custom ratios for how much data + * to cache. The ratios denote how many multiples of the currently used + * page size are kept in the cache in each direction. + * + * @param minimumRatio + * the minimum number of pages to keep in the cache in each + * direction + * @param maximumRatio + * the maximum number of pages to keep in the cache in each + * direction + */ + public DefaultCacheStrategy(int minimumRatio, int maximumRatio) { + this.minimumRatio = minimumRatio; + this.maximumRatio = maximumRatio; + } + + @Override + public int getMinimumCacheSize(int pageSize) { + return pageSize * minimumRatio; + } + + @Override + public int getMaximumCacheSize(int pageSize) { + return pageSize * maximumRatio; + } + } + + /** + * Called whenever data requested by the data source has arrived. This + * information can e.g. be used for measuring how long it takes to fetch + * different number of rows from the server. + * <p> + * A cache strategy implementation cannot use this information to keep track + * of which items are in the cache since the data source might discard items + * without notifying the cache strategy. + * + * @param roundTripTime + * the total number of milliseconds elapsed from requesting the + * data until the response was passed to the data source + * @param rowCount + * the number of received rows + */ + public void onDataArrive(double roundTripTime, int rowCount); + + /** + * Gets the minimum row range that should be cached. The data source will + * fetch new data if the currently cached range does not fill the entire + * minimum cache range. + * + * @param displayedRange + * the range of currently displayed rows + * @param cachedRange + * the range of currently cached rows + * @param estimatedAvailableRange + * the estimated range of rows available for the data source + * + * @return the minimum range of rows that should be cached, should at least + * include the displayed range and should not exceed the total + * estimated available range + */ + public Range getMinCacheRange(Range displayedRange, Range cachedRange, + Range estimatedAvailableRange); + + /** + * Gets the maximum row range that should be cached. The data source will + * discard cached rows that are outside the maximum range. + * + * @param displayedRange + * the range of currently displayed rows + * @param cachedRange + * the range of currently cached rows + * @param estimatedAvailableRange + * the estimated range of rows available for the data source + * + * @return the maximum range of rows that should be cached, should at least + * include the displayed range and should not exceed the total + * estimated available range + */ + public Range getMaxCacheRange(Range displayedRange, Range cachedRange, + Range estimatedAvailableRange); +} diff --git a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java index 9fb962c495..91bd6b032d 100644 --- a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java +++ b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java @@ -18,6 +18,7 @@ package com.vaadin.tests.components.grid; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import com.vaadin.data.Item; import com.vaadin.data.util.IndexedContainer; @@ -48,7 +49,14 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> { protected Grid constructComponent() { // Build data source - ds = new IndexedContainer(); + ds = new IndexedContainer() { + @Override + public List<Object> getItemIds(int startIndex, int numberOfIds) { + log("Requested items " + startIndex + " - " + + (startIndex + numberOfIds)); + return super.getItemIds(startIndex, numberOfIds); + } + }; for (int col = 0; col < COLUMNS; col++) { ds.addContainerProperty(getColumnProperty(col), String.class, ""); |