/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set ts=8 sts=2 et sw=2 tw=80: * 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> #include <utility> #include "frontend/SourceNotes.h" // SrcNote, SrcNoteType, SrcNoteIterator #include "gc/Zone.h" #include "util/GetPidProvider.h" // getpid() #include "util/Text.h" #include "vm/BytecodeUtil.h" #include "vm/JSScript.h" #include "vm/Realm.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> { // SF:<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, UniqueChars name) : name_(std::move(name)), outFN_(alloc), outFNDA_(alloc), numFunctionsFound_(0), numFunctionsHit_(0), outBRDA_(alloc), numBranchesFound_(0), numBranchesHit_(0), numLinesInstrumented_(0), numLinesHit_(0), maxLineHit_(0), hasTopLevelScript_(false), hadOOM_(false) {} void LCovSource::exportInto(GenericPrinter& out) { if (hadOutOfMemory()) { out.reportOutOfMemory(); } else { out.printf("SF:%s\n", name_.get()); outFN_.exportInto(out); outFNDA_.exportInto(out); out.printf("FNF:%zu\n", numFunctionsFound_); out.printf("FNH:%zu\n", numFunctionsHit_); outBRDA_.exportInto(out); out.printf("BRF:%zu\n", numBranchesFound_); out.printf("BRH:%zu\n", numBranchesHit_); if (!linesHit_.empty()) { for (size_t lineno = 1; lineno <= maxLineHit_; ++lineno) { if (auto p = linesHit_.lookup(lineno)) { out.printf("DA:%zu,%" PRIu64 "\n", lineno, p->value()); } } } out.printf("LF:%zu\n", numLinesInstrumented_); out.printf("LH:%zu\n", numLinesHit_); out.put("end_of_record\n"); } outFN_.clear(); outFNDA_.clear(); numFunctionsFound_ = 0; numFunctionsHit_ = 0; outBRDA_.clear(); numBranchesFound_ = 0; numBranchesHit_ = 0; linesHit_.clear(); numLinesInstrumented_ = 0; numLinesHit_ = 0; maxLineHit_ = 0; } void LCovSource::writeScript(JSScript* script, const char* scriptName) { if (hadOutOfMemory()) { return; } numFunctionsFound_++; outFN_.printf("FN:%u,%s\n", script->lineno(), scriptName); 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 ",%s\n", counts->numExec(), scriptName); // Set the hit count of the pre-main code to 1, if the function ever got // visited. hits = 1; } jsbytecode* snpc = script->code(); const SrcNote* sn = script->notes(); const SrcNote* snEnd = script->notesEnd(); if (sn < snEnd) { snpc += sn->delta(); } size_t lineno = script->lineno(); jsbytecode* end = script->codeEnd(); size_t branchId = 0; bool firstLineHasBeenWritten = false; for (jsbytecode* pc = script->code(); pc != end; pc = GetNextPc(pc)) { MOZ_ASSERT(script->code() <= pc && pc < end); JSOp op = JSOp(*pc); bool jump = IsJumpOpcode(op) || op == JSOp::TableSwitch; bool fallsthrough = BytecodeFallsThrough(op); // 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 || !firstLineHasBeenWritten) { size_t oldLine = lineno; SrcNoteIterator iter(sn, snEnd); while (!iter.atEnd() && snpc <= pc) { sn = *iter; SrcNoteType type = sn->type(); if (type == SrcNoteType::SetLine) { lineno = SrcNote::SetLine::getLine(sn, script->lineno()); } else if (type == SrcNoteType::SetLineColumn) { lineno = SrcNote::SetLineColumn::getLine(sn, script->lineno()); } else if (type == SrcNoteType::NewLine || type == SrcNoteType::NewLineColumn) { lineno++; } ++iter; if (!iter.atEnd()) { snpc += (*iter)->delta(); } } sn = *iter; if ((oldLine != lineno || !firstLineHasBeenWritten) && pc >= script->main() && fallsthrough) { auto p = linesHit_.lookupForAdd(lineno); if (!p) { if (!linesHit_.add(p, lineno, hits)) { hadOOM_ = true; return; } numLinesInstrumented_++; if (hits != 0) { numLinesHit_++; } maxLineHit_ = std::max(lineno, maxLineHit_); } else { if (p->value() == 0 && hits != 0) { numLinesHit_++; } p->value() += hits; } firstLineHasBeenWritten = true; } } // 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:%zu,%zu,0,", lineno, branchId); if (hits) { outBRDA_.printf("%" PRIu64 "\n", taken); } else { outBRDA_.put("-\n", 2); } outBRDA_.printf("BRDA:%zu,%zu,1,", lineno, branchId); if (hits) { 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) { // Get the default pc. jsbytecode* defaultpc = pc + GET_JUMP_OFFSET(pc); MOZ_ASSERT(script->code() <= defaultpc && defaultpc < end); MOZ_ASSERT(defaultpc > pc); // 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; auto getCaseOrDefaultPc = [&](size_t index) { if (index < numCases) { return script->tableSwitchCasePC(pc, index); } MOZ_ASSERT(index == numCases); return defaultpc; }; jsbytecode* firstCaseOrDefaultPc = end; for (size_t j = 0; j < numCases + 1; j++) { jsbytecode* testpc = getCaseOrDefaultPc(j); MOZ_ASSERT(script->code() <= testpc && testpc < end); if (testpc < firstCaseOrDefaultPc) { firstCaseOrDefaultPc = 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 case and default. size_t caseId = 0; for (size_t i = 0; i < numCases + 1; i++) { jsbytecode* caseOrDefaultPc = getCaseOrDefaultPc(i); MOZ_ASSERT(script->code() <= caseOrDefaultPc && caseOrDefaultPc < end); // PCs might not be in increasing order of case indexes. jsbytecode* lastCaseOrDefaultPc = firstCaseOrDefaultPc - 1; bool foundLastCaseOrDefault = false; for (size_t j = 0; j < numCases + 1; j++) { jsbytecode* testpc = getCaseOrDefaultPc(j); MOZ_ASSERT(script->code() <= testpc && testpc < end); if (lastCaseOrDefaultPc < testpc && (testpc < caseOrDefaultPc || (j < i && testpc == caseOrDefaultPc))) { lastCaseOrDefaultPc = testpc; foundLastCaseOrDefault = true; } } // If multiple case instruction have the same code block, only // register the code coverage the first time we hit this case. if (!foundLastCaseOrDefault || caseOrDefaultPc != lastCaseOrDefaultPc) { uint64_t caseOrDefaultHits = 0; if (sc) { if (i < numCases) { // Case (i + low) const PCCounts* counts = sc->maybeGetPCCounts(script->pcToOffset(caseOrDefaultPc)); if (counts) { caseOrDefaultHits = counts->numExec(); } // Remove fallthrough. fallsThroughHits = 0; if (foundLastCaseOrDefault) { // Walk from the previous case to the current one to // check if it fallthrough into the current block. MOZ_ASSERT(lastCaseOrDefaultPc != firstCaseOrDefaultPc - 1); jsbytecode* endpc = lastCaseOrDefaultPc; while (GetNextPc(endpc) < caseOrDefaultPc) { endpc = GetNextPc(endpc); MOZ_ASSERT(script->code() <= endpc && endpc < end); } if (BytecodeFallsThrough(JSOp(*endpc))) { fallsThroughHits = script->getHitCount(endpc); } } caseOrDefaultHits -= fallsThroughHits; } else { caseOrDefaultHits = defaultHits; } } outBRDA_.printf("BRDA:%zu,%zu,%zu,", lineno, branchId, caseId); if (hits) { outBRDA_.printf("%" PRIu64 "\n", caseOrDefaultHits); } else { outBRDA_.put("-\n", 2); } numBranchesFound_++; numBranchesHit_ += !!caseOrDefaultHits; if (i < numCases) { defaultHits -= caseOrDefaultHits; } caseId++; } } } } if (outFN_.hadOutOfMemory() || outFNDA_.hadOutOfMemory() || outBRDA_.hadOutOfMemory()) { hadOOM_ = true; return; } // 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; } } LCovRealm::LCovRealm(JS::Realm* realm) : alloc_(4096), outTN_(&alloc_), sources_(alloc_) { // Record realm name. If we wait until finalization, the embedding may not be // able to provide us the name anymore. writeRealmName(realm); } LCovRealm::~LCovRealm() { // The LCovSource are in the LifoAlloc but we must still manually invoke // destructors to avoid leaks. while (!sources_.empty()) { LCovSource* source = sources_.popCopy(); source->~LCovSource(); } } LCovSource* LCovRealm::lookupOrAdd(const char* name) { // Find existing source if it exists. for (LCovSource* source : sources_) { if (source->match(name)) { return source; } } UniqueChars source_name = DuplicateString(name); if (!source_name) { outTN_.reportOutOfMemory(); return nullptr; } // Allocate a new LCovSource for the current top-level. LCovSource* source = alloc_.new_<LCovSource>(&alloc_, std::move(source_name)); if (!source) { outTN_.reportOutOfMemory(); return nullptr; } if (!sources_.emplaceBack(source)) { outTN_.reportOutOfMemory(); return nullptr; } return source; } void LCovRealm::exportInto(GenericPrinter& out, bool* isEmpty) const { if (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 (LCovSource* sc : sources_) { // Only write if everything got recorded. if (sc->isComplete()) { sc->exportInto(out); } } } void LCovRealm::writeRealmName(JS::Realm* realm) { JSContext* cx = TlsContext.get(); // lcov trace files are starting with an optional test case name, that we // recycle to be a realm 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->runtime()->realmNameCallback) { char name[1024]; { // Hazard analysis cannot tell that the callback does not GC. JS::AutoSuppressGCAnalysis nogc; (*cx->runtime()->realmNameCallback)(cx, realm, name, sizeof(name), nogc); } 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("Realm_%p%p\n", (void*)size_t('_'), realm); } } const char* LCovRealm::getScriptName(JSScript* script) { JSFunction* fun = script->function(); if (fun && fun->fullDisplayAtom()) { JSAtom* atom = fun->fullDisplayAtom(); size_t lenWithNull = js::PutEscapedString(nullptr, 0, atom, 0) + 1; char* name = alloc_.newArray<char>(lenWithNull); if (name) { js::PutEscapedString(name, lenWithNull, atom, 0); } return name; } return "top-level"; } bool gLCovIsEnabled = false; void InitLCov() { const char* outDir = getenv("JS_CODE_COVERAGE_OUTPUT_DIR"); if (outDir && *outDir != 0) { EnableLCov(); } } void EnableLCov() { MOZ_ASSERT(!JSRuntime::hasLiveRuntimes(), "EnableLCov must not be called after creating a runtime!"); gLCovIsEnabled = true; } LCovRuntime::LCovRuntime() : pid_(getpid()), isEmpty_(true) {} 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 "-%" PRIu32 "-%zu.info", outDir, timestamp, pid_, rid); if (len < 0 || size_t(len) >= length) { fprintf(stderr, "Warning: LCovRuntime::init: Cannot serialize file name.\n"); 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'.\n", 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(LCovRealm& realm) { if (!out_.isInitialized()) { init(); if (!out_.isInitialized()) { return; } } uint32_t p = getpid(); if (pid_ != p) { pid_ = p; finishFile(); init(); if (!out_.isInitialized()) { return; } } realm.exportInto(out_, &isEmpty_); out_.flush(); finishFile(); } bool InitScriptCoverage(JSContext* cx, JSScript* script) { MOZ_ASSERT(IsLCovEnabled()); MOZ_ASSERT(script->hasBytecode(), "Only initialize coverage data for fully initialized scripts."); const char* filename = script->filename(); if (!filename) { return true; } // Create LCovRealm if necessary. LCovRealm* lcovRealm = script->realm()->lcovRealm(); if (!lcovRealm) { ReportOutOfMemory(cx); return false; } // Create LCovSource if necessary. LCovSource* source = lcovRealm->lookupOrAdd(filename); if (!source) { ReportOutOfMemory(cx); return false; } // Computed the formated script name. const char* scriptName = lcovRealm->getScriptName(script); if (!scriptName) { ReportOutOfMemory(cx); return false; } // Create Zone::scriptLCovMap if necessary. JS::Zone* zone = script->zone(); if (!zone->scriptLCovMap) { zone->scriptLCovMap = cx->make_unique<ScriptLCovMap>(); } if (!zone->scriptLCovMap) { return false; } MOZ_ASSERT(script->hasBytecode()); // Save source in map for when we collect coverage. if (!zone->scriptLCovMap->putNew(script, std::make_tuple(source, scriptName))) { ReportOutOfMemory(cx); return false; } return true; } bool CollectScriptCoverage(JSScript* script, bool finalizing) { MOZ_ASSERT(IsLCovEnabled()); ScriptLCovMap* map = script->zone()->scriptLCovMap.get(); if (!map) { return false; } auto p = map->lookup(script); if (!p.found()) { return false; } auto [source, scriptName] = p->value(); if (script->hasBytecode()) { source->writeScript(script, scriptName); } if (finalizing) { map->remove(p); } // Propagate the failure in case caller wants to terminate early. return !source->hadOutOfMemory(); } } // namespace coverage } // namespace js