CodeCoverage.cpp (19143B)
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 // 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 // } 57 // 58 // [1] http://ltp.sourceforge.net/coverage/lcov/geninfo.1.php 59 // 60 namespace js { 61 namespace coverage { 62 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 78 void LCovSource::exportInto(GenericPrinter& out) { 79 if (hadOutOfMemory()) { 80 out.setPendingOutOfMemory(); 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 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 const SrcNote* snEnd = script->notesEnd(); 145 if (sn < snEnd) { 146 snpc += sn->delta(); 147 } 148 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); 158 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(); 165 } 166 } 167 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++; 183 } 184 ++iter; 185 if (!iter.atEnd()) { 186 snpc += (*iter)->delta(); 187 } 188 } 189 sn = *iter; 190 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; 198 } 199 numLinesInstrumented_++; 200 if (hits != 0) { 201 numLinesHit_++; 202 } 203 maxLineHit_ = std::max(lineno, maxLineHit_); 204 } else { 205 if (p->value() == 0 && hits != 0) { 206 numLinesHit_++; 207 } 208 p->value() += hits; 209 } 210 211 firstLineHasBeenWritten = true; 212 } 213 } 214 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(); 221 } 222 } 223 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(); 234 } 235 } 236 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); 243 } 244 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); 250 } 251 252 // Count the number of branches, and the number of branches hit. 253 numBranchesFound_ += 2; 254 if (hits) { 255 numBranchesHit_ += !!taken + !!fallthroughHits; 256 } 257 branchId++; 258 } 259 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); 267 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; 273 274 auto getCaseOrDefaultPc = [&](size_t index) { 275 if (index < numCases) { 276 return script->tableSwitchCasePC(pc, index); 277 } 278 MOZ_ASSERT(index == numCases); 279 return defaultpc; 280 }; 281 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; 288 } 289 } 290 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; 294 295 // Count the number of hits of the previous case entry. 296 uint64_t fallsThroughHits = 0; 297 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); 303 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; 315 } 316 } 317 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(); 329 } 330 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); 341 } 342 343 if (BytecodeFallsThrough(JSOp(*endpc))) { 344 fallsThroughHits = script->getHitCount(endpc); 345 } 346 } 347 caseOrDefaultHits -= fallsThroughHits; 348 } else { 349 caseOrDefaultHits = defaultHits; 350 } 351 } 352 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); 358 } 359 360 numBranchesFound_++; 361 numBranchesHit_ += !!caseOrDefaultHits; 362 if (i < numCases) { 363 defaultHits -= caseOrDefaultHits; 364 } 365 caseId++; 366 } 367 } 368 } 369 } 370 371 if (outFN_.hadOutOfMemory() || outFNDA_.hadOutOfMemory() || 372 outBRDA_.hadOutOfMemory()) { 373 hadOOM_ = true; 374 return; 375 } 376 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; 382 } 383 } 384 385 LCovRealm::LCovRealm(JS::Realm* realm) 386 : alloc_(4096, js::MallocArena), 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); 390 } 391 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(); 398 } 399 } 400 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; 406 } 407 } 408 409 UniqueChars source_name = DuplicateString(name); 410 if (!source_name) { 411 return nullptr; 412 } 413 414 // Allocate a new LCovSource for the current top-level. 415 LCovSource* source = alloc_.new_<LCovSource>(&alloc_, std::move(source_name)); 416 if (!source) { 417 return nullptr; 418 } 419 420 if (!sources_.emplaceBack(source)) { 421 return nullptr; 422 } 423 424 return source; 425 } 426 427 void LCovRealm::exportInto(GenericPrinter& out, bool* isEmpty) const { 428 if (outTN_.hadOutOfMemory()) { 429 return; 430 } 431 432 // If we only have cloned function, then do not serialize anything. 433 bool someComplete = false; 434 for (const LCovSource* sc : sources_) { 435 if (sc->isComplete()) { 436 someComplete = true; 437 break; 438 }; 439 } 440 441 if (!someComplete) { 442 return; 443 } 444 445 *isEmpty = false; 446 outTN_.exportInto(out); 447 for (LCovSource* sc : sources_) { 448 // Only write if everything got recorded. 449 if (sc->isComplete()) { 450 sc->exportInto(out); 451 } 452 } 453 } 454 455 void LCovRealm::writeRealmName(JS::Realm* realm) { 456 JSContext* cx = TlsContext.get(); 457 458 // lcov trace files are starting with an optional test case name, that we 459 // recycle to be a realm name. 460 // 461 // Note: The test case name has some constraint in terms of valid character, 462 // thus we escape invalid chracters with a "_" symbol in front of its 463 // hexadecimal code. 464 outTN_.put("TN:"); 465 if (cx->runtime()->realmNameCallback) { 466 char name[1024]; 467 { 468 // Hazard analysis cannot tell that the callback does not GC. 469 JS::AutoSuppressGCAnalysis nogc; 470 (*cx->runtime()->realmNameCallback)(cx, realm, name, sizeof(name), nogc); 471 } 472 for (char* s = name; s < name + sizeof(name) && *s; s++) { 473 if (('a' <= *s && *s <= 'z') || ('A' <= *s && *s <= 'Z') || 474 ('0' <= *s && *s <= '9')) { 475 outTN_.put(s, 1); 476 continue; 477 } 478 outTN_.printf("_%p", (void*)size_t(*s)); 479 } 480 outTN_.put("\n", 1); 481 } else { 482 outTN_.printf("Realm_%p%p\n", (void*)size_t('_'), realm); 483 } 484 } 485 486 const char* LCovRealm::getScriptName(JSScript* script) { 487 JSFunction* fun = script->function(); 488 if (fun && fun->fullDisplayAtom()) { 489 JSAtom* atom = fun->fullDisplayAtom(); 490 size_t lenWithNull = js::PutEscapedString(nullptr, 0, atom, 0) + 1; 491 char* name = alloc_.newArray<char>(lenWithNull); 492 if (name) { 493 js::PutEscapedString(name, lenWithNull, atom, 0); 494 } 495 return name; 496 } 497 return "top-level"; 498 } 499 500 bool gLCovIsEnabled = false; 501 502 void InitLCov() { 503 const char* outDir = getenv("JS_CODE_COVERAGE_OUTPUT_DIR"); 504 if (outDir && *outDir != 0) { 505 EnableLCov(); 506 } 507 } 508 509 void EnableLCov() { 510 MOZ_ASSERT(!JSRuntime::hasLiveRuntimes(), 511 "EnableLCov must not be called after creating a runtime!"); 512 gLCovIsEnabled = true; 513 } 514 515 LCovRuntime::LCovRuntime() : pid_(getpid()), isEmpty_(true) {} 516 517 LCovRuntime::~LCovRuntime() { 518 if (out_.isInitialized()) { 519 finishFile(); 520 } 521 } 522 523 bool LCovRuntime::fillWithFilename(char* name, size_t length) { 524 const char* outDir = getenv("JS_CODE_COVERAGE_OUTPUT_DIR"); 525 if (!outDir || *outDir == 0) { 526 return false; 527 } 528 529 int64_t timestamp = static_cast<double>(PRMJ_Now()) / PRMJ_USEC_PER_SEC; 530 static mozilla::Atomic<size_t> globalRuntimeId(0); 531 size_t rid = globalRuntimeId++; 532 533 int len = snprintf(name, length, "%s/%" PRId64 "-%" PRIu32 "-%zu.info", 534 outDir, timestamp, pid_, rid); 535 if (len < 0 || size_t(len) >= length) { 536 fprintf(stderr, 537 "Warning: LCovRuntime::init: Cannot serialize file name.\n"); 538 return false; 539 } 540 541 return true; 542 } 543 544 void LCovRuntime::init() { 545 char name[1024]; 546 if (!fillWithFilename(name, sizeof(name))) { 547 return; 548 } 549 550 // If we cannot open the file, report a warning. 551 if (!out_.init(name)) { 552 fprintf(stderr, 553 "Warning: LCovRuntime::init: Cannot open file named '%s'.\n", name); 554 } 555 isEmpty_ = true; 556 } 557 558 void LCovRuntime::finishFile() { 559 MOZ_ASSERT(out_.isInitialized()); 560 out_.finish(); 561 562 if (isEmpty_) { 563 char name[1024]; 564 if (!fillWithFilename(name, sizeof(name))) { 565 return; 566 } 567 remove(name); 568 } 569 } 570 571 void LCovRuntime::writeLCovResult(LCovRealm& realm) { 572 if (!out_.isInitialized()) { 573 init(); 574 if (!out_.isInitialized()) { 575 return; 576 } 577 } 578 579 uint32_t p = getpid(); 580 if (pid_ != p) { 581 pid_ = p; 582 finishFile(); 583 init(); 584 if (!out_.isInitialized()) { 585 return; 586 } 587 } 588 589 realm.exportInto(out_, &isEmpty_); 590 out_.flush(); 591 finishFile(); 592 } 593 594 bool InitScriptCoverage(JSContext* cx, JSScript* script) { 595 MOZ_ASSERT(IsLCovEnabled()); 596 MOZ_ASSERT(script->hasBytecode(), 597 "Only initialize coverage data for fully initialized scripts."); 598 599 const char* filename = script->filename(); 600 if (!filename) { 601 return true; 602 } 603 604 // Create LCovRealm if necessary. 605 LCovRealm* lcovRealm = script->realm()->lcovRealm(); 606 if (!lcovRealm) { 607 ReportOutOfMemory(cx); 608 return false; 609 } 610 611 // Create LCovSource if necessary. 612 LCovSource* source = lcovRealm->lookupOrAdd(filename); 613 if (!source) { 614 ReportOutOfMemory(cx); 615 return false; 616 } 617 618 // Computed the formated script name. 619 const char* scriptName = lcovRealm->getScriptName(script); 620 if (!scriptName) { 621 ReportOutOfMemory(cx); 622 return false; 623 } 624 625 // Create Zone::scriptLCovMap if necessary. 626 JS::Zone* zone = script->zone(); 627 if (!zone->scriptLCovMap) { 628 zone->scriptLCovMap = cx->make_unique<ScriptLCovMap>(); 629 } 630 if (!zone->scriptLCovMap) { 631 return false; 632 } 633 634 MOZ_ASSERT(script->hasBytecode()); 635 636 // Save source in map for when we collect coverage. 637 if (!zone->scriptLCovMap->putNew(script, 638 std::make_tuple(source, scriptName))) { 639 ReportOutOfMemory(cx); 640 return false; 641 } 642 643 return true; 644 } 645 646 bool CollectScriptCoverage(JSScript* script, bool finalizing) { 647 MOZ_ASSERT(IsLCovEnabled()); 648 649 ScriptLCovMap* map = script->zone()->scriptLCovMap.get(); 650 if (!map) { 651 return false; 652 } 653 654 auto p = map->lookup(script); 655 if (!p.found()) { 656 return false; 657 } 658 659 auto [source, scriptName] = p->value(); 660 661 if (script->hasBytecode()) { 662 source->writeScript(script, scriptName); 663 } 664 665 if (finalizing) { 666 map->remove(p); 667 } 668 669 // Propagate the failure in case caller wants to terminate early. 670 return !source->hadOutOfMemory(); 671 } 672 673 } // namespace coverage 674 } // namespace js