Bug 170697 - CachedImage should cancel loading images for unsupported/unknown types
Summary: CachedImage should cancel loading images for unsupported/unknown types
Status: RESOLVED FIXED
Alias: None
Product: WebKit
Classification: Unclassified
Component: Images (show other bugs)
Version: WebKit Nightly Build
Hardware: Unspecified Unspecified
: P2 Normal
Assignee: Said Abou-Hallawa
URL:
Keywords: InRadar
Depends on:
Blocks: 170700
  Show dependency treegraph
 
Reported: 2017-04-10 15:14 PDT by Said Abou-Hallawa
Modified: 2017-04-20 17:51 PDT (History)
13 users (show)

See Also:


Attachments
Patch (4.87 KB, patch)
2017-04-10 20:59 PDT, Said Abou-Hallawa
no flags Details | Formatted Diff | Diff
Archive of layout-test-results from ews107 for mac-elcapitan-wk2 (900.67 KB, application/zip)
2017-04-10 21:49 PDT, Build Bot
no flags Details
Archive of layout-test-results from ews122 for ios-simulator-wk2 (1.32 MB, application/zip)
2017-04-10 22:16 PDT, Build Bot
no flags Details
Patch (4.19 KB, patch)
2017-04-11 09:42 PDT, Said Abou-Hallawa
no flags Details | Formatted Diff | Diff
Patch (4.24 KB, patch)
2017-04-11 12:19 PDT, Said Abou-Hallawa
no flags Details | Formatted Diff | Diff
Patch (1.86 KB, patch)
2017-04-16 22:43 PDT, Said Abou-Hallawa
no flags Details | Formatted Diff | Diff
Archive of layout-test-results from ews106 for mac-elcapitan-wk2 (877.71 KB, application/zip)
2017-04-17 00:00 PDT, Build Bot
no flags Details
Patch (2.45 KB, patch)
2017-04-19 17:27 PDT, Said Abou-Hallawa
no flags Details | Formatted Diff | Diff
Patch (2.36 KB, patch)
2017-04-20 12:03 PDT, Said Abou-Hallawa
no flags Details | Formatted Diff | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Said Abou-Hallawa 2017-04-10 15:14:31 PDT
To save CPU cycles and network bandwidth, it is worth cancelling loading images with unsupported/unknown types.
Comment 1 Radar WebKit Bug Importer 2017-04-10 15:16:03 PDT
<rdar://problem/31543168>
Comment 2 Said Abou-Hallawa 2017-04-10 20:59:36 PDT
Created attachment 306771 [details]
Patch
Comment 3 Build Bot 2017-04-10 21:49:41 PDT
Comment on attachment 306771 [details]
Patch

Attachment 306771 [details] did not pass mac-wk2-ews (mac-wk2):
Output: http://webkit-queues.webkit.org/results/3514938

Number of test failures exceeded the failure limit.
Comment 4 Build Bot 2017-04-10 21:49:43 PDT
Created attachment 306775 [details]
Archive of layout-test-results from ews107 for mac-elcapitan-wk2

The attached test failures were seen while running run-webkit-tests on the mac-wk2-ews.
Bot: ews107  Port: mac-elcapitan-wk2  Platform: Mac OS X 10.11.6
Comment 5 Build Bot 2017-04-10 22:16:57 PDT
Comment on attachment 306771 [details]
Patch

Attachment 306771 [details] did not pass ios-sim-ews (ios-simulator-wk2):
Output: http://webkit-queues.webkit.org/results/3514943

Number of test failures exceeded the failure limit.
Comment 6 Build Bot 2017-04-10 22:16:58 PDT
Created attachment 306778 [details]
Archive of layout-test-results from ews122 for ios-simulator-wk2

The attached test failures were seen while running run-webkit-tests on the ios-sim-ews.
Bot: ews122  Port: ios-simulator-wk2  Platform: Mac OS X 10.11.6
Comment 7 Said Abou-Hallawa 2017-04-11 09:42:40 PDT
Created attachment 306825 [details]
Patch
Comment 8 Said Abou-Hallawa 2017-04-11 12:19:36 PDT
Created attachment 306839 [details]
Patch
Comment 9 youenn fablet 2017-04-11 13:44:55 PDT
Comment on attachment 306839 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=306839&action=review

> Source/WebCore/loader/cache/CachedImage.cpp:408
> +            m_loader->cancel();

Should we set m_loader to nullptr?
It is sufficient to set m_loader to nullptr?

Ideally, CachedImage should notify its clients that something went wrong and all clients should clean themselves, and CachedImage should be destroyed, thus cancelling the load. This approach ensures the load gets cancelled so this is probably good enough.

> Source/WebCore/loader/cache/CachedImage.cpp:486
> +void CachedImage::cancelLoad()

Is it needed to do the clean-up once m_loader->cancel() is called.
If it is the case, can we do the clean-up up there?
If not, what does it bring?

> Source/WebCore/loader/cache/CachedImage.h:112
> +    void cancelLoad() override;

I am not sure we want that.
This adds some difficulty in understanding and CachedImage::cancelLoad seems very similar to CachedImage::cancelLoad.
Using an existing mechanism might be ok like checkNotify() for instance which is already virtual.
Would that be possible?
Comment 10 youenn fablet 2017-04-11 14:47:44 PDT
(In reply to youenn fablet from comment #9)
> Comment on attachment 306839 [details]
> Patch
> 
> View in context:
> https://bugs.webkit.org/attachment.cgi?id=306839&action=review
> 
> > Source/WebCore/loader/cache/CachedImage.cpp:408
> > +            m_loader->cancel();
> 
> Should we set m_loader to nullptr?
> It is sufficient to set m_loader to nullptr?
> 
> Ideally, CachedImage should notify its clients that something went wrong and
> all clients should clean themselves, and CachedImage should be destroyed,
> thus cancelling the load. This approach ensures the load gets cancelled so
> this is probably good enough.

'This approach' means the approach you are proposing in this patch.
Comment 11 Said Abou-Hallawa 2017-04-11 15:48:24 PDT
Comment on attachment 306839 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=306839&action=review

>> Source/WebCore/loader/cache/CachedImage.cpp:486
>> +void CachedImage::cancelLoad()
> 
> Is it needed to do the clean-up once m_loader->cancel() is called.
> If it is the case, can we do the clean-up up there?
> If not, what does it bring?

CachedImage::cancelLoad() gets called form  ResourceLoader::cancel() via SubresourceLoader::didCancel(). It marks the image as a broken image.  Without adding CachedImage::cancelLoad(), nothing will be drawn for the unspotted image not even the ? mark for the broken image. The code below is very similar to CachedImage::error().

>> Source/WebCore/loader/cache/CachedImage.h:112
>> +    void cancelLoad() override;
> 
> I am not sure we want that.
> This adds some difficulty in understanding and CachedImage::cancelLoad seems very similar to CachedImage::cancelLoad.
> Using an existing mechanism might be ok like checkNotify() for instance which is already virtual.
> Would that be possible?

Did you mean: "CachedImage::cancelLoad seems very similar to CachedImage::error" above?
Comment 12 youenn fablet 2017-04-11 18:12:47 PDT
(In reply to Said Abou-Hallawa from comment #11)
> Comment on attachment 306839 [details]
> Patch
> 
> View in context:
> https://bugs.webkit.org/attachment.cgi?id=306839&action=review
> 
> >> Source/WebCore/loader/cache/CachedImage.cpp:486
> >> +void CachedImage::cancelLoad()
> > 
> > Is it needed to do the clean-up once m_loader->cancel() is called.
> > If it is the case, can we do the clean-up up there?
> > If not, what does it bring?
> 
> CachedImage::cancelLoad() gets called form  ResourceLoader::cancel() via
> SubresourceLoader::didCancel(). It marks the image as a broken image. 
> Without adding CachedImage::cancelLoad(), nothing will be drawn for the
> unspotted image not even the ? mark for the broken image. The code below is
> very similar to CachedImage::error().

If this code is triggered only in the case CachedImage is calling m_loader->canvel(), it might be better to do that at the call site, not here.
> 
> >> Source/WebCore/loader/cache/CachedImage.h:112
> >> +    void cancelLoad() override;
> > 
> > I am not sure we want that.
> > This adds some difficulty in understanding and CachedImage::cancelLoad seems very similar to CachedImage::cancelLoad.
> > Using an existing mechanism might be ok like checkNotify() for instance which is already virtual.
> > Would that be possible?
> 
> Did you mean: "CachedImage::cancelLoad seems very similar to
> CachedImage::error" above?

I was meaning CachedResource::cancelLoad. Would be good if we could not change cacelLoad at all.
Comment 13 Said Abou-Hallawa 2017-04-13 11:54:16 PDT
Comment on attachment 306839 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=306839&action=review

>>>> Source/WebCore/loader/cache/CachedImage.cpp:486
>>>> +void CachedImage::cancelLoad()
>>> 
>>> Is it needed to do the clean-up once m_loader->cancel() is called.
>>> If it is the case, can we do the clean-up up there?
>>> If not, what does it bring?
>> 
>> CachedImage::cancelLoad() gets called form  ResourceLoader::cancel() via SubresourceLoader::didCancel(). It marks the image as a broken image.  Without adding CachedImage::cancelLoad(), nothing will be drawn for the unspotted image not even the ? mark for the broken image. The code below is very similar to CachedImage::error().
> 
> If this code is triggered only in the case CachedImage is calling m_loader->canvel(), it might be better to do that at the call site, not here.

Why do you think it might be better to move the code of this function to the call site?

I looked at the CachedResource class and i noticed it has these two function which are very similar.

void CachedResource::error(CachedResource::Status status)
{
    setStatus(status);
    ASSERT(errorOccurred());
    m_data = nullptr;

    setLoading(false);
    checkNotify();
}
    
void CachedResource::cancelLoad()
{
    if (!isLoading() && !stillNeedsLoad())
        return;

    setStatus(LoadError);
    setLoading(false);
    checkNotify();
}

CachedImage::error() calls CachedResource::error() in the following order. I am not sure if the order of the calls matters or not. But I think it matters because CachedResource::checkNotify() notifies the clients that the image has finished loading while CachedImage::notifyObservers() notifies the observers that they need to repaint themselves.

void CachedImage::error(CachedResource::Status status)
{
    checkShouldPaintBrokenImage();
    clear();
    CachedResource::error(status);
    notifyObservers();
}

I also wanted to do cancelling the image load to be very similar to the case of error except to stops loading data into the image. So I added the function CachedImage::cancelLoad() which is very similar to CachedImage::error():

void CachedImage::cancelLoad()
{
    checkShouldPaintBrokenImage();
    clear();
    CachedResource::cancelLoad();
    notifyObservers();
}

Now how do I call this function? Since ResourceLoader::cancel() currently calls CachedResource::cancelLoad() though SubresourceLoader::didCancel(), I just made CachedResource::cancelLoad() virtual and I called ResourceLoader::cancel() from CachedImage::addIncrementalDataBuffer() in the case of error/unsupported image format. This will make the sequence and the oder of calls for cancelling the load be the same as for the error case.

So unless you are sure the way I organized this code can cause problems, I think this order of calls is safer to use since it has been used for the error case without known issues. And also it makes the code more consistent.
Comment 14 youenn fablet 2017-04-13 18:36:03 PDT
Do you know whether checkNotify/notifyObservers order is important or not?
In case of regular loading end, checkNotify is done after notifyObservers.

I think your patch is working fine.
It is more a matter of clarity of the code.
If the call to checkShouldPaintBrokenImage/clear/... is done when checking for EncodedDataStatus::Error, the intent is very clear (paint a broken image and cancel the network load).

If we follow the error() approach, it is less easy to follow the code flow.
For instance, is the error() approach done so that we actually ensure that checkNotify is called before notifyObservers for instance? Or is it purely a style artifact?

Maybe we need to follow the error() approach for the reason you mentioned.
If not, why not trying to simplify things. A little refactoring would also help group code common to CachedImage::error() and your proposed CachedImage::cancelLoad() for instance.

As a minor thing, the cancelLoad approach is slightly less efficient: we are doing twice the checks of "!isLoading() && !stillNeedsLoad()", plus the cost of the virtual cancelLoad.
Comment 15 Said Abou-Hallawa 2017-04-14 12:53:40 PDT
(In reply to youenn fablet from comment #14)
> Do you know whether checkNotify/notifyObservers order is important or not?
> In case of regular loading end, checkNotify is done after notifyObservers.
> 

I answered this question above: "I am not sure if the order of the calls matters or not. But I think it matters because CachedResource::checkNotify() notifies the clients that the image has finished loading while CachedImage::notifyObservers() notifies the observers that they need to repaint themselves."

> I think your patch is working fine.
> It is more a matter of clarity of the code.
> If the call to checkShouldPaintBrokenImage/clear/... is done when checking
> for EncodedDataStatus::Error, the intent is very clear (paint a broken image
> and cancel the network load).
> 

I can log a new bug for the code clarity issue "Investigate the order of calls made by CachedImage::error and CachedImage::cancelLoad()".

> If we follow the error() approach, it is less easy to follow the code flow.
> For instance, is the error() approach done so that we actually ensure that
> checkNotify is called before notifyObservers for instance? Or is it purely a
> style artifact?
> 

I don't know for sure but I think the current order of calls is correct.

> Maybe we need to follow the error() approach for the reason you mentioned.
> If not, why not trying to simplify things. A little refactoring would also
> help group code common to CachedImage::error() and your proposed
> CachedImage::cancelLoad() for instance.
> 

This will be covered by the new bug.

> As a minor thing, the cancelLoad approach is slightly less efficient: we are
> doing twice the checks of "!isLoading() && !stillNeedsLoad()", plus the cost
> of the virtual cancelLoad.

I would have agreed with this analysis if this code were in a critical code path. But this code will run when loading an image  is canceled because of decoding error or unsupported format. The new code will save a lot time which is currently spent in loading the rest of the encoded image data even though we know for sure it is just waste and will be thrown away. Also I think it is common practice to make the function itself protected against bad arguments or wrong states regardless who calls it. So you can see the same check is done in multiple levels of the code because the lowest level function might be called from a caller which does not make the same check. For example in the code below if (x < 0) will be executed twice when calling func1(). And the reason is func1() needs it because it does not want to do_some_stuff1() if (x < 0). And func3() has it because func2() wants to do_some_stuff2() regardless of the value of x.

void func1(int x)
{
    if (x < 0)
        return;
    func3(x);
    do_some_stuff1();
}

void func2(int x)
{
    func3(x);
    do_some_stuff2();
}

void func3(int x)
{
    if (x < 0)
        return;
    do_other_stuff3();
}

If you think the patch is fine, please r+ since I need to move forward with the patch.
Comment 16 youenn fablet 2017-04-14 22:53:24 PDT
What about the following?

void CachedImage::addIncrementalDataBuffer(SharedBuffer& data)
{
...
   if (encodedDataStatus == EncodedDataStatus::Error) {
        error(DecodeError);
        if (m_loader)
            m_loader->cancel();
        if (inCache())
            MemoryCache::singleton().remove(*this);
        return;
   }
...
}

It seems easier to understand and, from a quick look, equivalent in terms of behavior.
Comment 17 Said Abou-Hallawa 2017-04-14 23:15:12 PDT
(In reply to youenn fablet from comment #16)
> What about the following?
> 
> void CachedImage::addIncrementalDataBuffer(SharedBuffer& data)
> {
> ...
>    if (encodedDataStatus == EncodedDataStatus::Error) {
>         error(DecodeError);
>         if (m_loader)
>             m_loader->cancel();
>         if (inCache())
>             MemoryCache::singleton().remove(*this);
>         return;
>    }
> ...
> }
> 
> It seems easier to understand and, from a quick look, equivalent in terms of
> behavior.

But this suggestion will call both CachedResource::error() and CachedResource::cancelLoad()

// CachedImage::addIncrementalDataBuffer() calls CachedImage::error() which calls CachedResource::error().
void CachedResource::error(CachedResource::Status status)
{
    setStatus(status);
    ASSERT(errorOccurred());
    m_data = nullptr;

    setLoading(false);
    checkNotify();
}

// CachedImage::addIncrementalDataBuffer() calls ResourceLoader::cancel() which SubresourceLoader::didCancel() which calls CachedResource::cancelLoad().
void CachedResource::cancelLoad()
{
    if (!isLoading() && !stillNeedsLoad())
        return;

    setStatus(LoadError);
    setLoading(false);
    checkNotify();
}

How can this be easier to understand? How can calling setStatus(DecodeError) then calling setStatus(LoadError) be easier to understand? Why do we call setLoading(false) and checkNotify() twice?
Comment 18 youenn fablet 2017-04-15 09:38:10 PDT
(In reply to Said Abou-Hallawa from comment #17)
> (In reply to youenn fablet from comment #16)
> > What about the following?
> > 
> > void CachedImage::addIncrementalDataBuffer(SharedBuffer& data)
> > {
> > ...
> >    if (encodedDataStatus == EncodedDataStatus::Error) {
> >         error(DecodeError);
> >         if (m_loader)
> >             m_loader->cancel();
> >         if (inCache())
> >             MemoryCache::singleton().remove(*this);
> >         return;
> >    }
> > ...
> > }
> > 
> > It seems easier to understand and, from a quick look, equivalent in terms of
> > behavior.
> 
> But this suggestion will call both CachedResource::error() and
> CachedResource::cancelLoad()
> 
> // CachedImage::addIncrementalDataBuffer() calls CachedImage::error() which
> calls CachedResource::error().
> void CachedResource::error(CachedResource::Status status)
> {
>     setStatus(status);
>     ASSERT(errorOccurred());
>     m_data = nullptr;
> 
>     setLoading(false);
>     checkNotify();
> }
> 
> // CachedImage::addIncrementalDataBuffer() calls ResourceLoader::cancel()
> which SubresourceLoader::didCancel() which calls
> CachedResource::cancelLoad().
> void CachedResource::cancelLoad()
> {
>     if (!isLoading() && !stillNeedsLoad())
>         return;
> 
>     setStatus(LoadError);
>     setLoading(false);
>     checkNotify();
> }
> 
> How can this be easier to understand? How can calling setStatus(DecodeError)
> then calling setStatus(LoadError) be easier to understand? Why do we call
> setLoading(false) and checkNotify() twice?

CachedResource::isLoading() will return false.
I would expect CachedResource::stillNeedsLoad() to do the same.
If so cancelLoad will exit early.
Comment 19 Said Abou-Hallawa 2017-04-16 22:43:03 PDT
Created attachment 307254 [details]
Patch
Comment 20 youenn fablet 2017-04-16 23:16:50 PDT
Comment on attachment 307254 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=307254&action=review

> Source/WebCore/loader/cache/CachedImage.cpp:407
>          // Image decoding failed. Either we need more image data or the image data is malformed.

This comment seems wrong to me from reading the code below.
If we need more image data, we should not call error(). We should just exit early in order to wait for more data.
Can you check we are doing the right thing for this case and update the comment accordingly?

> Source/WebCore/loader/cache/CachedImage.cpp:409
> +        if (m_loader)

Can we have the case of a null m_loader in CachedImage::addIncrementalDataBuffer.
Probably not, so we might be able to call m_loader->cancel() directly. Maybe with an ASSERT(m_loader) before?
Comment 21 Build Bot 2017-04-17 00:00:30 PDT
Comment on attachment 307254 [details]
Patch

Attachment 307254 [details] did not pass mac-wk2-ews (mac-wk2):
Output: http://webkit-queues.webkit.org/results/3549497

New failing tests:
webrtc/multi-video.html
Comment 22 Build Bot 2017-04-17 00:00:32 PDT
Created attachment 307256 [details]
Archive of layout-test-results from ews106 for mac-elcapitan-wk2

The attached test failures were seen while running run-webkit-tests on the mac-wk2-ews.
Bot: ews106  Port: mac-elcapitan-wk2  Platform: Mac OS X 10.11.6
Comment 23 Said Abou-Hallawa 2017-04-17 15:42:55 PDT
Comment on attachment 307254 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=307254&action=review

>> Source/WebCore/loader/cache/CachedImage.cpp:409
>> +        if (m_loader)
> 
> Can we have the case of a null m_loader in CachedImage::addIncrementalDataBuffer.
> Probably not, so we might be able to call m_loader->cancel() directly. Maybe with an ASSERT(m_loader) before?

Yes we can have a null m_loader. It happens with ImageDocument. The loader is the document loader. CachedImage does not hold a loader in this case.
Comment 24 Said Abou-Hallawa 2017-04-19 17:27:56 PDT
Created attachment 307527 [details]
Patch
Comment 25 Said Abou-Hallawa 2017-04-20 12:03:38 PDT
Created attachment 307608 [details]
Patch
Comment 26 WebKit Commit Bot 2017-04-20 13:09:51 PDT
Comment on attachment 307608 [details]
Patch

Clearing flags on attachment: 307608

Committed r215574: <http://trac.webkit.org/changeset/215574>
Comment 27 WebKit Commit Bot 2017-04-20 13:09:53 PDT
All reviewed patches have been landed.  Closing bug.
Comment 28 youenn fablet 2017-04-20 14:25:00 PDT
Comment on attachment 307608 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=307608&action=review

> Source/WebCore/loader/cache/CachedImage.cpp:410
> +            m_loader->cancel();

I know your changes are not causing that, but CachedImage::addIncrementalDataBuffer code seems still a bit odd.
For instance, why are we removing the image from the MemoryCache if it is waiting for more data? We shouldn't do that I guess.

Should we just return early if m_image->isNull() && encodedDataStatus != EncodedDataStatus::Error?
Maybe writing this method with a switch(encodedDataStatus) would make things clearer.
Wdyt?