/****************************************************************************
**
** Copyright (C) 2013 Digia Plc
** All rights reserved.
** For any questions to Digia, please use contact form at http://qt.digia.com
**
** This file is part of the Qt Enterprise Qt Quick Profiler Add-on.
**
** Licensees holding valid Qt Enterprise licenses may use this file in
** accordance with the Qt Enterprise License Agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Digia.
**
** If you have questions regarding the use of this file, please use
** contact form at http://qt.digia.com
**
****************************************************************************/
#include "pixmapcachemodel.h"
#include "qmldebug/qmlprofilereventtypes.h"
#include "qmlprofiler/qmlprofilermodelmanager.h"
namespace QmlProfilerExtension {
namespace Internal {
using namespace QmlProfiler;
PixmapCacheModel::PixmapCacheModel(QmlProfilerModelManager *manager, QObject *parent) :
QmlProfilerTimelineModel(manager,
tr(QmlProfilerModelManager::featureName(QmlDebug::ProfilePixmapCache)),
QmlDebug::PixmapCacheEvent, QmlDebug::MaximumRangeType, parent)
{
m_maxCacheSize = 1;
announceFeatures(1 << QmlDebug::ProfilePixmapCache);
}
int PixmapCacheModel::rowMaxValue(int rowNumber) const
{
if (rowNumber == 1) {
return m_maxCacheSize;
} else {
return QmlProfilerTimelineModel::rowMaxValue(rowNumber);
}
}
int PixmapCacheModel::row(int index) const
{
if (expanded())
return selectionId(index) + 1;
return m_data[index].rowNumberCollapsed;
}
int PixmapCacheModel::typeId(int index) const
{
return m_data[index].typeId;
}
QColor PixmapCacheModel::color(int index) const
{
if (m_data[index].pixmapEventType == PixmapCacheCountChanged)
return colorByHue(s_pixmapCacheCountHue);
return colorBySelectionId(index);
}
float PixmapCacheModel::relativeHeight(int index) const
{
if (m_data[index].pixmapEventType == PixmapCacheCountChanged)
return (float)m_data[index].cacheSize / (float)m_maxCacheSize;
else
return 1.0f;
}
QString getFilenameOnly(QString absUrl)
{
int characterPos = absUrl.lastIndexOf(QLatin1Char('/'))+1;
if (characterPos < absUrl.length())
absUrl = absUrl.mid(characterPos);
return absUrl;
}
QVariantList PixmapCacheModel::labels() const
{
QVariantList result;
// Cache Size
QVariantMap element;
element.insert(QLatin1String("description"), QVariant(QLatin1String("Cache Size")));
element.insert(QLatin1String("id"), QVariant(0));
result << element;
for (int i=0; i < m_pixmaps.count(); i++) {
// Loading
QVariantMap element;
element.insert(QLatin1String("displayName"), m_pixmaps[i].url);
element.insert(QLatin1String("description"),
QVariant(getFilenameOnly(m_pixmaps[i].url)));
element.insert(QLatin1String("id"), QVariant(i+1));
result << element;
}
return result;
}
QVariantMap PixmapCacheModel::details(int index) const
{
QVariantMap result;
const PixmapCacheEvent *ev = &m_data[index];
if (ev->pixmapEventType == PixmapCacheCountChanged) {
result.insert(QLatin1String("displayName"), tr("Image Cached"));
} else {
if (ev->pixmapEventType == PixmapLoadingStarted) {
result.insert(QLatin1String("displayName"), tr("Image Loaded"));
if (m_pixmaps[ev->urlIndex].sizes[ev->sizeIndex].loadState != Finished)
result.insert(tr("Result"), tr("Load Error"));
}
result.insert(tr("Duration"), QmlProfilerBaseModel::formatTime(duration(index)));
}
result.insert(tr("Cache Size"), QString::fromLatin1("%1 px").arg(ev->cacheSize));
result.insert(tr("File"), getFilenameOnly(m_pixmaps[ev->urlIndex].url));
result.insert(tr("Width"), QString::fromLatin1("%1 px")
.arg(m_pixmaps[ev->urlIndex].sizes[ev->sizeIndex].size.width()));
result.insert(tr("Height"), QString::fromLatin1("%1 px")
.arg(m_pixmaps[ev->urlIndex].sizes[ev->sizeIndex].size.height()));
return result;
}
/* Ultimately there is no way to know which cache entry a given event refers to as long as we only
* receive the pixmap URL from the application. Multiple copies of different sizes may be cached
* for each URL. However, we can apply some heuristics to make the result somewhat plausible by
* using the following assumptions:
*
* - PixmapSizeKnown will happen at most once for every cache entry.
* - PixmapSizeKnown cannot happen for entries with PixmapLoadingError and vice versa.
* - PixmapCacheCountChanged can happen for entries with PixmapLoadingError but doesn't have to.
* - Decreasing PixmapCacheCountChanged events can only happen for entries that have seen an
* increasing PixmapCacheCountChanged (but that may have happened before the trace).
* - PixmapCacheCountChanged can happen before or after PixmapSizeKnown.
* - For every PixmapLoadingFinished or PixmapLoadingError there is exactly one
* PixmapLoadingStarted event, but it may be before the trace.
* - For every PixmapLoadingStarted there is exactly one PixmapLoadingFinished or
* PixmapLoadingError, but it may be after the trace.
* - Decreasing PixmapCacheCountChanged events in the presence of corrupt cache entries are more
* likely to clear those entries than other, correctly loaded ones.
* - Increasing PixmapCacheCountChanged events are more likely to refer to correctly loaded entries
* than to ones with PixmapLoadingError.
* - PixmapLoadingFinished and PixmapLoadingError are more likely to refer to cache entries that
* have seen a PixmapLoadingStarted than to ones that haven't.
*
* For each URL we keep an ordered list of pixmaps possibly being loaded and assign new events to
* the first entry that "fits". If multiple sizes of the same pixmap are being loaded concurrently
* we generally assume that the PixmapLoadingFinished and PixmapLoadingError events occur in the
* order we learn about the existence of these sizes, subject to the above constraints. This is not
* necessarily the order the pixmaps are really loaded but it's the best we can do with the given
* information. If they're loaded sequentially the representation is correct.
*/
void PixmapCacheModel::loadData()
{
clear();
QmlProfilerDataModel *simpleModel = modelManager()->qmlModel();
if (simpleModel->isEmpty())
return;
int lastCacheSizeEvent = -1;
int cumulatedCount = 0;
const QVector &types = simpleModel->getEventTypes();
foreach (const QmlProfilerDataModel::QmlEventData &event, simpleModel->getEvents()) {
const QmlProfilerDataModel::QmlEventTypeData &type = types[event.typeIndex];
if (!accepted(type))
continue;
PixmapCacheEvent newEvent;
newEvent.pixmapEventType = static_cast(type.detailType);
qint64 pixmapStartTime = event.startTime;
newEvent.urlIndex = -1;
for (QVector::const_iterator it(m_pixmaps.cend()); it != m_pixmaps.cbegin();) {
if ((--it)->url == type.location.filename) {
newEvent.urlIndex = it - m_pixmaps.cbegin();
break;
}
}
newEvent.sizeIndex = -1;
if (newEvent.urlIndex == -1) {
newEvent.urlIndex = m_pixmaps.count();
m_pixmaps << Pixmap(type.location.filename);
}
Pixmap &pixmap = m_pixmaps[newEvent.urlIndex];
switch (newEvent.pixmapEventType) {
case PixmapSizeKnown: {// pixmap size
// Look for pixmaps for which we don't know the size, yet and which have actually been
// loaded.
for (QVector::iterator i(pixmap.sizes.begin());
i != pixmap.sizes.end(); ++i) {
if (i->size.isValid() || i->cacheState == Uncacheable || i->cacheState == Corrupt)
continue;
// We can't have cached it before we knew the size
Q_ASSERT(i->cacheState != Cached);
i->size.setWidth(event.numericData1);
i->size.setHeight(event.numericData2);
newEvent.sizeIndex = i - pixmap.sizes.begin();
break;
}
if (newEvent.sizeIndex == -1) {
newEvent.sizeIndex = pixmap.sizes.length();
pixmap.sizes << PixmapState(event.numericData1, event.numericData2);
}
PixmapState &state = pixmap.sizes[newEvent.sizeIndex];
if (state.cacheState == ToBeCached) {
lastCacheSizeEvent = updateCacheCount(lastCacheSizeEvent, pixmapStartTime,
state.size.width() * state.size.height(), newEvent,
event.typeIndex);
state.cacheState = Cached;
}
break;
}
case PixmapCacheCountChanged: {// Cache Size Changed Event
pixmapStartTime = event.startTime + 1; // delay 1 ns for proper sorting
bool uncache = cumulatedCount > event.numericData3;
cumulatedCount = event.numericData3;
qint64 pixSize = 0;
// First try to find a preferred pixmap, which either is Corrupt and will be uncached
// or is uncached and will be cached.
for (QVector::iterator i(pixmap.sizes.begin());
i != pixmap.sizes.end(); ++i) {
if (uncache && i->cacheState == Corrupt) {
newEvent.sizeIndex = i - pixmap.sizes.begin();
i->cacheState = Uncacheable;
break;
} else if (!uncache && i->cacheState == Uncached) {
newEvent.sizeIndex = i - pixmap.sizes.begin();
if (i->size.isValid()) {
pixSize = i->size.width() * i->size.height();
i->cacheState = Cached;
} else {
i->cacheState = ToBeCached;
}
break;
}
}
// If none found, check for cached or ToBeCached pixmaps that shall be uncached or
// Error pixmaps that become corrupt cache entries. We also accept Initial to be
// uncached as we may have missed the matching PixmapCacheCountChanged that cached it.
if (newEvent.sizeIndex == -1) {
for (QVector::iterator i(pixmap.sizes.begin());
i != pixmap.sizes.end(); ++i) {
if (uncache && (i->cacheState == Cached || i->cacheState == ToBeCached ||
i->cacheState == Uncached)) {
newEvent.sizeIndex = i - pixmap.sizes.begin();
if (i->size.isValid())
pixSize = -i->size.width() * i->size.height();
i->cacheState = Uncached;
break;
} else if (!uncache && i->cacheState == Uncacheable) {
newEvent.sizeIndex = i - pixmap.sizes.begin();
i->cacheState = Corrupt;
break;
}
}
}
// If that does't work, create a new entry.
if (newEvent.sizeIndex == -1) {
newEvent.sizeIndex = pixmap.sizes.length();
pixmap.sizes << PixmapState(uncache ? Uncached : ToBeCached);
}
lastCacheSizeEvent = updateCacheCount(lastCacheSizeEvent, pixmapStartTime, pixSize,
newEvent, event.typeIndex);
break;
}
case PixmapLoadingStarted: { // Load
// Look for a pixmap that hasn't been started, yet. There may have been a refcount
// event, which we ignore.
for (QVector::const_iterator i(pixmap.sizes.cbegin());
i != pixmap.sizes.cend(); ++i) {
if (i->loadState == Initial) {
newEvent.sizeIndex = i - pixmap.sizes.cbegin();
break;
}
}
if (newEvent.sizeIndex == -1) {
newEvent.sizeIndex = pixmap.sizes.length();
pixmap.sizes << PixmapState();
}
PixmapState &state = pixmap.sizes[newEvent.sizeIndex];
state.loadState = Loading;
newEvent.typeId = event.typeIndex;
state.started = insertStart(pixmapStartTime, newEvent.urlIndex + 1);
m_data.insert(state.started, newEvent);
break;
}
case PixmapLoadingFinished:
case PixmapLoadingError: {
// First try to find one that has already started
for (QVector::const_iterator i(pixmap.sizes.cbegin());
i != pixmap.sizes.cend(); ++i) {
if (i->loadState != Loading)
continue;
// Pixmaps with known size cannot be errors and vice versa
if (newEvent.pixmapEventType == PixmapLoadingError && i->size.isValid())
continue;
newEvent.sizeIndex = i - pixmap.sizes.cbegin();
break;
}
// If none was found use any other compatible one
if (newEvent.sizeIndex == -1) {
for (QVector::const_iterator i(pixmap.sizes.cbegin());
i != pixmap.sizes.cend(); ++i) {
if (i->loadState != Initial)
continue;
// Pixmaps with known size cannot be errors and vice versa
if (newEvent.pixmapEventType == PixmapLoadingError && i->size.isValid())
continue;
newEvent.sizeIndex = i - pixmap.sizes.cbegin();
break;
}
}
// If again none was found, create one.
if (newEvent.sizeIndex == -1) {
newEvent.sizeIndex = pixmap.sizes.length();
pixmap.sizes << PixmapState();
}
PixmapState &state = pixmap.sizes[newEvent.sizeIndex];
// If the pixmap loading wasn't started, start it at traceStartTime()
if (state.loadState == Initial) {
newEvent.pixmapEventType = PixmapLoadingStarted;
newEvent.typeId = event.typeIndex;
qint64 traceStart = modelManager()->traceTime()->startTime();
state.started = insert(traceStart, pixmapStartTime - traceStart,
newEvent.urlIndex + 1);
m_data.insert(state.started, newEvent);
// All other indices are wrong now as we've prepended. Fix them ...
if (lastCacheSizeEvent >= state.started)
++lastCacheSizeEvent;
for (int pixmapIndex = 0; pixmapIndex < m_pixmaps.count(); ++pixmapIndex) {
Pixmap &brokenPixmap = m_pixmaps[pixmapIndex];
for (int sizeIndex = 0; sizeIndex < brokenPixmap.sizes.count(); ++sizeIndex) {
PixmapState &brokenSize = brokenPixmap.sizes[sizeIndex];
if ((pixmapIndex != newEvent.urlIndex || sizeIndex != newEvent.sizeIndex) &&
brokenSize.started >= state.started) {
++brokenSize.started;
}
}
}
}
insertEnd(state.started, pixmapStartTime - startTime(state.started));
if (newEvent.pixmapEventType == PixmapLoadingError) {
state.loadState = Error;
switch (state.cacheState) {
case Uncached:
state.cacheState = Uncacheable;
break;
case ToBeCached:
state.cacheState = Corrupt;
break;
default:
// Cached cannot happen as size would have to be known and Corrupt or
// Uncacheable cannot happen as we only accept one finish or error event per
// pixmap.
Q_ASSERT(false);
}
} else {
state.loadState = Finished;
}
break;
}
default:
break;
}
updateProgress(count(), 2 * simpleModel->getEvents().count());
}
if (lastCacheSizeEvent != -1)
insertEnd(lastCacheSizeEvent, modelManager()->traceTime()->endTime() -
startTime(lastCacheSizeEvent));
resizeUnfinishedLoads();
computeMaxCacheSize();
flattenLoads();
computeNesting();
updateProgress(1, 1);
}
void PixmapCacheModel::clear()
{
m_pixmaps.clear();
m_maxCacheSize = 1;
m_data.clear();
QmlProfilerTimelineModel::clear();
}
void PixmapCacheModel::computeMaxCacheSize()
{
m_maxCacheSize = 1;
foreach (const PixmapCacheModel::PixmapCacheEvent &event, m_data) {
if (event.pixmapEventType == PixmapCacheModel::PixmapCacheCountChanged) {
if (event.cacheSize > m_maxCacheSize)
m_maxCacheSize = event.cacheSize;
}
}
}
void PixmapCacheModel::resizeUnfinishedLoads()
{
// all the "load start" events with duration 0 continue till the end of the trace
for (int i = 0; i < count(); i++) {
if (m_data[i].pixmapEventType == PixmapCacheModel::PixmapLoadingStarted &&
duration(i) == 0) {
insertEnd(i, modelManager()->traceTime()->endTime() - startTime(i));
}
}
}
void PixmapCacheModel::flattenLoads()
{
int collapsedRowCount = 0;
// computes "compressed row"
QVector eventEndTimes;
for (int i = 0; i < count(); i++) {
PixmapCacheModel::PixmapCacheEvent &event = m_data[i];
if (event.pixmapEventType == PixmapCacheModel::PixmapLoadingStarted) {
event.rowNumberCollapsed = 0;
while (eventEndTimes.count() > event.rowNumberCollapsed &&
eventEndTimes[event.rowNumberCollapsed] > startTime(i))
event.rowNumberCollapsed++;
if (eventEndTimes.count() == event.rowNumberCollapsed)
eventEndTimes << 0; // increase stack length, proper value added below
eventEndTimes[event.rowNumberCollapsed] = endTime(i);
// readjust to account for category empty row and bargraph
event.rowNumberCollapsed += 2;
}
if (event.rowNumberCollapsed > collapsedRowCount)
collapsedRowCount = event.rowNumberCollapsed;
}
// Starting from 0, count is maxIndex+1
setCollapsedRowCount(collapsedRowCount + 1);
setExpandedRowCount(m_pixmaps.count() + 2);
}
int PixmapCacheModel::updateCacheCount(int lastCacheSizeEvent,
qint64 pixmapStartTime, qint64 pixSize, PixmapCacheEvent &newEvent, int typeId)
{
newEvent.pixmapEventType = PixmapCacheCountChanged;
newEvent.rowNumberCollapsed = 1;
qint64 prevSize = 0;
if (lastCacheSizeEvent != -1) {
prevSize = m_data[lastCacheSizeEvent].cacheSize;
insertEnd(lastCacheSizeEvent, pixmapStartTime - startTime(lastCacheSizeEvent));
}
newEvent.cacheSize = prevSize + pixSize;
newEvent.typeId = typeId;
int index = insertStart(pixmapStartTime, 0);
m_data.insert(index, newEvent);
return index;
}
} // namespace Internal
} // namespace QmlProfilerExtension