Bug 1204554 part 2 - Split LCov functions to make the aggregation of results incremental. r=terrence,bhackett

This commit is contained in:
Nicolas B. Pierron 2015-10-01 12:41:40 +02:00
parent 070ec8e3ee
commit 848c5c4b1c
5 changed files with 495 additions and 229 deletions

View File

@ -37,6 +37,7 @@
#include "frontend/BytecodeCompiler.h"
#include "frontend/SourceNotes.h"
#include "js/CharacterEncoding.h"
#include "vm/CodeCoverage.h"
#include "vm/Opcodes.h"
#include "vm/ScopeObject.h"
#include "vm/Shape.h"
@ -1944,160 +1945,6 @@ js::GetPCCountScriptContents(JSContext* cx, size_t index)
return buf.finishString();
}
static bool
LcovWriteScriptName(GenericPrinter& out, JSScript* script)
{
JSFunction* fun = script->functionNonDelazifying();
if (fun && fun->displayAtom())
return EscapedStringPrinter(out, fun->displayAtom(), 0);
out.printf("top-level");
return true;
}
struct LcovSourceFile
{
const char* filename;
LSprinter outFN;
LSprinter outFNDA;
size_t numFunctionsFound;
size_t numFunctionsHit;
LSprinter outBRDA;
size_t numBranchesFound;
size_t numBranchesHit;
LSprinter outDA;
size_t numLinesInstrumented;
size_t numLinesHit;
LcovSourceFile(LifoAlloc* alloc, JSScript *script)
: filename(script->filename()),
outFN(alloc),
outFNDA(alloc),
numFunctionsFound(0),
numFunctionsHit(0),
outBRDA(alloc),
numBranchesFound(0),
numBranchesHit(0),
outDA(alloc),
numLinesInstrumented(0),
numLinesHit(0)
{ }
};
static bool
LcovWriteScript(LcovSourceFile& lsf, JSScript* script)
{
lsf.numFunctionsFound++;
lsf.outFN.printf("FN:%d,", script->lineno());
if (!LcovWriteScriptName(lsf.outFN, script))
return false;
lsf.outFN.put("\n", 1);
uint64_t hits = 0;
ScriptCounts* sc = nullptr;
if (script->hasScriptCounts()) {
sc = &script->getScriptCounts();
lsf.numFunctionsHit++;
const PCCounts* counts = sc->maybeGetPCCounts(script->pcToOffset(script->main()));
lsf.outFNDA.printf("FNDA:%" PRIu64 ",", counts->numExec());
if (!LcovWriteScriptName(lsf.outFNDA, script))
return false;
lsf.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 blockId = 0;
for (jsbytecode* pc = script->code(); pc != end; pc = GetNextPc(pc)) {
JSOp op = JSOp(*pc);
bool jump = IsJumpOpcode(op);
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) {
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++;
sn = SN_NEXT(sn);
snpc += SN_DELTA(sn);
}
if (oldLine != lineno && fallsthrough) {
lsf.outDA.printf("DA:%d,%" PRIu64 "\n", lineno, hits);
// Count the number of lines instrumented & hit.
lsf.numLinesInstrumented++;
if (hits)
lsf.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* target = pc + GET_JUMP_OFFSET(pc);
jsbytecode* fallthroughTarget = GetNextPc(pc);
uint64_t fallthroughHits = 0;
if (sc) {
const PCCounts* counts = sc->maybeGetPCCounts(script->pcToOffset(fallthroughTarget));
if (counts)
fallthroughHits = counts->numExec();
}
size_t targetId = script->pcToOffset(target);
uint64_t taken = hits - fallthroughHits;
lsf.outBRDA.printf("BRDA:%d,%d,%d,", lineno, blockId, targetId);
if (hits)
lsf.outBRDA.printf("%d\n", taken);
else
lsf.outBRDA.put("-\n", 2);
// Count the number of branches, and the number of branches hit.
lsf.numBranchesFound++;
if (hits)
lsf.numBranchesHit++;
// Update the blockId when there is a discontinuity.
blockId = script->pcToOffset(fallthroughTarget);
}
}
return true;
}
static bool
GenerateLcovInfo(JSContext* cx, JSCompartment* comp, GenericPrinter& out)
{
@ -2108,17 +1955,7 @@ GenerateLcovInfo(JSContext* cx, JSCompartment* comp, GenericPrinter& out)
for (ZonesIter zone(rt, SkipAtoms); !zone.done(); zone.next()) {
for (ZoneCellIter i(zone, AllocKind::SCRIPT); !i.done(); i.next()) {
JSScript* script = i.get<JSScript>();
if (script->compartment() != comp)
continue;
// If we evaluate some code which contains a syntax error, then we
// might produce a JSScript which has no associated bytecode. This
// line filters out this kind of scripts.
if (!script->code())
continue;
// Filter out any JSScript which is not the top-level of a file.
if (script->functionNonDelazifying())
if (script->compartment() != comp || !script->isTopLevel())
continue;
if (!topScripts.append(script))
@ -2132,47 +1969,17 @@ GenerateLcovInfo(JSContext* cx, JSCompartment* comp, GenericPrinter& out)
// Sort the information to avoid generating multiple file entries, and to
// generate functions in the right order.
auto lessFun = [](const JSScript* lhs, const JSScript* rhs) -> bool {
int d = strcmp(lhs->filename(), rhs->filename());
/*
This should not be necessary as we are supposed to have only the
top-level script.
d = (d != 0) ? d : lhs->lineno() - rhs->lineno();
d = (d != 0) ? d : lhs->column() - rhs->column();
*/
return d < 0;
return strcmp(lhs->filename(), rhs->filename()) < 0;
};
std::sort(topScripts.begin(), topScripts.end(), lessFun);
// lcov trace files are starting with an optional test case name, that we
// recycle to be a compartment name.
out.put("TN:");
if (rt->compartmentNameCallback) {
char name[1024];
(*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'))
{
out.put(s, 1);
continue;
}
out.printf("_%p", (void*) size_t(*s));
}
out.put("\n", 1);
} else {
out.printf("Compartment_%p%p\n", (void*) size_t('_'), comp);
}
// For each source file
LifoAlloc printerAlloc(4096);
// Collect code coverage info for one compartment.
coverage::LCovCompartment compCover;
for (JSScript* topLevel: topScripts) {
LifoAllocScope printerScope(&printerAlloc);
LcovSourceFile lsf(&printerAlloc, topLevel);
RootedScript topScript(cx, topLevel);
// We found the top-level script, visit all the functions reachable
// from the top-level function.
// from the top-level function, and delazify them.
Rooted<ScriptVector> queue(cx, ScriptVector(cx));
if (!queue.append(topLevel))
return false;
@ -2181,10 +1988,6 @@ GenerateLcovInfo(JSContext* cx, JSCompartment* comp, GenericPrinter& out)
do {
script = queue.popCopy();
// Code the current script before pushing.
if (!LcovWriteScript(lsf, script))
return false;
// Iterate from the last to the first object in order to have
// the functions them visited in the opposite order when popping
// elements from the stack of remaining scripts, such that the
@ -2211,33 +2014,10 @@ GenerateLcovInfo(JSContext* cx, JSCompartment* comp, GenericPrinter& out)
}
} while (!queue.empty());
if (lsf.outFN.hadOutOfMemory() ||
lsf.outFNDA.hadOutOfMemory() ||
lsf.outBRDA.hadOutOfMemory() ||
lsf.outDA.hadOutOfMemory())
{
out.reportOutOfMemory();
return false;
}
out.printf("SF:%s\n", lsf.filename);
lsf.outFN.exportInto(out);
lsf.outFNDA.exportInto(out);
out.printf("FNF:%d\n", lsf.numFunctionsFound);
out.printf("FNH:%d\n", lsf.numFunctionsHit);
lsf.outBRDA.exportInto(out);
out.printf("BRF:%d\n", lsf.numBranchesFound);
out.printf("BRH:%d\n", lsf.numBranchesHit);
lsf.outDA.exportInto(out);
out.printf("LF:%d\n", lsf.numLinesInstrumented);
out.printf("LH:%d\n", lsf.numLinesHit);
out.put("end_of_record\n");
compCover.collectCodeCoverageInfo(comp, topScript);
}
compCover.exportInto(out);
if (out.hadOutOfMemory())
return false;
return true;

View File

@ -1612,6 +1612,18 @@ class JSScript : public js::gc::TenuredCell
/* Return whether this script was compiled for 'eval' */
bool isForEval() { return isCachedEval() || isActiveEval(); }
/*
* Return whether this script is a top-level script.
*
* If we evaluate some code which contains a syntax error, then we might
* produce a JSScript which has no associated bytecode. Testing with
* |code()| filters out this kind of scripts.
*
* If this script has a function associated to it, then it is not the
* top-level of a file.
*/
bool isTopLevel() { return code() && !functionNonDelazifying(); }
/* Ensure the script has a TypeScript. */
inline bool ensureHasTypes(JSContext* cx);

View File

@ -288,6 +288,7 @@ UNIFIED_SOURCES += [
'vm/ArrayBufferObject.cpp',
'vm/CallNonGenericMethod.cpp',
'vm/CharacterEncoding.cpp',
'vm/CodeCoverage.cpp',
'vm/Compression.cpp',
'vm/DateTime.cpp',
'vm/Debugger.cpp',

365
js/src/vm/CodeCoverage.cpp Normal file
View File

@ -0,0 +1,365 @@
/* -*- 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/IntegerPrintfMacros.h"
#include "jscompartment.h"
#include "jsopcode.h"
#include "jsscript.h"
#include "vm/Runtime.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)
: outSF_(alloc),
outFN_(alloc),
outFNDA_(alloc),
numFunctionsFound_(0),
numFunctionsHit_(0),
outBRDA_(alloc),
numBranchesFound_(0),
numBranchesHit_(0),
outDA_(alloc),
numLinesInstrumented_(0),
numLinesHit_(0)
{
}
void
LCovSource::exportInto(GenericPrinter& out) const
{
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::writeTopLevelScript(JSScript* script)
{
MOZ_ASSERT(script->isTopLevel());
if (!writeSourceFilename(outSF_, script))
return false;
Vector<JSScript*, 8, SystemAllocPolicy> queue;
if (!queue.append(script))
return false;
do {
script = queue.popCopy();
// Save the lcov output of the current script.
if (!writeScript(script))
return false;
// Iterate from the last to the first object in order to have
// the functions them visited in the opposite order when popping
// elements from the stack of remaining scripts, such that the
// functions are listed with increasing line numbers.
if (!script->hasObjects())
continue;
size_t idx = script->objects()->length;
while (idx--) {
JSObject* obj = script->getObject(idx);
// Only continue on JSFunction objects.
if (!obj->is<JSFunction>())
continue;
JSFunction& fun = obj->as<JSFunction>();
// Let's skip asm.js for now.
if (!fun.isInterpreted())
continue;
MOZ_ASSERT(!fun.isInterpretedLazy());
// Queue the script in the list of script associated to the
// current source.
if (!queue.append(fun.nonLazyScript()))
return false;
}
} while (!queue.empty());
return !(outFN_.hadOutOfMemory() ||
outFNDA_.hadOutOfMemory() ||
outBRDA_.hadOutOfMemory() ||
outDA_.hadOutOfMemory());
}
bool
LCovSource::writeSourceFilename(LSprinter& out, JSScript* script)
{
out.printf("SF:%s\n", script->filename());
return !out.hadOutOfMemory();
}
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 blockId = 0;
for (jsbytecode* pc = script->code(); pc != end; pc = GetNextPc(pc)) {
JSOp op = JSOp(*pc);
bool jump = IsJumpOpcode(op);
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) {
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++;
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* target = pc + GET_JUMP_OFFSET(pc);
jsbytecode* fallthroughTarget = GetNextPc(pc);
uint64_t fallthroughHits = 0;
if (sc) {
const PCCounts* counts = sc->maybeGetPCCounts(script->pcToOffset(fallthroughTarget));
if (counts)
fallthroughHits = counts->numExec();
}
size_t targetId = script->pcToOffset(target);
uint64_t taken = hits - fallthroughHits;
outBRDA_.printf("BRDA:%d,%d,%d,", lineno, blockId, targetId);
if (hits)
outBRDA_.printf("%d\n", taken);
else
outBRDA_.put("-\n", 2);
// Count the number of branches, and the number of branches hit.
numBranchesFound_++;
if (hits)
numBranchesHit_++;
// Update the blockId when there is a discontinuity.
blockId = script->pcToOffset(fallthroughTarget);
}
}
return true;
}
LCovCompartment::LCovCompartment()
: alloc_(4096),
outTN_(&alloc_),
sources_(nullptr)
{
MOZ_ASSERT(alloc_.isEmpty());
}
void
LCovCompartment::collectCodeCoverageInfo(JSCompartment* comp, JSScript* topLevel)
{
// Skip any operation if we already some out-of memory issues.
if (outTN_.hadOutOfMemory())
return;
// On the first call, write the compartment name, and allocate a LCovSource
// vector in the LifoAlloc.
if (!sources_) {
if (!writeCompartmentName(comp))
return;
LCovSourceVector* raw = alloc_.pod_malloc<LCovSourceVector>();
if (!raw) {
outTN_.reportOutOfMemory();
return;
}
sources_ = new(raw) LCovSourceVector(alloc_);
}
// Allocate a new LCovSource for the current top-level.
if (!sources_->append(Move(LCovSource(&alloc_)))) {
outTN_.reportOutOfMemory();
return;
}
// Write code coverage data into the allocated LCovSource.
if (!sources_->back().writeTopLevelScript(topLevel)) {
outTN_.reportOutOfMemory();
return;
}
}
void
LCovCompartment::exportInto(GenericPrinter& out) const
{
if (!sources_ || outTN_.hadOutOfMemory())
return;
outTN_.exportInto(out);
for (const LCovSource& sc : *sources_)
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();
}
} // namespace coverage
} // namespace js

108
js/src/vm/CodeCoverage.h Normal file
View File

@ -0,0 +1,108 @@
/* -*- 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/. */
#ifndef vm_CodeCoverage_h
#define vm_CodeCoverage_h
#include "mozilla/Vector.h"
#include "ds/LifoAlloc.h"
#include "vm/Printer.h"
struct JSCompartment;
class JSScript;
namespace js {
namespace coverage {
class LCovCompartment;
class LCovSource
{
public:
explicit LCovSource(LifoAlloc* alloc);
// Visit all JSScript in pre-order, and collect the lcov output based on
// the ScriptCounts counters.
//
// In case of the where this function is called during the finalization,
// this assumes that all of the children scripts are still alive, and
// not finalized yet.
bool writeTopLevelScript(JSScript* script);
// Write the Lcov output in a buffer, such as the one associated with
// the runtime code coverage trace file.
void exportInto(GenericPrinter& out) const;
private:
// Write the script name in out.
bool writeSourceFilename(LSprinter& out, JSScript* script);
// Write the script name in out.
bool writeScriptName(LSprinter& out, JSScript* script);
// Iterate over the bytecode and collect the lcov output based on the
// ScriptCounts counters.
bool writeScript(JSScript* script);
private:
// LifoAlloc string which hold the filename of the source.
LSprinter outSF_;
// LifoAlloc strings which hold the filename of each function as
// well as the number of hits for each function.
LSprinter outFN_;
LSprinter outFNDA_;
size_t numFunctionsFound_;
size_t numFunctionsHit_;
// LifoAlloc string which hold branches statistics.
LSprinter outBRDA_;
size_t numBranchesFound_;
size_t numBranchesHit_;
// LifoAlloc string which hold lines statistics.
LSprinter outDA_;
size_t numLinesInstrumented_;
size_t numLinesHit_;
};
class LCovCompartment
{
public:
LCovCompartment();
// Collect code coverage information
void collectCodeCoverageInfo(JSCompartment* comp, JSScript* topLevel);
// Write the Lcov output in a buffer, such as the one associated with
// the runtime code coverage trace file.
void exportInto(GenericPrinter& out) const;
private:
// Write the script name in out.
bool writeCompartmentName(JSCompartment* comp);
private:
typedef Vector<LCovSource, 16, LifoAllocPolicy<Fallible>> LCovSourceVector;
// LifoAlloc backend for all temporary allocations needed to stash the
// strings to be written in the file.
LifoAlloc alloc_;
// LifoAlloc string which hold the name of the compartment.
LSprinter outTN_;
// Vector of all sources which are used in this compartment.
LCovSourceVector* sources_;
};
} // namespace coverage
} // namespace js
#endif // vm_Printer_h