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