summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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> "