blob: 0f93f18f4ff5da5da8a6c6ccfec65ceb392064ea [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "platform/graphics/paint/PaintController.h"
#include "platform/TraceEvent.h"
#include "platform/graphics/GraphicsLayer.h"
#include "platform/graphics/paint/DrawingDisplayItem.h"
#include "third_party/skia/include/core/SkPictureAnalyzer.h"
#ifndef NDEBUG
#include "platform/graphics/LoggingCanvas.h"
#include "wtf/text/StringBuilder.h"
#include <stdio.h>
#endif
namespace blink {
static PaintChunker::ItemBehavior behaviorOfItemType(DisplayItem::Type type)
{
if (DisplayItem::isForeignLayerType(type))
return PaintChunker::RequiresSeparateChunk;
return PaintChunker::DefaultBehavior;
}
const PaintArtifact& PaintController::paintArtifact() const
{
DCHECK(m_newDisplayItemList.isEmpty());
DCHECK(m_newPaintChunks.isInInitialState());
return m_currentPaintArtifact;
}
bool PaintController::lastDisplayItemIsNoopBegin() const
{
if (m_newDisplayItemList.isEmpty())
return false;
const auto& lastDisplayItem = m_newDisplayItemList.last();
return lastDisplayItem.isBegin() && !lastDisplayItem.drawsContent();
}
void PaintController::removeLastDisplayItem()
{
if (m_newDisplayItemList.isEmpty())
return;
#if DCHECK_IS_ON()
// Also remove the index pointing to the removed display item.
DisplayItemIndicesByClientMap::iterator it = m_newDisplayItemIndicesByClient.find(&m_newDisplayItemList.last().client());
if (it != m_newDisplayItemIndicesByClient.end()) {
Vector<size_t>& indices = it->value;
if (!indices.isEmpty() && indices.last() == (m_newDisplayItemList.size() - 1))
indices.removeLast();
}
#endif
m_newDisplayItemList.removeLast();
if (RuntimeEnabledFeatures::slimmingPaintV2Enabled())
m_newPaintChunks.decrementDisplayItemIndex();
}
void PaintController::processNewItem(DisplayItem& displayItem)
{
DCHECK(!m_constructionDisabled);
DCHECK(!skippingCache() || !displayItem.isCached());
#if CHECK_DISPLAY_ITEM_CLIENT_ALIVENESS
if (!skippingCache() && (displayItem.isCacheable() || displayItem.isCached()))
displayItem.client().beginShouldKeepAlive();
#endif
if (displayItem.isCached())
++m_numCachedNewItems;
#if DCHECK_IS_ON()
// Verify noop begin/end pairs have been removed.
if (m_newDisplayItemList.size() >= 2 && displayItem.isEnd()) {
const auto& beginDisplayItem = m_newDisplayItemList[m_newDisplayItemList.size() - 2];
if (beginDisplayItem.isBegin() && beginDisplayItem.getType() != DisplayItem::Subsequence && !beginDisplayItem.drawsContent())
DCHECK(!displayItem.isEndAndPairedWith(beginDisplayItem.getType()));
}
#endif
if (!m_scopeStack.isEmpty())
displayItem.setScope(m_scopeStack.last());
#if DCHECK_IS_ON()
size_t index = findMatchingItemFromIndex(displayItem.nonCachedId(), m_newDisplayItemIndicesByClient, m_newDisplayItemList);
if (index != kNotFound) {
#ifndef NDEBUG
showDebugData();
WTFLogAlways("DisplayItem %s has duplicated id with previous %s (index=%d)\n",
displayItem.asDebugString().utf8().data(), m_newDisplayItemList[index].asDebugString().utf8().data(), static_cast<int>(index));
#endif
NOTREACHED();
}
addItemToIndexIfNeeded(displayItem, m_newDisplayItemList.size() - 1, m_newDisplayItemIndicesByClient);
#endif // DCHECK_IS_ON()
if (skippingCache())
displayItem.setSkippedCache();
if (RuntimeEnabledFeatures::slimmingPaintV2Enabled())
m_newPaintChunks.incrementDisplayItemIndex(behaviorOfItemType(displayItem.getType()));
}
void PaintController::updateCurrentPaintChunkProperties(const PaintChunkProperties& newProperties)
{
m_newPaintChunks.updateCurrentPaintChunkProperties(newProperties);
}
const PaintChunkProperties& PaintController::currentPaintChunkProperties() const
{
return m_newPaintChunks.currentPaintChunkProperties();
}
void PaintController::beginScope()
{
SECURITY_DCHECK(m_nextScope < UINT_MAX);
m_scopeStack.append(m_nextScope++);
beginSkippingCache();
}
void PaintController::endScope()
{
m_scopeStack.removeLast();
endSkippingCache();
}
void PaintController::displayItemClientWasInvalidated(const DisplayItemClient& client)
{
#if DCHECK_IS_ON()
// Slimming paint v1 CompositedLayerMapping may invalidate client on extra layers.
if (RuntimeEnabledFeatures::slimmingPaintV2Enabled() || clientCacheIsValid(client))
m_invalidations.append(client.debugName());
// Should not invalidate already painted clients.
DCHECK(!m_newDisplayItemIndicesByClient.contains(&client));
#endif
}
void PaintController::invalidateAll()
{
// Can only be called during layout/paintInvalidation, not during painting.
DCHECK(m_newDisplayItemList.isEmpty());
m_currentPaintArtifact.reset();
m_currentCacheGeneration.invalidate();
}
bool PaintController::clientCacheIsValid(const DisplayItemClient& client) const
{
#if CHECK_DISPLAY_ITEM_CLIENT_ALIVENESS
CHECK(client.isAlive());
#endif
if (skippingCache())
return false;
return client.displayItemsAreCached(m_currentCacheGeneration);
}
void PaintController::invalidatePaintOffset(const DisplayItemClient& client)
{
DCHECK(RuntimeEnabledFeatures::slimmingPaintInvalidationEnabled());
displayItemClientWasInvalidated(client);
client.setDisplayItemsUncached();
#if DCHECK_IS_ON()
DCHECK(!paintOffsetWasInvalidated(client));
m_clientsWithPaintOffsetInvalidations.add(&client);
#endif
}
#if DCHECK_IS_ON()
bool PaintController::paintOffsetWasInvalidated(const DisplayItemClient& client) const
{
DCHECK(RuntimeEnabledFeatures::slimmingPaintInvalidationEnabled());
return m_clientsWithPaintOffsetInvalidations.contains(&client);
}
#endif
size_t PaintController::findMatchingItemFromIndex(const DisplayItem::Id& id, const DisplayItemIndicesByClientMap& displayItemIndicesByClient, const DisplayItemList& list)
{
DisplayItemIndicesByClientMap::const_iterator it = displayItemIndicesByClient.find(&id.client);
if (it == displayItemIndicesByClient.end())
return kNotFound;
const Vector<size_t>& indices = it->value;
for (size_t index : indices) {
const DisplayItem& existingItem = list[index];
DCHECK(!existingItem.hasValidClient() || existingItem.client() == id.client);
if (id.matches(existingItem))
return index;
}
return kNotFound;
}
void PaintController::addItemToIndexIfNeeded(const DisplayItem& displayItem, size_t index, DisplayItemIndicesByClientMap& displayItemIndicesByClient)
{
if (!displayItem.isCacheable())
return;
DisplayItemIndicesByClientMap::iterator it = displayItemIndicesByClient.find(&displayItem.client());
Vector<size_t>& indices = it == displayItemIndicesByClient.end() ?
displayItemIndicesByClient.add(&displayItem.client(), Vector<size_t>()).storedValue->value : it->value;
indices.append(index);
}
struct PaintController::OutOfOrderIndexContext {
STACK_ALLOCATED();
OutOfOrderIndexContext(DisplayItemList::iterator begin) : nextItemToIndex(begin) { }
DisplayItemList::iterator nextItemToIndex;
DisplayItemIndicesByClientMap displayItemIndicesByClient;
};
DisplayItemList::iterator PaintController::findOutOfOrderCachedItem(const DisplayItem::Id& id, OutOfOrderIndexContext& context)
{
DCHECK(clientCacheIsValid(id.client));
size_t foundIndex = findMatchingItemFromIndex(id, context.displayItemIndicesByClient, m_currentPaintArtifact.getDisplayItemList());
if (foundIndex != kNotFound)
return m_currentPaintArtifact.getDisplayItemList().begin() + foundIndex;
return findOutOfOrderCachedItemForward(id, context);
}
// Find forward for the item and index all skipped indexable items.
DisplayItemList::iterator PaintController::findOutOfOrderCachedItemForward(const DisplayItem::Id& id, OutOfOrderIndexContext& context)
{
DisplayItemList::iterator currentEnd = m_currentPaintArtifact.getDisplayItemList().end();
for (; context.nextItemToIndex != currentEnd; ++context.nextItemToIndex) {
const DisplayItem& item = *context.nextItemToIndex;
DCHECK(item.hasValidClient());
if (id.matches(item))
return context.nextItemToIndex++;
if (item.isCacheable())
addItemToIndexIfNeeded(item, context.nextItemToIndex - m_currentPaintArtifact.getDisplayItemList().begin(), context.displayItemIndicesByClient);
}
return currentEnd;
}
void PaintController::copyCachedSubsequence(const DisplayItemList& currentList, DisplayItemList::iterator& currentIt, DisplayItemList& updatedList, SkPictureGpuAnalyzer& gpuAnalyzer)
{
DCHECK(currentIt->getType() == DisplayItem::Subsequence);
DCHECK(!currentIt->scope());
DisplayItem::Id endSubsequenceId(currentIt->client(), DisplayItem::EndSubsequence, 0);
do {
// We should always find the EndSubsequence display item.
DCHECK(currentIt != m_currentPaintArtifact.getDisplayItemList().end());
DCHECK(currentIt->hasValidClient());
updatedList.appendByMoving(*currentIt, currentList.visualRect(currentIt - m_currentPaintArtifact.getDisplayItemList().begin()), gpuAnalyzer);
++currentIt;
} while (!endSubsequenceId.matches(updatedList.last()));
}
static IntRect visualRectForDisplayItem(const DisplayItem& displayItem, const LayoutSize& offsetFromLayoutObject)
{
LayoutRect visualRect = displayItem.client().visualRect();
visualRect.move(-offsetFromLayoutObject);
return enclosingIntRect(visualRect);
}
// Update the existing display items by removing invalidated entries, updating
// repainted ones, and appending new items.
// - For cached drawing display item, copy the corresponding cached DrawingDisplayItem;
// - For cached subsequence display item, copy the cached display items between the
// corresponding SubsequenceDisplayItem and EndSubsequenceDisplayItem (incl.);
// - Otherwise, copy the new display item.
//
// The algorithm is O(|m_currentDisplayItemList| + |m_newDisplayItemList|).
// Coefficients are related to the ratio of out-of-order CachedDisplayItems
// and the average number of (Drawing|Subsequence)DisplayItems per client.
//
void PaintController::commitNewDisplayItems(const LayoutSize& offsetFromLayoutObject)
{
TRACE_EVENT2("blink,benchmark", "PaintController::commitNewDisplayItems",
"current_display_list_size", (int)m_currentPaintArtifact.getDisplayItemList().size(),
"num_non_cached_new_items", (int)m_newDisplayItemList.size() - m_numCachedNewItems);
m_numCachedNewItems = 0;
// These data structures are used during painting only.
DCHECK(m_scopeStack.isEmpty());
m_scopeStack.clear();
m_nextScope = 1;
DCHECK(!skippingCache());
#if DCHECK_IS_ON()
m_newDisplayItemIndicesByClient.clear();
m_clientsWithPaintOffsetInvalidations.clear();
m_invalidations.clear();
#endif
SkPictureGpuAnalyzer gpuAnalyzer;
if (m_currentPaintArtifact.isEmpty()) {
#if DCHECK_IS_ON()
for (const auto& item : m_newDisplayItemList)
DCHECK(!item.isCached());
#endif
for (const auto& item : m_newDisplayItemList) {
m_newDisplayItemList.appendVisualRect(visualRectForDisplayItem(item, offsetFromLayoutObject));
// No reason to continue the analysis once we have a veto.
if (gpuAnalyzer.suitableForGpuRasterization())
item.analyzeForGpuRasterization(gpuAnalyzer);
}
m_currentPaintArtifact = PaintArtifact(std::move(m_newDisplayItemList), m_newPaintChunks.releasePaintChunks(), gpuAnalyzer.suitableForGpuRasterization());
m_newDisplayItemList = DisplayItemList(kInitialDisplayItemListCapacityBytes);
updateCacheGeneration();
return;
}
// Stores indices to valid DrawingDisplayItems in m_currentDisplayItems that have not been matched
// by CachedDisplayItems during synchronized matching. The indexed items will be matched
// by later out-of-order CachedDisplayItems in m_newDisplayItemList. This ensures that when
// out-of-order CachedDisplayItems occur, we only traverse at most once over m_currentDisplayItems
// looking for potential matches. Thus we can ensure that the algorithm runs in linear time.
OutOfOrderIndexContext outOfOrderIndexContext(m_currentPaintArtifact.getDisplayItemList().begin());
// TODO(jbroman): Consider revisiting this heuristic.
DisplayItemList updatedList(std::max(m_currentPaintArtifact.getDisplayItemList().usedCapacityInBytes(), m_newDisplayItemList.usedCapacityInBytes()));
Vector<PaintChunk> updatedPaintChunks;
DisplayItemList::iterator currentIt = m_currentPaintArtifact.getDisplayItemList().begin();
DisplayItemList::iterator currentEnd = m_currentPaintArtifact.getDisplayItemList().end();
for (DisplayItemList::iterator newIt = m_newDisplayItemList.begin(); newIt != m_newDisplayItemList.end(); ++newIt) {
const DisplayItem& newDisplayItem = *newIt;
const DisplayItem::Id newDisplayItemId = newDisplayItem.nonCachedId();
bool newDisplayItemHasCachedType = newDisplayItem.getType() != newDisplayItemId.type;
bool isSynchronized = currentIt != currentEnd && newDisplayItemId.matches(*currentIt);
if (newDisplayItemHasCachedType) {
#if DCHECK_IS_ON()
DCHECK(newDisplayItem.isCached());
DCHECK(clientCacheIsValid(newDisplayItem.client()) || (RuntimeEnabledFeatures::slimmingPaintInvalidationEnabled() && !paintOffsetWasInvalidated(newDisplayItem.client())));
#endif
if (!isSynchronized) {
currentIt = findOutOfOrderCachedItem(newDisplayItemId, outOfOrderIndexContext);
if (currentIt == currentEnd) {
#ifndef NDEBUG
showDebugData();
WTFLogAlways("%s not found in m_currentDisplayItemList\n", newDisplayItem.asDebugString().utf8().data());
#endif
NOTREACHED();
// We did not find the cached display item. This should be impossible, but may occur if there is a bug
// in the system, such as under-invalidation, incorrect cache checking or duplicate display ids.
// In this case, attempt to recover rather than crashing or bailing on display of the rest of the display list.
continue;
}
}
#if DCHECK_IS_ON()
if (RuntimeEnabledFeatures::slimmingPaintUnderInvalidationCheckingEnabled()) {
DisplayItemList::iterator temp = currentIt;
checkUnderInvalidation(newIt, temp);
}
#endif
if (newDisplayItem.isCachedDrawing()) {
updatedList.appendByMoving(*currentIt, m_currentPaintArtifact.getDisplayItemList().visualRect(currentIt - m_currentPaintArtifact.getDisplayItemList().begin()),
gpuAnalyzer);
++currentIt;
} else {
DCHECK(newDisplayItem.getType() == DisplayItem::CachedSubsequence);
copyCachedSubsequence(m_currentPaintArtifact.getDisplayItemList(), currentIt, updatedList, gpuAnalyzer);
DCHECK(updatedList.last().getType() == DisplayItem::EndSubsequence);
}
} else {
#if DCHECK_IS_ON()
DCHECK(!newDisplayItem.isDrawing()
|| newDisplayItem.skippedCache()
|| !clientCacheIsValid(newDisplayItem.client())
|| (RuntimeEnabledFeatures::slimmingPaintInvalidationEnabled() && paintOffsetWasInvalidated(newDisplayItem.client())));
#endif
updatedList.appendByMoving(*newIt, visualRectForDisplayItem(*newIt, offsetFromLayoutObject), gpuAnalyzer);
if (isSynchronized)
++currentIt;
}
// Items before currentIt should have been copied so we don't need to index them.
if (currentIt - outOfOrderIndexContext.nextItemToIndex > 0)
outOfOrderIndexContext.nextItemToIndex = currentIt;
}
#if DCHECK_IS_ON()
if (RuntimeEnabledFeatures::slimmingPaintUnderInvalidationCheckingEnabled())
checkNoRemainingCachedDisplayItems();
#endif
// TODO(jbroman): When subsequence caching applies to SPv2, we'll need to
// merge the paint chunks as well.
m_currentPaintArtifact = PaintArtifact(std::move(updatedList), m_newPaintChunks.releasePaintChunks(), gpuAnalyzer.suitableForGpuRasterization());
m_newDisplayItemList = DisplayItemList(kInitialDisplayItemListCapacityBytes);
updateCacheGeneration();
}
size_t PaintController::approximateUnsharedMemoryUsage() const
{
size_t memoryUsage = sizeof(*this);
// Memory outside this class due to m_currentPaintArtifact.
memoryUsage += m_currentPaintArtifact.approximateUnsharedMemoryUsage() - sizeof(m_currentPaintArtifact);
// TODO(jbroman): If display items begin to have significant external memory
// usage that's not shared with the embedder, we should account for it here.
//
// External objects, shared with the embedder, such as SkPicture, should be
// excluded to avoid double counting. It is the embedder's responsibility to
// count such objects.
//
// At time of writing, the only known case of unshared external memory was
// the rounded clips vector in ClipDisplayItem, which is not expected to
// contribute significantly to memory usage.
// Memory outside this class due to m_newDisplayItemList.
DCHECK(m_newDisplayItemList.isEmpty());
memoryUsage += m_newDisplayItemList.memoryUsageInBytes();
return memoryUsage;
}
void PaintController::updateCacheGeneration()
{
m_currentCacheGeneration = DisplayItemCacheGeneration::next();
for (const DisplayItem& displayItem : m_currentPaintArtifact.getDisplayItemList()) {
if (!displayItem.isCacheable())
continue;
displayItem.client().setDisplayItemsCached(m_currentCacheGeneration);
#if CHECK_DISPLAY_ITEM_CLIENT_ALIVENESS
displayItem.client().endShouldKeepAlive();
#endif
}
}
#if DCHECK_IS_ON()
void PaintController::checkUnderInvalidation(DisplayItemList::iterator& newIt, DisplayItemList::iterator& currentIt)
{
DCHECK(RuntimeEnabledFeatures::slimmingPaintUnderInvalidationCheckingEnabled());
DCHECK(newIt->isCached());
// When under-invalidation-checking is enabled, the forced painting is following the cached display item.
DisplayItem::Type nextItemType = DisplayItem::nonCachedType(newIt->getType());
++newIt;
DCHECK(newIt->getType() == nextItemType);
if (newIt->isDrawing()) {
checkCachedDisplayItemIsUnchanged("", *newIt, *currentIt);
return;
}
DCHECK(newIt->getType() == DisplayItem::Subsequence);
#ifndef NDEBUG
CString messagePrefix = String::format("(In CachedSubsequence of %s)", newIt->clientDebugString().utf8().data()).utf8();
#else
CString messagePrefix = "(In CachedSubsequence)";
#endif
DisplayItem::Id endSubsequenceId(newIt->client(), DisplayItem::EndSubsequence, 0);
while (true) {
DCHECK(newIt != m_newDisplayItemList.end());
if (newIt->isCached())
checkUnderInvalidation(newIt, currentIt);
else
checkCachedDisplayItemIsUnchanged(messagePrefix.data(), *newIt, *currentIt);
if (endSubsequenceId.matches(*newIt))
break;
++newIt;
++currentIt;
}
}
static void showUnderInvalidationError(const char* messagePrefix, const char* reason, const DisplayItem* newItem, const DisplayItem* oldItem)
{
#ifndef NDEBUG
WTFLogAlways("%s %s:\nNew display item: %s\nOld display item: %s\nSee http://crbug.com/450725.", messagePrefix, reason,
newItem ? newItem->asDebugString().utf8().data() : "None",
oldItem ? oldItem->asDebugString().utf8().data() : "None");
#else
WTFLogAlways("%s %s. Run debug build to get more details\nSee http://crbug.com/450725.", messagePrefix, reason);
#endif // NDEBUG
}
void PaintController::checkCachedDisplayItemIsUnchanged(const char* messagePrefix, const DisplayItem& newItem, const DisplayItem& oldItem)
{
DCHECK(RuntimeEnabledFeatures::slimmingPaintUnderInvalidationCheckingEnabled());
DCHECK(!newItem.isCached());
DCHECK(!oldItem.isCached());
if (newItem.skippedCache()) {
showUnderInvalidationError(messagePrefix, "ERROR: under-invalidation: skipped-cache in cached subsequence", &newItem, &oldItem);
NOTREACHED();
}
if (newItem.isCacheable() && !clientCacheIsValid(newItem.client())) {
showUnderInvalidationError(messagePrefix, "ERROR: under-invalidation: invalidated in cached subsequence", &newItem, &oldItem);
NOTREACHED();
}
if (newItem.equals(oldItem))
return;
showUnderInvalidationError(messagePrefix, "ERROR: under-invalidation: display item changed", &newItem, &oldItem);
#ifndef NDEBUG
if (newItem.isDrawing()) {
RefPtr<const SkPicture> newPicture = static_cast<const DrawingDisplayItem&>(newItem).picture();
RefPtr<const SkPicture> oldPicture = static_cast<const DrawingDisplayItem&>(oldItem).picture();
String oldPictureDebugString = oldPicture ? pictureAsDebugString(oldPicture.get()) : "None";
String newPictureDebugString = newPicture ? pictureAsDebugString(newPicture.get()) : "None";
WTFLogAlways("old picture:\n%s\n", oldPictureDebugString.utf8().data());
WTFLogAlways("new picture:\n%s\n", newPictureDebugString.utf8().data());
}
#endif // NDEBUG
NOTREACHED();
}
void PaintController::checkNoRemainingCachedDisplayItems()
{
DCHECK(RuntimeEnabledFeatures::slimmingPaintUnderInvalidationCheckingEnabled());
for (const auto& displayItem : m_currentPaintArtifact.getDisplayItemList()) {
if (!displayItem.hasValidClient() || !displayItem.isCacheable() || !clientCacheIsValid(displayItem.client()))
continue;
showUnderInvalidationError("", "May be under-invalidation: no new display item", nullptr, &displayItem);
}
}
#endif // DCHECK_IS_ON()
#ifndef NDEBUG
WTF::String PaintController::displayItemListAsDebugString(const DisplayItemList& list) const
{
StringBuilder stringBuilder;
size_t i = 0;
for (auto it = list.begin(); it != list.end(); ++it, ++i) {
const DisplayItem& displayItem = *it;
if (i)
stringBuilder.append(",\n");
stringBuilder.append(String::format("{index: %d, ", (int)i));
displayItem.dumpPropertiesAsDebugString(stringBuilder);
if (displayItem.hasValidClient()) {
stringBuilder.append(", cacheIsValid: ");
stringBuilder.append(clientCacheIsValid(displayItem.client()) ? "true" : "false");
}
stringBuilder.append('}');
}
return stringBuilder.toString();
}
void PaintController::showDebugData() const
{
WTFLogAlways("current display item list: [%s]\n", displayItemListAsDebugString(m_currentPaintArtifact.getDisplayItemList()).utf8().data());
WTFLogAlways("new display item list: [%s]\n", displayItemListAsDebugString(m_newDisplayItemList).utf8().data());
}
#endif // ifndef NDEBUG
} // namespace blink