#include "retracer.h"
#include "apitracecall.h"
+#include "thumbnail.h"
#include "image.hpp"
+#include "trace_profiler.hpp"
+
#include <QDebug>
#include <QVariant>
#include <QList>
#include <qjson/parser.h>
+/**
+ * Wrapper around a QProcess which enforces IO to block .
+ *
+ * Several QIODevice users (notably QJSON) expect blocking semantics, e.g.,
+ * they expect that QIODevice::read() will blocked until the requested ammount
+ * of bytes is read or end of file is reached. But by default QProcess, does
+ * not block. And passing QIODevice::Unbuffered mitigates but does not fully
+ * address the problem either.
+ *
+ * This class wraps around QProcess, providing QIODevice interface, while
+ * ensuring that all reads block.
+ *
+ * This class also works around a bug in QProcess::atEnd() implementation.
+ *
+ * See also:
+ * - http://qt-project.org/wiki/Simple_Crypt_IO_Device
+ * - http://qt-project.org/wiki/Custom_IO_Device
+ */
+class BlockingIODevice : public QIODevice
+{
+ /* We don't use the Q_OBJECT in this class given we don't declare any
+ * signals and slots or use any other services provided by Qt's meta-object
+ * system. */
+public:
+ BlockingIODevice(QProcess * io);
+ bool isSequential() const;
+ bool atEnd() const;
+ bool waitForReadyRead(int msecs = -1);
+
+protected:
+ qint64 readData(char * data, qint64 maxSize);
+ qint64 writeData(const char * data, qint64 maxSize);
+
+private:
+ QProcess *m_device;
+};
+
+BlockingIODevice::BlockingIODevice(QProcess * io) :
+ m_device(io)
+{
+ /*
+ * We pass QIODevice::Unbuffered to prevent the base QIODevice class to do
+ * its own buffering on top of the overridden readData() method.
+ *
+ * The only buffering used will be to satisfy QIODevice::peek() and
+ * QIODevice::ungetChar().
+ */
+ setOpenMode(ReadOnly | Unbuffered);
+}
+
+bool BlockingIODevice::isSequential() const
+{
+ return true;
+}
+
+bool BlockingIODevice::atEnd() const
+{
+ /*
+ * XXX: QProcess::atEnd() documentation is wrong -- it will return true
+ * even when the process is running --, so we try to workaround that here.
+ */
+ if (m_device->atEnd()) {
+ if (m_device->state() == QProcess::Running) {
+ if (!m_device->waitForReadyRead(-1)) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+bool BlockingIODevice::waitForReadyRead(int msecs)
+{
+ Q_UNUSED(msecs);
+ return true;
+}
+
+qint64 BlockingIODevice::readData(char * data, qint64 maxSize)
+{
+ qint64 bytesToRead = maxSize;
+ qint64 readSoFar = 0;
+ do {
+ qint64 chunkSize = m_device->read(data + readSoFar, bytesToRead);
+ if (chunkSize < 0) {
+ if (readSoFar) {
+ return readSoFar;
+ } else {
+ return chunkSize;
+ }
+ }
+ Q_ASSERT(chunkSize <= bytesToRead);
+ bytesToRead -= chunkSize;
+ readSoFar += chunkSize;
+ if (bytesToRead) {
+ if (!m_device->waitForReadyRead(-1)) {
+ qDebug() << "waitForReadyRead failed\n";
+ break;
+ }
+ }
+ } while(bytesToRead);
+
+ return readSoFar;
+}
+
+qint64 BlockingIODevice::writeData(const char * data, qint64 maxSize)
+{
+ Q_ASSERT(false);
+ return -1;
+}
+
+Q_DECLARE_METATYPE(QList<ApiTraceError>);
+
Retracer::Retracer(QObject *parent)
: QThread(parent),
m_benchmarking(false),
m_doubleBuffered(true),
m_captureState(false),
- m_captureCall(0)
+ m_captureCall(0),
+ m_profileGpu(false),
+ m_profileCpu(false),
+ m_profilePixels(false)
{
+ qRegisterMetaType<QList<ApiTraceError> >();
+
#ifdef Q_OS_WIN
QString format = QLatin1String("%1;");
#else
m_doubleBuffered = db;
}
+bool Retracer::isProfilingGpu() const
+{
+ return m_profileGpu;
+}
+
+bool Retracer::isProfilingCpu() const
+{
+ return m_profileCpu;
+}
+
+bool Retracer::isProfilingPixels() const
+{
+ return m_profilePixels;
+}
+
+bool Retracer::isProfiling() const
+{
+ return m_profileGpu || m_profileCpu || m_profilePixels;
+}
+
+void Retracer::setProfiling(bool gpu, bool cpu, bool pixels)
+{
+ m_profileGpu = gpu;
+ m_profileCpu = cpu;
+ m_profilePixels = pixels;
+}
+
void Retracer::setCaptureAtCallNumber(qlonglong num)
{
m_captureCall = num;
m_captureThumbnails = enable;
}
-
+/**
+ * Starting point for the retracing thread.
+ *
+ * Overrides QThread::run().
+ */
void Retracer::run()
{
- RetraceProcess *retrace = new RetraceProcess();
- retrace->process()->setProcessEnvironment(m_processEnvironment);
-
- retrace->setFileName(m_fileName);
- retrace->setAPI(m_api);
- retrace->setBenchmarking(m_benchmarking);
- retrace->setDoubleBuffered(m_doubleBuffered);
- retrace->setCaptureState(m_captureState);
- retrace->setCaptureThumbnails(m_captureThumbnails);
- retrace->setCaptureAtCallNumber(m_captureCall);
-
- connect(retrace, SIGNAL(finished(const QString&)),
- this, SLOT(cleanup()));
- connect(retrace, SIGNAL(error(const QString&)),
- this, SLOT(cleanup()));
- connect(retrace, SIGNAL(finished(const QString&)),
- this, SIGNAL(finished(const QString&)));
- connect(retrace, SIGNAL(error(const QString&)),
- this, SIGNAL(error(const QString&)));
- connect(retrace, SIGNAL(foundState(ApiTraceState*)),
- this, SIGNAL(foundState(ApiTraceState*)));
- connect(retrace, SIGNAL(foundThumbnails(const QList<QImage>&)),
- this, SIGNAL(foundThumbnails(const QList<QImage>&)));
- connect(retrace, SIGNAL(retraceErrors(const QList<ApiTraceError>&)),
- this, SIGNAL(retraceErrors(const QList<ApiTraceError>&)));
-
- retrace->start();
-
- exec();
-
- /* means we need to kill the process */
- if (retrace->process()->state() != QProcess::NotRunning) {
- retrace->terminate();
- }
-
- delete retrace;
-}
+ QString msg = QLatin1String("Replay finished!");
+ /*
+ * Construct command line
+ */
-void RetraceProcess::start()
-{
QString prog;
QStringList arguments;
- if (m_api == trace::API_GL) {
+ switch (m_api) {
+ case trace::API_GL:
prog = QLatin1String("glretrace");
- } else if (m_api == trace::API_EGL) {
+ break;
+ case trace::API_EGL:
prog = QLatin1String("eglretrace");
- } else {
- assert(0);
+ break;
+ case trace::API_DX:
+ case trace::API_D3D7:
+ case trace::API_D3D8:
+ case trace::API_D3D9:
+ case trace::API_D3D10:
+ case trace::API_D3D10_1:
+ case trace::API_D3D11:
+#ifdef Q_OS_WIN
+ prog = QLatin1String("d3dretrace");
+#else
+ prog = QLatin1String("wine");
+ arguments << QLatin1String("d3dretrace.exe");
+#endif
+ break;
+ default:
+ emit finished(QLatin1String("Unsupported API"));
return;
}
- if (m_doubleBuffered) {
- arguments << QLatin1String("-db");
- } else {
- arguments << QLatin1String("-sb");
- }
+ if (m_captureState) {
+ arguments << QLatin1String("-D");
+ arguments << QString::number(m_captureCall);
+ } else if (m_captureThumbnails) {
+ arguments << QLatin1String("-s"); // emit snapshots
+ arguments << QLatin1String("-"); // emit to stdout
+ } else if (isProfiling()) {
+ if (m_profileGpu) {
+ arguments << QLatin1String("--pgpu");
+ }
- if (m_captureState || m_captureThumbnails) {
- if (m_captureState) {
- arguments << QLatin1String("-D");
- arguments << QString::number(m_captureCall);
+ if (m_profileCpu) {
+ arguments << QLatin1String("--pcpu");
}
- if (m_captureThumbnails) {
- arguments << QLatin1String("-s"); // emit snapshots
- arguments << QLatin1String("-"); // emit to stdout
+
+ if (m_profilePixels) {
+ arguments << QLatin1String("--ppd");
}
} else {
+ if (m_doubleBuffered) {
+ arguments << QLatin1String("--db");
+ } else {
+ arguments << QLatin1String("--sb");
+ }
+
if (m_benchmarking) {
arguments << QLatin1String("-b");
}
arguments << m_fileName;
- m_process->start(prog, arguments);
-}
+ /*
+ * Start the process.
+ */
+ QProcess process;
-void RetraceProcess::replayFinished(int exitCode, QProcess::ExitStatus exitStatus)
-{
- QString msg;
+ process.start(prog, arguments, QIODevice::ReadOnly);
+ if (!process.waitForStarted(-1)) {
+ emit finished(QLatin1String("Could not start process"));
+ return;
+ }
- if (exitStatus != QProcess::NormalExit) {
- msg = QLatin1String("Process crashed");
- } else if (exitCode != 0) {
- msg = QLatin1String("Process exited with non zero exit code");
- } else {
- if (m_captureState || m_captureThumbnails) {
- if (m_captureState) {
- bool ok = false;
- m_process->setReadChannel(QProcess::StandardOutput);
- QVariantMap parsedJson = m_jsonParser->parse(m_process, &ok).toMap();
- ApiTraceState *state = new ApiTraceState(parsedJson);
- emit foundState(state);
- msg = tr("State fetched.");
- }
- if (m_captureThumbnails) {
- m_process->setReadChannel(QProcess::StandardOutput);
+ /*
+ * Process standard output
+ */
- QList<QImage> thumbnails;
+ QList<QImage> thumbnails;
+ QVariantMap parsedJson;
+ trace::Profile* profile = NULL;
- while (!m_process->atEnd()) {
- unsigned channels = 0;
- unsigned width = 0;
- unsigned height = 0;
+ process.setReadChannel(QProcess::StandardOutput);
+ if (process.waitForReadyRead(-1)) {
+ BlockingIODevice io(&process);
+
+ if (m_captureState) {
+ /*
+ * Parse JSON from the output.
+ *
+ * XXX: QJSON's scanner is inneficient as it abuses single
+ * character QIODevice::peek (not cheap), instead of maintaining a
+ * lookahead character on its own.
+ */
+
+ bool ok = false;
+ QJson::Parser jsonParser;
+
+ // Allow Nan/Infinity
+ jsonParser.allowSpecialNumbers(true);
+#if 0
+ parsedJson = jsonParser.parse(&io, &ok).toMap();
+#else
+ /*
+ * XXX: QJSON expects blocking IO, and it looks like
+ * BlockingIODevice does not work reliably in all cases.
+ */
+ process.waitForFinished(-1);
+ parsedJson = jsonParser.parse(&process, &ok).toMap();
+#endif
+ if (!ok) {
+ msg = QLatin1String("failed to parse JSON");
+ }
+ } else if (m_captureThumbnails) {
+ /*
+ * Parse concatenated PNM images from output.
+ */
+
+ while (!io.atEnd()) {
+ unsigned channels = 0;
+ unsigned width = 0;
+ unsigned height = 0;
+
+ char header[512];
+ qint64 headerSize = 0;
+ int headerLines = 3; // assume no optional comment line
+
+ for (int headerLine = 0; headerLine < headerLines; ++headerLine) {
+ qint64 headerRead = io.readLine(&header[headerSize], sizeof(header) - headerSize);
+
+ // if header actually contains optional comment line, ...
+ if (headerLine == 1 && header[headerSize] == '#') {
+ ++headerLines;
+ }
- char header[512];
- qint64 headerSize = 0;
- int headerLines = 3; // assume no optional comment line
+ headerSize += headerRead;
+ }
- for (int headerLine = 0; headerLine < headerLines; ++headerLine) {
- qint64 headerRead = m_process->readLine(&header[headerSize], sizeof(header) - headerSize);
+ const char *headerEnd = image::readPNMHeader(header, headerSize, &channels, &width, &height);
- // if header actually contains optional comment line, ...
- if (headerLine == 1 && header[headerSize] == '#') {
- ++headerLines;
- }
+ // if invalid PNM header was encountered, ...
+ if (header == headerEnd) {
+ qDebug() << "error: invalid snapshot stream encountered";
+ break;
+ }
- headerSize += headerRead;
- }
+ // qDebug() << "channels: " << channels << ", width: " << width << ", height: " << height";
- const char *headerEnd = image::readPNMHeader(header, headerSize, &channels, &width, &height);
+ QImage snapshot = QImage(width, height, channels == 1 ? QImage::Format_Mono : QImage::Format_RGB888);
- // if invalid PNM header was encountered, ...
- if (header == headerEnd) {
- qDebug() << "error: invalid snapshot stream encountered\n";
- break;
- }
+ int rowBytes = channels * width;
+ for (int y = 0; y < height; ++y) {
+ unsigned char *scanLine = snapshot.scanLine(y);
+ qint64 readBytes = io.read((char *) scanLine, rowBytes);
+ Q_ASSERT(readBytes == rowBytes);
+ }
- //qDebug() << "channels: " << channels << ", width: " << width << ", height: " << height << "\n";
+ QImage thumb = thumbnail(snapshot);
+ thumbnails.append(thumb);
+ }
- QImage snapshot = QImage(width, height, channels == 1 ? QImage::Format_Mono : QImage::Format_RGB888);
+ Q_ASSERT(process.state() != QProcess::Running);
+ } else if (isProfiling()) {
+ profile = new trace::Profile();
- int rowBytes = channels * width;
- for (int y = 0; y < height; ++y) {
- unsigned char *scanLine = snapshot.scanLine(y);
- m_process->read((char *) scanLine, rowBytes);
- }
+ while (!io.atEnd()) {
+ char line[256];
+ qint64 lineLength;
- QImage thumbnail = snapshot.scaled(16, 16, Qt::KeepAspectRatio, Qt::FastTransformation);
- thumbnails.append(thumbnail);
- }
+ lineLength = io.readLine(line, 256);
- emit foundThumbnails(thumbnails);
- msg = tr("Thumbnails fetched.");
+ if (lineLength == -1)
+ break;
+
+ trace::Profiler::parseLine(line, profile);
}
} else {
QByteArray output;
- output = m_process->readAllStandardOutput();
- msg = QString::fromUtf8(output);
+ output = process.readAllStandardOutput();
+ if (output.length() < 80) {
+ msg = QString::fromUtf8(output);
+ }
}
}
- m_process->setReadChannel(QProcess::StandardError);
+ /*
+ * Wait for process termination
+ */
+
+ process.waitForFinished(-1);
+
+ if (process.exitStatus() != QProcess::NormalExit) {
+ msg = QLatin1String("Process crashed");
+ } else if (process.exitCode() != 0) {
+ msg = QLatin1String("Process exited with non zero exit code");
+ }
+
+ /*
+ * Parse errors.
+ */
+
QList<ApiTraceError> errors;
+ process.setReadChannel(QProcess::StandardError);
QRegExp regexp("(^\\d+): +(\\b\\w+\\b): ([^\\r\\n]+)[\\r\\n]*$");
- while (!m_process->atEnd()) {
- QString line = m_process->readLine();
+ while (!process.atEnd()) {
+ QString line = process.readLine();
if (regexp.indexIn(line) != -1) {
ApiTraceError error;
error.callIndex = regexp.cap(1).toInt();
error.type = regexp.cap(2);
error.message = regexp.cap(3);
errors.append(error);
+ } else if (!errors.isEmpty()) {
+ // Probably a multiligne message
+ ApiTraceError &previous = errors.last();
+ if (line.endsWith("\n")) {
+ line.chop(1);
+ }
+ previous.message.append('\n');
+ previous.message.append(line);
}
}
- if (!errors.isEmpty()) {
- emit retraceErrors(errors);
- }
- emit finished(msg);
-}
-void RetraceProcess::replayError(QProcess::ProcessError err)
-{
/*
- * XXX: this function is likely unnecessary and should be eliminated given
- * that replayFinished is always called, even on errors.
+ * Emit signals
*/
-#if 0
- qDebug()<<"Process error = "<<err;
- qDebug()<<"\terr = "<<m_process->readAllStandardError();
- qDebug()<<"\tout = "<<m_process->readAllStandardOutput();
-#endif
-
- emit error(
- tr("Couldn't execute the replay file '%1'").arg(m_fileName));
-}
-
-Q_DECLARE_METATYPE(QList<ApiTraceError>);
-RetraceProcess::RetraceProcess(QObject *parent)
- : QObject(parent)
-{
- m_process = new QProcess(this);
- m_jsonParser = new QJson::Parser();
-
- qRegisterMetaType<QList<ApiTraceError> >();
-
- connect(m_process, SIGNAL(finished(int, QProcess::ExitStatus)),
- this, SLOT(replayFinished(int, QProcess::ExitStatus)));
- connect(m_process, SIGNAL(error(QProcess::ProcessError)),
- this, SLOT(replayError(QProcess::ProcessError)));
-}
-
-QProcess * RetraceProcess::process() const
-{
- return m_process;
-}
-
-QString RetraceProcess::fileName() const
-{
- return m_fileName;
-}
-
-void RetraceProcess::setFileName(const QString &name)
-{
- m_fileName = name;
-}
-
-void RetraceProcess::setAPI(trace::API api)
-{
- m_api = api;
-}
-
-bool RetraceProcess::isBenchmarking() const
-{
- return m_benchmarking;
-}
-
-void RetraceProcess::setBenchmarking(bool bench)
-{
- m_benchmarking = bench;
-}
-
-bool RetraceProcess::isDoubleBuffered() const
-{
- return m_doubleBuffered;
-}
-
-void RetraceProcess::setDoubleBuffered(bool db)
-{
- m_doubleBuffered = db;
-}
-
-void RetraceProcess::setCaptureAtCallNumber(qlonglong num)
-{
- m_captureCall = num;
-}
-
-qlonglong RetraceProcess::captureAtCallNumber() const
-{
- return m_captureCall;
-}
-
-bool RetraceProcess::captureState() const
-{
- return m_captureState;
-}
-
-void RetraceProcess::setCaptureState(bool enable)
-{
- m_captureState = enable;
-}
-
-bool RetraceProcess::captureThumbnails() const
-{
- return m_captureThumbnails;
-}
+ if (m_captureState) {
+ ApiTraceState *state = new ApiTraceState(parsedJson);
+ emit foundState(state);
+ msg = QLatin1String("State fetched.");
+ }
-void RetraceProcess::setCaptureThumbnails(bool enable)
-{
- m_captureThumbnails = enable;
-}
+ if (m_captureThumbnails && !thumbnails.isEmpty()) {
+ emit foundThumbnails(thumbnails);
+ }
-void RetraceProcess::terminate()
-{
- if (m_process) {
- m_process->terminate();
- emit finished(tr("Process terminated."));
+ if (isProfiling() && profile) {
+ emit foundProfile(profile);
}
-}
-void Retracer::cleanup()
-{
- quit();
-}
+ if (!errors.isEmpty()) {
+ emit retraceErrors(errors);
+ }
-RetraceProcess::~RetraceProcess()
-{
- delete m_jsonParser;
+ emit finished(msg);
}
#include "retracer.moc"