/* Web Polygraph http://www.web-polygraph.org/ * (C) 2003-2006 The Measurement Factory * Licensed under the Apache License, Version 2.0 */ #include "base/polygraph.h" #include "xstd/h/iomanip.h" #include "xstd/gadgets.h" #include "base/polyLogCats.h" #include "base/polyVersion.h" #include "runtime/HttpDate.h" #include "xml/XmlAttr.h" #include "xml/XmlNodes.h" #include "xml/XmlTable.h" #include "xml/XmlSection.h" #include "xml/XmlParagraph.h" #include "xml/XmlText.h" #include "loganalyzers/ReportBlob.h" #include "loganalyzers/BlobDb.h" #include "loganalyzers/InfoScopes.h" #include "loganalyzers/PhaseInfo.h" #include "loganalyzers/SideInfo.h" #include "loganalyzers/TestInfo.h" static XmlAttr algnLeft("align", "left"); static XmlAttr algnRight("align", "right"); TestInfo::TestInfo(const String &aLabel): theLabel(aLabel), theSides(lgcEnd) { theSides.count(lgcEnd); theSides[lgcCltSide] = new SideInfo(lgcCltSide); theSides[lgcCltSide]->test(this); theSides[lgcSrvSide] = new SideInfo(lgcSrvSide); theSides[lgcSrvSide]->test(this); theExecScope.addSide("client"); theExecScope.addSide("server"); theExecScope.name("baseline"); } TestInfo::~TestInfo() { while (theSides.count()) { if (theSides.last()) theSides.last()->test(0); delete theSides.pop(); } while (theScopes.count()) delete theScopes.pop(); } void TestInfo::execScope(const Scope &aScope) { theExecScope = aScope; } const TestInfo::Scope &TestInfo::guessExecScope() { Assert(!theExecScope); const SideInfo &side = aSide(); // find last phase with peak (highest) request rate String bestName; double peakRate = -1; for (int i = 0; i < side.phaseCount(); ++i) { const PhaseInfo &phase = side.phase(i); const double rate = phase.availStats().reqRate(); // allow for 1% rate diff among phases with the same configured rate if (!bestName || peakRate <= 1.01*rate) { peakRate = rate; bestName = phase.name(); } } if (Should(bestName)) { clog << "no executive summary phases specified, using '" << bestName << "' phase" << endl; theExecScope.addPhase(bestName); } return theExecScope; } const String &TestInfo::label() const { return theLabel; } const String &TestInfo::pglCfg() const { return thePglCfg; } Time TestInfo::startTime() const { return theStartTime; } const InfoScope &TestInfo::execScope() const { return theExecScope; } const SideInfo *TestInfo::cltSideExists() const { return side(lgcCltSide).procCount() ? theSides[lgcCltSide] : 0; } const SideInfo *TestInfo::srvSideExists() const { return side(lgcSrvSide).procCount() ? theSides[lgcSrvSide] : 0; } SideInfo &TestInfo::cltSide() { return side(lgcCltSide); } SideInfo &TestInfo::srvSide() { return side(lgcSrvSide); } SideInfo &TestInfo::side(int logCat) { Assert(logCat == lgcCltSide || logCat == lgcSrvSide); Assert(theSides[logCat]); return *theSides[logCat]; } const SideInfo &TestInfo::aSide() const { return cltSideExists() ? cltSide() : srvSide(); } const SideInfo &TestInfo::cltSide() const { return side(lgcCltSide); } const SideInfo &TestInfo::srvSide() const { return side(lgcSrvSide); } const SideInfo &TestInfo::side(int logCat) const { Assert(logCat == lgcCltSide || logCat == lgcSrvSide); Assert(theSides[logCat]); return *theSides[logCat]; } int TestInfo::scopes(InfoScopes &res) const { if (!twoSided()) return aSide().scopes(res); for (int i = 0; i < theScopes.count(); ++i) res.add(*theScopes[i]); return res.count(); } void TestInfo::checkCommonPglCfg() { if (!cltSideExists() && srvSideExists()) thePglCfg = srvSide().pglCfg(); else if (cltSideExists() && !srvSideExists()) thePglCfg = cltSide().pglCfg(); else if (cltSide().pglCfg() == srvSide().pglCfg()) thePglCfg = cltSide().pglCfg(); else { cerr << label() << ": warning: client- and server-side PGL configurations" << " differ" << endl; thePglCfg = String(); } } void TestInfo::checkCommonBenchmarkVersion() { if (!cltSideExists() && srvSideExists()) theBenchmarkVersion = srvSide().benchmarkVersion(); else if (cltSideExists() && !srvSideExists()) theBenchmarkVersion = cltSide().benchmarkVersion(); else if (cltSide().benchmarkVersion() == srvSide().benchmarkVersion()) { theBenchmarkVersion = cltSide().benchmarkVersion(); } else { cerr << label() << ": warning: client- and server-side" << " benchmark versions differ" << endl; theBenchmarkVersion = String(); } } void TestInfo::checkCommonStartTime() { if (!cltSideExists() && srvSideExists()) theStartTime = srvSide().startTime(); else if (cltSideExists() && !srvSideExists()) theStartTime = cltSide().startTime(); else if (cltSide().startTime() >= 0 && srvSide().startTime() >= 0) { const Time diff = Max(cltSide().startTime(), srvSide().startTime()) - Min(cltSide().startTime(), srvSide().startTime()); if (diff > Time::Sec(5*60)) cerr << label() << ": warning: client- and server-side process " << "start times differ by " << diff << endl; // should we not set a start time in this case (like with thePglCfg) theStartTime = Min(cltSide().startTime(), srvSide().startTime()); } } void TestInfo::checkConsistency() { if (!cltSideExists() && !srvSideExists()) { cerr << "no client- or server-side information found in the logs, exiting" << endl << xexit; } if (!cltSideExists() || !srvSideExists()) { const String &sname = cltSideExists() ? cltSide().name() : srvSide().name(); theOneSideWarn = String("Only ") + sname + "-side information found in the logs."; cerr << "warning: " << theOneSideWarn << " The report will be incomplete and less accurate" << endl; } if (cltSideExists()) cltSide().checkConsistency(); if (srvSideExists()) srvSide().checkConsistency(); checkCommonBenchmarkVersion(); checkCommonPglCfg(); checkCommonStartTime(); //checkCommonPhases(); } int TestInfo::repCount(const Scope &scope) const { return cltSideExists() ? cltSide().repCount(scope) : -1; } int TestInfo::hitCount(const Scope &scope) const { return twoSided() ? cltSide().repCount(scope) - srvSide().repCount(scope) : -1; } BigSize TestInfo::repVolume(const Scope &scope) const { return cltSideExists() ? cltSide().repVolume(scope) : BigSize(); } BigSize TestInfo::hitVolume(const Scope &scope) const { return twoSided() ? cltSide().repVolume(scope) - srvSide().repVolume(scope) : BigSize(); } void TestInfo::cmplExecSumVars(BlobDb &db) { static const String tlLabel = "test label"; addMeasBlob(db, "label", theLabel, "string", tlLabel); { ostringstream buf; HttpDatePrint(buf, startTime()); buf << ends; static const String tlStartTime = "test start time"; addMeasBlob(db, "start.time", buf.str().c_str(), "string", tlStartTime); streamFreeze(buf, false); } { static const String tlTitle = "benchmark software version"; ReportBlob blob("benchmark.version" + theExecScope, tlTitle); if (theBenchmarkVersion) { blob << XmlText(theBenchmarkVersion); } else { XmlParagraph p; p << XmlText("cannot show a single benchmark version because "); p << db.ptr(BlobDb::Key("benchmark.version", execScope().oneSide("client")), XmlText("client-")); p << XmlText(" and "); p << db.ptr(BlobDb::Key("benchmark.version", execScope().oneSide("server")), XmlText("server-side")); p << XmlText(" versions differ"); blob << p; } db << blob; } { static const String tlTitle = "reporter software version"; ReportBlob blob("reporter.version" + theExecScope, tlTitle); blob << XmlText(PolyVersion()); db << blob; } } void TestInfo::cmplExecSum(BlobDb &db) { const Scope &cltScope = theExecScope.oneSide("client"); static const String tlTitle = "executive summary"; ReportBlob blob("summary.exec.table" + theExecScope, tlTitle); blob << XmlAttr("vprimitive", "Test summary"); XmlTable table; table << XmlAttr::Int("border", 0); { XmlTableRec tr; tr << algnLeft << XmlTableHeading("label:"); XmlTableCell cell; cell << db.include("label"); tr << cell; table << tr; } { XmlTableRec tr; tr << algnLeft << XmlTableHeading("throughput:"); XmlTableCell cell; cell << db.quote("rep.rate" + cltScope); cell << XmlText(" or "); cell << db.quote("rep.bwidth" + cltScope); tr << cell; table << tr; } { XmlTableRec tr; tr << algnLeft << XmlTableHeading("response time:"); XmlTableCell cell; //cell << db.quote("object.hits.rptm.mean" + cltScope); //cell << XmlText(" hit, "); cell << db.quote("rep.rptm.mean" + cltScope); cell << XmlText(" mean"); //cell << db.quote("object.misses.rptm.mean" + cltScope); //cell << XmlText(" miss"); tr << cell; table << tr; } { XmlTableRec tr; tr << algnLeft << XmlTableHeading("hit ratios:"); XmlTableCell cell; cell << db.quote("hit.ratio.obj" + theExecScope); cell << XmlText(" DHR and "); cell << db.quote("hit.ratio.byte" + theExecScope); cell << XmlText(" BHR"); tr << cell; table << tr; } { XmlTableRec tr; tr << algnLeft << XmlTableHeading("errors:"); XmlTableCell cell; cell << db.quote("xact.error.ratio" + cltScope); cell << XmlText(" ("); cell << db.quote("xact.error.count" + cltScope); cell << XmlText(" out of "); cell << db.quote("xact.count" + cltScope); cell << XmlText(")"); tr << cell; table << tr; } { XmlTableRec tr; tr << algnLeft << XmlTableHeading("duration:"); XmlTableCell cell; cell << db.include("duration" + cltScope); tr << cell; table << tr; } { XmlTableRec tr; tr << algnLeft << XmlTableHeading("start time:"); XmlTableCell cell; cell << db.include("start.time"); tr << cell; table << tr; } { XmlTableRec tr; tr << algnLeft << XmlTableHeading("workload:"); XmlTableCell cell; cell << db.ptr("workload" + theExecScope, XmlText("available")); tr << cell; table << tr; } { XmlTableRec tr; tr << algnLeft << XmlTableHeading("Polygraph version:"); XmlTableCell cell; cell << db.include("benchmark.version" + theExecScope); tr << cell; table << tr; } { XmlTableRec tr; tr << algnLeft << XmlTableHeading("reporter version:"); XmlTableCell cell; cell << db.include("reporter.version" + theExecScope); tr << cell; table << tr; } blob << table; { // XXX: out of place (this is not a "table record") XmlParagraph p; XmlText text; text.buf() << "This executive summary and baseline report statistics" << " are based on the following " << theExecScope.phases().count() << " test phase(s): "; {for (int i = 0; i < theExecScope.phases().count(); ++i) { if (i) text.buf() << ", "; text.buf() << *theExecScope.phases().item(i); }} text.buf() << ". The test has the following " << aSide().phaseCount() << " phase(s): "; {for (int i = 0; i < aSide().phaseCount(); ++i) { if (i) text.buf() << ", "; text.buf() << aSide().phase(i).name(); }} text.buf() << '.'; p << text; blob << p; } db << blob; } void TestInfo::cmplWorkload(BlobDb &db) { static const String tlTitle = "test workload"; ReportBlob blob(BlobDb::Key("workload", theExecScope), tlTitle); if (!thePglCfg) { XmlParagraph p; p << XmlText("cannot show a single test workload because "); p << db.ptr(BlobDb::Key("workload.code", execScope().oneSide("client")), XmlText("client-")); p << XmlText(" and "); p << db.ptr(BlobDb::Key("workload.code", execScope().oneSide("server")), XmlText("server-side")); p << XmlText(" PGL configurations differ"); blob << p; return; } { XmlSection sect("English interpretation"); sect << XmlTextTag("TBD."); blob << sect; } { XmlSection sect("PGL code"); static const String tlPglTitle = "PGL code"; ReportBlob code(BlobDb::Key("workload.code", theExecScope), tlPglTitle); XmlTag codesample("codesample"); codesample << XmlText(thePglCfg); code << codesample; db << code; sect << code; blob << sect; } db << blob; } void TestInfo::cmplHitRatioVars(BlobDb &db, const Scope &scope) { if (twoSided()) { const String sfx = BlobDb::KeySuffix(scope); const double dhr = Percent(hitCount(scope), repCount(scope)); static const String tlDhr = "document hit ratio"; addMeasBlob(db, "hit.ratio.obj" + sfx, dhr, "%", tlDhr); const double bhr = Percent(hitVolume(scope).byted(), repVolume(scope).byted()); static const String tlBhr = "byte hit ratio"; addMeasBlob(db, "hit.ratio.byte" + sfx, bhr, "%", tlBhr); } else { // XXX: put err pointer to the theOneSideWarn-based description Should(false); } } void TestInfo::cmplHitRatio(BlobDb &db, const Scope &scope) { static const String tlTitle = "hit ratios"; ReportBlob blob(BlobDb::Key("hit.ratio", scope), tlTitle); blob << XmlAttr("vprimitive", "Hit Ratios"); if (twoSided()) { cmplHitRatioTable(db, blob, scope); { XmlTag descr("description"); XmlTextTag p1; p1.buf() << "The hit ratios table shows measured hit " << "ratios. Hits are calculated based on client- and " << "server-side traffic comparison. Offered hits are " << "counted for 'basic' transactions only (simple HTTP GET " << "requests with '200 OK' responses). Measured hit stats " << "are based on all transactions. Thus, 'offered' hit ratio " << "are not the same as 'ideal' hit ratio in this context. "; descr << p1; XmlTextTag p2; p2.buf() << "Measured hit count or volume is the difference " << "between client- and server-side traffic counts or " << "volumes. " << "DHR, Document Hit Ratio, is the ratio of the total " << "number of hits to the number of all transactions. " << "BHR, Byte Hit Ratio, is the ratio of " << "the total volume (a sum of response sizes) of hits to the " << "total volume of all transactions. " << "Negative measured hit ratios are possible if server-side " << "traffic of a cache exceeds client-side traffic (e.g., " << "due to optimistic prefetching or extra freshness checks) " << "and if side measurements are out-of-sync. " << "Negative measured BHR can also be due to " << "aborted-by-robots transactions."; descr << p2; XmlParagraph p3; p3 << XmlText("A less accurate way to measure hit ratio is to " "detect hits on the client-side using custom HTTP headers. " "A hit ratio table based on client-side tricks is available "); p3 << db.ptr("hit.ratio" + scope.oneSide("client"), XmlText("elsewhere")); p3 << XmlText("."); descr << p3; blob << descr; } } else { XmlParagraph para; para << XmlText(theOneSideWarn); if (cltSideExists()) { para << XmlText(" See "); para << db.ptr("summary" + theExecScope.oneSide("client"), XmlText("client-side")); para << XmlText(" information for hit ratio estimations (if any)"); } else { para << XmlText(" No hit ratio measurements"); para << XmlText(" can be derived from server-side logs."); } blob << para; } db << blob; } void TestInfo::cmplHitRatioTable(BlobDb &db, XmlTag &parent, const Scope &scope) { Assert(twoSided()); static const String tlTitle = "hit ratio table"; ReportBlob blob(BlobDb::Key("hit.ratio.table", scope), tlTitle); XmlTable table; table << XmlAttr::Int("border", 1) << XmlAttr::Int("cellspacing", 1); { XmlTableRec tr; tr << XmlTableHeading("Hit Ratios"); XmlTableHeading dhr("DHR"); dhr << XmlTag("br") << XmlText("(%)"); tr << dhr; XmlTableHeading bhr("BHR"); bhr << XmlTag("br") << XmlText("(%)"); tr << bhr; table << tr; } { XmlTableRec tr; tr << algnLeft << XmlTableHeading("measured"); XmlTableCell dhr; dhr << algnRight << db.quote("hit.ratio.obj" + scope); tr << dhr; XmlTableCell bhr; bhr << algnRight << db.quote("hit.ratio.byte" + scope); tr << bhr; table << tr; } blob << table; db << blob; parent << blob; } void TestInfo::cmplBaseStats(BlobDb &db, const Scope &scope) { static const String tlTitle = "baseline stats"; ReportBlob blob(BlobDb::Key("baseline", scope), tlTitle); blob << db.quote(BlobDb::Key("load", scope)); blob << db.quote(BlobDb::Key("hit.ratio", scope)); db << blob; } void TestInfo::cmplTraffic(BlobDb &db, const Scope &scope) { static const String tlTitle = "test traffic stats"; ReportBlob blob("traffic" + scope, tlTitle); XmlTag title("title"); title << XmlText("Traffic rates, counts, and volumes"); blob << title; blob << XmlTextTag("This information is based on the client-side measurements."); blob << db.quote(BlobDb::Key("load", scope.oneSide("client"))); blob << db.quote(BlobDb::Key("stream.table", scope.oneSide("client"))); db << blob; } void TestInfo::cmplRptm(BlobDb &db, const Scope &scope) { static const String tlTitle = "test response time stats"; ReportBlob blob("rptm" + scope, tlTitle); XmlTag title("title"); title << XmlText("Response times"); blob << title; blob << XmlTextTag("This information is based on the client-side measurements."); blob << db.quote(BlobDb::Key("rptm.trace", scope.oneSide("client"))); blob << db.quote("object.table" + scope.oneSide("client")); db << blob; } void TestInfo::cmplSavings(BlobDb &db, const Scope &scope) { static const String tlTitle = "cache effectiveness"; ReportBlob blob("savings" + scope, tlTitle); XmlTag title("title"); title << XmlText("Savings"); blob << title; blob << db.quote("hit.ratio" + scope); db << blob; } void TestInfo::cmplLevels(BlobDb &db, const Scope &scope) { static const String tlTitle = "test transaction concurrency and population levels"; ReportBlob blob("levels" + scope, tlTitle); XmlTag title("title"); title << XmlText("Concurrency levels and robot population"); blob << title; blob << XmlTextTag("This information is based on the client-side measurements."); const InfoScope cltScope = scope.oneSide("client"); { XmlSection s("concurrent HTTP/TCP connections"); //s << db.quote("conn.level.fig" + cltScope); s << db.quote("conn.level.table" + cltScope); blob << s; } { XmlSection s("population level"); //s << db.quote("populus.level.fig" + cltScope); s << db.quote("populus.level.table" + cltScope); blob << s; } { XmlSection s("concurrent HTTP transactions"); //s << db.quote("xact.level.fig" + cltScope); s << db.quote("xact.level.table" + cltScope); blob << s; } db << blob; } void TestInfo::cmplErrors(BlobDb &db, const Scope &scope) { static const String tlTitle = "test errors"; ReportBlob blob("errors" + scope, tlTitle); XmlTag title("title"); title << XmlText("Errors"); blob << title; { XmlSection s("client-side errors"); s << db.include("errors.table" + scope.oneSide("client")); blob << s; } { XmlSection s("server-side errors"); s << db.include("errors.table" + scope.oneSide("server")); blob << s; } db << blob; } void TestInfo::cmplNotes(BlobDb &db) { static const String tlTitle = "report notes"; ReportBlob blob("report_notes", tlTitle); XmlSearchRes res; if (db.blobs().selByAttrName("report_note", res)) { XmlTag list("ul"); for (int i = 0; i < res.count(); ++i) list << db.include(res[i]->attrs()->value("key")); blob << list; } db << blob; } void TestInfo::cmplSynonyms(BlobDb &db, const Scope &scope) { addLink(db, BlobDb::Key("load", scope), BlobDb::Key("load", scope.oneSide("client"))); addLink(db, BlobDb::Key("load.table", scope), BlobDb::Key("load.table", scope.oneSide("client"))); addLink(db, BlobDb::Key("stream.table", scope), BlobDb::Key("stream.table", scope.oneSide("client"))); } void TestInfo::compileStats(BlobDb &db) { if (!theExecScope) guessExecScope(); if (cltSideExists()) cltSide().compileStats(db); else SideInfo::CompileEmptyStats(db, execScope().oneSide("client")); if (srvSideExists()) srvSide().compileStats(db); else SideInfo::CompileEmptyStats(db, execScope().oneSide("server")); cmplExecSumVars(db); cmplExecSum(db); cmplWorkload(db); // build theScopes array theScopes.append(new Scope(execScope())); if (twoSided()) { Scope *allScope = new Scope; allScope->name("all phases"); theScopes.append(allScope); for (int i = 0; i < cltSide().phaseCount(); ++i) { const String &pname = cltSide().phase(i).name(); // include common phases only if (!srvSide().scope().hasPhase(pname)) continue; theScopes.append(new Scope(theExecScope.onePhase(pname))); theScopes.last()->name(pname); allScope->add(*theScopes.last()); } } for (int s = 0; s < theScopes.count(); ++s) { const Scope &scope = *theScopes[s]; cmplSynonyms(db, scope); cmplHitRatioVars(db, scope); cmplHitRatio(db, scope); cmplBaseStats(db, scope); cmplTraffic(db, scope); cmplRptm(db, scope); cmplSavings(db, scope); cmplLevels(db, scope); cmplErrors(db, scope); } cmplNotes(db); }