Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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>
// }
//
//
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