Bug 1842773 - Part 32: Allow constructing growable SharedArrayBuffers. r=sfink
[gecko.git] / js / src / vm / CodeCoverage.cpp
blob2a8c81215ab00f9aeed2302eecb710eab304ea1e
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"
12 #include <stdio.h>
13 #include <utility>
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"
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> {
33 // SF:<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 // }
58 // [1] http://ltp.sourceforge.net/coverage/lcov/geninfo.1.php
60 namespace js {
61 namespace coverage {
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) {}
78 void LCovSource::exportInto(GenericPrinter& out) {
79 if (hadOutOfMemory()) {
80 out.reportOutOfMemory();
81 } else {
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");
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;
120 void LCovSource::writeScript(JSScript* script, const char* scriptName) {
121 if (hadOutOfMemory()) {
122 return;
125 numFunctionsFound_++;
126 outFN_.printf("FN:%u,%s\n", script->lineno(), scriptName);
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);
137 // Set the hit count of the pre-main code to 1, if the function ever got
138 // visited.
139 hits = 1;
142 jsbytecode* snpc = script->code();
143 const SrcNote* sn = script->notes();
144 const SrcNote* snEnd = script->notesEnd();
145 if (sn < snEnd) {
146 snpc += sn->delta();
149 size_t lineno = script->lineno();
150 jsbytecode* end = script->codeEnd();
151 size_t branchId = 0;
152 bool firstLineHasBeenWritten = false;
153 for (jsbytecode* pc = script->code(); pc != end; pc = GetNextPc(pc)) {
154 MOZ_ASSERT(script->code() <= pc && pc < end);
155 JSOp op = JSOp(*pc);
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.
161 if (sc) {
162 const PCCounts* counts = sc->maybeGetPCCounts(script->pcToOffset(pc));
163 if (counts) {
164 hits = counts->numExec();
168 // If we have additional source notes, walk all the source notes of the
169 // current pc.
170 if (snpc <= pc || !firstLineHasBeenWritten) {
171 size_t oldLine = lineno;
172 SrcNoteIterator iter(sn, snEnd);
173 while (!iter.atEnd() && snpc <= pc) {
174 sn = *iter;
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) {
182 lineno++;
184 ++iter;
185 if (!iter.atEnd()) {
186 snpc += (*iter)->delta();
189 sn = *iter;
191 if ((oldLine != lineno || !firstLineHasBeenWritten) &&
192 pc >= script->main() && fallsthrough) {
193 auto p = linesHit_.lookupForAdd(lineno);
194 if (!p) {
195 if (!linesHit_.add(p, lineno, hits)) {
196 hadOOM_ = true;
197 return;
199 numLinesInstrumented_++;
200 if (hits != 0) {
201 numLinesHit_++;
203 maxLineHit_ = std::max(lineno, maxLineHit_);
204 } else {
205 if (p->value() == 0 && hits != 0) {
206 numLinesHit_++;
208 p->value() += hits;
211 firstLineHasBeenWritten = true;
215 // If the current instruction has thrown, then decrement the hit counts
216 // with the number of throws.
217 if (sc) {
218 const PCCounts* counts = sc->maybeGetThrowCounts(script->pcToOffset(pc));
219 if (counts) {
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;
229 if (sc) {
230 const PCCounts* counts =
231 sc->maybeGetPCCounts(script->pcToOffset(fallthroughTarget));
232 if (counts) {
233 fallthroughHits = counts->numExec();
237 uint64_t taken = hits - fallthroughHits;
238 outBRDA_.printf("BRDA:%zu,%zu,0,", lineno, branchId);
239 if (hits) {
240 outBRDA_.printf("%" PRIu64 "\n", taken);
241 } else {
242 outBRDA_.put("-\n", 2);
245 outBRDA_.printf("BRDA:%zu,%zu,1,", lineno, branchId);
246 if (hits) {
247 outBRDA_.printf("%" PRIu64 "\n", fallthroughHits);
248 } else {
249 outBRDA_.put("-\n", 2);
252 // Count the number of branches, and the number of branches hit.
253 numBranchesFound_ += 2;
254 if (hits) {
255 numBranchesHit_ += !!taken + !!fallthroughHits;
257 branchId++;
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);
279 return defaultpc;
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.
299 size_t caseId = 0;
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;
322 if (sc) {
323 if (i < numCases) {
324 // Case (i + low)
325 const PCCounts* counts =
326 sc->maybeGetPCCounts(script->pcToOffset(caseOrDefaultPc));
327 if (counts) {
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;
348 } else {
349 caseOrDefaultHits = defaultHits;
353 outBRDA_.printf("BRDA:%zu,%zu,%zu,", lineno, branchId, caseId);
354 if (hits) {
355 outBRDA_.printf("%" PRIu64 "\n", caseOrDefaultHits);
356 } else {
357 outBRDA_.put("-\n", 2);
360 numBranchesFound_++;
361 numBranchesHit_ += !!caseOrDefaultHits;
362 if (i < numCases) {
363 defaultHits -= caseOrDefaultHits;
365 caseId++;
371 if (outFN_.hadOutOfMemory() || outFNDA_.hadOutOfMemory() ||
372 outBRDA_.hadOutOfMemory()) {
373 hadOOM_ = true;
374 return;
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)) {
405 return source;
409 UniqueChars source_name = DuplicateString(name);
410 if (!source_name) {
411 outTN_.reportOutOfMemory();
412 return nullptr;
415 // Allocate a new LCovSource for the current top-level.
416 LCovSource* source = alloc_.new_<LCovSource>(&alloc_, std::move(source_name));
417 if (!source) {
418 outTN_.reportOutOfMemory();
419 return nullptr;
422 if (!sources_.emplaceBack(source)) {
423 outTN_.reportOutOfMemory();
424 return nullptr;
427 return source;
430 void LCovRealm::exportInto(GenericPrinter& out, bool* isEmpty) const {
431 if (outTN_.hadOutOfMemory()) {
432 return;
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()) {
439 someComplete = true;
440 break;
444 if (!someComplete) {
445 return;
448 *isEmpty = false;
449 outTN_.exportInto(out);
450 for (LCovSource* sc : sources_) {
451 // Only write if everything got recorded.
452 if (sc->isComplete()) {
453 sc->exportInto(out);
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
466 // hexadecimal code.
467 outTN_.put("TN:");
468 if (cx->runtime()->realmNameCallback) {
469 char name[1024];
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')) {
478 outTN_.put(s, 1);
479 continue;
481 outTN_.printf("_%p", (void*)size_t(*s));
483 outTN_.put("\n", 1);
484 } else {
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);
495 if (name) {
496 js::PutEscapedString(name, lenWithNull, atom, 0);
498 return name;
500 return "top-level";
503 bool gLCovIsEnabled = false;
505 void InitLCov() {
506 const char* outDir = getenv("JS_CODE_COVERAGE_OUTPUT_DIR");
507 if (outDir && *outDir != 0) {
508 EnableLCov();
512 void EnableLCov() {
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()) {
522 finishFile();
526 bool LCovRuntime::fillWithFilename(char* name, size_t length) {
527 const char* outDir = getenv("JS_CODE_COVERAGE_OUTPUT_DIR");
528 if (!outDir || *outDir == 0) {
529 return false;
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) {
539 fprintf(stderr,
540 "Warning: LCovRuntime::init: Cannot serialize file name.\n");
541 return false;
544 return true;
547 void LCovRuntime::init() {
548 char name[1024];
549 if (!fillWithFilename(name, sizeof(name))) {
550 return;
553 // If we cannot open the file, report a warning.
554 if (!out_.init(name)) {
555 fprintf(stderr,
556 "Warning: LCovRuntime::init: Cannot open file named '%s'.\n", name);
558 isEmpty_ = true;
561 void LCovRuntime::finishFile() {
562 MOZ_ASSERT(out_.isInitialized());
563 out_.finish();
565 if (isEmpty_) {
566 char name[1024];
567 if (!fillWithFilename(name, sizeof(name))) {
568 return;
570 remove(name);
574 void LCovRuntime::writeLCovResult(LCovRealm& realm) {
575 if (!out_.isInitialized()) {
576 init();
577 if (!out_.isInitialized()) {
578 return;
582 uint32_t p = getpid();
583 if (pid_ != p) {
584 pid_ = p;
585 finishFile();
586 init();
587 if (!out_.isInitialized()) {
588 return;
592 realm.exportInto(out_, &isEmpty_);
593 out_.flush();
594 finishFile();
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();
603 if (!filename) {
604 return true;
607 // Create LCovRealm if necessary.
608 LCovRealm* lcovRealm = script->realm()->lcovRealm();
609 if (!lcovRealm) {
610 ReportOutOfMemory(cx);
611 return false;
614 // Create LCovSource if necessary.
615 LCovSource* source = lcovRealm->lookupOrAdd(filename);
616 if (!source) {
617 ReportOutOfMemory(cx);
618 return false;
621 // Computed the formated script name.
622 const char* scriptName = lcovRealm->getScriptName(script);
623 if (!scriptName) {
624 ReportOutOfMemory(cx);
625 return false;
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) {
634 return false;
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);
643 return false;
646 return true;
649 bool CollectScriptCoverage(JSScript* script, bool finalizing) {
650 MOZ_ASSERT(IsLCovEnabled());
652 ScriptLCovMap* map = script->zone()->scriptLCovMap.get();
653 if (!map) {
654 return false;
657 auto p = map->lookup(script);
658 if (!p.found()) {
659 return false;
662 auto [source, scriptName] = p->value();
664 if (script->hasBytecode()) {
665 source->writeScript(script, scriptName);
668 if (finalizing) {
669 map->remove(p);
672 // Propagate the failure in case caller wants to terminate early.
673 return !source->hadOutOfMemory();
676 } // namespace coverage
677 } // namespace js