1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2 * vim: set ts=8 sts=2 et sw=2 tw=80:
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "vm/CodeCoverage.h"
9 #include "mozilla/Atomics.h"
10 #include "mozilla/IntegerPrintfMacros.h"
15 #include "frontend/SourceNotes.h" // SrcNote, SrcNoteType, SrcNoteIterator
17 #include "util/GetPidProvider.h" // getpid()
18 #include "util/Text.h"
19 #include "vm/BytecodeUtil.h"
20 #include "vm/JSScript.h"
22 #include "vm/Runtime.h"
25 // This file contains a few functions which are used to produce files understood
26 // by lcov tools. A detailed description of the format is available in the man
27 // page for "geninfo" [1]. To make it short, the following paraphrases what is
28 // commented in the man page by using curly braces prefixed by for-each to
29 // express repeated patterns.
31 // TN:<compartment name>
32 // for-each <source file> {
34 // for-each <script> {
37 // for-each <script> {
40 // FNF:<number of scripts>
41 // FNH:<sum of scripts hits>
42 // for-each <script> {
43 // for-each <branch> {
44 // BRDA:<line>,<block id>,<target id>,<taken>
47 // BRF:<number of branches>
48 // BRH:<sum of branches hits>
49 // for-each <script> {
54 // LF:<number of lines>
55 // LH:<sum of lines hits>
58 // [1] http://ltp.sourceforge.net/coverage/lcov/geninfo.1.php
63 LCovSource::LCovSource(LifoAlloc
* alloc
, UniqueChars name
)
64 : name_(std::move(name
)),
67 numFunctionsFound_(0),
72 numLinesInstrumented_(0),
75 hasTopLevelScript_(false),
78 void LCovSource::exportInto(GenericPrinter
& out
) {
79 if (hadOutOfMemory()) {
80 out
.reportOutOfMemory();
82 out
.printf("SF:%s\n", name_
.get());
84 outFN_
.exportInto(out
);
85 outFNDA_
.exportInto(out
);
86 out
.printf("FNF:%zu\n", numFunctionsFound_
);
87 out
.printf("FNH:%zu\n", numFunctionsHit_
);
89 outBRDA_
.exportInto(out
);
90 out
.printf("BRF:%zu\n", numBranchesFound_
);
91 out
.printf("BRH:%zu\n", numBranchesHit_
);
93 if (!linesHit_
.empty()) {
94 for (size_t lineno
= 1; lineno
<= maxLineHit_
; ++lineno
) {
95 if (auto p
= linesHit_
.lookup(lineno
)) {
96 out
.printf("DA:%zu,%" PRIu64
"\n", lineno
, p
->value());
101 out
.printf("LF:%zu\n", numLinesInstrumented_
);
102 out
.printf("LH:%zu\n", numLinesHit_
);
104 out
.put("end_of_record\n");
109 numFunctionsFound_
= 0;
110 numFunctionsHit_
= 0;
112 numBranchesFound_
= 0;
115 numLinesInstrumented_
= 0;
120 void LCovSource::writeScript(JSScript
* script
, const char* scriptName
) {
121 if (hadOutOfMemory()) {
125 numFunctionsFound_
++;
126 outFN_
.printf("FN:%u,%s\n", script
->lineno(), scriptName
);
129 ScriptCounts
* sc
= nullptr;
130 if (script
->hasScriptCounts()) {
131 sc
= &script
->getScriptCounts();
133 const PCCounts
* counts
=
134 sc
->maybeGetPCCounts(script
->pcToOffset(script
->main()));
135 outFNDA_
.printf("FNDA:%" PRIu64
",%s\n", counts
->numExec(), scriptName
);
137 // Set the hit count of the pre-main code to 1, if the function ever got
142 jsbytecode
* snpc
= script
->code();
143 const SrcNote
* sn
= script
->notes();
144 const SrcNote
* snEnd
= script
->notesEnd();
149 size_t lineno
= script
->lineno();
150 jsbytecode
* end
= script
->codeEnd();
152 bool firstLineHasBeenWritten
= false;
153 for (jsbytecode
* pc
= script
->code(); pc
!= end
; pc
= GetNextPc(pc
)) {
154 MOZ_ASSERT(script
->code() <= pc
&& pc
< end
);
156 bool jump
= IsJumpOpcode(op
) || op
== JSOp::TableSwitch
;
157 bool fallsthrough
= BytecodeFallsThrough(op
);
159 // If the current script & pc has a hit-count report, then update the
160 // current number of hits.
162 const PCCounts
* counts
= sc
->maybeGetPCCounts(script
->pcToOffset(pc
));
164 hits
= counts
->numExec();
168 // If we have additional source notes, walk all the source notes of the
170 if (snpc
<= pc
|| !firstLineHasBeenWritten
) {
171 size_t oldLine
= lineno
;
172 SrcNoteIterator
iter(sn
, snEnd
);
173 while (!iter
.atEnd() && snpc
<= pc
) {
175 SrcNoteType type
= sn
->type();
176 if (type
== SrcNoteType::SetLine
) {
177 lineno
= SrcNote::SetLine::getLine(sn
, script
->lineno());
178 } else if (type
== SrcNoteType::SetLineColumn
) {
179 lineno
= SrcNote::SetLineColumn::getLine(sn
, script
->lineno());
180 } else if (type
== SrcNoteType::NewLine
||
181 type
== SrcNoteType::NewLineColumn
) {
186 snpc
+= (*iter
)->delta();
191 if ((oldLine
!= lineno
|| !firstLineHasBeenWritten
) &&
192 pc
>= script
->main() && fallsthrough
) {
193 auto p
= linesHit_
.lookupForAdd(lineno
);
195 if (!linesHit_
.add(p
, lineno
, hits
)) {
199 numLinesInstrumented_
++;
203 maxLineHit_
= std::max(lineno
, maxLineHit_
);
205 if (p
->value() == 0 && hits
!= 0) {
211 firstLineHasBeenWritten
= true;
215 // If the current instruction has thrown, then decrement the hit counts
216 // with the number of throws.
218 const PCCounts
* counts
= sc
->maybeGetThrowCounts(script
->pcToOffset(pc
));
220 hits
-= counts
->numExec();
224 // If the current pc corresponds to a conditional jump instruction, then
225 // reports branch hits.
226 if (jump
&& fallsthrough
) {
227 jsbytecode
* fallthroughTarget
= GetNextPc(pc
);
228 uint64_t fallthroughHits
= 0;
230 const PCCounts
* counts
=
231 sc
->maybeGetPCCounts(script
->pcToOffset(fallthroughTarget
));
233 fallthroughHits
= counts
->numExec();
237 uint64_t taken
= hits
- fallthroughHits
;
238 outBRDA_
.printf("BRDA:%zu,%zu,0,", lineno
, branchId
);
240 outBRDA_
.printf("%" PRIu64
"\n", taken
);
242 outBRDA_
.put("-\n", 2);
245 outBRDA_
.printf("BRDA:%zu,%zu,1,", lineno
, branchId
);
247 outBRDA_
.printf("%" PRIu64
"\n", fallthroughHits
);
249 outBRDA_
.put("-\n", 2);
252 // Count the number of branches, and the number of branches hit.
253 numBranchesFound_
+= 2;
255 numBranchesHit_
+= !!taken
+ !!fallthroughHits
;
260 // If the current pc corresponds to a pre-computed switch case, then
261 // reports branch hits for each case statement.
262 if (jump
&& op
== JSOp::TableSwitch
) {
263 // Get the default pc.
264 jsbytecode
* defaultpc
= pc
+ GET_JUMP_OFFSET(pc
);
265 MOZ_ASSERT(script
->code() <= defaultpc
&& defaultpc
< end
);
266 MOZ_ASSERT(defaultpc
> pc
);
268 // Get the low and high from the tableswitch
269 int32_t low
= GET_JUMP_OFFSET(pc
+ JUMP_OFFSET_LEN
* 1);
270 int32_t high
= GET_JUMP_OFFSET(pc
+ JUMP_OFFSET_LEN
* 2);
271 MOZ_ASSERT(high
- low
+ 1 >= 0);
272 size_t numCases
= high
- low
+ 1;
274 auto getCaseOrDefaultPc
= [&](size_t index
) {
275 if (index
< numCases
) {
276 return script
->tableSwitchCasePC(pc
, index
);
278 MOZ_ASSERT(index
== numCases
);
282 jsbytecode
* firstCaseOrDefaultPc
= end
;
283 for (size_t j
= 0; j
< numCases
+ 1; j
++) {
284 jsbytecode
* testpc
= getCaseOrDefaultPc(j
);
285 MOZ_ASSERT(script
->code() <= testpc
&& testpc
< end
);
286 if (testpc
< firstCaseOrDefaultPc
) {
287 firstCaseOrDefaultPc
= testpc
;
291 // Count the number of hits of the default branch, by subtracting
292 // the number of hits of each cases.
293 uint64_t defaultHits
= hits
;
295 // Count the number of hits of the previous case entry.
296 uint64_t fallsThroughHits
= 0;
298 // Record branches for each case and default.
300 for (size_t i
= 0; i
< numCases
+ 1; i
++) {
301 jsbytecode
* caseOrDefaultPc
= getCaseOrDefaultPc(i
);
302 MOZ_ASSERT(script
->code() <= caseOrDefaultPc
&& caseOrDefaultPc
< end
);
304 // PCs might not be in increasing order of case indexes.
305 jsbytecode
* lastCaseOrDefaultPc
= firstCaseOrDefaultPc
- 1;
306 bool foundLastCaseOrDefault
= false;
307 for (size_t j
= 0; j
< numCases
+ 1; j
++) {
308 jsbytecode
* testpc
= getCaseOrDefaultPc(j
);
309 MOZ_ASSERT(script
->code() <= testpc
&& testpc
< end
);
310 if (lastCaseOrDefaultPc
< testpc
&&
311 (testpc
< caseOrDefaultPc
||
312 (j
< i
&& testpc
== caseOrDefaultPc
))) {
313 lastCaseOrDefaultPc
= testpc
;
314 foundLastCaseOrDefault
= true;
318 // If multiple case instruction have the same code block, only
319 // register the code coverage the first time we hit this case.
320 if (!foundLastCaseOrDefault
|| caseOrDefaultPc
!= lastCaseOrDefaultPc
) {
321 uint64_t caseOrDefaultHits
= 0;
325 const PCCounts
* counts
=
326 sc
->maybeGetPCCounts(script
->pcToOffset(caseOrDefaultPc
));
328 caseOrDefaultHits
= counts
->numExec();
331 // Remove fallthrough.
332 fallsThroughHits
= 0;
333 if (foundLastCaseOrDefault
) {
334 // Walk from the previous case to the current one to
335 // check if it fallthrough into the current block.
336 MOZ_ASSERT(lastCaseOrDefaultPc
!= firstCaseOrDefaultPc
- 1);
337 jsbytecode
* endpc
= lastCaseOrDefaultPc
;
338 while (GetNextPc(endpc
) < caseOrDefaultPc
) {
339 endpc
= GetNextPc(endpc
);
340 MOZ_ASSERT(script
->code() <= endpc
&& endpc
< end
);
343 if (BytecodeFallsThrough(JSOp(*endpc
))) {
344 fallsThroughHits
= script
->getHitCount(endpc
);
347 caseOrDefaultHits
-= fallsThroughHits
;
349 caseOrDefaultHits
= defaultHits
;
353 outBRDA_
.printf("BRDA:%zu,%zu,%zu,", lineno
, branchId
, caseId
);
355 outBRDA_
.printf("%" PRIu64
"\n", caseOrDefaultHits
);
357 outBRDA_
.put("-\n", 2);
361 numBranchesHit_
+= !!caseOrDefaultHits
;
363 defaultHits
-= caseOrDefaultHits
;
371 if (outFN_
.hadOutOfMemory() || outFNDA_
.hadOutOfMemory() ||
372 outBRDA_
.hadOutOfMemory()) {
377 // If this script is the top-level script, then record it such that we can
378 // assume that the code coverage report is complete, as this script has
379 // references on all inner scripts.
380 if (script
->isTopLevel()) {
381 hasTopLevelScript_
= true;
385 LCovRealm::LCovRealm(JS::Realm
* realm
)
386 : alloc_(4096), outTN_(&alloc_
), sources_(alloc_
) {
387 // Record realm name. If we wait until finalization, the embedding may not be
388 // able to provide us the name anymore.
389 writeRealmName(realm
);
392 LCovRealm::~LCovRealm() {
393 // The LCovSource are in the LifoAlloc but we must still manually invoke
394 // destructors to avoid leaks.
395 while (!sources_
.empty()) {
396 LCovSource
* source
= sources_
.popCopy();
397 source
->~LCovSource();
401 LCovSource
* LCovRealm::lookupOrAdd(const char* name
) {
402 // Find existing source if it exists.
403 for (LCovSource
* source
: sources_
) {
404 if (source
->match(name
)) {
409 UniqueChars source_name
= DuplicateString(name
);
411 outTN_
.reportOutOfMemory();
415 // Allocate a new LCovSource for the current top-level.
416 LCovSource
* source
= alloc_
.new_
<LCovSource
>(&alloc_
, std::move(source_name
));
418 outTN_
.reportOutOfMemory();
422 if (!sources_
.emplaceBack(source
)) {
423 outTN_
.reportOutOfMemory();
430 void LCovRealm::exportInto(GenericPrinter
& out
, bool* isEmpty
) const {
431 if (outTN_
.hadOutOfMemory()) {
435 // If we only have cloned function, then do not serialize anything.
436 bool someComplete
= false;
437 for (const LCovSource
* sc
: sources_
) {
438 if (sc
->isComplete()) {
449 outTN_
.exportInto(out
);
450 for (LCovSource
* sc
: sources_
) {
451 // Only write if everything got recorded.
452 if (sc
->isComplete()) {
458 void LCovRealm::writeRealmName(JS::Realm
* realm
) {
459 JSContext
* cx
= TlsContext
.get();
461 // lcov trace files are starting with an optional test case name, that we
462 // recycle to be a realm name.
464 // Note: The test case name has some constraint in terms of valid character,
465 // thus we escape invalid chracters with a "_" symbol in front of its
468 if (cx
->runtime()->realmNameCallback
) {
471 // Hazard analysis cannot tell that the callback does not GC.
472 JS::AutoSuppressGCAnalysis nogc
;
473 (*cx
->runtime()->realmNameCallback
)(cx
, realm
, name
, sizeof(name
), nogc
);
475 for (char* s
= name
; s
< name
+ sizeof(name
) && *s
; s
++) {
476 if (('a' <= *s
&& *s
<= 'z') || ('A' <= *s
&& *s
<= 'Z') ||
477 ('0' <= *s
&& *s
<= '9')) {
481 outTN_
.printf("_%p", (void*)size_t(*s
));
485 outTN_
.printf("Realm_%p%p\n", (void*)size_t('_'), realm
);
489 const char* LCovRealm::getScriptName(JSScript
* script
) {
490 JSFunction
* fun
= script
->function();
491 if (fun
&& fun
->fullDisplayAtom()) {
492 JSAtom
* atom
= fun
->fullDisplayAtom();
493 size_t lenWithNull
= js::PutEscapedString(nullptr, 0, atom
, 0) + 1;
494 char* name
= alloc_
.newArray
<char>(lenWithNull
);
496 js::PutEscapedString(name
, lenWithNull
, atom
, 0);
503 bool gLCovIsEnabled
= false;
506 const char* outDir
= getenv("JS_CODE_COVERAGE_OUTPUT_DIR");
507 if (outDir
&& *outDir
!= 0) {
513 MOZ_ASSERT(!JSRuntime::hasLiveRuntimes(),
514 "EnableLCov must not be called after creating a runtime!");
515 gLCovIsEnabled
= true;
518 LCovRuntime::LCovRuntime() : pid_(getpid()), isEmpty_(true) {}
520 LCovRuntime::~LCovRuntime() {
521 if (out_
.isInitialized()) {
526 bool LCovRuntime::fillWithFilename(char* name
, size_t length
) {
527 const char* outDir
= getenv("JS_CODE_COVERAGE_OUTPUT_DIR");
528 if (!outDir
|| *outDir
== 0) {
532 int64_t timestamp
= static_cast<double>(PRMJ_Now()) / PRMJ_USEC_PER_SEC
;
533 static mozilla::Atomic
<size_t> globalRuntimeId(0);
534 size_t rid
= globalRuntimeId
++;
536 int len
= snprintf(name
, length
, "%s/%" PRId64
"-%" PRIu32
"-%zu.info",
537 outDir
, timestamp
, pid_
, rid
);
538 if (len
< 0 || size_t(len
) >= length
) {
540 "Warning: LCovRuntime::init: Cannot serialize file name.\n");
547 void LCovRuntime::init() {
549 if (!fillWithFilename(name
, sizeof(name
))) {
553 // If we cannot open the file, report a warning.
554 if (!out_
.init(name
)) {
556 "Warning: LCovRuntime::init: Cannot open file named '%s'.\n", name
);
561 void LCovRuntime::finishFile() {
562 MOZ_ASSERT(out_
.isInitialized());
567 if (!fillWithFilename(name
, sizeof(name
))) {
574 void LCovRuntime::writeLCovResult(LCovRealm
& realm
) {
575 if (!out_
.isInitialized()) {
577 if (!out_
.isInitialized()) {
582 uint32_t p
= getpid();
587 if (!out_
.isInitialized()) {
592 realm
.exportInto(out_
, &isEmpty_
);
597 bool InitScriptCoverage(JSContext
* cx
, JSScript
* script
) {
598 MOZ_ASSERT(IsLCovEnabled());
599 MOZ_ASSERT(script
->hasBytecode(),
600 "Only initialize coverage data for fully initialized scripts.");
602 const char* filename
= script
->filename();
607 // Create LCovRealm if necessary.
608 LCovRealm
* lcovRealm
= script
->realm()->lcovRealm();
610 ReportOutOfMemory(cx
);
614 // Create LCovSource if necessary.
615 LCovSource
* source
= lcovRealm
->lookupOrAdd(filename
);
617 ReportOutOfMemory(cx
);
621 // Computed the formated script name.
622 const char* scriptName
= lcovRealm
->getScriptName(script
);
624 ReportOutOfMemory(cx
);
628 // Create Zone::scriptLCovMap if necessary.
629 JS::Zone
* zone
= script
->zone();
630 if (!zone
->scriptLCovMap
) {
631 zone
->scriptLCovMap
= cx
->make_unique
<ScriptLCovMap
>();
633 if (!zone
->scriptLCovMap
) {
637 MOZ_ASSERT(script
->hasBytecode());
639 // Save source in map for when we collect coverage.
640 if (!zone
->scriptLCovMap
->putNew(script
,
641 std::make_tuple(source
, scriptName
))) {
642 ReportOutOfMemory(cx
);
649 bool CollectScriptCoverage(JSScript
* script
, bool finalizing
) {
650 MOZ_ASSERT(IsLCovEnabled());
652 ScriptLCovMap
* map
= script
->zone()->scriptLCovMap
.get();
657 auto p
= map
->lookup(script
);
662 auto [source
, scriptName
] = p
->value();
664 if (script
->hasBytecode()) {
665 source
->writeScript(script
, scriptName
);
672 // Propagate the failure in case caller wants to terminate early.
673 return !source
->hadOutOfMemory();
676 } // namespace coverage