blob: e63bb44ea50ccd880858aa95a96ca5855c21fbe8 [file] [log] [blame]
/* -*- 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(STARBOARD)
#include "starboard/thread.h"
#elif 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:%d\n", numFunctionsFound_);
out.printf("FNH:%d\n", numFunctionsHit_);
outBRDA_.exportInto(out);
out.printf("BRF:%d\n", numBranchesFound_);
out.printf("BRH:%d\n", numBranchesHit_);
outDA_.exportInto(out);
out.printf("LF:%d\n", numLinesInstrumented_);
out.printf("LH:%d\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:%d,", 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:%d,%" 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:%d,%d,0,", lineno, branchId);
if (taken)
outBRDA_.printf("%d\n", taken);
else
outBRDA_.put("-\n", 2);
outBRDA_.printf("BRDA:%d,%d,1,", lineno, branchId);
if (fallthroughHits)
outBRDA_.printf("%d\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);
int32_t numCases = high - low + 1;
jsbytecode* jumpTable = pc + JUMP_OFFSET_LEN * 3;
jsbytecode* firstcasepc = exitpc;
for (int j = 0; j < numCases; j++) {
jsbytecode* testpc = pc + GET_JUMP_OFFSET(jumpTable + JUMP_OFFSET_LEN * j);
if (testpc < firstcasepc)
firstcasepc = testpc;
}
jsbytecode* lastcasepc = firstcasepc;
uint64_t allCaseHits = 0;
for (int 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.
lastcasepc = firstcasepc - 1;
for (int j = 0; j < numCases; j++) {
jsbytecode* testpc = pc + GET_JUMP_OFFSET(jumpTable + JUMP_OFFSET_LEN * j);
if (lastcasepc < testpc && 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.
if (casepc != firstcasepc) {
jsbytecode* endpc = lastcasepc;
while (GetNextPc(endpc) < casepc)
endpc = GetNextPc(endpc);
if (BytecodeFallsThrough(JSOp(*endpc)))
caseHits -= script->getHitCount(endpc);
}
allCaseHits += caseHits;
}
outBRDA_.printf("BRDA:%d,%d,%d,", lineno, branchId, i);
if (caseHits)
outBRDA_.printf("%d\n", caseHits);
else
outBRDA_.put("-\n", 2);
numBranchesFound_++;
numBranchesHit_ += !!caseHits;
lastcasepc = casepc;
}
}
// Add one branch entry for the default statement.
uint64_t defaultHits = 0;
if (sc) {
const PCCounts* counts = sc->maybeGetPCCounts(script->pcToOffset(defaultpc));
if (counts)
defaultHits = counts->numExec();
// Note: currently we do not track edges, so we might have
// false-positive if we have any throw / return inside some
// of the case statements.
defaultHits -= allCaseHits;
}
outBRDA_.printf("BRDA:%d,%d,%d,", lineno, branchId, numCases);
if (defaultHits)
outBRDA_.printf("%d\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)
{
JSRuntime* rt = comp->runtimeFromMainThread();
// 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 (rt->compartmentNameCallback) {
char name[1024];
{
// Hazard analysis cannot tell that the callback does not GC.
JS::AutoSuppressGCAnalysis nogc;
(*rt->compartmentNameCallback)(rt, 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(STARBOARD)
pid_(SbThreadGetId()),
#elif 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 = js_sb_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++;
size_t len = JS_snprintf(name, length, "%s/%" PRId64 "-%d-%d.info",
outDir, timestamp, size_t(pid_), rid);
if (length <= 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(STARBOARD)
size_t p = SbThreadGetId();
#elif 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