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/. */
6
7 #include "vm/CodeCoverage.h"
8
9 #include "mozilla/Atomics.h"
10 #include "mozilla/IntegerPrintfMacros.h"
11
12 #include <stdio.h>
13 #include <utility>
14
15 #include "frontend/SourceNotes.h" // SrcNote, SrcNoteType, SrcNoteIterator
16 #include "gc/Zone.h"
17 #include "util/GetPidProvider.h" // getpid()
18 #include "util/Text.h"
19 #include "vm/BytecodeUtil.h"
20 #include "vm/JSScript.h"
21 #include "vm/Realm.h"
22 #include "vm/Runtime.h"
23 #include "vm/Time.h"
24
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.
30 //
31 // TN:<compartment name>
32 // for-each <source file> {
33 // SN:<filename>
34 // for-each <script> {
35 // FN:<line>,<name>
36 // }
37 // for-each <script> {
38 // FNDA:<hits>,<name>
39 // }
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>
45 // }
46 // }
47 // BRF:<number of branches>
48 // BRH:<sum of branches hits>
49 // for-each <script> {
50 // for-each <line> {
51 // DA:<line>,<hits>
52 // }
53 // }
54 // LF:<number of lines>
55 // LH:<sum of lines hits>
56 // }
57 //
58 // [1] http://ltp.sourceforge.net/coverage/lcov/geninfo.1.php
59 //
60 namespace js {
61 namespace coverage {
62
LCovSource(LifoAlloc * alloc,UniqueChars name)63 LCovSource::LCovSource(LifoAlloc* alloc, UniqueChars name)
64 : name_(std::move(name)),
65 outFN_(alloc),
66 outFNDA_(alloc),
67 numFunctionsFound_(0),
68 numFunctionsHit_(0),
69 outBRDA_(alloc),
70 numBranchesFound_(0),
71 numBranchesHit_(0),
72 numLinesInstrumented_(0),
73 numLinesHit_(0),
74 maxLineHit_(0),
75 hasTopLevelScript_(false),
76 hadOOM_(false) {}
77
exportInto(GenericPrinter & out)78 void LCovSource::exportInto(GenericPrinter& out) {
79 if (hadOutOfMemory()) {
80 out.reportOutOfMemory();
81 } else {
82 out.printf("SF:%s\n", name_.get());
83
84 outFN_.exportInto(out);
85 outFNDA_.exportInto(out);
86 out.printf("FNF:%zu\n", numFunctionsFound_);
87 out.printf("FNH:%zu\n", numFunctionsHit_);
88
89 outBRDA_.exportInto(out);
90 out.printf("BRF:%zu\n", numBranchesFound_);
91 out.printf("BRH:%zu\n", numBranchesHit_);
92
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());
97 }
98 }
99 }
100
101 out.printf("LF:%zu\n", numLinesInstrumented_);
102 out.printf("LH:%zu\n", numLinesHit_);
103
104 out.put("end_of_record\n");
105 }
106
107 outFN_.clear();
108 outFNDA_.clear();
109 numFunctionsFound_ = 0;
110 numFunctionsHit_ = 0;
111 outBRDA_.clear();
112 numBranchesFound_ = 0;
113 numBranchesHit_ = 0;
114 linesHit_.clear();
115 numLinesInstrumented_ = 0;
116 numLinesHit_ = 0;
117 maxLineHit_ = 0;
118 }
119
writeScript(JSScript * script,const char * scriptName)120 void LCovSource::writeScript(JSScript* script, const char* scriptName) {
121 if (hadOutOfMemory()) {
122 return;
123 }
124
125 numFunctionsFound_++;
126 outFN_.printf("FN:%u,%s\n", script->lineno(), scriptName);
127
128 uint64_t hits = 0;
129 ScriptCounts* sc = nullptr;
130 if (script->hasScriptCounts()) {
131 sc = &script->getScriptCounts();
132 numFunctionsHit_++;
133 const PCCounts* counts =
134 sc->maybeGetPCCounts(script->pcToOffset(script->main()));
135 outFNDA_.printf("FNDA:%" PRIu64 ",%s\n", counts->numExec(), scriptName);
136
137 // Set the hit count of the pre-main code to 1, if the function ever got
138 // visited.
139 hits = 1;
140 }
141
142 jsbytecode* snpc = script->code();
143 const SrcNote* sn = script->notes();
144 if (!sn->isTerminator()) {
145 snpc += sn->delta();
146 }
147
148 size_t lineno = script->lineno();
149 jsbytecode* end = script->codeEnd();
150 size_t branchId = 0;
151 bool firstLineHasBeenWritten = false;
152 for (jsbytecode* pc = script->code(); pc != end; pc = GetNextPc(pc)) {
153 MOZ_ASSERT(script->code() <= pc && pc < end);
154 JSOp op = JSOp(*pc);
155 bool jump = IsJumpOpcode(op) || op == JSOp::TableSwitch;
156 bool fallsthrough = BytecodeFallsThrough(op) && op != JSOp::Gosub;
157
158 // If the current script & pc has a hit-count report, then update the
159 // current number of hits.
160 if (sc) {
161 const PCCounts* counts = sc->maybeGetPCCounts(script->pcToOffset(pc));
162 if (counts) {
163 hits = counts->numExec();
164 }
165 }
166
167 // If we have additional source notes, walk all the source notes of the
168 // current pc.
169 if (snpc <= pc || !firstLineHasBeenWritten) {
170 size_t oldLine = lineno;
171 SrcNoteIterator iter(sn);
172 while (!iter.atEnd() && snpc <= pc) {
173 sn = *iter;
174 SrcNoteType type = sn->type();
175 if (type == SrcNoteType::SetLine) {
176 lineno = SrcNote::SetLine::getLine(sn, script->lineno());
177 } else if (type == SrcNoteType::NewLine) {
178 lineno++;
179 }
180 ++iter;
181 snpc += (*iter)->delta();
182 }
183 sn = *iter;
184
185 if ((oldLine != lineno || !firstLineHasBeenWritten) &&
186 pc >= script->main() && fallsthrough) {
187 auto p = linesHit_.lookupForAdd(lineno);
188 if (!p) {
189 if (!linesHit_.add(p, lineno, hits)) {
190 hadOOM_ = true;
191 return;
192 }
193 numLinesInstrumented_++;
194 if (hits != 0) {
195 numLinesHit_++;
196 }
197 maxLineHit_ = std::max(lineno, maxLineHit_);
198 } else {
199 if (p->value() == 0 && hits != 0) {
200 numLinesHit_++;
201 }
202 p->value() += hits;
203 }
204
205 firstLineHasBeenWritten = true;
206 }
207 }
208
209 // If the current instruction has thrown, then decrement the hit counts
210 // with the number of throws.
211 if (sc) {
212 const PCCounts* counts = sc->maybeGetThrowCounts(script->pcToOffset(pc));
213 if (counts) {
214 hits -= counts->numExec();
215 }
216 }
217
218 // If the current pc corresponds to a conditional jump instruction, then
219 // reports branch hits.
220 if (jump && fallsthrough) {
221 jsbytecode* fallthroughTarget = GetNextPc(pc);
222 uint64_t fallthroughHits = 0;
223 if (sc) {
224 const PCCounts* counts =
225 sc->maybeGetPCCounts(script->pcToOffset(fallthroughTarget));
226 if (counts) {
227 fallthroughHits = counts->numExec();
228 }
229 }
230
231 uint64_t taken = hits - fallthroughHits;
232 outBRDA_.printf("BRDA:%zu,%zu,0,", lineno, branchId);
233 if (hits) {
234 outBRDA_.printf("%" PRIu64 "\n", taken);
235 } else {
236 outBRDA_.put("-\n", 2);
237 }
238
239 outBRDA_.printf("BRDA:%zu,%zu,1,", lineno, branchId);
240 if (hits) {
241 outBRDA_.printf("%" PRIu64 "\n", fallthroughHits);
242 } else {
243 outBRDA_.put("-\n", 2);
244 }
245
246 // Count the number of branches, and the number of branches hit.
247 numBranchesFound_ += 2;
248 if (hits) {
249 numBranchesHit_ += !!taken + !!fallthroughHits;
250 }
251 branchId++;
252 }
253
254 // If the current pc corresponds to a pre-computed switch case, then
255 // reports branch hits for each case statement.
256 if (jump && op == JSOp::TableSwitch) {
257 // Get the default pc.
258 jsbytecode* defaultpc = pc + GET_JUMP_OFFSET(pc);
259 MOZ_ASSERT(script->code() <= defaultpc && defaultpc < end);
260 MOZ_ASSERT(defaultpc > pc);
261
262 // Get the low and high from the tableswitch
263 int32_t low = GET_JUMP_OFFSET(pc + JUMP_OFFSET_LEN * 1);
264 int32_t high = GET_JUMP_OFFSET(pc + JUMP_OFFSET_LEN * 2);
265 MOZ_ASSERT(high - low + 1 >= 0);
266 size_t numCases = high - low + 1;
267
268 auto getCaseOrDefaultPc = [&](size_t index) {
269 if (index < numCases) {
270 return script->tableSwitchCasePC(pc, index);
271 }
272 MOZ_ASSERT(index == numCases);
273 return defaultpc;
274 };
275
276 jsbytecode* firstCaseOrDefaultPc = end;
277 for (size_t j = 0; j < numCases + 1; j++) {
278 jsbytecode* testpc = getCaseOrDefaultPc(j);
279 MOZ_ASSERT(script->code() <= testpc && testpc < end);
280 if (testpc < firstCaseOrDefaultPc) {
281 firstCaseOrDefaultPc = testpc;
282 }
283 }
284
285 // Count the number of hits of the default branch, by subtracting
286 // the number of hits of each cases.
287 uint64_t defaultHits = hits;
288
289 // Count the number of hits of the previous case entry.
290 uint64_t fallsThroughHits = 0;
291
292 // Record branches for each case and default.
293 size_t caseId = 0;
294 for (size_t i = 0; i < numCases + 1; i++) {
295 jsbytecode* caseOrDefaultPc = getCaseOrDefaultPc(i);
296 MOZ_ASSERT(script->code() <= caseOrDefaultPc && caseOrDefaultPc < end);
297
298 // PCs might not be in increasing order of case indexes.
299 jsbytecode* lastCaseOrDefaultPc = firstCaseOrDefaultPc - 1;
300 bool foundLastCaseOrDefault = false;
301 for (size_t j = 0; j < numCases + 1; j++) {
302 jsbytecode* testpc = getCaseOrDefaultPc(j);
303 MOZ_ASSERT(script->code() <= testpc && testpc < end);
304 if (lastCaseOrDefaultPc < testpc &&
305 (testpc < caseOrDefaultPc ||
306 (j < i && testpc == caseOrDefaultPc))) {
307 lastCaseOrDefaultPc = testpc;
308 foundLastCaseOrDefault = true;
309 }
310 }
311
312 // If multiple case instruction have the same code block, only
313 // register the code coverage the first time we hit this case.
314 if (!foundLastCaseOrDefault || caseOrDefaultPc != lastCaseOrDefaultPc) {
315 uint64_t caseOrDefaultHits = 0;
316 if (sc) {
317 if (i < numCases) {
318 // Case (i + low)
319 const PCCounts* counts =
320 sc->maybeGetPCCounts(script->pcToOffset(caseOrDefaultPc));
321 if (counts) {
322 caseOrDefaultHits = counts->numExec();
323 }
324
325 // Remove fallthrough.
326 fallsThroughHits = 0;
327 if (foundLastCaseOrDefault) {
328 // Walk from the previous case to the current one to
329 // check if it fallthrough into the current block.
330 MOZ_ASSERT(lastCaseOrDefaultPc != firstCaseOrDefaultPc - 1);
331 jsbytecode* endpc = lastCaseOrDefaultPc;
332 while (GetNextPc(endpc) < caseOrDefaultPc) {
333 endpc = GetNextPc(endpc);
334 MOZ_ASSERT(script->code() <= endpc && endpc < end);
335 }
336
337 if (BytecodeFallsThrough(JSOp(*endpc))) {
338 fallsThroughHits = script->getHitCount(endpc);
339 }
340 }
341 caseOrDefaultHits -= fallsThroughHits;
342 } else {
343 caseOrDefaultHits = defaultHits;
344 }
345 }
346
347 outBRDA_.printf("BRDA:%zu,%zu,%zu,", lineno, branchId, caseId);
348 if (hits) {
349 outBRDA_.printf("%" PRIu64 "\n", caseOrDefaultHits);
350 } else {
351 outBRDA_.put("-\n", 2);
352 }
353
354 numBranchesFound_++;
355 numBranchesHit_ += !!caseOrDefaultHits;
356 if (i < numCases) {
357 defaultHits -= caseOrDefaultHits;
358 }
359 caseId++;
360 }
361 }
362 }
363 }
364
365 if (outFN_.hadOutOfMemory() || outFNDA_.hadOutOfMemory() ||
366 outBRDA_.hadOutOfMemory()) {
367 hadOOM_ = true;
368 return;
369 }
370
371 // If this script is the top-level script, then record it such that we can
372 // assume that the code coverage report is complete, as this script has
373 // references on all inner scripts.
374 if (script->isTopLevel()) {
375 hasTopLevelScript_ = true;
376 }
377 }
378
LCovRealm(JS::Realm * realm)379 LCovRealm::LCovRealm(JS::Realm* realm)
380 : alloc_(4096), outTN_(&alloc_), sources_(alloc_) {
381 // Record realm name. If we wait until finalization, the embedding may not be
382 // able to provide us the name anymore.
383 writeRealmName(realm);
384 }
385
~LCovRealm()386 LCovRealm::~LCovRealm() {
387 // The LCovSource are in the LifoAlloc but we must still manually invoke
388 // destructors to avoid leaks.
389 while (!sources_.empty()) {
390 LCovSource* source = sources_.popCopy();
391 source->~LCovSource();
392 }
393 }
394
lookupOrAdd(const char * name)395 LCovSource* LCovRealm::lookupOrAdd(const char* name) {
396 // Find existing source if it exists.
397 for (LCovSource* source : sources_) {
398 if (source->match(name)) {
399 return source;
400 }
401 }
402
403 UniqueChars source_name = DuplicateString(name);
404 if (!source_name) {
405 outTN_.reportOutOfMemory();
406 return nullptr;
407 }
408
409 // Allocate a new LCovSource for the current top-level.
410 LCovSource* source = alloc_.new_<LCovSource>(&alloc_, std::move(source_name));
411 if (!source) {
412 outTN_.reportOutOfMemory();
413 return nullptr;
414 }
415
416 if (!sources_.emplaceBack(source)) {
417 outTN_.reportOutOfMemory();
418 return nullptr;
419 }
420
421 return source;
422 }
423
exportInto(GenericPrinter & out,bool * isEmpty) const424 void LCovRealm::exportInto(GenericPrinter& out, bool* isEmpty) const {
425 if (outTN_.hadOutOfMemory()) {
426 return;
427 }
428
429 // If we only have cloned function, then do not serialize anything.
430 bool someComplete = false;
431 for (const LCovSource* sc : sources_) {
432 if (sc->isComplete()) {
433 someComplete = true;
434 break;
435 };
436 }
437
438 if (!someComplete) {
439 return;
440 }
441
442 *isEmpty = false;
443 outTN_.exportInto(out);
444 for (LCovSource* sc : sources_) {
445 // Only write if everything got recorded.
446 if (sc->isComplete()) {
447 sc->exportInto(out);
448 }
449 }
450 }
451
writeRealmName(JS::Realm * realm)452 void LCovRealm::writeRealmName(JS::Realm* realm) {
453 JSContext* cx = TlsContext.get();
454
455 // lcov trace files are starting with an optional test case name, that we
456 // recycle to be a realm name.
457 //
458 // Note: The test case name has some constraint in terms of valid character,
459 // thus we escape invalid chracters with a "_" symbol in front of its
460 // hexadecimal code.
461 outTN_.put("TN:");
462 if (cx->runtime()->realmNameCallback) {
463 char name[1024];
464 {
465 // Hazard analysis cannot tell that the callback does not GC.
466 JS::AutoSuppressGCAnalysis nogc;
467 (*cx->runtime()->realmNameCallback)(cx, realm, name, sizeof(name), nogc);
468 }
469 for (char* s = name; s < name + sizeof(name) && *s; s++) {
470 if (('a' <= *s && *s <= 'z') || ('A' <= *s && *s <= 'Z') ||
471 ('0' <= *s && *s <= '9')) {
472 outTN_.put(s, 1);
473 continue;
474 }
475 outTN_.printf("_%p", (void*)size_t(*s));
476 }
477 outTN_.put("\n", 1);
478 } else {
479 outTN_.printf("Realm_%p%p\n", (void*)size_t('_'), realm);
480 }
481 }
482
getScriptName(JSScript * script)483 const char* LCovRealm::getScriptName(JSScript* script) {
484 JSFunction* fun = script->function();
485 if (fun && fun->displayAtom()) {
486 JSAtom* atom = fun->displayAtom();
487 size_t lenWithNull = js::PutEscapedString(nullptr, 0, atom, 0) + 1;
488 char* name = alloc_.newArray<char>(lenWithNull);
489 if (name) {
490 js::PutEscapedString(name, lenWithNull, atom, 0);
491 }
492 return name;
493 }
494 return "top-level";
495 }
496
497 bool gLCovIsEnabled = false;
498
InitLCov()499 void InitLCov() {
500 const char* outDir = getenv("JS_CODE_COVERAGE_OUTPUT_DIR");
501 if (outDir && *outDir != 0) {
502 EnableLCov();
503 }
504 }
505
EnableLCov()506 void EnableLCov() {
507 MOZ_ASSERT(!JSRuntime::hasLiveRuntimes(),
508 "EnableLCov must not be called after creating a runtime!");
509 gLCovIsEnabled = true;
510 }
511
LCovRuntime()512 LCovRuntime::LCovRuntime() : out_(), pid_(getpid()), isEmpty_(true) {}
513
~LCovRuntime()514 LCovRuntime::~LCovRuntime() {
515 if (out_.isInitialized()) {
516 finishFile();
517 }
518 }
519
fillWithFilename(char * name,size_t length)520 bool LCovRuntime::fillWithFilename(char* name, size_t length) {
521 const char* outDir = getenv("JS_CODE_COVERAGE_OUTPUT_DIR");
522 if (!outDir || *outDir == 0) {
523 return false;
524 }
525
526 int64_t timestamp = static_cast<double>(PRMJ_Now()) / PRMJ_USEC_PER_SEC;
527 static mozilla::Atomic<size_t> globalRuntimeId(0);
528 size_t rid = globalRuntimeId++;
529
530 int len = snprintf(name, length, "%s/%" PRId64 "-%" PRIu32 "-%zu.info",
531 outDir, timestamp, pid_, rid);
532 if (len < 0 || size_t(len) >= length) {
533 fprintf(stderr,
534 "Warning: LCovRuntime::init: Cannot serialize file name.\n");
535 return false;
536 }
537
538 return true;
539 }
540
init()541 void LCovRuntime::init() {
542 char name[1024];
543 if (!fillWithFilename(name, sizeof(name))) {
544 return;
545 }
546
547 // If we cannot open the file, report a warning.
548 if (!out_.init(name)) {
549 fprintf(stderr,
550 "Warning: LCovRuntime::init: Cannot open file named '%s'.\n", name);
551 }
552 isEmpty_ = true;
553 }
554
finishFile()555 void LCovRuntime::finishFile() {
556 MOZ_ASSERT(out_.isInitialized());
557 out_.finish();
558
559 if (isEmpty_) {
560 char name[1024];
561 if (!fillWithFilename(name, sizeof(name))) {
562 return;
563 }
564 remove(name);
565 }
566 }
567
writeLCovResult(LCovRealm & realm)568 void LCovRuntime::writeLCovResult(LCovRealm& realm) {
569 if (!out_.isInitialized()) {
570 init();
571 if (!out_.isInitialized()) {
572 return;
573 }
574 }
575
576 uint32_t p = getpid();
577 if (pid_ != p) {
578 pid_ = p;
579 finishFile();
580 init();
581 if (!out_.isInitialized()) {
582 return;
583 }
584 }
585
586 realm.exportInto(out_, &isEmpty_);
587 out_.flush();
588 finishFile();
589 }
590
InitScriptCoverage(JSContext * cx,JSScript * script)591 bool InitScriptCoverage(JSContext* cx, JSScript* script) {
592 MOZ_ASSERT(IsLCovEnabled());
593 MOZ_ASSERT(script->hasBytecode(),
594 "Only initialize coverage data for fully initialized scripts.");
595
596 // Don't allocate LCovSource if we on helper thread since we will have our
597 // realm migrated. The 'GCRunime::mergeRealms' code will do this
598 // initialization.
599 if (cx->isHelperThreadContext()) {
600 return true;
601 }
602
603 const char* filename = script->filename();
604 if (!filename) {
605 return true;
606 }
607
608 // Create LCovRealm if necessary.
609 LCovRealm* lcovRealm = script->realm()->lcovRealm();
610 if (!lcovRealm) {
611 ReportOutOfMemory(cx);
612 return false;
613 }
614
615 // Create LCovSource if necessary.
616 LCovSource* source = lcovRealm->lookupOrAdd(filename);
617 if (!source) {
618 ReportOutOfMemory(cx);
619 return false;
620 }
621
622 // Computed the formated script name.
623 const char* scriptName = lcovRealm->getScriptName(script);
624 if (!scriptName) {
625 ReportOutOfMemory(cx);
626 return false;
627 }
628
629 // Create Zone::scriptLCovMap if necessary.
630 JS::Zone* zone = script->zone();
631 if (!zone->scriptLCovMap) {
632 zone->scriptLCovMap = cx->make_unique<ScriptLCovMap>();
633 }
634 if (!zone->scriptLCovMap) {
635 return false;
636 }
637
638 MOZ_ASSERT(script->hasBytecode());
639
640 // Save source in map for when we collect coverage.
641 if (!zone->scriptLCovMap->putNew(script,
642 mozilla::MakeTuple(source, scriptName))) {
643 ReportOutOfMemory(cx);
644 return false;
645 }
646
647 return true;
648 }
649
CollectScriptCoverage(JSScript * script,bool finalizing)650 bool CollectScriptCoverage(JSScript* script, bool finalizing) {
651 MOZ_ASSERT(IsLCovEnabled());
652
653 ScriptLCovMap* map = script->zone()->scriptLCovMap.get();
654 if (!map) {
655 return false;
656 }
657
658 auto p = map->lookup(script);
659 if (!p.found()) {
660 return false;
661 }
662
663 LCovSource* source;
664 const char* scriptName;
665 mozilla::Tie(source, scriptName) = p->value();
666
667 if (script->hasBytecode()) {
668 source->writeScript(script, scriptName);
669 }
670
671 if (finalizing) {
672 map->remove(p);
673 }
674
675 // Propagate the failure in case caller wants to terminate early.
676 return !source->hadOutOfMemory();
677 }
678
679 } // namespace coverage
680 } // namespace js
681