diff options
Diffstat (limited to 'js/src/vm/CodeCoverage.cpp')
-rw-r--r-- | js/src/vm/CodeCoverage.cpp | 636 |
1 files changed, 636 insertions, 0 deletions
diff --git a/js/src/vm/CodeCoverage.cpp b/js/src/vm/CodeCoverage.cpp new file mode 100644 index 000000000..e7146a32a --- /dev/null +++ b/js/src/vm/CodeCoverage.cpp @@ -0,0 +1,636 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "vm/CodeCoverage.h" + +#include "mozilla/Atomics.h" +#include "mozilla/IntegerPrintfMacros.h" + +#include <stdio.h> +#if defined(XP_WIN) +# include <windows.h> +#else +# include <unistd.h> +#endif + +#include "jscompartment.h" +#include "jsopcode.h" +#include "jsprf.h" +#include "jsscript.h" + +#include "vm/Runtime.h" +#include "vm/Time.h" + +// This file contains a few functions which are used to produce files understood +// by lcov tools. A detailed description of the format is available in the man +// page for "geninfo" [1]. To make it short, the following paraphrases what is +// commented in the man page by using curly braces prefixed by for-each to +// express repeated patterns. +// +// TN:<compartment name> +// for-each <source file> { +// SN:<filename> +// for-each <script> { +// FN:<line>,<name> +// } +// for-each <script> { +// FNDA:<hits>,<name> +// } +// FNF:<number of scripts> +// FNH:<sum of scripts hits> +// for-each <script> { +// for-each <branch> { +// BRDA:<line>,<block id>,<target id>,<taken> +// } +// } +// BRF:<number of branches> +// BRH:<sum of branches hits> +// for-each <script> { +// for-each <line> { +// DA:<line>,<hits> +// } +// } +// LF:<number of lines> +// LH:<sum of lines hits> +// } +// +// [1] http://ltp.sourceforge.net/coverage/lcov/geninfo.1.php +// +namespace js { +namespace coverage { + +LCovSource::LCovSource(LifoAlloc* alloc, JSObject* sso) + : source_(sso), + outSF_(alloc), + outFN_(alloc), + outFNDA_(alloc), + numFunctionsFound_(0), + numFunctionsHit_(0), + outBRDA_(alloc), + numBranchesFound_(0), + numBranchesHit_(0), + outDA_(alloc), + numLinesInstrumented_(0), + numLinesHit_(0), + hasFilename_(false), + hasTopLevelScript_(false) +{ +} + +void +LCovSource::exportInto(GenericPrinter& out) const +{ + // Only write if everything got recorded. + if (!hasFilename_ || !hasTopLevelScript_) + return; + + outSF_.exportInto(out); + + outFN_.exportInto(out); + outFNDA_.exportInto(out); + out.printf("FNF:%" PRIuSIZE "\n", numFunctionsFound_); + out.printf("FNH:%" PRIuSIZE "\n", numFunctionsHit_); + + outBRDA_.exportInto(out); + out.printf("BRF:%" PRIuSIZE "\n", numBranchesFound_); + out.printf("BRH:%" PRIuSIZE "\n", numBranchesHit_); + + outDA_.exportInto(out); + out.printf("LF:%" PRIuSIZE "\n", numLinesInstrumented_); + out.printf("LH:%" PRIuSIZE "\n", numLinesHit_); + + out.put("end_of_record\n"); +} + +bool +LCovSource::writeSourceFilename(ScriptSourceObject* sso) +{ + outSF_.printf("SF:%s\n", sso->source()->filename()); + if (outSF_.hadOutOfMemory()) + return false; + + hasFilename_ = true; + return true; +} + +bool +LCovSource::writeScriptName(LSprinter& out, JSScript* script) +{ + JSFunction* fun = script->functionNonDelazifying(); + if (fun && fun->displayAtom()) + return EscapedStringPrinter(out, fun->displayAtom(), 0); + out.printf("top-level"); + return true; +} + +bool +LCovSource::writeScript(JSScript* script) +{ + numFunctionsFound_++; + outFN_.printf("FN:%" PRIuSIZE ",", script->lineno()); + if (!writeScriptName(outFN_, script)) + return false; + outFN_.put("\n", 1); + + uint64_t hits = 0; + ScriptCounts* sc = nullptr; + if (script->hasScriptCounts()) { + sc = &script->getScriptCounts(); + numFunctionsHit_++; + const PCCounts* counts = sc->maybeGetPCCounts(script->pcToOffset(script->main())); + outFNDA_.printf("FNDA:%" PRIu64 ",", counts->numExec()); + if (!writeScriptName(outFNDA_, script)) + return false; + outFNDA_.put("\n", 1); + + // Set the hit count of the pre-main code to 1, if the function ever got + // visited. + hits = 1; + } + + jsbytecode* snpc = script->code(); + jssrcnote* sn = script->notes(); + if (!SN_IS_TERMINATOR(sn)) + snpc += SN_DELTA(sn); + + size_t lineno = script->lineno(); + jsbytecode* end = script->codeEnd(); + size_t branchId = 0; + size_t tableswitchExitOffset = 0; + for (jsbytecode* pc = script->code(); pc != end; pc = GetNextPc(pc)) { + JSOp op = JSOp(*pc); + bool jump = IsJumpOpcode(op) || op == JSOP_TABLESWITCH; + bool fallsthrough = BytecodeFallsThrough(op) && op != JSOP_GOSUB; + + // If the current script & pc has a hit-count report, then update the + // current number of hits. + if (sc) { + const PCCounts* counts = sc->maybeGetPCCounts(script->pcToOffset(pc)); + if (counts) + hits = counts->numExec(); + } + + // If we have additional source notes, walk all the source notes of the + // current pc. + if (snpc <= pc) { + size_t oldLine = lineno; + while (!SN_IS_TERMINATOR(sn) && snpc <= pc) { + SrcNoteType type = (SrcNoteType) SN_TYPE(sn); + if (type == SRC_SETLINE) + lineno = size_t(GetSrcNoteOffset(sn, 0)); + else if (type == SRC_NEWLINE) + lineno++; + else if (type == SRC_TABLESWITCH) + tableswitchExitOffset = GetSrcNoteOffset(sn, 0); + + sn = SN_NEXT(sn); + snpc += SN_DELTA(sn); + } + + if (oldLine != lineno && fallsthrough) { + outDA_.printf("DA:%" PRIuSIZE ",%" PRIu64 "\n", lineno, hits); + + // Count the number of lines instrumented & hit. + numLinesInstrumented_++; + if (hits) + numLinesHit_++; + } + } + + // If the current instruction has thrown, then decrement the hit counts + // with the number of throws. + if (sc) { + const PCCounts* counts = sc->maybeGetThrowCounts(script->pcToOffset(pc)); + if (counts) + hits -= counts->numExec(); + } + + // If the current pc corresponds to a conditional jump instruction, then reports + // branch hits. + if (jump && fallsthrough) { + jsbytecode* fallthroughTarget = GetNextPc(pc); + uint64_t fallthroughHits = 0; + if (sc) { + const PCCounts* counts = sc->maybeGetPCCounts(script->pcToOffset(fallthroughTarget)); + if (counts) + fallthroughHits = counts->numExec(); + } + + uint64_t taken = hits - fallthroughHits; + outBRDA_.printf("BRDA:%" PRIuSIZE ",%" PRIuSIZE ",0,", lineno, branchId); + if (taken) + outBRDA_.printf("%" PRIu64 "\n", taken); + else + outBRDA_.put("-\n", 2); + + outBRDA_.printf("BRDA:%" PRIuSIZE ",%" PRIuSIZE ",1,", lineno, branchId); + if (fallthroughHits) + outBRDA_.printf("%" PRIu64 "\n", fallthroughHits); + else + outBRDA_.put("-\n", 2); + + // Count the number of branches, and the number of branches hit. + numBranchesFound_ += 2; + if (hits) + numBranchesHit_ += !!taken + !!fallthroughHits; + branchId++; + } + + // If the current pc corresponds to a pre-computed switch case, then + // reports branch hits for each case statement. + if (jump && op == JSOP_TABLESWITCH) { + MOZ_ASSERT(tableswitchExitOffset != 0); + + // Get the default and exit pc + jsbytecode* exitpc = pc + tableswitchExitOffset; + jsbytecode* defaultpc = pc + GET_JUMP_OFFSET(pc); + MOZ_ASSERT(defaultpc > pc && defaultpc <= exitpc); + + // Get the low and high from the tableswitch + int32_t low = GET_JUMP_OFFSET(pc + JUMP_OFFSET_LEN * 1); + int32_t high = GET_JUMP_OFFSET(pc + JUMP_OFFSET_LEN * 2); + MOZ_ASSERT(high - low + 1 >= 0); + size_t numCases = high - low + 1; + jsbytecode* jumpTable = pc + JUMP_OFFSET_LEN * 3; + + jsbytecode* firstcasepc = exitpc; + for (size_t j = 0; j < numCases; j++) { + jsbytecode* testpc = pc + GET_JUMP_OFFSET(jumpTable + JUMP_OFFSET_LEN * j); + if (testpc < firstcasepc) + firstcasepc = testpc; + } + + // Count the number of hits of the default branch, by subtracting + // the number of hits of each cases. + uint64_t defaultHits = hits; + + // Count the number of hits of the previous case entry. + uint64_t fallsThroughHits = 0; + + // Record branches for each cases. + size_t caseId = 0; + for (size_t i = 0; i < numCases; i++) { + jsbytecode* casepc = pc + GET_JUMP_OFFSET(jumpTable + JUMP_OFFSET_LEN * i); + // The case is not present, and jumps to the default pc if used. + if (casepc == pc) + continue; + + // PCs might not be in increasing order of case indexes. + jsbytecode* lastcasepc = firstcasepc - 1; + for (size_t j = 0; j < numCases; j++) { + jsbytecode* testpc = pc + GET_JUMP_OFFSET(jumpTable + JUMP_OFFSET_LEN * j); + if (lastcasepc < testpc && (testpc < casepc || (j < i && testpc == casepc))) + lastcasepc = testpc; + } + + if (casepc != lastcasepc) { + // Case (i + low) + uint64_t caseHits = 0; + if (sc) { + const PCCounts* counts = sc->maybeGetPCCounts(script->pcToOffset(casepc)); + if (counts) + caseHits = counts->numExec(); + + // Remove fallthrough. + fallsThroughHits = 0; + if (casepc != firstcasepc) { + jsbytecode* endpc = lastcasepc; + while (GetNextPc(endpc) < casepc) + endpc = GetNextPc(endpc); + + if (BytecodeFallsThrough(JSOp(*endpc))) + fallsThroughHits = script->getHitCount(endpc); + } + + caseHits -= fallsThroughHits; + } + + outBRDA_.printf("BRDA:%" PRIuSIZE ",%" PRIuSIZE ",%" PRIuSIZE ",", + lineno, branchId, caseId); + if (caseHits) + outBRDA_.printf("%" PRIu64 "\n", caseHits); + else + outBRDA_.put("-\n", 2); + + numBranchesFound_++; + numBranchesHit_ += !!caseHits; + defaultHits -= caseHits; + caseId++; + } + } + + // Compute the number of hits of the default branch, if it has its + // own case clause. + bool defaultHasOwnClause = true; + if (defaultpc != exitpc) { + defaultHits = 0; + + // Look for the last case entry before the default pc. + jsbytecode* lastcasepc = firstcasepc - 1; + for (size_t j = 0; j < numCases; j++) { + jsbytecode* testpc = pc + GET_JUMP_OFFSET(jumpTable + JUMP_OFFSET_LEN * j); + if (lastcasepc < testpc && testpc <= defaultpc) + lastcasepc = testpc; + } + + if (lastcasepc == defaultpc) + defaultHasOwnClause = false; + + // Look if the last case entry fallthrough to the default case, + // in which case we have to remove the number of fallthrough + // hits out of the default case hits. + if (sc && lastcasepc != pc) { + jsbytecode* endpc = lastcasepc; + while (GetNextPc(endpc) < defaultpc) + endpc = GetNextPc(endpc); + + if (BytecodeFallsThrough(JSOp(*endpc))) + fallsThroughHits = script->getHitCount(endpc); + } + + if (sc) { + const PCCounts* counts = sc->maybeGetPCCounts(script->pcToOffset(defaultpc)); + if (counts) + defaultHits = counts->numExec(); + } + defaultHits -= fallsThroughHits; + } + + if (defaultHasOwnClause) { + outBRDA_.printf("BRDA:%" PRIuSIZE ",%" PRIuSIZE ",%" PRIuSIZE ",", + lineno, branchId, caseId); + if (defaultHits) + outBRDA_.printf("%" PRIu64 "\n", defaultHits); + else + outBRDA_.put("-\n", 2); + numBranchesFound_++; + numBranchesHit_ += !!defaultHits; + } + + // Increment the branch identifier, and go to the next instruction. + branchId++; + tableswitchExitOffset = 0; + } + } + + // Report any new OOM. + if (outFN_.hadOutOfMemory() || + outFNDA_.hadOutOfMemory() || + outBRDA_.hadOutOfMemory() || + outDA_.hadOutOfMemory()) + { + return false; + } + + // If this script is the top-level script, then record it such that we can + // assume that the code coverage report is complete, as this script has + // references on all inner scripts. + if (script->isTopLevel()) + hasTopLevelScript_ = true; + + return true; +} + +LCovCompartment::LCovCompartment() + : alloc_(4096), + outTN_(&alloc_), + sources_(nullptr) +{ + MOZ_ASSERT(alloc_.isEmpty()); +} + +void +LCovCompartment::collectCodeCoverageInfo(JSCompartment* comp, JSObject* sso, + JSScript* script) +{ + // Skip any operation if we already some out-of memory issues. + if (outTN_.hadOutOfMemory()) + return; + + if (!script->code()) + return; + + // Get the existing source LCov summary, or create a new one. + LCovSource* source = lookupOrAdd(comp, sso); + if (!source) + return; + + // Write code coverage data into the LCovSource. + if (!source->writeScript(script)) { + outTN_.reportOutOfMemory(); + return; + } +} + +void +LCovCompartment::collectSourceFile(JSCompartment* comp, ScriptSourceObject* sso) +{ + // Do not add sources if there is no file name associated to it. + if (!sso->source()->filename()) + return; + + // Skip any operation if we already some out-of memory issues. + if (outTN_.hadOutOfMemory()) + return; + + // Get the existing source LCov summary, or create a new one. + LCovSource* source = lookupOrAdd(comp, sso); + if (!source) + return; + + // Write source filename into the LCovSource. + if (!source->writeSourceFilename(sso)) { + outTN_.reportOutOfMemory(); + return; + } +} + +LCovSource* +LCovCompartment::lookupOrAdd(JSCompartment* comp, JSObject* sso) +{ + // On the first call, write the compartment name, and allocate a LCovSource + // vector in the LifoAlloc. + if (!sources_) { + if (!writeCompartmentName(comp)) + return nullptr; + + LCovSourceVector* raw = alloc_.pod_malloc<LCovSourceVector>(); + if (!raw) { + outTN_.reportOutOfMemory(); + return nullptr; + } + + sources_ = new(raw) LCovSourceVector(alloc_); + } else { + // Find the first matching source. + for (LCovSource& source : *sources_) { + if (source.match(sso)) + return &source; + } + } + + // Allocate a new LCovSource for the current top-level. + if (!sources_->append(Move(LCovSource(&alloc_, sso)))) { + outTN_.reportOutOfMemory(); + return nullptr; + } + + return &sources_->back(); +} + +void +LCovCompartment::exportInto(GenericPrinter& out, bool* isEmpty) const +{ + if (!sources_ || outTN_.hadOutOfMemory()) + return; + + // If we only have cloned function, then do not serialize anything. + bool someComplete = false; + for (const LCovSource& sc : *sources_) { + if (sc.isComplete()) { + someComplete = true; + break; + }; + } + + if (!someComplete) + return; + + *isEmpty = false; + outTN_.exportInto(out); + for (const LCovSource& sc : *sources_) { + if (sc.isComplete()) + sc.exportInto(out); + } +} + +bool +LCovCompartment::writeCompartmentName(JSCompartment* comp) +{ + JSContext* cx = comp->contextFromMainThread(); + + // lcov trace files are starting with an optional test case name, that we + // recycle to be a compartment name. + // + // Note: The test case name has some constraint in terms of valid character, + // thus we escape invalid chracters with a "_" symbol in front of its + // hexadecimal code. + outTN_.put("TN:"); + if (cx->compartmentNameCallback) { + char name[1024]; + { + // Hazard analysis cannot tell that the callback does not GC. + JS::AutoSuppressGCAnalysis nogc; + (*cx->compartmentNameCallback)(cx, comp, name, sizeof(name)); + } + for (char *s = name; s < name + sizeof(name) && *s; s++) { + if (('a' <= *s && *s <= 'z') || + ('A' <= *s && *s <= 'Z') || + ('0' <= *s && *s <= '9')) + { + outTN_.put(s, 1); + continue; + } + outTN_.printf("_%p", (void*) size_t(*s)); + } + outTN_.put("\n", 1); + } else { + outTN_.printf("Compartment_%p%p\n", (void*) size_t('_'), comp); + } + + return !outTN_.hadOutOfMemory(); +} + +LCovRuntime::LCovRuntime() + : out_(), +#if defined(XP_WIN) + pid_(GetCurrentProcessId()), +#else + pid_(getpid()), +#endif + isEmpty_(false) +{ +} + +LCovRuntime::~LCovRuntime() +{ + if (out_.isInitialized()) + finishFile(); +} + +bool +LCovRuntime::fillWithFilename(char *name, size_t length) +{ + const char* outDir = getenv("JS_CODE_COVERAGE_OUTPUT_DIR"); + if (!outDir || *outDir == 0) + return false; + + int64_t timestamp = static_cast<double>(PRMJ_Now()) / PRMJ_USEC_PER_SEC; + static mozilla::Atomic<size_t> globalRuntimeId(0); + size_t rid = globalRuntimeId++; + + int len = snprintf(name, length, "%s/%" PRId64 "-%" PRIuSIZE "-%" PRIuSIZE ".info", + outDir, timestamp, pid_, rid); + if (length != size_t(len)) { + fprintf(stderr, "Warning: LCovRuntime::init: Cannot serialize file name."); + return false; + } + + return true; +} + +void +LCovRuntime::init() +{ + char name[1024]; + if (!fillWithFilename(name, sizeof(name))) + return; + + // If we cannot open the file, report a warning. + if (!out_.init(name)) + fprintf(stderr, "Warning: LCovRuntime::init: Cannot open file named '%s'.", name); + isEmpty_ = true; +} + +void +LCovRuntime::finishFile() +{ + MOZ_ASSERT(out_.isInitialized()); + out_.finish(); + + if (isEmpty_) { + char name[1024]; + if (!fillWithFilename(name, sizeof(name))) + return; + remove(name); + } +} + +void +LCovRuntime::writeLCovResult(LCovCompartment& comp) +{ + if (!out_.isInitialized()) + return; + +#if defined(XP_WIN) + size_t p = GetCurrentProcessId(); +#else + size_t p = getpid(); +#endif + if (pid_ != p) { + pid_ = p; + finishFile(); + init(); + if (!out_.isInitialized()) + return; + } + + comp.exportInto(out_, &isEmpty_); + out_.flush(); +} + +} // namespace coverage +} // namespace js |