summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorjasplin <qt-info@nokia.com>2010-04-26 07:43:38 +0200
committerjasplin <qt-info@nokia.com>2010-04-26 07:43:38 +0200
commit5526b7e6d476962fe7a14dd6af774a93d2c50be9 (patch)
tree6974a4e463da4b4a4f96bf27014d9a83c2253c1a
parent5a1e7fa55051285110a8a106c90483e110f415ee (diff)
Upgraded the index feature.
1: The index itself is computed so that the _change_ in index value between any two points in time indicates the average performance increase during this time period in terms of a log2-based difference. An index value _change_ of 1, 0, and -1 thus indicates a doubled, unchanged, and halved performance respectively. (The corresponding linear factors would be 2, 1, and 0.5.). Note: Individual index values bear no useful meaning when regarded in isolation. The graph is however shifted vertically so that each individual index value indicates the change from the index value at the given base time (the index value of the latter is then 0 and 1 in the log2 and linear domain respectively). 2: Clicking on a particular data point in the index graph now opens a page that shows the top-10 results histories that contributed most significantly to this particular change in the index value. Each contributor graph indicates the data points that were used for computing the contribution for this index value.
-rw-r--r--src/bm/bm.pro4
-rw-r--r--src/bm/bmmisc.cpp31
-rw-r--r--src/bm/bmmisc.h33
-rw-r--r--src/bm/bmrequest.cpp702
-rw-r--r--src/bm/bmrequest.h25
-rw-r--r--src/bm/index.cpp288
-rw-r--r--src/bm/index.h97
-rw-r--r--src/bm/plotter.cpp345
-rw-r--r--src/bm/plotter.h36
-rw-r--r--src/bm/resulthistoryinfo.cpp43
-rw-r--r--src/bm/resulthistoryinfo.h23
-rw-r--r--src/bmclient/main.cpp100
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: &gt;%2&lt;<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: &gt;%2&lt;<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> &timestamps, 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> &timestamps, 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> &timestamps, 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> &timestamps_, 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> &timestamps, 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> "