aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--client/src/com/vaadin/client/data/AbstractRemoteDataSource.java98
-rw-r--r--client/src/com/vaadin/client/data/CacheStrategy.java183
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java10
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, "");