| Index: third_party/WebKit/Source/core/fetch/ImageResource.cpp
|
| diff --git a/third_party/WebKit/Source/core/fetch/ImageResource.cpp b/third_party/WebKit/Source/core/fetch/ImageResource.cpp
|
| index be3e5de6e7041ee64971b7cbd566affdb0d64bc1..c697eb3efb1384d4f8e67ff6db09dbb4904def71 100644
|
| --- a/third_party/WebKit/Source/core/fetch/ImageResource.cpp
|
| +++ b/third_party/WebKit/Source/core/fetch/ImageResource.cpp
|
| @@ -29,10 +29,12 @@
|
| #include "core/fetch/ResourceFetcher.h"
|
| #include "core/fetch/ResourceLoader.h"
|
| #include "core/fetch/ResourceLoadingLog.h"
|
| +#include "core/fetch/SubstituteData.h"
|
| #include "core/svg/graphics/SVGImage.h"
|
| #include "platform/RuntimeEnabledFeatures.h"
|
| #include "platform/SharedBuffer.h"
|
| #include "platform/TraceEvent.h"
|
| +#include "platform/geometry/IntSize.h"
|
| #include "platform/graphics/BitmapImage.h"
|
| #include "public/platform/Platform.h"
|
| #include "public/platform/WebCachePolicy.h"
|
| @@ -40,12 +42,286 @@
|
| #include "wtf/HashCountedSet.h"
|
| #include "wtf/StdLibExtras.h"
|
| #include "wtf/Vector.h"
|
| +#include "wtf/text/StringToNumber.h"
|
| +#include <cstdio>
|
| #include <memory>
|
| #include <v8.h>
|
|
|
| namespace blink {
|
|
|
| -ImageResource* ImageResource::fetch(FetchRequest& request, ResourceFetcher* fetcher)
|
| +// Helper class that manages the state involved with attempting to load a
|
| +// placeholder for an image.
|
| +class ImageResource::PlaceholderLoaderJob : public GarbageCollectedFinalized<PlaceholderLoaderJob> {
|
| +public:
|
| + explicit PlaceholderLoaderJob(ResourceFetcher*);
|
| + ~PlaceholderLoaderJob();
|
| +
|
| + DECLARE_TRACE();
|
| +
|
| + void onImageFetchComplete(ImageResource*);
|
| +
|
| +private:
|
| + enum class State {
|
| + Range = 0,
|
| + CacheOnly,
|
| + Placeholder,
|
| + BypassCacheFull
|
| + };
|
| +
|
| + void finish(ImageResource* image)
|
| + {
|
| + DCHECK_EQ(image->m_placeholderLoaderJob.get(), this);
|
| + image->m_placeholderLoaderJob = nullptr;
|
| + }
|
| +
|
| + void onRangeFetchComplete(ImageResource*);
|
| + void doCacheOnlyFetch(ImageResource*);
|
| + void onCacheOnlyFetchComplete(ImageResource*);
|
| + void loadPlaceholderOrBypassCache(ImageResource*);
|
| + void doPlaceholderFetch(ImageResource*, const IntSize&);
|
| + void doBypassCacheFullFetch(ImageResource*);
|
| +
|
| + State m_state;
|
| + Member<ResourceFetcher> m_fetcher;
|
| + IntSize m_dimensionsFromRange;
|
| +};
|
| +
|
| +ImageResource::PlaceholderLoaderJob::PlaceholderLoaderJob(ResourceFetcher* fetcher)
|
| + : m_state(State::Range)
|
| + , m_fetcher(fetcher)
|
| +{
|
| + DCHECK(fetcher);
|
| +}
|
| +
|
| +ImageResource::PlaceholderLoaderJob::~PlaceholderLoaderJob() {}
|
| +
|
| +DEFINE_TRACE(ImageResource::PlaceholderLoaderJob)
|
| +{
|
| + visitor->trace(m_fetcher);
|
| +}
|
| +
|
| +void ImageResource::PlaceholderLoaderJob::onImageFetchComplete(ImageResource* image)
|
| +{
|
| + DCHECK(image);
|
| +
|
| + switch (m_state) {
|
| + case State::Range:
|
| + onRangeFetchComplete(image);
|
| + break;
|
| + case State::CacheOnly:
|
| + onCacheOnlyFetchComplete(image);
|
| + break;
|
| + default:
|
| + finish(image);
|
| + break;
|
| + }
|
| +}
|
| +
|
| +// Parses the values from a Content-Range response header of the form
|
| +// "bytes %u-%u/%u". Returns true iff parsing is successful.
|
| +static bool parseContentRangeHeader(const String& contentRange, uint64_t* rangeFirst, uint64_t* rangeLast, uint64_t* totalSize)
|
| +{
|
| + // Example Content-Range header: "bytes 0-2047/16366".
|
| + if (!contentRange.startsWith("bytes "))
|
| + return false;
|
| + size_t dashPos = contentRange.find('-', arraysize("bytes ") - 1);
|
| + if (dashPos == kNotFound)
|
| + return false;
|
| + size_t slashPos = contentRange.find('/', dashPos + 1);
|
| + if (slashPos == kNotFound)
|
| + return false;
|
| +
|
| + const size_t rangeFirstPos = arraysize("bytes ") - 1, rangeLastPos = dashPos + 1, totalSizePos = slashPos + 1;
|
| + bool rangeFirstOk = false, rangeLastOk = false, totalSizeOk = false;
|
| + if (contentRange.is8Bit()) {
|
| + *rangeFirst = charactersToUInt64Strict(contentRange.characters8() + rangeFirstPos, dashPos - rangeFirstPos, &rangeFirstOk);
|
| + *rangeLast = charactersToUInt64Strict(contentRange.characters8() + rangeLastPos, slashPos - rangeLastPos, &rangeLastOk);
|
| + *totalSize = charactersToUInt64Strict(contentRange.characters8() + totalSizePos, contentRange.length() - totalSizePos, &totalSizeOk);
|
| + } else {
|
| + *rangeFirst = charactersToUInt64Strict(contentRange.characters16() + rangeFirstPos, dashPos - rangeFirstPos, &rangeFirstOk);
|
| + *rangeLast = charactersToUInt64Strict(contentRange.characters16() + rangeLastPos, slashPos - rangeLastPos, &rangeLastOk);
|
| + *totalSize = charactersToUInt64Strict(contentRange.characters16() + totalSizePos, contentRange.length() - totalSizePos, &totalSizeOk);
|
| + }
|
| + return rangeFirstOk && rangeLastOk && totalSizeOk;
|
| +}
|
| +
|
| +// Returns true if |response| likely represents the entire resource instead of
|
| +// just a partial range.
|
| +static bool hasEntireResource(const ResourceResponse& response)
|
| +{
|
| + if (response.httpStatusCode() != 206)
|
| + return true;
|
| +
|
| + const String& contentRangeHeader = response.httpHeaderField("content-range");
|
| + if (contentRangeHeader.isNull()) {
|
| + // If there's no Content-Range header, then assume that the response is
|
| + // the entire resource.
|
| + return true;
|
| + }
|
| +
|
| + uint64_t rangeFirst, rangeLast, totalSize;
|
| + if (!parseContentRangeHeader(contentRangeHeader, &rangeFirst, &rangeLast, &totalSize))
|
| + return false;
|
| +
|
| + // Check if the returned range is the entire resource.
|
| + return rangeFirst == 0 && rangeLast + 1 == totalSize;
|
| +}
|
| +
|
| +void ImageResource::PlaceholderLoaderJob::onRangeFetchComplete(ImageResource* image)
|
| +{
|
| + DCHECK_EQ(State::Range, m_state);
|
| +
|
| + // If the response consists of the entire image, e.g. if the image was
|
| + // smaller than the requested range or if the server responded with a 200
|
| + // response for the full image, then just use the full response to show the
|
| + // full image.
|
| + // Also avoid re-fetching requests that led to 204 responses, since those
|
| + // typically indicate that it was for a tracking request, and there's likely
|
| + // no point in attempting to re-fetch the image.
|
| + if ((image->response().httpStatusCode() == 204 || !image->willPaintBrokenImage()) && hasEntireResource(image->response())) {
|
| + image->setIsPlaceholder(false);
|
| + finish(image);
|
| + return;
|
| + }
|
| +
|
| + if (image->hasImage())
|
| + m_dimensionsFromRange = image->getImage()->size();
|
| +
|
| + if (image->response().wasCached() && image->response().httpStatusCode() == 206 && image->resourceRequest().getCachePolicy() != WebCachePolicy::BypassingCache) {
|
| + // If the range response came from the cache, then that means that it
|
| + // was fresh in the cache and there's a chance that the entire image
|
| + // response is fresh and in the cache, so attempt to fetch the entire
|
| + // image from the cache.
|
| + doCacheOnlyFetch(image);
|
| + return;
|
| + }
|
| + loadPlaceholderOrBypassCache(image);
|
| +}
|
| +
|
| +void ImageResource::PlaceholderLoaderJob::doCacheOnlyFetch(ImageResource* image)
|
| +{
|
| + m_state = State::CacheOnly;
|
| +
|
| + ResourceRequest request = image->getOriginalResourceRequest();
|
| + request.setCachePolicy(WebCachePolicy::ReturnCacheDataDontLoad);
|
| + image->reload(m_fetcher.get(), request, SubstituteData());
|
| +}
|
| +
|
| +void ImageResource::PlaceholderLoaderJob::onCacheOnlyFetchComplete(ImageResource* image)
|
| +{
|
| + DCHECK_EQ(State::CacheOnly, m_state);
|
| +
|
| + if (!image->willPaintBrokenImage() && hasEntireResource(image->response())) {
|
| + image->setIsPlaceholder(false);
|
| + finish(image);
|
| + return;
|
| + }
|
| + loadPlaceholderOrBypassCache(image);
|
| +}
|
| +
|
| +void ImageResource::PlaceholderLoaderJob::loadPlaceholderOrBypassCache(ImageResource* image)
|
| +{
|
| + if (!m_dimensionsFromRange.isEmpty()) {
|
| + doPlaceholderFetch(image, m_dimensionsFromRange);
|
| + return;
|
| + }
|
| + if (image->getOriginalResourceRequest().getCachePolicy() != WebCachePolicy::ReturnCacheDataDontLoad) {
|
| + doBypassCacheFullFetch(image);
|
| + return;
|
| + }
|
| + // Return the broken image since there's nothing else that can be done here.
|
| + image->setIsPlaceholder(false);
|
| + finish(image);
|
| +}
|
| +
|
| +static Vector<char> generatePlaceholderBox(const IntSize& dimensions)
|
| +{
|
| + DCHECK(!dimensions.isEmpty());
|
| +
|
| + static const char kFormat[] = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n"
|
| + "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"%d\" height=\"%d\">\n"
|
| + "<rect width=\"100%%\" height=\"100%%\" style=\"fill:rgba(127,127,127,0.4);stroke-width:%d;stroke:black\" />\n"
|
| + "</svg>";
|
| + // Don't add the black border to tiny images, to avoid coloring the entire image black.
|
| + int strokeWidth = dimensions.width() >= 10 && dimensions.height() >= 10 ? 2 : 0;
|
| +
|
| + int expectedResult = std::snprintf(nullptr, 0, kFormat, dimensions.width(), dimensions.height(), strokeWidth);
|
| + DCHECK_LE(0, expectedResult);
|
| + Vector<char> out(safeCast<size_t>(expectedResult) + 1 /* for trailing '\0' */);
|
| + int result = std::snprintf(out.data(), out.size(), kFormat, dimensions.width(), dimensions.height(), strokeWidth);
|
| + DCHECK_EQ(expectedResult, result);
|
| + DCHECK_EQ('\0', out.last());
|
| + out.removeLast(); // Cut off the trailing '\0'.
|
| + return out;
|
| +}
|
| +
|
| +void ImageResource::PlaceholderLoaderJob::doPlaceholderFetch(ImageResource* image, const IntSize& dimensions)
|
| +{
|
| + DCHECK(!dimensions.isEmpty());
|
| + m_state = State::Placeholder;
|
| +
|
| + Vector<char> svgBox = generatePlaceholderBox(dimensions);
|
| + SubstituteData substituteData(SharedBuffer::adoptVector(svgBox), "image/svg+xml", "utf-8", KURL(), LoadNormally);
|
| + DCHECK(image->isPlaceholder());
|
| + image->reload(m_fetcher.get(), image->getOriginalResourceRequest(), substituteData);
|
| +}
|
| +
|
| +void ImageResource::PlaceholderLoaderJob::doBypassCacheFullFetch(ImageResource* image)
|
| +{
|
| + m_state = State::BypassCacheFull;
|
| +
|
| + // This reload is done bypassing the cache for several reasons:
|
| + //
|
| + // (1) CacheOnly fetches fail for sparse entries in the disk cache even if
|
| + // all the ranges of the full resource are available separately, so
|
| + // bypassing the cache here ensures that the full response is cached
|
| + // non-sparsely, so future fetches of the same image will be properly
|
| + // satisfied by the full cached response, reducing the cost of future
|
| + // fetches of this same image.
|
| + //
|
| + // (2) It's possible that the origin server doesn't understand ranges,
|
| + // and it returned a broken range, so bypassing the cache for this reload
|
| + // will hopefully return the full unbroken image instead of attempting to
|
| + // fetch the latter range of the image and combine that with the existing
|
| + // potentially broken range.
|
| + //
|
| + // TODO(sclittle): Investigate if the issue with (1) can be fixed, i.e. make
|
| + // the disk cache support sparse cache entries together with
|
| + // WebCachePolicy::ReturnCacheDataDontLoad.
|
| + ResourceRequest request = image->getOriginalResourceRequest();
|
| + request.setCachePolicy(WebCachePolicy::BypassingCache);
|
| + image->setIsPlaceholder(false);
|
| + image->reload(m_fetcher.get(), request, SubstituteData());
|
| +}
|
| +
|
| +static void modifyRequestToFetchPlaceholderRange(ResourceRequest* request)
|
| +{
|
| + DCHECK_EQ(nullAtom, request->httpHeaderField("range"));
|
| +
|
| + // Fetch the first few bytes of the image. This number is tuned to both (a)
|
| + // likely capture the entire image for small images and (b) likely contain the
|
| + // dimensions for larger images.
|
| + // TODO(sclittle): Calculate the optimal value for this number.
|
| + request->setHTTPHeaderField("range", "bytes=0-2047");
|
| +}
|
| +
|
| +static void unmodifyRequestToFetchPlaceholderRange(ResourceRequest* request)
|
| +{
|
| + DCHECK_EQ("bytes=0-2047", request->httpHeaderField("range"));
|
| + request->clearHTTPHeaderField("range");
|
| +}
|
| +
|
| +Resource* ImageResource::ImageResourceFactory::create(const ResourceRequest& request, const ResourceLoaderOptions& options, const String&) const
|
| +{
|
| + if (m_isPlaceholder) {
|
| + ResourceRequest originalRequest = request;
|
| + unmodifyRequestToFetchPlaceholderRange(&originalRequest);
|
| + return new ImageResource(request, options, originalRequest, m_isPlaceholder);
|
| + }
|
| + return new ImageResource(request, options, request, m_isPlaceholder);
|
| +}
|
| +
|
| +ImageResource* ImageResource::fetch(FetchRequest& request, ResourceFetcher* fetcher, PlaceholderRequestType placeholderRequestType)
|
| {
|
| if (request.resourceRequest().requestContext() == WebURLRequest::RequestContextUnspecified)
|
| request.mutableResourceRequest().setRequestContext(WebURLRequest::RequestContextImage);
|
| @@ -56,14 +332,42 @@ ImageResource* ImageResource::fetch(FetchRequest& request, ResourceFetcher* fetc
|
| return nullptr;
|
| }
|
|
|
| - return toImageResource(fetcher->requestResource(request, ImageResourceFactory()));
|
| + bool attemptPlaceholder = placeholderRequestType == PlaceholderRequestType::AllowPlaceholder && request.url().protocolIsInHTTPFamily()
|
| + // Only issue range requests for GET requests, since that way it should
|
| + // be safe to re-issue the request without side effects.
|
| + && request.resourceRequest().httpMethod() == "GET"
|
| + // If the request already has a range request header, then don't
|
| + // overwrite that range header, and just attempt to fetch the image
|
| + // normally without generating a placeholder.
|
| + && request.resourceRequest().httpHeaderField("range").isNull();
|
| +
|
| + if (attemptPlaceholder)
|
| + modifyRequestToFetchPlaceholderRange(&request.mutableResourceRequest());
|
| + ImageResource* image = toImageResource(fetcher->requestResource(request, ImageResourceFactory(attemptPlaceholder)));
|
| +
|
| + // Check if the image is loading a placeholder already, otherwise add a PlaceholderLoaderJob.
|
| + if (attemptPlaceholder && (image->isLoading() || image->stillNeedsLoad()) && image->isPlaceholder() && !image->m_placeholderLoaderJob)
|
| + image->m_placeholderLoaderJob = new PlaceholderLoaderJob(fetcher);
|
| +
|
| + // If this image shouldn't be a placeholder but it is, then reload it as not a placeholder.
|
| + if (!attemptPlaceholder && image->isPlaceholder()) {
|
| + image->m_placeholderLoaderJob = nullptr;
|
| + // Note that the cache is not bypassed for this reload - it should be fine
|
| + // to use a cached copy if it exists.
|
| + image->setIsPlaceholder(false);
|
| + image->reload(fetcher, request.resourceRequest(), SubstituteData());
|
| + }
|
| + return image;
|
| }
|
|
|
| -ImageResource::ImageResource(const ResourceRequest& resourceRequest, const ResourceLoaderOptions& options)
|
| +ImageResource::ImageResource(const ResourceRequest& resourceRequest, const ResourceLoaderOptions& options, const ResourceRequest& originalRequest, bool isPlaceholder)
|
| : Resource(resourceRequest, Image, options)
|
| , m_devicePixelRatioHeaderValue(1.0)
|
| , m_image(nullptr)
|
| , m_hasDevicePixelRatioHeaderValue(false)
|
| + , m_originalRequest(originalRequest)
|
| + , m_isPlaceholder(isPlaceholder)
|
| + , m_isSchedulingReload(false)
|
| {
|
| RESOURCE_LOADING_DVLOG(1) << "new ImageResource(ResourceRequest) " << this;
|
| }
|
| @@ -73,6 +377,9 @@ ImageResource::ImageResource(blink::Image* image, const ResourceLoaderOptions& o
|
| , m_devicePixelRatioHeaderValue(1.0)
|
| , m_image(image)
|
| , m_hasDevicePixelRatioHeaderValue(false)
|
| + , m_originalRequest(resourceRequest())
|
| + , m_isPlaceholder(false)
|
| + , m_isSchedulingReload(false)
|
| {
|
| RESOURCE_LOADING_DVLOG(1) << "new ImageResource(Image) " << this;
|
| setStatus(Cached);
|
| @@ -87,6 +394,7 @@ ImageResource::~ImageResource()
|
| DEFINE_TRACE(ImageResource)
|
| {
|
| visitor->trace(m_multipartParser);
|
| + visitor->trace(m_placeholderLoaderJob);
|
| Resource::trace(visitor);
|
| ImageObserver::trace(visitor);
|
| MultipartImageResourceParser::Client::trace(visitor);
|
| @@ -94,6 +402,15 @@ DEFINE_TRACE(ImageResource)
|
|
|
| void ImageResource::checkNotify()
|
| {
|
| + if (m_isSchedulingReload)
|
| + return;
|
| +
|
| + if (m_placeholderLoaderJob)
|
| + m_placeholderLoaderJob->onImageFetchComplete(this);
|
| +
|
| + if (m_isSchedulingReload || m_placeholderLoaderJob)
|
| + return;
|
| +
|
| notifyObserversInternal(MarkFinishedOption::ShouldMarkFinished);
|
| Resource::checkNotify();
|
| }
|
| @@ -114,6 +431,9 @@ void ImageResource::notifyObserversInternal(MarkFinishedOption markFinishedOptio
|
|
|
| void ImageResource::markObserverFinished(ImageResourceObserver* observer)
|
| {
|
| + if (m_isSchedulingReload || m_placeholderLoaderJob)
|
| + return;
|
| +
|
| if (m_observers.contains(observer)) {
|
| m_finishedObservers.add(observer);
|
| m_observers.remove(observer);
|
| @@ -345,11 +665,19 @@ void ImageResource::clear()
|
|
|
| inline void ImageResource::createImage()
|
| {
|
| + bool wantSVGImage = response().mimeType() == "image/svg+xml";
|
| + // It's possible for the existing |m_image| to be a bitmap image while the
|
| + // response is now an SVG image, or vice versa, when an image is reloaded.
|
| + // In these cases, clear and delete the existing image so that a new image
|
| + // of the correct type can be created and used instead.
|
| + if (m_image && m_image->isSVGImage() != wantSVGImage)
|
| + clearImage();
|
| +
|
| // Create the image if it doesn't yet exist.
|
| if (m_image)
|
| return;
|
|
|
| - if (response().mimeType() == "image/svg+xml") {
|
| + if (wantSVGImage) {
|
| m_image = SVGImage::create(this);
|
| } else {
|
| m_image = BitmapImage::create(this);
|
| @@ -399,7 +727,11 @@ void ImageResource::updateImage(bool allDataReceived)
|
| setStatus(DecodeError);
|
| if (!allDataReceived && loader())
|
| loader()->didFinishLoading(nullptr, monotonicallyIncreasingTime(), size);
|
| - memoryCache()->remove(this);
|
| + // It's possible that the error could have been resolved by the
|
| + // didFinishLoading() call, so check again that the image is still in an
|
| + // error state before proceeding.
|
| + if (errorOccurred())
|
| + memoryCache()->remove(this);
|
| }
|
|
|
| // It would be nice to only redraw the decoded band of the image, but with the current design
|
| @@ -534,18 +866,62 @@ void ImageResource::updateImageAnimationPolicy()
|
|
|
| void ImageResource::reloadIfLoFi(ResourceFetcher* fetcher)
|
| {
|
| - if (resourceRequest().loFiState() != WebURLRequest::LoFiOn)
|
| + if (!m_isPlaceholder && (resourceRequest().loFiState() != WebURLRequest::LoFiOn || (isLoaded() && !response().httpHeaderField("chrome-proxy").contains("q=low"))))
|
| return;
|
| - if (isLoaded() && !response().httpHeaderField("chrome-proxy").contains("q=low"))
|
| - return;
|
| - setCachePolicyBypassingCache();
|
| - setLoFiStateOff();
|
| +
|
| + ResourceRequest reloadRequest = m_originalRequest;
|
| + reloadRequest.setCachePolicy(WebCachePolicy::BypassingCache);
|
| + reloadRequest.setLoFiState(WebURLRequest::LoFiOff);
|
| + m_isPlaceholder = false;
|
| + m_placeholderLoaderJob = nullptr;
|
| +
|
| + reload(fetcher, reloadRequest, SubstituteData());
|
| +}
|
| +
|
| +static void loadStaticResponse(ImageResource* resource, const SubstituteData& substituteData)
|
| +{
|
| + DCHECK(resource);
|
| + DCHECK(substituteData.isValid());
|
| +
|
| + // This code was adapted from ResourceFetcher::resourceForStaticData().
|
| + ResourceResponse response(resource->resourceRequest().url(), substituteData.mimeType(), substituteData.content()->size(), substituteData.textEncoding(), String());
|
| + response.setHTTPStatusCode(200);
|
| + response.setHTTPStatusText("OK");
|
| +
|
| + resource->setNeedsSynchronousCacheHit(substituteData.forceSynchronousLoad());
|
| + resource->responseReceived(response, nullptr);
|
| + resource->setDataBufferingPolicy(BufferData);
|
| + resource->setResourceBuffer(PassRefPtr<SharedBuffer>(substituteData.content()));
|
| + resource->finish();
|
| +}
|
| +
|
| +void ImageResource::reload(ResourceFetcher* fetcher, const ResourceRequest& request, const SubstituteData& substituteData)
|
| +{
|
| + DCHECK(!m_isSchedulingReload);
|
| + m_isSchedulingReload = true;
|
| +
|
| if (isLoading())
|
| loader()->cancel();
|
| + clearError();
|
| clear();
|
| - notifyObservers();
|
|
|
| + if (!substituteData.isValid()) {
|
| + // If the reload will be done asynchronously, notify observers to update
|
| + // the image's appearance while waiting for the async reload to
|
| + // complete.
|
| + notifyObservers();
|
| + }
|
| setStatus(NotStarted);
|
| +
|
| + DCHECK(m_isSchedulingReload);
|
| + m_isSchedulingReload = false;
|
| +
|
| + setResourceRequest(request);
|
| +
|
| + if (substituteData.isValid()) {
|
| + loadStaticResponse(this, substituteData);
|
| + return;
|
| + }
|
| fetcher->startLoad(this);
|
| }
|
|
|
|
|