/**************************************************************************** ** ** 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" #include "qmlprofiler/abstracttimelinemodel_p.h" #include #include namespace QmlProfilerExtension { namespace Internal { using namespace QmlProfiler; enum CacheState { Uncached, // After loading started (or some other proof of existence) or after uncaching ToBeCached, // After determining the pixmap is to be cached but before knowing its size Cached, // After caching a pixmap or determining the size of a ToBeCached pixmap Uncacheable, // If loading failed without ToBeCached or after a corrupt pixmap has been uncached Corrupt // If after ToBeCached we learn that loading failed }; enum LoadState { Initial, Loading, Finished, Error }; struct PixmapState { PixmapState(int width, int height, CacheState cache = Uncached) : size(width, height), started(-1), loadState(Initial), cacheState(cache) {} PixmapState(CacheState cache = Uncached) : started(-1), loadState(Initial), cacheState(cache) {} QSize size; int started; LoadState loadState; CacheState cacheState; }; struct Pixmap { Pixmap() {} Pixmap(const QString &url) : url(url), sizes(1) {} QString url; QVector sizes; }; class PixmapCacheModel::PixmapCacheModelPrivate : public AbstractTimelineModelPrivate { public: void computeMaxCacheSize(); void resizeUnfinishedLoads(); void flattenLoads(); int updateCacheCount(int lastCacheSizeEvent, qint64 startTime, qint64 pixSize, PixmapCacheEvent &newEvent, int typeId); QVector data; QVector pixmaps; qint64 maxCacheSize; private: Q_DECLARE_PUBLIC(PixmapCacheModel) }; PixmapCacheModel::PixmapCacheModel(QObject *parent) : AbstractTimelineModel(new PixmapCacheModelPrivate(), tr(QmlProfilerModelManager::featureName(QmlDebug::ProfilePixmapCache)), QmlDebug::PixmapCacheEvent, QmlDebug::MaximumRangeType, parent) { Q_D(PixmapCacheModel); d->maxCacheSize = 1; } quint64 PixmapCacheModel::features() const { return 1 << QmlDebug::ProfilePixmapCache; } int PixmapCacheModel::rowMaxValue(int rowNumber) const { Q_D(const PixmapCacheModel); if (rowNumber == 1) { return d->maxCacheSize; } else { return AbstractTimelineModel::rowMaxValue(rowNumber); } } int PixmapCacheModel::row(int index) const { Q_D(const PixmapCacheModel); if (d->expanded) return selectionId(index) + 1; return d->data[index].rowNumberCollapsed; } int PixmapCacheModel::selectionId(int index) const { Q_D(const PixmapCacheModel); return d->data[index].pixmapEventType == PixmapCacheCountChanged ? 0 : d->data[index].urlIndex + 1; } QColor PixmapCacheModel::color(int index) const { Q_D(const PixmapCacheModel); if (d->data[index].pixmapEventType == PixmapCacheCountChanged) return colorByHue(PixmapCacheCountHue); return colorBySelectionId(index); } float PixmapCacheModel::relativeHeight(int index) const { Q_D(const PixmapCacheModel); if (d->data[index].pixmapEventType == PixmapCacheCountChanged) return (float)d->data[index].cacheSize / (float)d->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 { Q_D(const PixmapCacheModel); QVariantList result; if (d->expanded && !d->hidden && !isEmpty()) { { // 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 < d->pixmaps.count(); i++) { // Loading QVariantMap element; element.insert(QLatin1String("displayName"), d->pixmaps[i].url); element.insert(QLatin1String("description"), QVariant(getFilenameOnly(d->pixmaps[i].url))); element.insert(QLatin1String("id"), QVariant(i+1)); result << element; } } return result; } QVariantMap PixmapCacheModel::details(int index) const { Q_D(const PixmapCacheModel); QVariantMap result; const PixmapCacheEvent *ev = &d->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 (d->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(d->pixmaps[ev->urlIndex].url)); result.insert(tr("Width"), QString::fromLatin1("%1 px") .arg(d->pixmaps[ev->urlIndex].sizes[ev->sizeIndex].size.width())); result.insert(tr("Height"), QString::fromLatin1("%1 px") .arg(d->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() { Q_D(PixmapCacheModel); clear(); QmlProfilerDataModel *simpleModel = d->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 = type.detailType; qint64 pixmapStartTime = event.startTime; newEvent.urlIndex = -1; for (QVector::const_iterator it(d->pixmaps.cend()); it != d->pixmaps.cbegin();) { if ((--it)->url == type.location.filename) { newEvent.urlIndex = it - d->pixmaps.cbegin(); break; } } newEvent.sizeIndex = -1; if (newEvent.urlIndex == -1) { newEvent.urlIndex = d->pixmaps.count(); d->pixmaps << Pixmap(type.location.filename); } Pixmap &pixmap = d->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 = d->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 = d->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; state.started = insertStart(pixmapStartTime, event.typeIndex); d->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; state.started = insert(d->modelManager->traceTime()->startTime(), pixmapStartTime - d->modelManager->traceTime()->startTime(), event.typeIndex); d->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 < d->pixmaps.count(); ++pixmapIndex) { Pixmap &brokenPixmap = d->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; } d->modelManager->modelProxyCountUpdated(d->modelId, count(), 2 * simpleModel->getEvents().count()); } if (lastCacheSizeEvent != -1) insertEnd(lastCacheSizeEvent, d->modelManager->traceTime()->endTime() - startTime(lastCacheSizeEvent)); d->resizeUnfinishedLoads(); d->computeMaxCacheSize(); d->flattenLoads(); computeNesting(); d->modelManager->modelProxyCountUpdated(d->modelId, 1, 1); } void PixmapCacheModel::clear() { Q_D(PixmapCacheModel); d->pixmaps.clear(); d->maxCacheSize = 1; d->data.clear(); AbstractTimelineModel::clear(); } void PixmapCacheModel::PixmapCacheModelPrivate::computeMaxCacheSize() { maxCacheSize = 1; foreach (const PixmapCacheModel::PixmapCacheEvent &event, data) { if (event.pixmapEventType == PixmapCacheModel::PixmapCacheCountChanged) { if (event.cacheSize > maxCacheSize) maxCacheSize = event.cacheSize; } } } void PixmapCacheModel::PixmapCacheModelPrivate::resizeUnfinishedLoads() { Q_Q(PixmapCacheModel); // all the "load start" events with duration 0 continue till the end of the trace for (int i = 0; i < q->count(); i++) { if (data[i].pixmapEventType == PixmapCacheModel::PixmapLoadingStarted && ranges[i].duration == 0) { q->insertEnd(i, modelManager->traceTime()->endTime() - ranges[i].start); } } } void PixmapCacheModel::PixmapCacheModelPrivate::flattenLoads() { Q_Q(PixmapCacheModel); collapsedRowCount = 0; // computes "compressed row" QVector eventEndTimes; for (int i = 0; i < q->count(); i++) { PixmapCacheModel::PixmapCacheEvent &event = data[i]; const Range &start = ranges[i]; if (event.pixmapEventType == PixmapCacheModel::PixmapLoadingStarted) { event.rowNumberCollapsed = 0; while (eventEndTimes.count() > event.rowNumberCollapsed && eventEndTimes[event.rowNumberCollapsed] > start.start) event.rowNumberCollapsed++; if (eventEndTimes.count() == event.rowNumberCollapsed) eventEndTimes << 0; // increase stack length, proper value added below eventEndTimes[event.rowNumberCollapsed] = start.start + start.duration; // 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 collapsedRowCount++; expandedRowCount = pixmaps.count() + 2; } int PixmapCacheModel::PixmapCacheModelPrivate::updateCacheCount(int lastCacheSizeEvent, qint64 startTime, qint64 pixSize, PixmapCacheEvent &newEvent, int typeId) { Q_Q(PixmapCacheModel); newEvent.pixmapEventType = PixmapCacheCountChanged; newEvent.rowNumberCollapsed = 1; qint64 prevSize = 0; if (lastCacheSizeEvent != -1) { prevSize = data[lastCacheSizeEvent].cacheSize; q->insertEnd(lastCacheSizeEvent, startTime - ranges[lastCacheSizeEvent].start); } newEvent.cacheSize = prevSize + pixSize; int index = q->insertStart(startTime, typeId); data.insert(index, newEvent); return index; } } // namespace Internal } // namespace QmlProfilerExtension