diff options
-rw-r--r-- | src/bm/bm.pro | 4 | ||||
-rw-r--r-- | src/bm/bmmisc.cpp | 31 | ||||
-rw-r--r-- | src/bm/bmmisc.h | 33 | ||||
-rw-r--r-- | src/bm/bmrequest.cpp | 702 | ||||
-rw-r--r-- | src/bm/bmrequest.h | 25 | ||||
-rw-r--r-- | src/bm/index.cpp | 288 | ||||
-rw-r--r-- | src/bm/index.h | 97 | ||||
-rw-r--r-- | src/bm/plotter.cpp | 345 | ||||
-rw-r--r-- | src/bm/plotter.h | 36 | ||||
-rw-r--r-- | src/bm/resulthistoryinfo.cpp | 43 | ||||
-rw-r--r-- | src/bm/resulthistoryinfo.h | 23 | ||||
-rw-r--r-- | src/bmclient/main.cpp | 100 |
12 files changed, 1471 insertions, 256 deletions
diff --git a/src/bm/bm.pro b/src/bm/bm.pro index ca971d1..cff490b 100644 --- a/src/bm/bm.pro +++ b/src/bm/bm.pro @@ -1,8 +1,8 @@ TEMPLATE = lib CONFIG += shared TARGET = bm -SOURCES += bm.cpp bmrequest.cpp bmmisc.cpp plotter.cpp resulthistoryinfo.cpp cache.cpp -HEADERS += bm.h bmrequest.h bmmisc.h plotter.h resulthistoryinfo.h cache.h +SOURCES += bm.cpp bmrequest.cpp bmmisc.cpp plotter.cpp resulthistoryinfo.cpp cache.cpp index.cpp +HEADERS += bm.h bmrequest.h bmmisc.h plotter.h resulthistoryinfo.h cache.h index.h QT += network QT += xml QT += sql diff --git a/src/bm/bmmisc.cpp b/src/bm/bmmisc.cpp index 5ab7404..90d4062 100644 --- a/src/bm/bmmisc.cpp +++ b/src/bm/bmmisc.cpp @@ -162,6 +162,25 @@ bool BMMisc::getMultiOption( return true; } +// ### 2 B DOCUMENTED! +bool BMMisc::getMultiOption2( + const QStringList &args, const QString &option, QList<QStringList> *values, int n, + QString *error) +{ + for (int i = 0; ; ++i) { + QStringList values_; + if (getOption(args, option, &values_, n, i, error)) { + Q_ASSERT(values_.size() == n); + values->append(values_); + } else { + if (!error->isEmpty()) + return false; + break; + } + } + return true; +} + bool BMMisc::hasOption(const QStringList &args, const QString &option) { QStringList dummyValues; @@ -177,6 +196,18 @@ qreal BMMisc::median(const QList<qreal> &values) return sortedValues.at(sortedValues.size() / 2); } +// Computes the (zero-based) position of the median value of the numbers in \a values. +// If the median value occurs multiple times, the position of either the last or first +// duplicate is returned depending on whether \a lastDuplicate is true or false respectively. +int BMMisc::medianPos(const QList<qreal> &values, bool lastDuplicate) +{ + Q_ASSERT(!values.isEmpty()); + QList<qreal> sortedValues = values; + qSort(sortedValues); + const qreal medianValue = sortedValues.at(sortedValues.size() / 2); + return lastDuplicate ? values.lastIndexOf(medianValue) : values.indexOf(medianValue); +} + qreal BMMisc::v2y( const qreal v, const qreal ymax, const qreal vmin, const qreal yfact, const qreal ydefault) { diff --git a/src/bm/bmmisc.h b/src/bm/bmmisc.h index a3d711a..057adac 100644 --- a/src/bm/bmmisc.h +++ b/src/bm/bmmisc.h @@ -27,6 +27,7 @@ #include <QVector> #include <QString> #include <QStringList> +#include <QPair> #include <QImage> #include <QDateTime> #include <QSqlQuery> @@ -52,9 +53,12 @@ public: static bool getMultiOption( const QStringList &args, const QString &option, QStringList *values, QString *error, bool unique = true); + static bool getMultiOption2( + const QStringList &args, const QString &option, QList<QStringList> *values, int n, + QString *error); static bool hasOption(const QStringList &args, const QString &option); static qreal median(const QList<qreal> &values); - + static int medianPos(const QList<qreal> &values, bool lastDuplicate = true); static qreal v2y( const qreal v, const qreal ymax, const qreal vmin, const qreal yfact, const qreal ydefault); static QDateTime createCurrDateTime(int timestamp); @@ -69,6 +73,33 @@ public: static qreal log2diff(qreal val1, qreal val2, const QString &metric); static bool normalize(QList<qreal> &v); static QString compactDateString(int timestamp); + + // ### 2 B DOCUMENTED! + // (Note that a function template cannot be compiled and linked independently, so its + // definition needs to go here) + template <typename T> static void insertRankedId( + QList<QPair<T, qreal> > *ranked, int limit, T t, qreal value) + { + Q_ASSERT(limit >= 0); + + int i; + for (i = 0; (i < limit) && (i < ranked->size()); ++i) { + if (value > ranked->at(i).second) + break; + } + + if (i == limit) + return; + + Q_ASSERT(i <= ranked->size()); + ranked->insert(i, qMakePair(t, value)); + + if (ranked->size() > limit) { + Q_ASSERT(ranked->size() == (limit + 1)); + ranked->removeLast(); + Q_ASSERT(ranked->size() == limit); + } + } }; #endif // BMMISC_H diff --git a/src/bm/bmrequest.cpp b/src/bm/bmrequest.cpp index 4da76f8..46d1bf1 100644 --- a/src/bm/bmrequest.cpp +++ b/src/bm/bmrequest.cpp @@ -104,6 +104,8 @@ BMRequest * BMRequest::create(const QByteArray &data, const QString &msgType) request = new BMRequest_GetHistories(doc); else if (type == "GetBMTree") request = new BMRequest_GetBMTree(doc); + else if (type == "GetIXHistories") + request = new BMRequest_GetIXHistories(doc); #ifdef BMDEBUG else qDebug() << "invalid request type:" << type; @@ -4873,6 +4875,7 @@ QByteArray BMRequest_IndexGetValues::toRequestBuffer(QString *) return xmlConvert(request); } +// ### OBSOLETE! void BMRequest_IndexGetValues::computeIndexValues( const QList<ResultHistoryInfo *> &rhInfos, int baseTimestamp, const QList<int> &evalTimestamps, int medianWinSize, const QList<qreal> &baseDistSums, qreal minBaseDistSum, @@ -5104,15 +5107,20 @@ void BMRequest_IndexGetValues::computeIndexValues( // if (sizes.at(j) < size050percentile) // continue; - qreal targetValue = -1.0; + int targetPos = -1; qreal distSum = -1.0; - if (rhInfo->getSmoothValue( - evalTimestamps.at(i), medianWinSize, &targetValue, false, &distSum)) { - qreal baseValue = -1.0; - ok = rhInfo->getSmoothValue(baseTimestamp, medianWinSize, &baseValue, true); + if (rhInfo->getSmoothPos( + evalTimestamps.at(i), medianWinSize, &targetPos, false, &distSum)) { + + const qreal targetVal = rhInfo->value(targetPos); + + int basePos = -1; + ok = rhInfo->getSmoothPos(baseTimestamp, medianWinSize, &basePos, true); Q_ASSERT(ok); - const qreal diff = BMMisc::log2diff(targetValue, baseValue, rhInfo->metric()); + const qreal baseVal = rhInfo->value(basePos); + + const qreal diff = BMMisc::log2diff(targetVal, baseVal, rhInfo->metric()); diffs.append(diff); contr.append(j); @@ -5167,7 +5175,6 @@ void BMRequest_IndexGetValues::computeIndexValues( fprintf(stderr, "computing index values (100%% done)\n"); } - QByteArray BMRequest_IndexGetValues::toReplyBuffer() { QDomElement argsElem = doc.elementsByTagName("args").at(0).toElement(); @@ -5278,13 +5285,9 @@ QByteArray BMRequest_IndexGetValues::toReplyBuffer() QSqlQuery *query; - - // ***************************************************************************************** - // *** PHASE 1: Build the list of result histories that will be examined for a - // contribution at each evaluation timestamp. - - // Get the BM context ID and metric for all result histories that match the - // logical OR-values. NOTE: Results measuring the "events" metric are skipped for now since + // Get the initial list of candidate result histories by getting the BM context ID and metric + // for all result histories that match the logical OR-values. + // NOTE: Results measuring the "events" metric are skipped for now since // they seem to be of little value when computing the performance index (they are // typically integers close to (and often at) zero) ... query = createQuery(); @@ -5370,15 +5373,6 @@ QByteArray BMRequest_IndexGetValues::toReplyBuffer() QList<ResultHistoryInfo *> rhInfos; - // Statistics of rejected result histories: - int nonPositiveRH = 0; // contains non-positive value - int invalidBaseRH = 0; // contains too few values before base timestamp - - QList<qreal> baseDistSums; // sums of distances of values contributing to base timestamp - qreal minBaseDistSum = -1.0; - qreal maxBaseDistSum = -1.0; - - // Loop over initial result history candidates ... for (int i = 0; i < bmcontextIds.size(); ++i) { @@ -5404,46 +5398,15 @@ QByteArray BMRequest_IndexGetValues::toReplyBuffer() } QList<int> timestamps; QList<qreal> values; - bool nonPositiveValueFound = false; while (query->next()) { timestamps += query->value(0).toInt(); - const qreal value = query->value(1).toDouble(); - if (value > 0.0) { - values += value; - } else { - nonPositiveValueFound = true; - break; - } + values += query->value(1).toDouble(); } deleteQuery(query); - if (nonPositiveValueFound) { - // Reject candidate since at least one non-positive value was found ... - ++nonPositiveRH; - continue; - } - - qreal baseDistSum; - if (!ResultHistoryInfo::findTargetPos( - timestamps, baseTimestamp, medianWinSize, 0, &baseDistSum)) { - // Reject candidate if the base timestamp is not preceded by enough values - // (and thus has a higher risk of being an outlier) ... - ++invalidBaseRH; - continue; - } - - // Accept candidate ... ResultHistoryInfo *rhInfo = new ResultHistoryInfo(bmcontextIds.at(i), timestamps, values, metrics.at(i)); rhInfos.append(rhInfo); - - baseDistSums.append(baseDistSum); - if (baseDistSums.size() == 1) { - minBaseDistSum = maxBaseDistSum = baseDistSum; - } else { - minBaseDistSum = qMin(minBaseDistSum, baseDistSum); - maxBaseDistSum = qMax(maxBaseDistSum, baseDistSum); - } } if (debugPrint) @@ -5453,16 +5416,57 @@ QByteArray BMRequest_IndexGetValues::toReplyBuffer() reply = QString("<reply type=\"%1\" >").arg(name()); + int nonPositiveRH = 0; + int baseValuePos; + if (!rhInfos.isEmpty()) { - computeIndexValues( - rhInfos, baseTimestamp, evalTimestamps, medianWinSize, baseDistSums, minBaseDistSum, - maxBaseDistSum, debugPrint, &reply); - // Free dynamic memory ... - for (int i = 0; i < rhInfos.size(); ++i) - delete rhInfos.at(i); - } + // Candidate result histories exist, so compute index values ... + IndexAlgorithm1 index( + rhInfos, baseTimestamp, evalTimestamps, medianWinSize, &nonPositiveRH); + if (!index.isValid()) { + return xmlConvert( + errorReply( + name(), QString("failed to compute index (1): %1").arg(index.invalidReason()))); + } + + QList<qreal> indexValues; + QList<int> contrCounts; + QList<QList<Index::RankedInfo> > topContr; + const int topContrLimit = 10; // ### hard-coded for now! + QString error_; + if (!index.computeValues( + &indexValues, &baseValuePos, &contrCounts, &error_, &topContr, topContrLimit)) { + return xmlConvert( + errorReply(name(), QString("failed to compute index (2): %1").arg(error_))); + } + + // Add index values to reply ... + Q_ASSERT(indexValues.size() == evalTimestamps.size()); + Q_ASSERT(indexValues.size() == contrCounts.size()); + for (int i = 0; i < indexValues.size(); ++i) { + reply += + QString("<value timestamp=\"%1\" indexValue=\"%2\" contributions=\"%3\" >") + .arg(evalTimestamps.at(i)) + .arg(indexValues.at(i)) + .arg(contrCounts.at(i)); + + for (int j = 0; j < topContr.at(i).size(); ++j) { + reply += + QString( + "<rankedInfo id=\"%1\" basePos=\"%2\" diffPos1=\"%3\" " + "diffPos2=\"%4\" descr=\"%5\" />") + .arg(topContr.at(i).at(j).bmcontextId) + .arg(topContr.at(i).at(j).basePos) + .arg(topContr.at(i).at(j).diffPos1) + .arg(topContr.at(i).at(j).diffPos2) + .arg(topContr.at(i).at(j).descr); + } + + reply += "</value>"; + } + } //---------------------------------------------------------------------------------------- @@ -5486,9 +5490,9 @@ QByteArray BMRequest_IndexGetValues::toReplyBuffer() cacheKey_ = Cache::instance()->nextId(); reply += QString( "<args baseTimestamp=\"%1\" medianWinSize=\"%2\" totalRH=\"%3\" nonPositiveRH=\"%4\" " - "invalidBaseRH=\"%5\" cacheKey=\"%6\" /></reply>") - .arg(baseTimestamp).arg(medianWinSize).arg(bmcontextIds.size()) - .arg(nonPositiveRH).arg(invalidBaseRH).arg(cacheKey_); + "baseValuePos=\"%5\" cacheKey=\"%6\" /></reply>") + .arg(baseTimestamp).arg(medianWinSize).arg(bmcontextIds.size()).arg(nonPositiveRH) + .arg(baseValuePos).arg(cacheKey_); Cache::instance()->put(cacheKey_, reply); return xmlConvert(reply); @@ -5513,6 +5517,14 @@ void BMRequest_IndexGetValues::appendToFilterTable( void BMRequest_IndexGetValues::handleReply_HTML(const QStringList &args) const { QString error; + + error = doc.elementsByTagName("reply").at(0).toElement().attributeNode("error").value(); + if (!error.isEmpty()) { + BMMisc::printHTMLErrorPage( + QString("failed to create details page: error evaluating index: %1").arg(error)); + return; + } + QStringList optValues; // Get server ... @@ -5541,16 +5553,27 @@ void BMRequest_IndexGetValues::handleReply_HTML(const QStringList &args) const reply += "<span style=\"font-size:18\">Qt Performance Index</span>"; reply += "<br /><br /><table><tr><td style=\"background-color:#eeeeee\">"; - reply += "This page shows the performance index evaluated at certain timestamps "; - reply += "and for certain result histories (i.e. for certain benchmarks in certain contexts, "; - reply += "where the context represents platform etc.)."; + + reply += "This page shows the performance index for certain benchmarks in certain contexts "; + reply += "(where the context represents platform etc.)."; + reply += "<br /><br />"; - reply += "The index value at a given time shows the aggregated performance at that time "; - reply += "relative to the aggregated performance at a given base time in terms of percentage "; - reply += "of the latter. A value of 200 thus indicates a doubled performance (i.e. twice "; - reply += "the FPS or half the walltime), while 50 indicates a halved performance."; - reply += "</td></tr></table>"; + reply += "The <u>change</u> in index value between any two points in time indicates the "; + reply += "average performance increase during this time period in terms of a log2-based "; + reply += "difference. "; + reply += "An index value <u>change</u> of 1, 0, and -1 thus indicates a doubled, unchanged, "; + reply += "and halved performance respectively. "; + reply += "(The corresponding linear factors would be 2, 1, and 0.5.)."; + + reply += "<br /><br />"; + + reply += "<b>Note:</b> Individual index values bear no useful meaning when regarded in "; + reply += "isolation. The graph is however shifted vertically so that each individual index "; + reply += "value indicates the change from the index value at the given base time (the index "; + reply += "value of the latter is then 0 and 1 in the log2 and linear domain respectively)."; + + reply += "</td></tr></table>\n"; QDomElement argsElem = doc.elementsByTagName("args").at(0).toElement(); @@ -5561,7 +5584,11 @@ void BMRequest_IndexGetValues::handleReply_HTML(const QStringList &args) const // reply += QString("arg %1: >%2<<br />").arg(i).arg(args.at(i)); // reply += "<br />"; - reply += "<br />\n<img src=\"bmclientwrapper?command="; + // Image ... + reply += + "<br />\n<img usemap=\"#plotmap\" style=\"border-style:none\" " + "src=\"bmclientwrapper?command="; + for (int i = args.indexOf("-server"); i < args.size(); ++i) reply += QString("%1 ") .arg((args.at(i) == "detailspage") ? QLatin1String("plot") : args.at(i)); @@ -5569,15 +5596,96 @@ void BMRequest_IndexGetValues::handleReply_HTML(const QStringList &args) const reply += "-httpheader "; reply += "\" />\n<br />\n<br />\n"; + // Image map ... + reply += "<br />\n<map name=\"plotmap\">\n"; + bool ok; - error = doc.elementsByTagName("reply").at(0).toElement().attributeNode("error").value(); - if (!error.isEmpty()) { - reply = QString("failed to create table: error evaluating index: %1").arg(error); - BMMisc::printHTMLErrorPage(reply); + // ... get timestamps, index values, and contributions ... + QList<int> timestamps; + QList<qreal> indexValues; + QList<qreal> linearIndexValues; + QList<int> contributions; + QDomNodeList valueNodes = doc.elementsByTagName("value"); + for (int i = 0; i < valueNodes.size(); ++i) { + QDomElement valueElem = valueNodes.at(i).toElement(); + + timestamps += valueElem.attributeNode("timestamp").value().toInt(&ok); + Q_ASSERT(ok); + + const qreal indexValue = valueElem.attributeNode("indexValue").value().toDouble(&ok); + Q_ASSERT(ok); + indexValues += indexValue; + linearIndexValues += qPow(2, indexValue); + + contributions += valueElem.attributeNode("contributions").value().toInt(&ok); + Q_ASSERT(ok); + } + + // ... get point infos ... + Plotter *plotter = new IndexPlotter(timestamps, indexValues, contributions); + QList<Plotter::PointInfo> pointInfos; + if (plotter->createImage(&error, &pointInfos).isNull()) { + BMMisc::printHTMLErrorPage( + QString("failed to extract point infos: %1").arg(error)); + return; + } + Q_ASSERT(pointInfos.size() == timestamps.size()); + + // ... extract the web server from the style sheet ... + QRegExp rx("^(\\S+:\\d+)\\D+$"); + if (rx.indexIn(styleSheet) == -1) { + BMMisc::printHTMLErrorPage(QString("failed to extract web server from style sheet")); return; } + const QString webServer = rx.cap(1); + + // ... add <area> tags for valid points ... + for (int i = 0; i < pointInfos.size(); ++i) { + if (contributions.at(i) == 0) + continue; // skip invalid point + + QString url = QString("%1/cgi-bin/bmclientwrapper").arg(webServer); + url += QString("?command=-server %1").arg(server); + url += QString(" get ixhistories detailspage -stylesheet %1").arg(styleSheet); + url += QString(" -evaltimestamp %1").arg(timestamps.at(i)); + + QDomElement valueElem = valueNodes.at(i).toElement(); + + QDomNodeList rankedInfoNodes = valueElem.elementsByTagName("rankedInfo"); + if (rankedInfoNodes.size() == 0) + continue; // skip if no ranked infos were found (typically the case for for the first + // valid point) + + for (int j = 0; j < rankedInfoNodes.size(); ++j) { + QDomElement rankedInfoElem = rankedInfoNodes.at(j).toElement(); + + const int bmcontextId = rankedInfoElem.attributeNode("id").value().toInt(&ok); + Q_ASSERT(ok); + const int basePos = rankedInfoElem.attributeNode("basePos").value().toInt(&ok); + Q_ASSERT(ok); + const int diffPos1 = rankedInfoElem.attributeNode("diffPos1").value().toInt(&ok); + Q_ASSERT(ok); + const int diffPos2 = rankedInfoElem.attributeNode("diffPos2").value().toInt(&ok); + Q_ASSERT(ok); + const QString descr = rankedInfoElem.attributeNode("descr").value(); + + url += QString(" -rankedinfo %1 %2 %3 %4 '%5'") + .arg(bmcontextId).arg(basePos).arg(diffPos1).arg(diffPos2).arg(descr); + } + + QRect rect = pointInfos.at(i).rect.toAlignedRect(); + reply += QString( + "<area shape=\"rect\" coords=\"%1,%2,%3,%4\" title=\"%5\" href=\"%6\" />\n") + .arg(rect.topLeft().x()) + .arg(rect.topLeft().y()) + .arg(rect.bottomRight().x()) + .arg(rect.bottomRight().y()) + .arg(pointInfos.at(i).value) + .arg(url); + } + + reply += "</map>\n"; - bool ok; QDateTime dateTime; @@ -5637,12 +5745,7 @@ void BMRequest_IndexGetValues::handleReply_HTML(const QStringList &args) const reply += QString("<tr><td>Rejected due to existence of non-positive values:</td><td>%1</td></tr>\n") .arg(nonPositiveRH); - const int invalidBaseRH = argsElem.attributeNode("invalidBaseRH").value().toInt(&ok); - Q_ASSERT(ok); - reply += QString( - "<tr><td>Rejected due to too few values before the base time:</td><td>%1</td></tr>\n") - .arg(invalidBaseRH); - const int finalTotalRH = totalRH - (nonPositiveRH + invalidBaseRH); + const int finalTotalRH = totalRH - nonPositiveRH; Q_ASSERT(ok); reply += QString("<tr><td>Final set:</td><td>%1</td></tr>\n").arg(finalTotalRH); reply += "</table>\n"; @@ -5652,19 +5755,14 @@ void BMRequest_IndexGetValues::handleReply_HTML(const QStringList &args) const reply += "\n<br /><br /><table style=\"border: 0px\">\n"; reply += "<tr style=\"border: 0px\">\n"; - QDomNodeList valueNodes = doc.elementsByTagName("value"); - - // Left table ... + // Table 1 ... reply += "<td style=\"border: 0px\"><table>\n"; reply += "<tr><th></th><th>Date</th><th>Contributions</th></tr>\n"; for (int i = 0; i < valueNodes.size(); ++i) { const int i_ = (valueNodes.size() - 1) - i; - QDomElement valueElem = valueNodes.at(i_).toElement(); - const int timestamp = valueElem.attributeNode("timestamp").value().toInt(&ok); - Q_ASSERT(ok); - const int contributions = valueElem.attributeNode("contributions").value().toInt(&ok); - Q_ASSERT(ok); + const int timestamp = timestamps.at(i_); + const int contributions_ = contributions.at(i_); const QString style = QString("%1") .arg((timestamp >= baseTimestamp) @@ -5677,23 +5775,21 @@ void BMRequest_IndexGetValues::handleReply_HTML(const QStringList &args) const reply += QString("<td class=\"%1\">%2</td>").arg(style).arg(dateTime.toString()); reply += QString("<td class=\"%1\">%2%3</td>") .arg(style) - .arg(contributions) + .arg(contributions_) .arg((finalTotalRH == 0) ? QString() - : QString(" (%1%)").arg(100 * (qreal(contributions) / finalTotalRH), 0, 'f', 2)); + : QString(" (%1%)").arg(100 * (qreal(contributions_) / finalTotalRH), 0, 'f', 2)); reply += "</tr>\n"; } reply += "</td></table>\n"; - // Middle table ... + // Table 2 ... reply += "<td style=\"border: 0px\"><table>\n"; reply += "<tr><th>Timestamp</th></tr>\n"; for (int i = 0; i < valueNodes.size(); ++i) { const int i_ = (valueNodes.size() - 1) - i; - QDomElement valueElem = valueNodes.at(i_).toElement(); - const int timestamp = valueElem.attributeNode("timestamp").value().toInt(&ok); - Q_ASSERT(ok); + const int timestamp = timestamps.at(i_); const QString style = QString("%1") .arg((timestamp >= baseTimestamp) @@ -5706,28 +5802,29 @@ void BMRequest_IndexGetValues::handleReply_HTML(const QStringList &args) const } reply += "</td></table>\n"; - // Right table ... + const int baseValuePos = argsElem.attributeNode("baseValuePos").value().toInt(&ok); + Q_ASSERT(ok); + const qreal baseValue = indexValues.at(baseValuePos); + const qreal linearBaseValue = qPow(2, baseValue); + + // Table 3 (log2 base diff factor) ... reply += "<td style=\"border: 0px\"><table>\n"; - reply += "<tr><th>Value</th></tr>\n"; + reply += "<tr><th>Base diff (log2)</th></tr>\n"; for (int i = 0; i < valueNodes.size(); ++i) { const int i_ = (valueNodes.size() - 1) - i; - QDomElement valueElem = valueNodes.at(i_).toElement(); - const qreal indexValue = valueElem.attributeNode("indexValue").value().toDouble(&ok); - Q_ASSERT(ok); - const qreal powIndexValue = 100 * qPow(2, indexValue); - const int contributions = valueElem.attributeNode("contributions").value().toInt(&ok); - Q_ASSERT(ok); + const qreal indexValue = indexValues.at(i_) - baseValue; + const int contributions_ = contributions.at(i_); const QString style = "index_table"; reply += "<tr>"; - if (contributions > 0) { + if (contributions_ > 0) { reply += QString( "<td class=\"%1\" style=\"background-color:%2\">%3</td>") .arg(style) .arg(BMMisc::redToGreenColor(indexValue, 0.01, 0.5, true)) - .arg(powIndexValue); + .arg(indexValue); } else { reply += QString( "<td class=\"%1\" style=\"color:red; background-color:white\">n/a</td>").arg(style); @@ -5736,6 +5833,32 @@ void BMRequest_IndexGetValues::handleReply_HTML(const QStringList &args) const } reply += "</td></table>\n"; + // Table 4 (linear base diff factor) ... + reply += "<td style=\"border: 0px\"><table>\n"; + reply += "<tr><th>Base diff (linear)</th></tr>\n"; + + for (int i = 0; i < valueNodes.size(); ++i) { + const int i_ = (valueNodes.size() - 1) - i; + const qreal indexValue = indexValues.at(i_) - baseValue; + const qreal linearIndexValue = linearIndexValues.at(i_) - linearBaseValue; + const int contributions_ = contributions.at(i_); + + const QString style = "index_table"; + + reply += "<tr>"; + if (contributions_ > 0) { + reply += QString( + "<td class=\"%1\" style=\"background-color:%2\">%3</td>") + .arg(style) + .arg(BMMisc::redToGreenColor(indexValue, 0.01, 0.5, true)) + .arg(linearIndexValue); + } else { + reply += QString( + "<td class=\"%1\" style=\"color:red; background-color:white\">n/a</td>").arg(style); + } + reply += "</tr>\n"; + } + reply += "</td></table>\n"; reply += "</table>\n"; @@ -5758,6 +5881,8 @@ void BMRequest_IndexGetValues::handleReply_Image(const QStringList &args) const const int baseTimestamp = argsElem.attributeNode("baseTimestamp").value().toInt(&ok); Q_ASSERT(ok); + const int baseValuePos = argsElem.attributeNode("baseValuePos").value().toInt(&ok); + Q_ASSERT(ok); // Get timestamps, index values, and contributions ... QList<int> timestamps; @@ -5772,7 +5897,7 @@ void BMRequest_IndexGetValues::handleReply_Image(const QStringList &args) const const qreal indexValue = valueElem.attributeNode("indexValue").value().toDouble(&ok); Q_ASSERT(ok); - indexValues += 100 * qPow(2, indexValue); + indexValues += indexValue; contributions += valueElem.attributeNode("contributions").value().toInt(&ok); Q_ASSERT(ok); @@ -5780,7 +5905,8 @@ void BMRequest_IndexGetValues::handleReply_Image(const QStringList &args) const // Create the plot ... QString error_; - Plotter *plotter = new IndexPlotter(timestamps, indexValues, contributions, baseTimestamp); + Plotter *plotter = + new IndexPlotter(timestamps, indexValues, contributions, baseTimestamp, baseValuePos); const QImage image = plotter->createImage(&error_); if (!image.isNull()) { BMMisc::printImageOutput( @@ -6501,9 +6627,15 @@ QByteArray BMRequest_GetHistories::toReplyBuffer() void BMRequest_GetHistories::handleReply_HTML(const QStringList &args) const { - // 2 B DONE! - QString error; + + error = doc.elementsByTagName("reply").at(0).toElement().attributeNode("error").value(); + if (!error.isEmpty()) { + BMMisc::printHTMLErrorPage( + QString("failed to create details page: error getting histories: %1").arg(error)); + return; + } + QStringList optValues; // Get server ... @@ -6578,14 +6710,6 @@ void BMRequest_GetHistories::handleReply_HTML(const QStringList &args) const reply += "-httpheader "; reply += "\" />\n<br />\n<br />\n"; - - error = doc.elementsByTagName("reply").at(0).toElement().attributeNode("error").value(); - if (!error.isEmpty()) { - reply = QString("failed to create details page: error getting histories: %1").arg(error); - BMMisc::printHTMLErrorPage(reply); - return; - } - reply += "</body></html>"; BMMisc::printHTMLOutput(reply); @@ -6824,3 +6948,339 @@ void BMRequest_GetBMTree::handleReply_JSON(const QStringList &args) const BMMisc::printJSONOutput(reply); } + + +// --- GetIXHistories --- +QByteArray BMRequest_GetIXHistories::toRequestBuffer(QString *) +{ + QString request = + QString( + "<request type=\"%1\"><args evalTimestamp=\"%2\" cacheKey=\"%3\" />") + .arg(name()).arg(evalTimestamp).arg(cacheKey); + + for (int i = 0; i < rankedInfos.size(); ++i) + request += QString( + "<rankedInfo id=\"%1\" basePos=\"%2\" diffPos1=\"%3\" diffPos2=\"%4\" descr=\"%5\" />") + .arg(rankedInfos.at(i).bmcontextId) + .arg(rankedInfos.at(i).basePos) + .arg(rankedInfos.at(i).diffPos1) + .arg(rankedInfos.at(i).diffPos2) + .arg(rankedInfos.at(i).descr); + + request += "</request>"; + + return xmlConvert(request); +} + +QByteArray BMRequest_GetIXHistories::toReplyBuffer() +{ + QDomElement argsElem = doc.elementsByTagName("args").at(0).toElement(); + + QString error; + QString reply; + bool ok; + + // Check if a cached reply can be returned ... + int cacheKey_ = argsElem.attributeNode("cacheKey").value().toInt(&ok); + if (ok) { + reply = Cache::instance()->get(cacheKey_); + if (!reply.isEmpty()) + return xmlConvert(reply); + } + + // Compute from scratch ... + + // Get eval timestamp ... + evalTimestamp = argsElem.attributeNode("evalTimestamp").value().toInt(&ok); + Q_ASSERT(ok); + + // Get ranked infos ... + QDomNodeList rankedInfoNodes = doc.elementsByTagName("rankedInfo"); + for (int i = 0; i < rankedInfoNodes.size(); ++i) { + QDomElement rankedInfoElem = rankedInfoNodes.at(i).toElement(); + const int bmcontextId = rankedInfoElem.attributeNode("id").value().toInt(&ok); + Q_ASSERT(ok); + const int basePos = rankedInfoElem.attributeNode("basePos").value().toInt(&ok); + Q_ASSERT(ok); + const int diffPos1 = rankedInfoElem.attributeNode("diffPos1").value().toInt(&ok); + Q_ASSERT(ok); + const int diffPos2 = rankedInfoElem.attributeNode("diffPos2").value().toInt(&ok); + Q_ASSERT(ok); + const QString descr = rankedInfoElem.attributeNode("descr").value(); + + rankedInfos.append(Index::RankedInfo(bmcontextId, basePos, diffPos1, diffPos2, descr)); + } + + QSqlQuery *query; + + reply = QString("<reply type=\"%1\" >").arg(name()); + + // Add result histories to reply ... + for (int i = 0; i < rankedInfos.size(); ++i) { + + // Get meta info ... + query = createQuery(); + if (!query->exec( + QString( + "SELECT testCase, testFunction, dataTag, metric.name, platform.name, " + " host.name, branch.gitRepo, branch.gitBranch " + " FROM bmcontext, benchmark, context, metric, platform, host, branch" + " WHERE benchmarkid = benchmark.id" + " AND contextId = context.id" + " AND metricId = metric.id" + " AND platformId = platform.id" + " AND hostId = host.id" + " AND branchId = branch.id" + " AND bmcontext.id = %1;") + .arg(rankedInfos.at(i).bmcontextId))) { + deleteQuery(query); + return xmlConvert( + errorReply( + *query, name(), "failed to get result history meta info (exec() failed)")); + } + QString testCase; + QString testFunction; + QString dataTag; + QString metric; + QString platform; + QString host; + QString gitRepo; + QString gitBranch; + while (query->next()) { + testCase = query->value(0).toString(); + testFunction = query->value(1).toString(); + dataTag = query->value(2).toString(); + metric = query->value(3).toString(); + platform = query->value(4).toString(); + host = query->value(5).toString(); + gitRepo = query->value(6).toString(); + gitBranch = query->value(7).toString(); + } + deleteQuery(query); + + reply += QString( + "<resultHistory testCase=\"%1\" testFunction=\"%2\" dataTag=\"%3\" metric=\"%4\" " + "platform=\"%5\" host=\"%6\" gitRepo=\"%7\" gitBranch=\"%8\" basePos=\"%9\" " + "diffPos1=\"%10\" diffPos2=\"%11\" descr=\"%12\" >") + .arg(testCase) + .arg(testFunction) + .arg(dataTag) + .arg(metric) + .arg(platform) + .arg(host) + .arg(gitRepo) + .arg(gitBranch) + .arg(rankedInfos.at(i).basePos) + .arg(rankedInfos.at(i).diffPos1) + .arg(rankedInfos.at(i).diffPos2) + .arg(rankedInfos.at(i).descr); + + // Get time series ... + query = createQuery(); + if (!query->exec( + QString( + "SELECT timestamp, value FROM result " + "WHERE bmcontextId=%1 " + "ORDER BY timestamp ASC;") + .arg(rankedInfos.at(i).bmcontextId))) { + deleteQuery(query); + return xmlConvert( + errorReply(*query, name(), "failed to get time series (exec() failed)")); + } + QList<int> timestamps; + QList<qreal> values; + while (query->next()) { + timestamps += query->value(0).toInt(); + values += query->value(1).toDouble(); + } + deleteQuery(query); + + for (int j = 0; j < timestamps.size(); ++j) + reply += QString("<val t=\"%1\" v=\"%2\" />").arg(timestamps.at(j)).arg(values.at(j)); + + reply += "</resultHistory>"; + } + + // Finalize the reply and cache it ... + cacheKey_ = Cache::instance()->nextId(); + reply += QString("<args evalTimestamp=\"%1\" cacheKey=\"%2\" /></reply>") + .arg(evalTimestamp) + .arg(cacheKey_); + Cache::instance()->put(cacheKey_, reply); + + return xmlConvert(reply); +} + +void BMRequest_GetIXHistories::handleReply_HTML(const QStringList &args) const +{ + QString error; + + error = doc.elementsByTagName("reply").at(0).toElement().attributeNode("error").value(); + if (!error.isEmpty()) { + BMMisc::printHTMLErrorPage( + QString( + "failed to create details page for main index contributors: %1").arg(error)); + return; + } + + QStringList optValues; + + // Get server ... + QString server; + if (BMMisc::getOption(args, "-server", &optValues, 1, 0, &error)) { + server = optValues.first().trimmed(); + } else { + BMMisc::printHTMLErrorPage("-server option not found"); + return; + } + + // Get style sheet ... + QString styleSheet; + if (BMMisc::getOption(args, "-stylesheet", &optValues, 1, 0, &error)) { + styleSheet = optValues.first().trimmed(); + } else { + BMMisc::printHTMLErrorPage("-stylesheet option not found"); + return; + } + + QDomElement argsElem = doc.elementsByTagName("args").at(0).toElement(); + + bool ok; + + // Get eval timestamp ... + evalTimestamp = argsElem.attributeNode("evalTimestamp").value().toInt(&ok); + Q_ASSERT(ok); + QDateTime evalDateTime; + evalDateTime.setTime_t(evalTimestamp); + + // *** Header *** + QString reply = QString("<html>\n<head>\n"); + reply += QString("<link rel=\"stylesheet\" type=\"text/css\" href=\"%1\" />\n").arg(styleSheet); + reply += QString("</head>\n<body>\n"); + + reply += "<span style=\"font-size:18\">Main Index Value Contributors</span>"; + + reply += "<br /><br /><table><tr><td style=\"background-color:#eeeeee\">"; + reply += QString( + "This page shows the %1 result histories that contributed most significantly ") + .arg(doc.elementsByTagName("resultHistory").size()); + reply += QString("to the index value at timestamp %1 (%2)") + .arg(evalTimestamp).arg(evalDateTime.toString()); + reply += "</td></tr></table>\n"; + + + // *** Plot *** + + // reply += "<br />args:<br />"; + // for (int i = 0; i < args.size(); ++i) + // reply += QString("arg %1: >%2<<br />").arg(i).arg(args.at(i)); + // reply += "<br />"; + + reply += "<br />\n<img src=\"bmclientwrapper?command="; + for (int i = args.indexOf("-server"); i < args.size(); ++i) + reply += QString("%1 ") + .arg((args.at(i) == "detailspage") ? QLatin1String("plot") : args.at(i)); + reply += QString("-cachekey %1 ").arg(argsElem.attributeNode("cacheKey").value()); + reply += "-httpheader "; + reply += "\" />\n<br />\n<br />\n"; + + + reply += "</body></html>"; + + BMMisc::printHTMLOutput(reply); +} + +void BMRequest_GetIXHistories::handleReply_Image(const QStringList &args) const +{ + const QString error = + doc.elementsByTagName("reply").at(0).toElement().attributeNode("error").value(); + + if (!error.isEmpty()) { + BMMisc::printHTMLErrorPage( + QString( + "failed to create plot for main index contributors: %1").arg(error)); + return; + } + + QDomElement argsElem = doc.elementsByTagName("args").at(0).toElement(); + + bool ok; + + // Get eval timestamp ... + evalTimestamp = argsElem.attributeNode("evalTimestamp").value().toInt(&ok); + Q_ASSERT(ok); + + QList<ResultHistoryInfo *> rhInfos; + QList<int> basePos; + QList<QList<int> > extraPos; + QStringList descr; + + // Loop over result histories ... + QDomNodeList rhNodes = doc.elementsByTagName("resultHistory"); + for (int i = 0; i < rhNodes.size(); ++i) { + QDomElement rhElem = rhNodes.at(i).toElement(); + + // Get the context ... + const QString metric = rhElem.attributeNode("metric").value(); + const QString platform = rhElem.attributeNode("platform").value(); + const QString host = rhElem.attributeNode("host").value(); + const QString gitRepo = rhElem.attributeNode("gitRepo").value(); + const QString gitBranch = rhElem.attributeNode("gitBranch").value(); + + // Get the benchmark ... + const QString testCase = rhElem.attributeNode("testCase").value(); + const QString testFunction = rhElem.attributeNode("testFunction").value(); + const QString dataTag = rhElem.attributeNode("dataTag").value(); + + // Get the time series ... + QList<int> timestamps; + QList<qreal> values; + QDomNodeList valueNodes = rhElem.elementsByTagName("val"); + for (int j = 0; j < valueNodes.size(); ++j) { + bool ok; + QDomElement valueElem = valueNodes.at(j).toElement(); + timestamps += valueElem.attributeNode("t").value().toInt(&ok); + Q_ASSERT(ok); + values += valueElem.attributeNode("v").value().toDouble(&ok); + Q_ASSERT(ok); + } + + rhInfos.append( + new ResultHistoryInfo( + -1, timestamps, values, metric, platform, host, gitRepo, gitBranch, testCase, + testFunction, dataTag)); + + // Get the base- and diff positions and description ... + const int basePos_ = rhElem.attributeNode("basePos").value().toInt(&ok); + Q_ASSERT(ok); + const int diffPos1 = rhElem.attributeNode("diffPos1").value().toInt(&ok); + Q_ASSERT(ok); + const int diffPos2 = rhElem.attributeNode("diffPos2").value().toInt(&ok); + Q_ASSERT(ok); + + basePos.append(basePos_); + extraPos.append(QList<int>() << diffPos1 << diffPos2); + + descr.append(rhElem.attributeNode("descr").value()); + } + + // Create image ... + QString error_; + Plotter *plotter = + new HistoriesPlotter(rhInfos, true, evalTimestamp, &basePos, &extraPos, &descr); + const QImage image = plotter->createImage(&error_); + + // Free dynamic memory ... + for (int i = 0; i < rhInfos.size(); ++i) + delete rhInfos.at(i); + + // Output image ... + if (!image.isNull()) { + BMMisc::printImageOutput( + image, "png", QString(), BMMisc::hasOption(args, "-httpheader")); + } else { + BMMisc::printHTMLErrorPage( + QString( + "failed to create plot for main index contributors: %1").arg(error_)); + } +} diff --git a/src/bm/bmrequest.h b/src/bm/bmrequest.h index a769e0f..0732778 100644 --- a/src/bm/bmrequest.h +++ b/src/bm/bmrequest.h @@ -25,6 +25,7 @@ #define BMREQUEST_H #include "bmmisc.h" +#include "index.h" #include <QtXml> #include <QImage> #include <QColor> @@ -693,6 +694,7 @@ private: }; // ### 2 B DOCUMENTED (in bmclient.html and bmproto.html) +// ### NOTE: This request should be renamed to GetTCHistories ... 2 B DONE! class BMRequest_GetHistories : public BMRequest { public: @@ -730,4 +732,27 @@ private: void handleReply_JSON(const QStringList &args) const; }; +// ### 2 B DOCUMENTED (in bmclient.html and bmproto.html) +class BMRequest_GetIXHistories : public BMRequest +{ +public: + BMRequest_GetIXHistories( + const int evalTimestamp, const QList<Index::RankedInfo> &rankedInfos, + const QString &cacheKey) + : evalTimestamp(evalTimestamp), rankedInfos(rankedInfos), cacheKey(cacheKey) {} + BMRequest_GetIXHistories(const QDomDocument &doc) : BMRequest(doc) {} +private: + mutable int evalTimestamp; + QList<Index::RankedInfo> rankedInfos; + QString cacheKey; + + QString name() const { return "GetIXHistories"; } + QByteArray toRequestBuffer(QString *error); + QByteArray toReplyBuffer(); + void handleReply_Raw(const QStringList &args) const { Q_UNUSED(args); /* 2 B DONE! */ }; + void handleReply_JSON(const QStringList &args) const { Q_UNUSED(args); /* 2 B DONE! */ } + void handleReply_HTML(const QStringList &args) const; + void handleReply_Image(const QStringList &args) const; +}; + #endif // BMREQUEST_H diff --git a/src/bm/index.cpp b/src/bm/index.cpp new file mode 100644 index 0000000..4e1cbd1 --- /dev/null +++ b/src/bm/index.cpp @@ -0,0 +1,288 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the BM project on Qt Labs. +** +** This file may be used under the terms of the GNU General Public +** License version 2.0 or 3.0 as published by the Free Software Foundation +** and appearing in the file LICENSE.GPL included in the packaging of +** this file. Please review the following information to ensure GNU +** General Public Licensing requirements will be met: +** http://www.fsf.org/licensing/licenses/info/GPLv2.html and +** http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +****************************************************************************/ + +#include "index.h" +#include "resulthistoryinfo.h" +#include "bmmisc.h" +#include <QBitArray> +#include <QDebug> + + +// ### 2 B DOCUMENTED! +// Note: The dynamic memory in \a candidateRHInfos is deleted by this class. +Index::Index( + const QList<ResultHistoryInfo *> &candidateRHInfos, const int baseTimestamp, + const QList<int> &evalTimestamps, int medianWinSize, int *rejectedNonPositive) + : baseTimestamp(baseTimestamp), evalTimestamps(evalTimestamps) + , medianWinSize(medianWinSize), valid(false), invalidReason_("uninitialized") +{ + init(candidateRHInfos, rejectedNonPositive); +} + +Index::~Index() +{ + // Free dynamic memory ... + for (int i = 0; i < rhInfos.size(); ++i) + delete rhInfos.at(i); +} + +// ### 2 B DOCUMENTED! +void Index::init(const QList<ResultHistoryInfo *> &candidateRHInfos, int *rejectedNonPositive) +{ + if (baseTimestamp < 0) { + valid = false; + invalidReason_ = "negative base timestamp"; + return; + } + + for (int i = 1; i < evalTimestamps.size(); ++i) { + if (evalTimestamps.at(i - 1) > evalTimestamps.at(i)) { + valid = false; + invalidReason_ = "non-increasing eval timestamp order"; + return; + } + } + + if (evalTimestamps.first() < 0) { + valid = false; + invalidReason_ = "negative eval timestamp"; + return; + } + + populateRHInfos(candidateRHInfos, rejectedNonPositive); + if (rhInfos.isEmpty()) { + valid = false; + invalidReason_ = "no valid result histories"; + return; + } + + valid = true; + invalidReason_ = ""; +} + +// Populates rhInfos from \a candidateRHInfos by rejecting certain result histories. +void Index::populateRHInfos( + const QList<ResultHistoryInfo *> &candidateRHInfos, int *rejectedNonPositive) +{ + int rejectedNonPositive_ = 0; + + for (int i = 0; i < candidateRHInfos.size(); ++i) { + + ResultHistoryInfo *rhInfo = candidateRHInfos.at(i); + Q_ASSERT(rhInfo->size() > 0); + + // Reject candidate if the time series contains at least one non-positive value ... + int j = 0; + for (; j < rhInfo->size(); ++j) { + if (rhInfo->value(j) <= 0.0) + break; + } + if (j < rhInfo->size()) { + delete rhInfo; + ++rejectedNonPositive_; + continue; + } + + // Accept candidate ... + rhInfos.append(rhInfo); + } + + if (rejectedNonPositive) + *rejectedNonPositive = rejectedNonPositive_; +} + + +//+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+= + +bool IndexAlgorithm1::computeValues( + QList<qreal> *values, int *baseValuePos, QList<int> *contrCounts, QString *error, + QList<QList<RankedInfo> > *topContr, int topContrLimit) const +{ + if (!isValid()) { + *error = invalidReason(); + return false; + } + + Q_ASSERT(rhInfos.size() > 0); + Q_ASSERT(values); + values->clear(); + Q_ASSERT(contrCounts); + contrCounts->clear(); + + QBitArray contr(rhInfos.size()); // Set of contributors (i.e. whether each result + // history is contributing at the current timestamp). + QBitArray hasBase(rhInfos.size()); // Whether a base value has been established for + // each result history. + QVector<int> basePos(rhInfos.size()); // Base value pos of each result history. + QVector<qreal> diffPrev(rhInfos.size()); // Previous base diff value of each result hist. + QVector<qreal> targetValPrev(rhInfos.size()); // Previous target value of each result history. + QVector<int> targetPosPrev(rhInfos.size()); // Previous target pos of each result history. + qreal hc = 0.0; // History constant to adjust for changes to the set of contributors. + + bool contr_prev_empty = true; + + *baseValuePos = -1; + + // Loop over evaluation timestamps ... + for (int i = 0; i < evalTimestamps.size(); ++i) { + + if (*baseValuePos == -1) { + // Determine if this evaluation timestamp represents the base timestamp ... + if (baseTimestamp < evalTimestamps.first()) { + *baseValuePos = 0; + } else if (((i < (evalTimestamps.size() - 1)) + && (baseTimestamp >= evalTimestamps.at(i)) + && (baseTimestamp < evalTimestamps.at(i + 1))) + || (i == (evalTimestamps.size() - 1))) + *baseValuePos = i; + } + + const QBitArray contr_prev = contr; + contr_prev_empty = (contr_prev.count(true) == 0); + + contr.fill(false); + + qreal dsum_all = 0.0; // Base diff sum of all contributors. + qreal dsum_existing = 0.0; // Base diff sum of all contributors that contributed at + // the previous evaluation timestamp as well. + + // Lists of most significant contributors ranked on BDC, where BDC is + // the base diff change between the current and the previous eval timestamp: + QList<QPair<RankedInfo, qreal> > rankedBest; // ... highest BDC + QList<QPair<RankedInfo, qreal> > rankedWorst; // ... lowest BDC + + // Loop over potential contributors ... + for (int j = 0; j < rhInfos.size(); ++j) { + + const ResultHistoryInfo *rhInfo = rhInfos.at(j); + + // Attempt to sample the result history at this timestamp ... + int targetPos = -1; + if (rhInfo->getSmoothPos(evalTimestamps.at(i), medianWinSize, &targetPos)) { + + const qreal targetVal = rhInfo->value(targetPos); + + if (hasBase.testBit(j)) { + + // Compute base diff and accumulate contribution ... + + const qreal baseVal = rhInfo->value(basePos.at(j)); + const qreal diff = BMMisc::log2diff(targetVal, baseVal, rhInfo->metric()); + + dsum_all += diff; + if (contr_prev.testBit(j)) + dsum_existing += diff; + + contr.setBit(j); + + const qreal diffChange = diff - diffPrev.at(j); + + const QString descr = + QString("base val: %1; val1: %2; val2: %3; abs log2 diff change: %4") + .arg(baseVal) + .arg(targetValPrev.at(j)) + .arg(targetVal) + .arg(qAbs(diffChange)); + RankedInfo rankedInfo( + rhInfo->bmcontextId(), basePos.at(j), targetPosPrev.at(j), targetPos, + descr); + + BMMisc::insertRankedId<RankedInfo>( + &rankedBest, topContrLimit, rankedInfo, diffChange); + BMMisc::insertRankedId<RankedInfo>( + &rankedWorst, topContrLimit, rankedInfo, -diffChange); + diffPrev.replace(j, diff); + + } else { + // Record base value position ... + basePos.replace(j, targetPos); + hasBase.setBit(j); + } + + targetValPrev.replace(j, targetVal); + targetPosPrev.replace(j, targetPos); + } + } + + contrCounts->append(contr.count(true)); // record contribution count + + qreal indexValue = -1.0; + + if (contr.count(true) > 0) { + // A base diff was computable for at least one contributor at this eval timestamp, + // so a valid index value may be derived ... + + const qreal dmean_all = dsum_all / contr.count(true); + + if (contr_prev.count(true) > 0) { + + // A base diff was computable for at least one contributor at the previous + // eval timestamp as well, so the set of contributors may potentially have + // been changed. + + // Compute the arithmetic mean of the existing contributors only (i.e. those + // who contributed both at this eval timestamp and the previous one) ... + const qreal dmean_existing = dsum_existing / contr_prev.count(true); + + // The index value should only be influenced by the existing contributors only ... + indexValue = dmean_existing - hc; + + if (contr != contr_prev) { + // The set of contributors changed, so adjust the history constant to ensure + // that the index value for the next eval timestamp is not influenced by the + // new contributors until their base diffs start to change ... + hc += (dmean_all - dmean_existing); + } + + } else { + + indexValue = dmean_all; // special initialization case + } + } + + values->append(indexValue); // record the raw index value (valid or not) + + if (topContr) { + + // Establish (if possible) a list of the most significant contributors ... + + QList<RankedInfo> tc; + + if ((i > 0) && (contrCounts->at(i) > 0) && (contrCounts->at(i - 1) > 0)) { + // The index value was computable for both this eval timestamp and the one + // preceding it, so rank wrt. high or low base diff change depending on whether + // the index value went up or down ... + QList<QPair<RankedInfo, qreal> > * ranked = + (values->at(i) > values->at(i - 1)) + ? &rankedBest // up + : &rankedWorst; // down + for (int j = 0; j < ranked->size(); ++j) + tc.append(ranked->at(j).first); + } + + topContr->append(tc); + } + } + + return true; +} diff --git a/src/bm/index.h b/src/bm/index.h new file mode 100644 index 0000000..75cf85c --- /dev/null +++ b/src/bm/index.h @@ -0,0 +1,97 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the BM project on Qt Labs. +** +** This file may be used under the terms of the GNU General Public +** License version 2.0 or 3.0 as published by the Free Software Foundation +** and appearing in the file LICENSE.GPL included in the packaging of +** this file. Please review the following information to ensure GNU +** General Public Licensing requirements will be met: +** http://www.fsf.org/licensing/licenses/info/GPLv2.html and +** http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +****************************************************************************/ + +#ifndef INDEX_H +#define INDEX_H + +#include <QString> +#include <QList> + +class ResultHistoryInfo; + +class Index { +public: + Index( + const QList<ResultHistoryInfo *> &candidateRHInfos, const int baseTimestamp, + const QList<int> &evalTimestamps, int medianWinSize, int *rejectedNonPositive = 0); + virtual ~Index(); + + bool isValid() const { return valid; } + QString invalidReason() const {return invalidReason_; } + + struct RankedInfo { + int bmcontextId; + int basePos; + int diffPos1; // target pos for the eval timestamp that precedes the main eval timestamp + int diffPos2; // target pos for the main eval timestamp + QString descr; // free description + RankedInfo( + const int bmcontextId, const int basePos, const int diffPos1, const int diffPos2, + const QString &descr) + : bmcontextId(bmcontextId), basePos(basePos), diffPos1(diffPos1), diffPos2(diffPos2) + , descr(descr) + {} + }; + + // Computes the index value at each evaluation timestamp into \a values. The position + // of the index value corresponding to the base timestamp is passed in \a baseValuePos. + // The umber of contributing result histories for each evaluation timestamp is passed + // in \a contrCounts. + // If non-null, the \a topContr list will upon return contain a list of the \a topContrLimit + // most significant contributors at each evaluation timestamp. + virtual bool computeValues( + QList<qreal> *values, int *baseValuePos, QList<int> *contrCounts, QString *error, + QList<QList<RankedInfo> > *topContr = 0, int topContrLimit = -1) const = 0; + +protected: + QList<ResultHistoryInfo *> rhInfos; + int baseTimestamp; + QList<int> evalTimestamps; + int medianWinSize; + +private: + bool valid; + QString invalidReason_; + + void init( + const QList<ResultHistoryInfo *> &candidateRHInfos, int *rejectedNonPositive = 0); + void populateRHInfos( + const QList<ResultHistoryInfo *> &candidateRHInfos, int *rejectedNonPositive = 0); +}; + +// ### 2 B DOCUMENTED! +class IndexAlgorithm1 : public Index { +public: + IndexAlgorithm1( + const QList<ResultHistoryInfo *> &candidateRHInfos, const int baseTimestamp, + const QList<int> &evalTimestamps, int medianWinSize, int *rejectedNonPositive = 0) + : Index( + candidateRHInfos, baseTimestamp, evalTimestamps, medianWinSize, rejectedNonPositive) + {} + + bool computeValues( + QList<qreal> *values, int *baseValuePos, QList<int> *contrCounts, QString *error, + QList<QList<RankedInfo> > *topContr = 0, int topContrLimit = -1) const; +}; + +#endif // INDEX_H diff --git a/src/bm/plotter.cpp b/src/bm/plotter.cpp index cbfca53..7a34145 100644 --- a/src/bm/plotter.cpp +++ b/src/bm/plotter.cpp @@ -27,13 +27,14 @@ #include <QGraphicsSimpleTextItem> #include <QPainter> #include <QDebug> +#include <qmath.h> // ### Note: There is a potential for refactoring in order to avoid code duplication // in this file. // Template method. -QImage Plotter::createImage(QString *error) const +QImage Plotter::createImage(QString *error, QList<PointInfo> *pointInfos) const { const QRectF sceneRect_ = sceneRect(); @@ -41,7 +42,7 @@ QImage Plotter::createImage(QString *error) const QGraphicsScene scene_mid_aa(sceneRect_); // Middle scene (antialiased) QGraphicsScene scene_near(sceneRect_); // Foreground scene (rendered last) - if (!drawScenes(&scene_far, &scene_mid_aa, &scene_near, error)) + if (!drawScenes(&scene_far, &scene_mid_aa, &scene_near, error, pointInfos)) return QImage(); Q_ASSERT(scene_far.sceneRect() == scene_mid_aa.sceneRect()); @@ -82,8 +83,10 @@ QRectF ResultHistoryPlotter::sceneRect() const // other *::drawScenes() implementations bool ResultHistoryPlotter::drawScenes( QGraphicsScene *scene_far, QGraphicsScene *scene_mid_aa, QGraphicsScene *scene_near, - QString *error) const + QString *error, QList<PointInfo> *pointInfos) const { + Q_UNUSED(pointInfos); + if (timestamps.isEmpty()) { if (error) *error = "no data to plot (no values)"; @@ -345,9 +348,9 @@ bool ResultHistoryPlotter::drawScenes( IndexPlotter::IndexPlotter( const QList<int> ×tamps, const QList<qreal> &values, const QList<int> &contributions, - const int baseTimestamp, const bool showBaseValue, const qreal baseValue) + const int baseTimestamp, const int baseValuePos) : timestamps(timestamps), values(values), contributions(contributions) - , baseTimestamp(baseTimestamp), showBaseValue(showBaseValue), baseValue(baseValue) + , baseTimestamp(baseTimestamp), baseValuePos(baseValuePos) { width = 1100; height = 450; @@ -363,7 +366,7 @@ QRectF IndexPlotter::sceneRect() const // other *::drawScenes() implementations bool IndexPlotter::drawScenes( QGraphicsScene *scene_far, QGraphicsScene *scene_mid_aa, QGraphicsScene *scene_near, - QString *error) const + QString *error, QList<PointInfo> *pointInfos) const { // --- BEGIN main graph --- @@ -399,32 +402,58 @@ bool IndexPlotter::drawScenes( xfact = tfact * (xmax - xmin); } + // Shift curve vertically according to base value if present ... + QList<qreal> dispValues(values); + const qreal dispBaseValue = 0.0; // by definition + const qreal linearDispBaseValue = 0.0; // by definition + qreal baseValue = 0.0; + + if (baseValuePos >= 0) { + Q_ASSERT(baseValuePos < values.size()); + baseValue = values.at(baseValuePos); + for (int i = 0; i < values.size(); ++i) + dispValues[i] -= baseValue; + } + QList<bool> missing; for (int i = 0; i < contributions.size(); ++i) missing.append(contributions.at(i) == 0); - qreal vmin = -1; - qreal vmax = -1; + int vminPos = -1; + int vmaxPos = -1; const int firstNonMissing = missing.indexOf(false); if (firstNonMissing >= 0) { - vmin = vmax = values.at(firstNonMissing); - for (int i = firstNonMissing + 1; i < values.size(); ++i) { + vminPos = vmaxPos = firstNonMissing; + for (int i = firstNonMissing + 1; i < dispValues.size(); ++i) { if (!missing.at(i)) { - vmin = qMin(vmin, values.at(i)); - vmax = qMax(vmax, values.at(i)); + if (dispValues.at(i) < dispValues.at(vminPos)) + vminPos = i; + if (dispValues.at(i) > dispValues.at(vmaxPos)) + vmaxPos = i; } } } + const qreal vmin = (vminPos >= 0) ? dispValues.at(vminPos) : -1; + const qreal vmax = (vmaxPos >= 0) ? dispValues.at(vmaxPos) : -1; + const qreal vfact = 1 / (vmax - vmin); // zero division handled elsewhere const qreal yfact = vfact * (ymax - ymin); + qreal linearVMin = 0.0; + qreal linearVMax = 0.0; + if ((baseValuePos >= 0) && (vminPos >= 0) && (vmaxPos >= 0)) { + const qreal linearBaseValue = qPow(2, baseValue); + linearVMin = qPow(2, values.at(vminPos)) - linearBaseValue; + linearVMax = qPow(2, values.at(vmaxPos)) - linearBaseValue; + } + // Compute scene coordinates of history curve ... QList<qreal> x; QList<qreal> y; - for (int i = 0; i < values.size(); ++i) { + for (int i = 0; i < timestamps.size(); ++i) { const qreal t = timestamps.at(i); - const qreal v = values.at(i); + const qreal v = dispValues.at(i); x.append(((timestamps.size() > 1) || (loTimestamp < hiTimestamp)) ? (xmin + (t - loTimestamp) * xfact) : xdefault); @@ -463,11 +492,11 @@ bool IndexPlotter::drawScenes( // Draw border rectangle ... scene_far->addRect(xmin, ymin, xmax - xmin, ymax - ymin, QPen(QColor(220, 220, 220, 255))); - if (showBaseValue) { + if (baseValuePos >= 0) { // Draw line indicating the base value ... QColor color(0, 0, 255); color.setAlpha(100); - const qreal y_base = BMMisc::v2y(baseValue, ymax, vmin, yfact, ydefault); + const qreal y_base = BMMisc::v2y(dispBaseValue, ymax, vmin, yfact, ydefault); scene_far->addLine(xmin, y_base, xmax, y_base, QPen(color, 1)); } @@ -495,22 +524,28 @@ bool IndexPlotter::drawScenes( scene_mid_aa->addPath(path, QPen(QColor(128, 128, 128, 255))); } - // Draw data points (except the one representing the base value if any) ... + // Draw data points ... { QPainterPath path; path.setFillRule(Qt::WindingFill); QPainterPath missingPath; const qreal ymid = 0.5 * (ymin + ymax); const qreal dpSize_2 = 0.5 * dpSize; - for (int i = 0; i < x.size(); ++i) + for (int i = 0; i < x.size(); ++i) { + QRectF rect; if (missing.at(i)) { - missingPath.moveTo(x.at(i) - dpSize_2, ymid - dpSize_2); - missingPath.lineTo(x.at(i) + dpSize_2, ymid + dpSize_2); - missingPath.moveTo(x.at(i) - dpSize_2, ymid + dpSize_2); - missingPath.lineTo(x.at(i) + dpSize_2, ymid - dpSize_2); + rect = QRectF(x.at(i) - dpSize_2, ymid - dpSize_2, dpSize, dpSize); + missingPath.moveTo(rect.topLeft()); + missingPath.lineTo(rect.bottomRight()); + missingPath.moveTo(rect.bottomLeft()); + missingPath.lineTo(rect.topRight()); } else { - path.addRect(x.at(i) - dpSize_2, y.at(i) - dpSize_2, dpSize, dpSize); + rect = QRectF(x.at(i) - dpSize_2, y.at(i) - dpSize_2, dpSize, dpSize); + path.addRect(rect); } + if (pointInfos) + pointInfos->append(PointInfo(rect, dispValues.at(i))); + } const QColor color(0, 0, 0, 255); scene_near->addPath(path, QPen(color), QBrush(color)); @@ -525,10 +560,9 @@ bool IndexPlotter::drawScenes( // ... left y axis ... { - const QString yCaption = "value"; // ### 4 NOW QFont font; font.setPointSize(14); - QGraphicsSimpleTextItem *text = scene_near->addSimpleText(yCaption, font); + QGraphicsSimpleTextItem *text = scene_near->addSimpleText("base diff (log2)", font); const qreal x_ = xmin - text->boundingRect().height() - captionPad; const qreal y_ = (ymin + ymax) / 2 + text->boundingRect().width() / 2; text->setTransform(QTransform().translate(x_, y_).rotate(-90)); @@ -549,21 +583,65 @@ bool IndexPlotter::drawScenes( ymin - text->boundingRect().height() / 2); } - if (showBaseValue && ((baseValue >= vmin) && (baseValue <= vmax))) { + if ((baseValuePos >= 0) && (dispBaseValue >= vmin) && (dispBaseValue <= vmax)) { QGraphicsSimpleTextItem *text = - scene_near->addSimpleText(QString().setNum(baseValue)); - const qreal y_base = BMMisc::v2y(baseValue, ymax, vmin, yfact, ydefault); + scene_near->addSimpleText(QString().setNum(dispBaseValue)); + const qreal y_base = BMMisc::v2y(dispBaseValue, ymax, vmin, yfact, ydefault); + qDebug() << "y_base left:" << y_base; text->setPos( xmin - captionPad - captionHeight - labelPad - text->boundingRect().width(), y_base - text->boundingRect().height() / 2); - // Avoid overlap + // Avoid overlap ... if (!scene_near->collidingItems(text).isEmpty()) { scene_near->removeItem(text); delete text; } } + + // ... right y axis ... + { + QFont font; + font.setPointSize(14); + QGraphicsSimpleTextItem *text = scene_near->addSimpleText("base diff (linear)", font); + const qreal x_ = xmax + captionPad; + const qreal y_ = (ymin + ymax) / 2 + text->boundingRect().width() / 2; + text->setTransform(QTransform().translate(x_, y_).rotate(-90)); + captionHeight = text->boundingRect().height(); + } + { + QGraphicsSimpleTextItem *text = + scene_near->addSimpleText(QString().setNum(linearVMin)); + text->setPos( + xmax + captionPad + captionHeight + labelPad, + ymax - text->boundingRect().height() / 2); + } + { + QGraphicsSimpleTextItem *text = + scene_near->addSimpleText(QString().setNum(linearVMax)); + text->setPos( + xmax + captionPad + captionHeight + labelPad, + ymin - text->boundingRect().height() / 2); + } + + if ((baseValuePos >= 0) + && (linearDispBaseValue >= linearVMin) && (linearDispBaseValue <= linearVMax)) { + QGraphicsSimpleTextItem *text = + scene_near->addSimpleText(QString().setNum(linearDispBaseValue)); + const qreal y_base = BMMisc::v2y(dispBaseValue, ymax, vmin, yfact, ydefault); + text->setPos( + xmax + captionPad + captionHeight + labelPad, + y_base - text->boundingRect().height() / 2); + + // Avoid overlap ... + if (!scene_near->collidingItems(text).isEmpty()) { + scene_near->removeItem(text); + delete text; + } + } + + // ... top x axis ... { QFont font; @@ -607,10 +685,18 @@ bool IndexPlotter::drawScenes( } -HistoriesPlotter::HistoriesPlotter(const QList<ResultHistoryInfo *> &rhInfos) +HistoriesPlotter::HistoriesPlotter( + const QList<ResultHistoryInfo *> &rhInfos, const bool showBenchmark, const int baseTimestamp, + const QList<int> *basePos, const QList<QList<int> > *extraPos, const QStringList *extraDescr) : rhInfos(rhInfos) + , showBenchmark(showBenchmark) + , baseTimestamp(baseTimestamp) + , basePos(basePos) + , extraPos(extraPos) + , extraDescr(extraDescr) , width(1200) - , rhHeight(qMin(150, qMax(100, 800 / rhInfos.size()))) + , rhHeight( + qMin(showBenchmark ? 200 : 150, qMax(showBenchmark ? 150 : 100, 800 / rhInfos.size()))) , pad_top(50) , pad_bottom(50) , height(pad_top + pad_bottom + rhInfos.size() * rhHeight) @@ -630,12 +716,40 @@ struct MetricInfo { }; // ### 2 B DOCUMENTED! +static void appendYAxisLabel( + const QString &label, const QColor &color, QGraphicsScene *scene, qreal *textYPos, + qreal xmax, qreal labelPad) +{ + QFont font; + font.setPointSize(10); + font.setFamily("mono"); + + const qreal labelPad_rh_ver = 3; + + QGraphicsSimpleTextItem *text = scene->addSimpleText(label, font); + text->setPos(xmax + labelPad, *textYPos); + (*textYPos) += text->boundingRect().height() + labelPad_rh_ver; + + // Adjust the scene rect horizontally to accommodate the label (assuming scene + // top-left corner is at (0, 0)) ... + const qreal requiredSceneWidth = text->pos().x() + text->boundingRect().width(); + if (requiredSceneWidth > scene->sceneRect().width()) + scene->setSceneRect(QRectF(0, 0, requiredSceneWidth, scene->sceneRect().height())); + + QGraphicsItem *bgRect = scene->addRect(text->boundingRect(), QPen(color), QBrush(color)); + bgRect->setPos(text->pos()); + bgRect->setZValue(-1); +} + +// ### 2 B DOCUMENTED! // ### 2 B DONE: Maybe factor out parts that this function has in common with // other *::drawScenes() implementations bool HistoriesPlotter::drawScenes( QGraphicsScene *scene_far, QGraphicsScene *scene_mid_aa, QGraphicsScene *scene_near, - QString *error) const + QString *error, QList<PointInfo> *pointInfos) const { + Q_UNUSED(pointInfos); + if (rhInfos.isEmpty()) { if (error) *error = "no result histories to plot"; @@ -695,6 +809,9 @@ bool HistoriesPlotter::drawScenes( QFont font; const qreal labelPad = 10; + Q_ASSERT((!extraPos) || (extraPos->size() == rhInfos.size())); + Q_ASSERT((!extraDescr) || (extraDescr->size() == rhInfos.size())); + // Plot the result histories below each other ... for (int rh = 0; rh < rhInfos.size(); ++rh) { @@ -712,7 +829,6 @@ bool HistoriesPlotter::drawScenes( // Compute scene coordinates of history curve ... QList<qreal> x; QList<qreal> y; - qDebug() << ""; for (int i = 0; i < rhInfo->size(); ++i) { const qreal t = rhInfo->timestamp(i); const qreal v = rhInfo->value(i); @@ -766,6 +882,14 @@ bool HistoriesPlotter::drawScenes( bgRect->setZValue(-1); } + if ((baseTimestamp >= loTimestamp) && (baseTimestamp <= hiTimestamp)) { + // Draw line indicating the base timestamp ... + QColor color(0, 0, 255); + color.setAlpha(200); + const qreal btx = xmin + (baseTimestamp - loTimestamp) * xfact; + scene_far->addLine(btx, ymin, btx, ymax, QPen(color, 2)); + } + const qreal dpSize = 4; // Draw history curve between data points ... @@ -788,6 +912,43 @@ bool HistoriesPlotter::drawScenes( scene_near->addPath(path, QPen(color), QBrush(color)); } + if (basePos) { + // Highlight base position ... + + const int bPos = basePos->at(rh); + + const qreal dpSize_large = dpSize * 4; + const qreal dpSize_large_2 = dpSize_large / 2; + + QPainterPath path; + path.addEllipse( + x.at(bPos) - dpSize_large_2, + y.at(bPos) - dpSize_large_2, + dpSize_large, dpSize_large); + scene_far->addPath( + path, QPen(QColor(0, 0, 255, 255)), QBrush(QColor(200, 200, 255, 255))); + } + + if (extraPos) { + // Highlight extra positions ... + + QList<int> exPos = extraPos->at(rh); + + const qreal dpSize_large = dpSize * 3; + const qreal dpSize_large_2 = dpSize_large / 2; + + for (int i = 0; i < exPos.size(); ++i) { + QPainterPath path; + path.addEllipse( + x.at(exPos.at(i)) - dpSize_large_2, + y.at(exPos.at(i)) - dpSize_large_2, + dpSize_large, dpSize_large); + scene_far->addPath( + path, QPen(QColor(255, 0, 0, 255)), QBrush(QColor(255, 200, 200, 255))); + } + } + + font.setPointSize(12); // Draw labels on left y axis ... @@ -807,68 +968,42 @@ bool HistoriesPlotter::drawScenes( // Draw labels on right y axis ... qreal textYPos = ymin; - const qreal labelPad_rh_ver = 3; - font.setPointSize(10); - font.setFamily("mono"); - // ... metric ... - { - QGraphicsSimpleTextItem *text = - scene_near->addSimpleText( - QString("M: %1").arg(rhInfo->metric()), font); - text->setPos(xmax + labelPad, textYPos); - textYPos += text->boundingRect().height() + labelPad_rh_ver; - - QColor color(255, 255, 200, 255); - QGraphicsItem *bgRect = scene_near->addRect( - text->boundingRect(), QPen(color), QBrush(color)); - bgRect->setPos(text->pos()); - bgRect->setZValue(-1); + appendYAxisLabel( + QString("MT: %1").arg(rhInfo->metric()), QColor(255, 255, 200, 255), + scene_near, &textYPos, xmax, labelPad); + appendYAxisLabel( + QString("PF: %1").arg(rhInfo->platform()), QColor(200, 255, 255, 255), + scene_near, &textYPos, xmax, labelPad); + appendYAxisLabel( + QString("HT: %1").arg(rhInfo->host()), QColor(230, 210, 255, 255), + scene_near, &textYPos, xmax, labelPad); + appendYAxisLabel( + QString("BR: %1 %2").arg(rhInfo->gitRepo()).arg(rhInfo->gitBranch()), + QColor(255, 230, 210, 255), + scene_near, &textYPos, xmax, labelPad); + if (showBenchmark) { + appendYAxisLabel( + QString("TC: %1").arg(rhInfo->testCase()), QColor(220, 220, 220, 255), + scene_near, &textYPos, xmax, labelPad); + appendYAxisLabel( + QString("TF: %1").arg(rhInfo->testFunction()), QColor(220, 220, 220, 255), + scene_near, &textYPos, xmax, labelPad); + appendYAxisLabel( + QString("DT: %1").arg(rhInfo->dataTag()), QColor(220, 220, 220, 255), + scene_near, &textYPos, xmax, labelPad); } - // ... platform ... - { - QGraphicsSimpleTextItem *text = - scene_near->addSimpleText( - QString("P: %1").arg(rhInfo->platform()), font); - text->setPos(xmax + labelPad, textYPos); - textYPos += text->boundingRect().height() + labelPad_rh_ver; - - QColor color(200, 255, 255, 255); - QGraphicsItem *bgRect = scene_near->addRect( - text->boundingRect(), QPen(color), QBrush(color)); - bgRect->setPos(text->pos()); - bgRect->setZValue(-1); + if (extraDescr) { + appendYAxisLabel( + QString("%1").arg(extraDescr->at(rh)), QColor(255, 255, 255, 255), + scene_near, &textYPos, xmax, labelPad); } - // ... host ... - { - QGraphicsSimpleTextItem *text = - scene_near->addSimpleText( - QString("H: %1").arg(rhInfo->host()), font); - text->setPos(xmax + labelPad, textYPos); - textYPos += text->boundingRect().height() + labelPad_rh_ver; - - QColor color(230, 210, 255, 255); - QGraphicsItem *bgRect = scene_near->addRect( - text->boundingRect(), QPen(color), QBrush(color)); - bgRect->setPos(text->pos()); - bgRect->setZValue(-1); - } - - // ... git repo/branch ... - { - QGraphicsSimpleTextItem *text = - scene_near->addSimpleText( - QString("B: %1 %2").arg(rhInfo->gitRepo()).arg(rhInfo->gitBranch()), font); - text->setPos(xmax + labelPad, textYPos); - textYPos += text->boundingRect().height() + labelPad_rh_ver; - - QColor color(255, 230, 210, 255); - QGraphicsItem *bgRect = scene_near->addRect( - text->boundingRect(), QPen(color), QBrush(color)); - bgRect->setPos(text->pos()); - bgRect->setZValue(-1); + // Compensate for a possible expansion of the scene rect of scene_near ... + if (scene_near->sceneRect() != scene_mid_aa->sceneRect()) { + scene_mid_aa->setSceneRect(scene_near->sceneRect()); + scene_far->setSceneRect(scene_near->sceneRect()); } // Draw vertical lines indicating display timestamps ... @@ -907,9 +1042,37 @@ bool HistoriesPlotter::drawScenes( QGraphicsSimpleTextItem *text = scene_near->addSimpleText(xLabel, font); text->setPos( xpos - text->boundingRect().width() / 2, - ymax + labelPad); + ymax + labelPad * 2); } } + if ((baseTimestamp >= loTimestamp) && (baseTimestamp <= hiTimestamp)) { + + const QString xLabel = BMMisc::compactDateString(baseTimestamp); + font.setPointSize(12); + + const qreal xpos = xmin + (baseTimestamp - loTimestamp) * xfact; + const QColor color(0, 0, 255); + + // ... top x axis label ... + { + QGraphicsSimpleTextItem *text = scene_near->addSimpleText(xLabel, font); + text->setPos( + xpos - text->boundingRect().width() / 2, + ymin - text->boundingRect().height() + labelPad / 2); + text->setBrush(color); + } + + // ... bottom x axis label ... + { + QGraphicsSimpleTextItem *text = scene_near->addSimpleText(xLabel, font); + text->setPos( + xpos - text->boundingRect().width() / 2, + ymax + labelPad / 4); + text->setBrush(color); + } + + } + return true; } diff --git a/src/bm/plotter.h b/src/bm/plotter.h index e67dded..d80ea24 100644 --- a/src/bm/plotter.h +++ b/src/bm/plotter.h @@ -35,12 +35,21 @@ class Plotter { public: virtual ~Plotter() {} - QImage createImage(QString *error = 0) const; + + struct PointInfo + { + QRectF rect; + qreal value; + PointInfo(const QRectF &rect, const qreal value) : rect(rect), value(value) {} + }; + + QImage createImage(QString *error = 0, QList<PointInfo> *pointInfos = 0) const; + private: virtual QRectF sceneRect() const = 0; virtual bool drawScenes( QGraphicsScene *scene_far, QGraphicsScene *scene_mid_aa, QGraphicsScene *scene_near, - QString *error = 0) const = 0; + QString *error = 0, QList<PointInfo> *pointInfos = 0) const = 0; }; class ResultHistoryPlotter : public Plotter @@ -61,7 +70,7 @@ private: QRectF sceneRect() const; bool drawScenes( QGraphicsScene *scene_far, QGraphicsScene *scene_mid_aa, QGraphicsScene *scene_near, - QString *error) const; + QString *error, QList<PointInfo> *pointInfos) const; }; class IndexPlotter : public Plotter @@ -69,16 +78,14 @@ class IndexPlotter : public Plotter public: IndexPlotter( const QList<int> ×tamps, const QList<qreal> &values, const QList<int> &contributions, - const int baseTimestamp = -1, const bool showBaseValue = true, - const qreal baseValue = 100.0); + const int baseTimestamp = -1, const int baseValuePos = -1); private: QList<int> timestamps; QList<qreal> values; QList<int> contributions; int baseTimestamp; - bool showBaseValue; - qreal baseValue; + int baseValuePos; qreal width; qreal height; @@ -86,17 +93,24 @@ private: QRectF sceneRect() const; bool drawScenes( QGraphicsScene *scene_far, QGraphicsScene *scene_mid_aa, QGraphicsScene *scene_near, - QString *error) const; + QString *error, QList<PointInfo> *pointInfos) const; }; class HistoriesPlotter : public Plotter { public: - HistoriesPlotter(const QList<ResultHistoryInfo *> &rhInfos); + HistoriesPlotter( + const QList<ResultHistoryInfo *> &rhInfos, const bool showBenchmark = false, + const int baseTimestamp = -1, const QList<int> *basePos = 0, + const QList<QList<int> > *extraPos = 0, const QStringList *extraDescr = 0); private: QList<ResultHistoryInfo *> rhInfos; - + bool showBenchmark; + int baseTimestamp; + const QList<int> *basePos; + const QList<QList<int> > *extraPos; + const QStringList *extraDescr; qreal width; qreal rhHeight; qreal pad_top; @@ -106,7 +120,7 @@ private: QRectF sceneRect() const; bool drawScenes( QGraphicsScene *scene_far, QGraphicsScene *scene_mid_aa, QGraphicsScene *scene_near, - QString *error) const; + QString *error, QList<PointInfo> *pointInfos) const; }; #endif // PLOTTER_H diff --git a/src/bm/resulthistoryinfo.cpp b/src/bm/resulthistoryinfo.cpp index 2f8f61e..94c69ff 100644 --- a/src/bm/resulthistoryinfo.cpp +++ b/src/bm/resulthistoryinfo.cpp @@ -26,25 +26,25 @@ // ### 2 B DOCUMENTED! bool ResultHistoryInfo::findTargetPos( - const QList<int> ×tamps, int timestamp, int medianWinSize, int *pos, qreal *distSum) + int timestamp, int medianWinSize, int *pos, qreal *distSum) const { int pos_ = -1; // Find target position ... - QList<int>::const_iterator it = qLowerBound(timestamps.begin(), timestamps.end(), timestamp); - if (it == timestamps.begin()) { - Q_ASSERT(timestamp <= timestamps.first()); - if (timestamp == timestamps.first()) + QList<int>::const_iterator it = qLowerBound(timestamps_.begin(), timestamps_.end(), timestamp); + if (it == timestamps_.begin()) { + Q_ASSERT(timestamp <= timestamps_.first()); + if (timestamp == timestamps_.first()) pos_ = 0; - } else if (it == timestamps.end()) { - Q_ASSERT(timestamp > timestamps.last()); - pos_ = timestamps.size() - 1; + } else if (it == timestamps_.end()) { + Q_ASSERT(timestamp > timestamps_.last()); + pos_ = timestamps_.size() - 1; } else { Q_ASSERT(timestamp > *(it - 1)); - pos_ = (it - 1) - timestamps.begin(); + pos_ = (it - 1) - timestamps_.begin(); } Q_ASSERT(pos_ >= -1); - Q_ASSERT(pos_ < timestamps.size()); + Q_ASSERT(pos_ < timestamps_.size()); // Return true iff the target position is high enough to accommodate a right-aligned window // (i.e. a window in which the target value is the rightmost value) ... @@ -56,8 +56,8 @@ bool ResultHistoryInfo::findTargetPos( // Compute sum of distances to the contributing values ... *distSum = 0.0; for (int i = (pos_ - medianWinSize) + 1; i <= pos_; ++i) { - Q_ASSERT(timestamps.at(i) <= timestamp); - (*distSum) += static_cast<qreal>((timestamp - timestamps.at(i))); + Q_ASSERT(timestamps_.at(i) <= timestamp); + (*distSum) += static_cast<qreal>((timestamp - timestamps_.at(i))); } } @@ -68,14 +68,14 @@ bool ResultHistoryInfo::findTargetPos( } // Returns true iff at least \a medianWinSize values exist at or before -// \a timestamp. Passes the smoothed value (if found) in \a value. -// If \a cache is true, the previously cached value will be replaced (or initially recorded). +// \a timestamp. Passes the zero-based position of the smoothed value (if found) in \a smoothPos. +// If \a cache is true, the previously cached position will be replaced (or initially recorded). // (Note that the cache capacity is 1, which explains why the \a cache argument needs to be // passed; only the client knows which timestamp is likely to be requested multiple times!) // If \a distSum is non-null, it will be set to the sum of the distances between \a timestamp // and the timestamp of each contributing value. -bool ResultHistoryInfo::getSmoothValue( - int timestamp, int medianWinSize, qreal *smoothValue, bool cache, qreal *distSum) const +bool ResultHistoryInfo::getSmoothPos( + int timestamp, int medianWinSize, int *smoothPos, bool cache, qreal *distSum) const { Q_ASSERT(timestamp >= 0); Q_ASSERT(medianWinSize > 0); @@ -83,26 +83,27 @@ bool ResultHistoryInfo::getSmoothValue( if (medianWinSize > timestamps_.size()) return false; - // Return cached value if possible ... + // Return cached position if possible ... if (timestamp == cachedTimestamp) { - *smoothValue = cachedSmoothValue; + *smoothPos = cachedSmoothPos; return true; } // Find a valid target position if possible ... int pos = -1; - if (!findTargetPos(timestamps_, timestamp, medianWinSize, &pos, distSum)) { + if (!findTargetPos(timestamp, medianWinSize, &pos, distSum)) { if (cache) cachedTimestamp = -1; // Invalidate cache return false; } // Compute value ... - *smoothValue = BMMisc::median(values.mid((pos - medianWinSize) + 1, medianWinSize)); + const int lo = (pos - medianWinSize) + 1; + *smoothPos = lo + BMMisc::medianPos(values.mid(lo, medianWinSize)); if (cache) { // Load cache ... - cachedSmoothValue = *smoothValue; + cachedSmoothPos = *smoothPos; cachedTimestamp = timestamp; } diff --git a/src/bm/resulthistoryinfo.h b/src/bm/resulthistoryinfo.h index e91ecb1..a3af335 100644 --- a/src/bm/resulthistoryinfo.h +++ b/src/bm/resulthistoryinfo.h @@ -32,10 +32,13 @@ public: ResultHistoryInfo( const int bmcontextId, const QList<int> ×tamps_, const QList<qreal> &values, const QString &metric, const QString &platform = QString(), const QString &host = QString(), - const QString &gitRepo = QString(), const QString &gitBranch = QString()) + const QString &gitRepo = QString(), const QString &gitBranch = QString(), + const QString &testCase = QString(), const QString &testFunction = QString(), + const QString &dataTag = QString()) : bmcontextId_(bmcontextId), timestamps_(timestamps_), values(values) , metric_(metric), platform_(platform), host_(host), gitRepo_(gitRepo) - , gitBranch_(gitBranch), cachedTimestamp(-1) + , gitBranch_(gitBranch), testCase_(testCase), testFunction_(testFunction) + , dataTag_(dataTag), cachedTimestamp(-1), cachedSmoothPos(-1) { Q_ASSERT(!timestamps_.isEmpty()); Q_ASSERT(timestamps_.size() == values.size()); @@ -44,8 +47,8 @@ public: } } - bool getSmoothValue( - int timestamp, int medianWinSize, qreal *smoothValue, bool cache = false, + bool getSmoothPos( + int timestamp, int medianWinSize, int *smoothPos, bool cache = false, qreal *distSum = 0) const; bool getSmoothValues(int medianWinSize, QList<qreal> *smoothValues) const; @@ -60,10 +63,11 @@ public: QString host() const { return host_; } QString gitRepo() const { return gitRepo_; } QString gitBranch() const { return gitBranch_; } + QString testCase() const { return testCase_; } + QString testFunction() const { return testFunction_; } + QString dataTag() const { return dataTag_; } - static bool findTargetPos( - const QList<int> ×tamps, int timestamp, int medianWinSize, int *pos = 0, - qreal *distSum = 0); + bool findTargetPos(int timestamp, int medianWinSize, int *pos = 0, qreal *distSum = 0) const; private: int bmcontextId_; @@ -74,8 +78,11 @@ private: QString host_; QString gitRepo_; QString gitBranch_; + QString testCase_; + QString testFunction_; + QString dataTag_; mutable bool cachedTimestamp; - mutable qreal cachedSmoothValue; + mutable qreal cachedSmoothPos; }; #endif // RESULTHISTORYINFO_H diff --git a/src/bmclient/main.cpp b/src/bmclient/main.cpp index 391a7b7..745f799 100644 --- a/src/bmclient/main.cpp +++ b/src/bmclient/main.cpp @@ -115,6 +115,7 @@ private: const QString &command = "index get values") const; BMRequest * createIndexPutConfigRequest(const QStringList &args, QString *error) const; BMRequest * createGetHistoriesRequest(const QStringList &args, QString *error) const; + BMRequest * createGetIXHistoriesRequest(const QStringList &args, QString *error) const; mutable BMRequest::OutputFormat explicitOutputFormat; mutable bool useExplicitOutputFormat; BMRequest::OutputFormat outputFormat() const; @@ -762,6 +763,31 @@ BMRequest * Executor::createRequest(const QStringList &args, QString *error) con return new BMRequest_GetBMTree(); + } else if ( + (args.size() >= 3) && (args.at(0) == "get") && (args.at(1) == "ixhistories") + && (args.at(2) != "plot") && (args.at(2) != "detailspage")) { + // --- 'get ixhistories' command --- + + return createGetIXHistoriesRequest(args, error); + + } else if ( + (args.size() >= 3) && (args.at(0) == "get") && (args.at(1) == "ixhistories") + && (args.at(2) == "plot")) { + // --- 'get ixhistories plot' command --- + + BMRequest *request = createGetIXHistoriesRequest(args, error); + setOutputFormat(BMRequest::Image); + return request; + + } else if ( + (args.size() >= 3) && (args.at(0) == "get") && (args.at(1) == "ixhistories") + && (args.at(2) == "detailspage")) { + // --- 'get ixhistories detailspage' command --- + + BMRequest *request = createGetIXHistoriesRequest(args, error); + setOutputFormat(BMRequest::HTML); + return request; + } else if ((args.size() >= 2) && (args.at(0) == "get") && (args.at(1) == "detailspage")) { // --- 'get detailspage' command --- @@ -1227,6 +1253,65 @@ BMRequest * Executor::createGetHistoriesRequest(const QStringList &args, QString return new BMRequest_GetHistories(testCase, testFunction, dataTag, cacheKey); } +BMRequest * Executor::createGetIXHistoriesRequest(const QStringList &args, QString *error) const +{ + QStringList values; + bool ok; + + // Get evaluation timestamp ... + if (!BMMisc::getOption(args, "-evaltimestamp", &values, 1, 0, error)) { + if (error->isEmpty()) + *error = "-evaltimestamp option not found"; + return 0; + } + const int evalTimestamp = values.first().toInt(&ok); + if ((!ok) || (evalTimestamp < 0)) { + *error = "failed to extract eval timestamp as a non-negative integer"; + return 0; + } + + // Get ranked infos ... + QList<QStringList> rankedStrings; + if (!BMMisc::getMultiOption2(args, "-rankedinfo", &rankedStrings, 5, error)) + return 0; + if (rankedStrings.isEmpty()) { + *error = "no ranked infos specified"; + return 0; + } + QList<Index::RankedInfo> rankedInfos; + for (int i = 0; i < rankedStrings.size(); ++i) { + QStringList strings = rankedStrings.at(i); + Q_ASSERT(strings.size() == 5); + const int bmcontextId = strings.at(0).toInt(&ok); + Q_ASSERT(ok); + const int basePos = strings.at(1).toInt(&ok); + Q_ASSERT(ok); + const int diffPos1 = strings.at(2).toInt(&ok); + Q_ASSERT(ok); + const int diffPos2 = strings.at(3).toInt(&ok); + Q_ASSERT(ok); + const QString descr = strings.at(4); + + rankedInfos.append(Index::RankedInfo(bmcontextId, basePos, diffPos1, diffPos2, descr)); + } + + // Get cache key ... + QString cacheKey; + if (BMMisc::getOption(args, "-cachekey", &values, 1, 0, error)) { + cacheKey = values.at(0).trimmed(); + bool ok; + cacheKey.toInt(&ok); + if (!ok) { + *error = "failed to extract cache key as an integer"; + return 0; + } + } else if (!error->isEmpty()) { + return 0; + } + + return new BMRequest_GetIXHistories(evalTimestamp, rankedInfos, cacheKey); +} + // ### 2 B DOCUMENTED! static void splitQuotedArgs(const QString &arg_s, QStringList *args) { @@ -1629,11 +1714,24 @@ class DirectExecutor : public Executor << "get histories detailspage <SAME AS 'get histories'> except that the mandatory \\\n" - " -stylesheet option is recognized\n" + " -stylesheet option is recognized\n" << "get bmtree\n" + << + "get ixhistories -evaltimestamp <...> \\\n" + " -rankedinfo <BM context ID> <base pos> <diff pos 1> <diff pos 2> \\\n" + " <descr> ...\n" + + << + "get ixhistories plot <SAME AS 'get ixhistories' except that the optional \\\n" + " -httpHeader and -cachekey options are recognized>\n" + + << + "get ixhistories detailspage <SAME AS 'get ixhistories'> except that the mandatory \\\n" + " -stylesheet option is recognized\n" + ; qDebug() << "\nNote: the -server option may be replaced by a BMSERVER=<host>:<port> " |