Changeset 39309 in webkit
- Timestamp:
- Dec 15, 2008 12:45:46 PM (15 years ago)
- Location:
- trunk/WebCore
- Files:
-
- 15 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/WebCore/ChangeLog
r39306 r39309 1 2008-12-15 Peter Kasting <pkasting@google.com> 2 3 Reviewed by David Hyatt. 4 5 https://bugs.webkit.org/show_bug.cgi?id=22108 6 Large animated GIFs weren't always animating. The code that deleted 7 the entire decoder after each frame of a large image was resulting in 8 us forgetting the loop count, breaking animations intermittently. 9 10 Instead of throwing the whole decoder away, we're more careful to just 11 delete frames we don't care about. This additionally addresses 12 problems in the Cairo and Chromium ports with excessive peak memory 13 use and CPU use when decoding large animated GIFs because it leads to 14 much less redecoding (O(n) instead of O(n^2) CPU, and O(1) instead of 15 O(n) memory). 16 17 This change has less impact on the CG decoder, which seems to throw 18 away frames automatically when their external references are dropped; 19 this means the CG decoder didn't suffer from the peak memory usage 20 issue before (and still doesn't), but it also still burns excessive 21 CPU redecoding earlier frames, that in theory it wouldn't need to 22 redecode if it would judiciously save the most recent frames. At 23 least this patch plumbs some useful info to the ImageSource so it can 24 help guide the CG decoder heuristics in the future. 25 26 * platform/graphics/BitmapImage.cpp: 27 (WebCore::frameBytes): 28 (WebCore::BitmapImage::destroyDecodedData): 29 (WebCore::BitmapImage::destroyDecodedDataIfNecessary): 30 (WebCore::BitmapImage::destroyMetadataAndNotify): 31 (WebCore::BitmapImage::clearFrame): 32 (WebCore::BitmapImage::cacheFrame): 33 (WebCore::BitmapImage::dataChanged): 34 (WebCore::BitmapImage::startAnimation): 35 (WebCore::BitmapImage::resetAnimation): 36 (WebCore::BitmapImage::internalAdvanceAnimation): 37 * platform/graphics/BitmapImage.h: 38 * platform/graphics/GeneratedImage.h: 39 (WebCore::GeneratedImage::destroyDecodedData): 40 * platform/graphics/Image.h: 41 * platform/graphics/ImageSource.h: 42 * platform/graphics/cairo/ImageSourceCairo.cpp: 43 (WebCore::ImageSource::~ImageSource): 44 (WebCore::ImageSource::clear): 45 * platform/graphics/cg/ImageSourceCG.cpp: 46 (WebCore::ImageSource::~ImageSource): 47 (WebCore::ImageSource::clear): 48 * platform/graphics/cg/PDFDocumentImage.h: 49 (WebCore::PDFDocumentImage::destroyDecodedData): 50 * platform/graphics/qt/StillImageQt.h: 51 (WebCore::StillImage::destroyDecodedData): 52 * platform/image-decoders/ImageDecoder.h: 53 (WebCore::RGBA32Buffer::clear): 54 (WebCore::ImageDecoder::clearFrameBufferCache): 55 * platform/image-decoders/gif/GIFImageDecoder.cpp: 56 (WebCore::GIFImageDecoder::repetitionCount): 57 (WebCore::GIFImageDecoder::clearFrameBufferCache): 58 (WebCore::GIFImageDecoder::initFrameBuffer): 59 * platform/image-decoders/gif/GIFImageDecoder.h: 60 * platform/image-decoders/gif/GIFImageReader.h: 61 (GIFImageReader::GIFImageReader): 62 * svg/graphics/SVGImage.h: 63 (WebCore::SVGImage::destroyDecodedData): 64 1 65 2008-12-15 Cameron Zwarich <zwarich@apple.com> 2 66 -
trunk/WebCore/platform/graphics/BitmapImage.cpp
r39185 r39309 39 39 namespace WebCore { 40 40 41 // Animated images >5MB are considered large enough that we'll only hang on to 42 // one frame at a time. 43 const unsigned cLargeAnimationCutoff = 5242880; 44 45 // When an animated image is more than five minutes out of date, don't try to 46 // resync on repaint, so we don't waste CPU cycles on an edge case the user 47 // doesn't care about. 48 const double cAnimationResyncCutoff = 5 * 60; 41 static int frameBytes(const IntSize& frameSize) 42 { 43 return frameSize.width() * frameSize.height() * 4; 44 } 49 45 50 46 BitmapImage::BitmapImage(ImageObserver* observer) … … 76 72 } 77 73 78 void BitmapImage::destroyDecodedData(bool incremental, bool preserveNearbyFrames) 79 { 80 // Destroy the cached images and release them. 81 if (m_frames.size()) { 82 int sizeChange = 0; 83 int frameSize = m_size.width() * m_size.height() * 4; 84 const size_t nextFrame = (preserveNearbyFrames && frameCount()) ? ((m_currentFrame + 1) % frameCount()) : 0; 85 for (unsigned i = incremental ? m_frames.size() - 1 : 0; i < m_frames.size(); i++) { 86 if (m_frames[i].m_frame && (!preserveNearbyFrames || (i != m_currentFrame && i != nextFrame))) { 87 sizeChange -= frameSize; 88 m_frames[i].clear(); 89 } 90 } 91 92 // We just always invalidate our platform data, even in the incremental case. 93 // This could be better, but it's not a big deal. 94 m_isSolidColor = false; 95 invalidatePlatformData(); 96 97 if (sizeChange) { 98 m_decodedSize += sizeChange; 99 if (imageObserver()) 100 imageObserver()->decodedSizeChanged(this, sizeChange); 101 } 102 } 103 if (!incremental) { 104 // Reset the image source, since Image I/O has an underlying cache that it uses 105 // while animating that it seems to never clear. 106 m_source.clear(); 107 if (m_data) 108 m_source.setData(m_data.get(), m_allDataReceived); 109 } 74 void BitmapImage::destroyDecodedData(bool destroyAll) 75 { 76 int framesCleared = 0; 77 const size_t clearBeforeFrame = destroyAll ? m_frames.size() : m_currentFrame; 78 for (size_t i = 0; i < clearBeforeFrame; ++i) 79 framesCleared += clearFrame(i); 80 81 destroyMetadataAndNotify(framesCleared); 82 83 m_source.clear(destroyAll, clearBeforeFrame); 84 if (m_data) 85 m_source.setData(m_data.get(), m_allDataReceived); 86 return; 87 } 88 89 void BitmapImage::destroyDecodedDataIfNecessary(bool destroyAll) 90 { 91 // Animated images >5MB are considered large enough that we'll only hang on 92 // to one frame at a time. 93 static const unsigned cLargeAnimationCutoff = 5242880; 94 if (frameCount() * frameBytes(m_size) > cLargeAnimationCutoff) 95 destroyDecodedData(destroyAll); 96 } 97 98 void BitmapImage::destroyMetadataAndNotify(int framesCleared) 99 { 100 m_isSolidColor = false; 101 invalidatePlatformData(); 102 103 const int deltaBytes = framesCleared * -frameBytes(m_size); 104 m_decodedSize += deltaBytes; 105 if (deltaBytes && imageObserver()) 106 imageObserver()->decodedSizeChanged(this, deltaBytes); 107 } 108 109 int BitmapImage::clearFrame(size_t frame) 110 { 111 if (!m_frames[frame].m_frame) 112 return 0; 113 114 m_frames[frame].clear(); 115 return 1; 110 116 } 111 117 … … 128 134 m_frames[index].m_hasAlpha = m_source.frameHasAlphaAtIndex(index); 129 135 130 int sizeChange; 131 if (index) { 132 IntSize frameSize = m_source.frameSizeAtIndex(index); 133 if (frameSize != m_size) 134 m_hasUniformFrameSize = false; 135 sizeChange = m_frames[index].m_frame ? frameSize.width() * frameSize.height() * 4 : 0; 136 } else 137 sizeChange = m_frames[index].m_frame ? m_size.width() * m_size.height() * 4 : 0; 138 139 if (sizeChange) { 140 m_decodedSize += sizeChange; 136 const IntSize frameSize(index ? m_source.frameSizeAtIndex(index) : m_size); 137 if (frameSize != m_size) 138 m_hasUniformFrameSize = false; 139 if (m_frames[index].m_frame) { 140 const int deltaBytes = frameBytes(frameSize); 141 m_decodedSize += deltaBytes; 141 142 if (imageObserver()) 142 imageObserver()->decodedSizeChanged(this, sizeChange);143 imageObserver()->decodedSizeChanged(this, deltaBytes); 143 144 } 144 145 } … … 162 163 bool BitmapImage::dataChanged(bool allDataReceived) 163 164 { 164 destroy DecodedData(true);165 destroyMetadataAndNotify(m_frames.isEmpty() ? 0 : clearFrame(m_frames.size() - 1)); 165 166 166 167 // Feed all the data we've seen so far to the image decoder. … … 278 279 } else { 279 280 m_desiredFrameStartTime += currentDuration; 280 // If we're too far behind, the user probably doesn't care about 281 // resyncing and we could burn a lot of time looping through frames 282 // below. Just reset the timings. 281 282 // When an animated image is more than five minutes out of date, the 283 // user probably doesn't care about resyncing and we could burn a lot of 284 // time looping through frames below. Just reset the timings. 285 const double cAnimationResyncCutoff = 5 * 60; 283 286 if ((time - m_desiredFrameStartTime) > cAnimationResyncCutoff) 284 287 m_desiredFrameStartTime = time + currentDuration; … … 373 376 m_desiredFrameStartTime = 0; 374 377 m_animationFinished = false; 375 int frameSize = m_size.width() * m_size.height() * 4;376 378 377 379 // For extremely large animations, when the animation is reset, we just throw everything away. 378 if (frameCount() * frameSize > cLargeAnimationCutoff) 379 destroyDecodedData(); 380 destroyDecodedDataIfNecessary(true); 380 381 } 381 382 … … 398 399 return false; 399 400 400 m_currentFrame++; 401 ++m_currentFrame; 402 bool advancedAnimation = true; 403 bool destroyAll = false; 401 404 if (m_currentFrame >= frameCount()) { 402 405 ++m_repetitionsComplete; 406 403 407 // Get the repetition count again. If we weren't able to get a 404 408 // repetition count before, we should have decoded the whole image by … … 407 411 m_animationFinished = true; 408 412 m_desiredFrameStartTime = 0; 409 m_currentFrame--; 410 if (skippingFrames) { 411 // Uh oh. We tried to skip past the end of the animation. We'd 412 // better draw this last frame. 413 notifyObserverAndTrimDecodedData(); 414 } 415 return false; 413 --m_currentFrame; 414 advancedAnimation = false; 415 } else { 416 m_currentFrame = 0; 417 destroyAll = true; 416 418 } 417 m_currentFrame = 0; 418 } 419 420 if (!skippingFrames) 421 notifyObserverAndTrimDecodedData(); 422 423 return true; 424 } 425 426 void BitmapImage::notifyObserverAndTrimDecodedData() 427 { 428 // Notify our observer that the animation has advanced. 429 imageObserver()->animationAdvanced(this); 430 431 // For large animated images, go ahead and throw away frames as we go to 432 // save footprint. 433 int frameSize = m_size.width() * m_size.height() * 4; 434 if (frameCount() * frameSize > cLargeAnimationCutoff) { 435 // Destroy all of our frames and just redecode every time. We save the 436 // current frame since we'll need it in draw() anyway. 437 destroyDecodedData(false, true); 438 } 439 } 440 441 } 419 } 420 destroyDecodedDataIfNecessary(destroyAll); 421 422 // We need to draw this frame if we advanced to it while not skipping, or if 423 // while trying to skip frames we hit the last frame and thus had to stop. 424 if (skippingFrames != advancedAnimation) 425 imageObserver()->animationAdvanced(this); 426 return advancedAnimation; 427 } 428 429 } -
trunk/WebCore/platform/graphics/BitmapImage.h
r39185 r39309 169 169 void cacheFrame(size_t index); 170 170 171 // Called to invalidate all our cached data. If an image is loading 172 // incrementally, we only invalidate the last cached frame. For large 173 // animated images, where we throw away the decoded data after every frame, 174 // |preserveNearbyFrames| can be set to preserve the current frame's data 175 // and eliminate some unnecessary duplicated decoding work. This also 176 // preserves the next frame's data, if available. In most cases this has no 177 // effect; either that frame isn't decoded yet, or it's already been 178 // destroyed by a previous call. But when we fall behind on the very first 179 // animation loop and startAnimation() needs to "catch up" one or more 180 // frames, this briefly preserves some of that decoding work, to ease CPU 181 // load and make it less likely that we'll keep falling behind. 182 virtual void destroyDecodedData(bool incremental = false, bool preserveNearbyFrames = false); 171 // Called to invalidate cached data. When |destroyAll| is true, we wipe out 172 // the entire frame buffer cache and tell the image source to destroy 173 // everything; this is used when e.g. we want to free some room in the image 174 // cache. If |destroyAll| is false, we only delete frames up to the current 175 // one; this is used while animating large images to keep memory footprint 176 // low without redecoding the whole image on every frame. 177 virtual void destroyDecodedData(bool destroyAll = true); 178 179 // If the image is large enough, calls destroyDecodedData() and passes 180 // |destroyAll| along. 181 void destroyDecodedDataIfNecessary(bool destroyAll); 182 183 // Generally called by destroyDecodedData(), destroys whole-image metadata 184 // and notifies observers that the memory footprint has (hopefully) 185 // decreased by |framesCleared| times the size (in bytes) of a frame. 186 void destroyMetadataAndNotify(int framesCleared); 187 188 // If frame |frame| is cached, clears the cache handle. Returns the number 189 // of frames actually cleared. 190 int clearFrame(size_t frame); 183 191 184 192 // Whether or not size is available yet. … … 198 206 bool internalAdvanceAnimation(bool skippingFrames); 199 207 200 // Helper for internalAdvanceAnimation().201 void notifyObserverAndTrimDecodedData();202 203 208 // Handle platform-specific data 204 209 void initPlatformData(); -
trunk/WebCore/platform/graphics/GeneratedImage.h
r37612 r39309 54 54 55 55 // Assume that generated content has no decoded data we need to worry about 56 virtual void destroyDecodedData(bool incremental = false, bool preserveNearbyFrames = false) { }56 virtual void destroyDecodedData(bool destroyAll = true) { } 57 57 virtual unsigned decodedSize() const { return 0; } 58 58 -
trunk/WebCore/platform/graphics/Image.h
r39185 r39309 111 111 virtual String filenameExtension() const { return String(); } // null string if unknown 112 112 113 virtual void destroyDecodedData(bool incremental = false, bool preserveNearbyFrames = false) = 0;113 virtual void destroyDecodedData(bool destroyAll = true) = 0; 114 114 virtual unsigned decodedSize() const = 0; 115 115 -
trunk/WebCore/platform/graphics/ImageSource.h
r39185 r39309 84 84 ~ImageSource(); 85 85 86 void clear(); 86 // Tells the ImageSource that the Image no longer cares about decoded frame 87 // data -- at all (if |destroyAll| is true), or before frame 88 // |clearBeforeFrame| (if |destroyAll| is false). The ImageSource should 89 // delete cached decoded data for these frames where possible to keep memory 90 // usage low. When |destroyAll| is true, the ImageSource should also reset 91 // any local state so that decoding can begin again. 92 // 93 // Implementations that delete less than what's specified above waste 94 // memory. Implementations that delete more may burn CPU re-decoding frames 95 // that could otherwise have been cached, or encounter errors if they're 96 // asked to decode frames they can't decode due to the loss of previous 97 // decoded frames. 98 // 99 // Callers should not call clear(false, n) and subsequently call 100 // createFrameAtIndex(m) with m < n, unless they first call clear(true). 101 // This ensures that stateful ImageSources/decoders will work properly. 102 void clear(bool destroyAll, size_t clearBeforeFrame = 0); 87 103 88 104 bool initialized() const; 89 105 90 106 void setData(SharedBuffer* data, bool allDataReceived); 91 107 String filenameExtension() const; … … 96 112 97 113 int repetitionCount(); 98 114 99 115 size_t frameCount() const; 100 116 117 // Callers should not call this after calling clear() with a higher index; 118 // see comments on clear() above. 101 119 NativeImagePtr createFrameAtIndex(size_t); 102 120 103 121 float frameDurationAtIndex(size_t); 104 122 bool frameHasAlphaAtIndex(size_t); // Whether or not the frame actually used any alpha. -
trunk/WebCore/platform/graphics/cairo/ImageSourceCairo.cpp
r39185 r39309 98 98 ImageSource::~ImageSource() 99 99 { 100 clear(); 101 } 102 103 void ImageSource::clear() 104 { 105 delete m_decoder; 106 m_decoder = 0; 100 clear(true); 101 } 102 103 void ImageSource::clear(bool destroyAll, size_t clearBeforeFrame) 104 { 105 if (destroyAll) { 106 delete m_decoder; 107 m_decoder = 0; 108 return; 109 } 110 111 if (m_decoder) 112 m_decoder->clearFrameBufferCache(clearBeforeFrame); 107 113 } 108 114 -
trunk/WebCore/platform/graphics/cg/ImageSourceCG.cpp
r39185 r39309 46 46 ImageSource::~ImageSource() 47 47 { 48 clear(); 49 } 50 51 void ImageSource::clear() 52 { 53 if (m_decoder) { 54 CFRelease(m_decoder); 55 m_decoder = 0; 56 } 48 clear(true); 49 } 50 51 void ImageSource::clear(bool destroyAll, size_t clearBeforeFrame) 52 { 53 if (destroyAll) { 54 if (m_decoder) { 55 CFRelease(m_decoder); 56 m_decoder = 0; 57 } 58 return; 59 } 60 61 // TODO(pkasting): If there was an appropriate API to do so, we could 62 // explicitly tell the CG decoder it can discard frames before 63 // |clearBeforeFrame| (not including anything it needs to keep around 64 // locally to continue decoding correctly). This might help the decoder 65 // optimize memory/CPU usage. Right now the decoder seems to throw away 66 // frames aggressively and then re-decode from the beginning each time, thus 67 // using more CPU than it needs to. 57 68 } 58 69 -
trunk/WebCore/platform/graphics/cg/PDFDocumentImage.h
r37612 r39309 51 51 // FIXME: PDF Images are underreporting decoded sizes and will be unable 52 52 // to prune because these functions are not implemented yet. 53 virtual void destroyDecodedData(bool incremental = false, bool preserveNearbyFrames = false) { }53 virtual void destroyDecodedData(bool destroyAll = true) { } 54 54 virtual unsigned decodedSize() const { return 0; } 55 55 -
trunk/WebCore/platform/graphics/qt/StillImageQt.h
r37615 r39309 42 42 // FIXME: StillImages are underreporting decoded sizes and will be unable 43 43 // to prune because these functions are not implemented yet. 44 virtual void destroyDecodedData(bool incremental = false, bool preserveNearbyFrames = false) { }44 virtual void destroyDecodedData(bool destroyAll = true) { } 45 45 virtual unsigned decodedSize() const { return 0; } 46 46 -
trunk/WebCore/platform/image-decoders/ImageDecoder.h
r28068 r39309 54 54 m_disposalMethod(DisposeNotSpecified), m_hasAlpha(false) 55 55 {} 56 57 void clear() { 58 m_bytes.clear(); 59 m_rect = IntRect(); 60 m_height = 0; 61 m_status = FrameEmpty; 62 m_duration = 0; 63 m_disposalMethod = DisposeNotSpecified; 64 m_hasAlpha = false; 65 } 56 66 57 67 const RGBA32Array& bytes() const { return m_bytes; } … … 136 146 void setFailed() { m_failed = true; } 137 147 148 // Wipe out frames in the frame buffer cache before |clearBeforeFrame|, 149 // assuming this can be done without breaking decoding. Different decoders 150 // place different restrictions on what frames are safe to destroy, so this 151 // is left to them to implement. 152 // For convenience's sake, we provide a default (empty) implementation, 153 // since in practice only GIFs will ever use this. 154 virtual void clearFrameBufferCache(size_t clearBeforeFrame) { } 155 138 156 protected: 139 157 RefPtr<SharedBuffer> m_data; // The encoded data. -
trunk/WebCore/platform/image-decoders/gif/GIFImageDecoder.cpp
r34200 r39309 157 157 // responsible for waiting until image decoding has finished to ask this if 158 158 // it needs an authoritative answer. In the meantime, we should default to 159 // "loop once", both in the reader and here. 160 if (m_reader) 161 m_repetitionCount = m_reader->repetitionCount(); 159 // "loop once". 160 if (m_reader) { 161 // Added wrinkle: ImageSource::clear() may destroy the reader, making 162 // the result from the reader _less_ authoritative on future calls. To 163 // detect this, the reader returns cLoopCountNotSeen (-2) instead of 164 // cAnimationLoopOnce (-1) when its current incarnation hasn't actually 165 // seen a loop count yet; in this case we return our previously-cached 166 // value. 167 const int repetitionCount = m_reader->repetitionCount(); 168 if (repetitionCount != cLoopCountNotSeen) 169 m_repetitionCount = repetitionCount; 170 } 162 171 return m_repetitionCount; 163 172 } … … 173 182 decode(GIFFullQuery, index+1); 174 183 return &frame; 184 } 185 186 void GIFImageDecoder::clearFrameBufferCache(size_t clearBeforeFrame) 187 { 188 // In some cases, like if the decoder was destroyed while animating, we 189 // can be asked to clear more frames than we currently have. 190 if (m_frameBufferCache.isEmpty()) 191 return; // Nothing to do. 192 // The "-1" here is tricky. It does not mean that |clearBeforeFrame| is the 193 // last frame we wish to preserve, but rather that we never want to clear 194 // the very last frame in the cache: it's empty (so clearing it is 195 // pointless), it's partial (so we don't want to clear it anyway), or the 196 // cache could be enlarged with a future setData() call and it could be 197 // needed to construct the next frame (see comments below). Callers can 198 // always use ImageSource::clear(true, ...) to completely free the memory in 199 // this case. 200 clearBeforeFrame = std::min(clearBeforeFrame, m_frameBufferCache.size() - 1); 201 const Vector<RGBA32Buffer>::iterator end(m_frameBufferCache.begin() + clearBeforeFrame); 202 for (Vector<RGBA32Buffer>::iterator i(m_frameBufferCache.begin()); i != end; ++i) { 203 if (i->status() == RGBA32Buffer::FrameEmpty) 204 continue; // Nothing to do. 205 206 // The layout of frames is: 207 // [empty frames][complete frames][partial frame][empty frames] 208 // ...where each of these groups may be empty. We should not clear a 209 // partial frame since that's what's being decoded right now, and we 210 // also should not clear the last complete frame, since it may be needed 211 // when constructing the next frame. Note that "i + 1" is safe since 212 // i < end < m_frameBufferCache.end(). 213 if ((i->status() == RGBA32Buffer::FramePartial) || ((i + 1)->status() != RGBA32Buffer::FrameComplete)) 214 break; 215 216 i->clear(); 217 } 175 218 } 176 219 … … 229 272 // DisposeOverwriteBgcolor below and reset to a completely empty image.) 230 273 const RGBA32Buffer* prevBuffer = &m_frameBufferCache[--frameIndex]; 274 ASSERT(prevBuffer->status() == RGBA32Buffer::FrameComplete); 231 275 RGBA32Buffer::FrameDisposalMethod prevMethod = 232 276 prevBuffer->disposalMethod(); -
trunk/WebCore/platform/image-decoders/gif/GIFImageDecoder.h
r34200 r39309 55 55 virtual RGBA32Buffer* frameBufferAtIndex(size_t index); 56 56 57 virtual void clearFrameBufferCache(size_t clearBeforeFrame); 58 57 59 virtual unsigned frameDurationAtIndex(size_t index) { return 0; } 58 60 -
trunk/WebCore/platform/image-decoders/gif/GIFImageReader.h
r28068 r39309 47 47 #define MAX_COLORS 256 48 48 #define MAX_HOLD_SIZE 256 49 50 const int cLoopCountNotSeen = -2; 49 51 50 52 /* gif2.h … … 188 190 screen_width = screen_height = 0; 189 191 global_colormap_size = images_decoded = images_count = 0; 190 loop_count = -1;192 loop_count = cLoopCountNotSeen; 191 193 count = 0; 192 194 } -
trunk/WebCore/svg/graphics/SVGImage.h
r37612 r39309 60 60 // FIXME: SVGImages are underreporting decoded sizes and will be unable 61 61 // to prune because these functions are not implemented yet. 62 virtual void destroyDecodedData(bool incremental = false, bool preserveNearbyFrames = false) { }62 virtual void destroyDecodedData(bool destroyAll = true) { } 63 63 virtual unsigned decodedSize() const { return 0; } 64 64
Note: See TracChangeset
for help on using the changeset viewer.