Line data Source code
1 : /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
2 : * vim: set ts=8 sts=4 et sw=4 tw=99:
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 : #ifdef XP_WIN
14 : #include <process.h>
15 : #define getpid _getpid
16 : #else
17 : #include <unistd.h>
18 : #endif
19 :
20 : #include "jscompartment.h"
21 : #include "jsopcode.h"
22 : #include "jsprf.h"
23 : #include "jsscript.h"
24 :
25 : #include "vm/Runtime.h"
26 : #include "vm/Time.h"
27 :
28 : // This file contains a few functions which are used to produce files understood
29 : // by lcov tools. A detailed description of the format is available in the man
30 : // page for "geninfo" [1]. To make it short, the following paraphrases what is
31 : // commented in the man page by using curly braces prefixed by for-each to
32 : // express repeated patterns.
33 : //
34 : // TN:<compartment name>
35 : // for-each <source file> {
36 : // SN:<filename>
37 : // for-each <script> {
38 : // FN:<line>,<name>
39 : // }
40 : // for-each <script> {
41 : // FNDA:<hits>,<name>
42 : // }
43 : // FNF:<number of scripts>
44 : // FNH:<sum of scripts hits>
45 : // for-each <script> {
46 : // for-each <branch> {
47 : // BRDA:<line>,<block id>,<target id>,<taken>
48 : // }
49 : // }
50 : // BRF:<number of branches>
51 : // BRH:<sum of branches hits>
52 : // for-each <script> {
53 : // for-each <line> {
54 : // DA:<line>,<hits>
55 : // }
56 : // }
57 : // LF:<number of lines>
58 : // LH:<sum of lines hits>
59 : // }
60 : //
61 : // [1] http://ltp.sourceforge.net/coverage/lcov/geninfo.1.php
62 : //
63 : namespace js {
64 : namespace coverage {
65 :
66 0 : LCovSource::LCovSource(LifoAlloc* alloc, const char* name)
67 : : name_(name),
68 : outFN_(alloc),
69 : outFNDA_(alloc),
70 : numFunctionsFound_(0),
71 : numFunctionsHit_(0),
72 : outBRDA_(alloc),
73 : numBranchesFound_(0),
74 : numBranchesHit_(0),
75 : outDA_(alloc),
76 : numLinesInstrumented_(0),
77 : numLinesHit_(0),
78 0 : hasTopLevelScript_(false)
79 : {
80 0 : }
81 :
82 0 : LCovSource::LCovSource(LCovSource&& src)
83 0 : : name_(src.name_),
84 : outFN_(src.outFN_),
85 : outFNDA_(src.outFNDA_),
86 0 : numFunctionsFound_(src.numFunctionsFound_),
87 0 : numFunctionsHit_(src.numFunctionsHit_),
88 : outBRDA_(src.outBRDA_),
89 0 : numBranchesFound_(src.numBranchesFound_),
90 0 : numBranchesHit_(src.numBranchesHit_),
91 : outDA_(src.outDA_),
92 0 : numLinesInstrumented_(src.numLinesInstrumented_),
93 0 : numLinesHit_(src.numLinesHit_),
94 0 : hasTopLevelScript_(src.hasTopLevelScript_)
95 : {
96 0 : src.name_ = nullptr;
97 0 : }
98 :
99 0 : LCovSource::~LCovSource()
100 : {
101 0 : js_delete(name_);
102 0 : }
103 :
104 : void
105 0 : LCovSource::exportInto(GenericPrinter& out) const
106 : {
107 : // Only write if everything got recorded.
108 0 : if (!hasTopLevelScript_)
109 0 : return;
110 :
111 0 : out.printf("SF:%s\n", name_);
112 :
113 0 : outFN_.exportInto(out);
114 0 : outFNDA_.exportInto(out);
115 0 : out.printf("FNF:%" PRIuSIZE "\n", numFunctionsFound_);
116 0 : out.printf("FNH:%" PRIuSIZE "\n", numFunctionsHit_);
117 :
118 0 : outBRDA_.exportInto(out);
119 0 : out.printf("BRF:%" PRIuSIZE "\n", numBranchesFound_);
120 0 : out.printf("BRH:%" PRIuSIZE "\n", numBranchesHit_);
121 :
122 0 : outDA_.exportInto(out);
123 0 : out.printf("LF:%" PRIuSIZE "\n", numLinesInstrumented_);
124 0 : out.printf("LH:%" PRIuSIZE "\n", numLinesHit_);
125 :
126 0 : out.put("end_of_record\n");
127 : }
128 :
129 : bool
130 0 : LCovSource::writeScriptName(LSprinter& out, JSScript* script)
131 : {
132 0 : JSFunction* fun = script->functionNonDelazifying();
133 0 : if (fun && fun->displayAtom())
134 0 : return EscapedStringPrinter(out, fun->displayAtom(), 0);
135 0 : out.printf("top-level");
136 0 : return true;
137 : }
138 :
139 : bool
140 0 : LCovSource::writeScript(JSScript* script)
141 : {
142 0 : numFunctionsFound_++;
143 0 : outFN_.printf("FN:%" PRIuSIZE ",", script->lineno());
144 0 : if (!writeScriptName(outFN_, script))
145 0 : return false;
146 0 : outFN_.put("\n", 1);
147 :
148 0 : uint64_t hits = 0;
149 0 : ScriptCounts* sc = nullptr;
150 0 : if (script->hasScriptCounts()) {
151 0 : sc = &script->getScriptCounts();
152 0 : numFunctionsHit_++;
153 0 : const PCCounts* counts = sc->maybeGetPCCounts(script->pcToOffset(script->main()));
154 0 : outFNDA_.printf("FNDA:%" PRIu64 ",", counts->numExec());
155 0 : if (!writeScriptName(outFNDA_, script))
156 0 : return false;
157 0 : outFNDA_.put("\n", 1);
158 :
159 : // Set the hit count of the pre-main code to 1, if the function ever got
160 : // visited.
161 0 : hits = 1;
162 : }
163 :
164 0 : jsbytecode* snpc = script->code();
165 0 : jssrcnote* sn = script->notes();
166 0 : if (!SN_IS_TERMINATOR(sn))
167 0 : snpc += SN_DELTA(sn);
168 :
169 0 : size_t lineno = script->lineno();
170 0 : jsbytecode* end = script->codeEnd();
171 0 : size_t branchId = 0;
172 0 : size_t tableswitchExitOffset = 0;
173 0 : for (jsbytecode* pc = script->code(); pc != end; pc = GetNextPc(pc)) {
174 0 : JSOp op = JSOp(*pc);
175 0 : bool jump = IsJumpOpcode(op) || op == JSOP_TABLESWITCH;
176 0 : bool fallsthrough = BytecodeFallsThrough(op) && op != JSOP_GOSUB;
177 :
178 : // If the current script & pc has a hit-count report, then update the
179 : // current number of hits.
180 0 : if (sc) {
181 0 : const PCCounts* counts = sc->maybeGetPCCounts(script->pcToOffset(pc));
182 0 : if (counts)
183 0 : hits = counts->numExec();
184 : }
185 :
186 : // If we have additional source notes, walk all the source notes of the
187 : // current pc.
188 0 : if (snpc <= pc) {
189 0 : size_t oldLine = lineno;
190 0 : while (!SN_IS_TERMINATOR(sn) && snpc <= pc) {
191 0 : SrcNoteType type = (SrcNoteType) SN_TYPE(sn);
192 0 : if (type == SRC_SETLINE)
193 0 : lineno = size_t(GetSrcNoteOffset(sn, 0));
194 0 : else if (type == SRC_NEWLINE)
195 0 : lineno++;
196 0 : else if (type == SRC_TABLESWITCH)
197 0 : tableswitchExitOffset = GetSrcNoteOffset(sn, 0);
198 :
199 0 : sn = SN_NEXT(sn);
200 0 : snpc += SN_DELTA(sn);
201 : }
202 :
203 0 : if (oldLine != lineno && fallsthrough) {
204 0 : outDA_.printf("DA:%" PRIuSIZE ",%" PRIu64 "\n", lineno, hits);
205 :
206 : // Count the number of lines instrumented & hit.
207 0 : numLinesInstrumented_++;
208 0 : if (hits)
209 0 : numLinesHit_++;
210 : }
211 : }
212 :
213 : // If the current instruction has thrown, then decrement the hit counts
214 : // with the number of throws.
215 0 : if (sc) {
216 0 : const PCCounts* counts = sc->maybeGetThrowCounts(script->pcToOffset(pc));
217 0 : if (counts)
218 0 : hits -= counts->numExec();
219 : }
220 :
221 : // If the current pc corresponds to a conditional jump instruction, then reports
222 : // branch hits.
223 0 : if (jump && fallsthrough) {
224 0 : jsbytecode* fallthroughTarget = GetNextPc(pc);
225 0 : uint64_t fallthroughHits = 0;
226 0 : if (sc) {
227 0 : const PCCounts* counts = sc->maybeGetPCCounts(script->pcToOffset(fallthroughTarget));
228 0 : if (counts)
229 0 : fallthroughHits = counts->numExec();
230 : }
231 :
232 0 : uint64_t taken = hits - fallthroughHits;
233 0 : outBRDA_.printf("BRDA:%" PRIuSIZE ",%" PRIuSIZE ",0,", lineno, branchId);
234 0 : if (taken)
235 0 : outBRDA_.printf("%" PRIu64 "\n", taken);
236 : else
237 0 : outBRDA_.put("-\n", 2);
238 :
239 0 : outBRDA_.printf("BRDA:%" PRIuSIZE ",%" PRIuSIZE ",1,", lineno, branchId);
240 0 : if (fallthroughHits)
241 0 : outBRDA_.printf("%" PRIu64 "\n", fallthroughHits);
242 : else
243 0 : outBRDA_.put("-\n", 2);
244 :
245 : // Count the number of branches, and the number of branches hit.
246 0 : numBranchesFound_ += 2;
247 0 : if (hits)
248 0 : numBranchesHit_ += !!taken + !!fallthroughHits;
249 0 : branchId++;
250 : }
251 :
252 : // If the current pc corresponds to a pre-computed switch case, then
253 : // reports branch hits for each case statement.
254 0 : if (jump && op == JSOP_TABLESWITCH) {
255 0 : MOZ_ASSERT(tableswitchExitOffset != 0);
256 :
257 : // Get the default and exit pc
258 0 : jsbytecode* exitpc = pc + tableswitchExitOffset;
259 0 : jsbytecode* defaultpc = pc + GET_JUMP_OFFSET(pc);
260 0 : MOZ_ASSERT(defaultpc > pc && defaultpc <= exitpc);
261 :
262 : // Get the low and high from the tableswitch
263 0 : int32_t low = GET_JUMP_OFFSET(pc + JUMP_OFFSET_LEN * 1);
264 0 : int32_t high = GET_JUMP_OFFSET(pc + JUMP_OFFSET_LEN * 2);
265 0 : MOZ_ASSERT(high - low + 1 >= 0);
266 0 : size_t numCases = high - low + 1;
267 0 : jsbytecode* jumpTable = pc + JUMP_OFFSET_LEN * 3;
268 :
269 0 : jsbytecode* firstcasepc = exitpc;
270 0 : for (size_t j = 0; j < numCases; j++) {
271 0 : jsbytecode* testpc = pc + GET_JUMP_OFFSET(jumpTable + JUMP_OFFSET_LEN * j);
272 0 : if (testpc < firstcasepc)
273 0 : firstcasepc = testpc;
274 : }
275 :
276 : // Count the number of hits of the default branch, by subtracting
277 : // the number of hits of each cases.
278 0 : uint64_t defaultHits = hits;
279 :
280 : // Count the number of hits of the previous case entry.
281 0 : uint64_t fallsThroughHits = 0;
282 :
283 : // Record branches for each cases.
284 0 : size_t caseId = 0;
285 0 : for (size_t i = 0; i < numCases; i++) {
286 0 : jsbytecode* casepc = pc + GET_JUMP_OFFSET(jumpTable + JUMP_OFFSET_LEN * i);
287 : // The case is not present, and jumps to the default pc if used.
288 0 : if (casepc == pc)
289 0 : continue;
290 :
291 : // PCs might not be in increasing order of case indexes.
292 0 : jsbytecode* lastcasepc = firstcasepc - 1;
293 0 : for (size_t j = 0; j < numCases; j++) {
294 0 : jsbytecode* testpc = pc + GET_JUMP_OFFSET(jumpTable + JUMP_OFFSET_LEN * j);
295 0 : if (lastcasepc < testpc && (testpc < casepc || (j < i && testpc == casepc)))
296 0 : lastcasepc = testpc;
297 : }
298 :
299 0 : if (casepc != lastcasepc) {
300 : // Case (i + low)
301 0 : uint64_t caseHits = 0;
302 0 : if (sc) {
303 0 : const PCCounts* counts = sc->maybeGetPCCounts(script->pcToOffset(casepc));
304 0 : if (counts)
305 0 : caseHits = counts->numExec();
306 :
307 : // Remove fallthrough.
308 0 : fallsThroughHits = 0;
309 0 : if (casepc != firstcasepc) {
310 0 : jsbytecode* endpc = lastcasepc;
311 0 : while (GetNextPc(endpc) < casepc)
312 0 : endpc = GetNextPc(endpc);
313 :
314 0 : if (BytecodeFallsThrough(JSOp(*endpc)))
315 0 : fallsThroughHits = script->getHitCount(endpc);
316 : }
317 :
318 0 : caseHits -= fallsThroughHits;
319 : }
320 :
321 0 : outBRDA_.printf("BRDA:%" PRIuSIZE ",%" PRIuSIZE ",%" PRIuSIZE ",",
322 0 : lineno, branchId, caseId);
323 0 : if (caseHits)
324 0 : outBRDA_.printf("%" PRIu64 "\n", caseHits);
325 : else
326 0 : outBRDA_.put("-\n", 2);
327 :
328 0 : numBranchesFound_++;
329 0 : numBranchesHit_ += !!caseHits;
330 0 : defaultHits -= caseHits;
331 0 : caseId++;
332 : }
333 : }
334 :
335 : // Compute the number of hits of the default branch, if it has its
336 : // own case clause.
337 0 : bool defaultHasOwnClause = true;
338 0 : if (defaultpc != exitpc) {
339 0 : defaultHits = 0;
340 :
341 : // Look for the last case entry before the default pc.
342 0 : jsbytecode* lastcasepc = firstcasepc - 1;
343 0 : for (size_t j = 0; j < numCases; j++) {
344 0 : jsbytecode* testpc = pc + GET_JUMP_OFFSET(jumpTable + JUMP_OFFSET_LEN * j);
345 0 : if (lastcasepc < testpc && testpc <= defaultpc)
346 0 : lastcasepc = testpc;
347 : }
348 :
349 0 : if (lastcasepc == defaultpc)
350 0 : defaultHasOwnClause = false;
351 :
352 : // Look if the last case entry fallthrough to the default case,
353 : // in which case we have to remove the number of fallthrough
354 : // hits out of the default case hits.
355 0 : if (sc && lastcasepc != pc) {
356 0 : jsbytecode* endpc = lastcasepc;
357 0 : while (GetNextPc(endpc) < defaultpc)
358 0 : endpc = GetNextPc(endpc);
359 :
360 0 : if (BytecodeFallsThrough(JSOp(*endpc)))
361 0 : fallsThroughHits = script->getHitCount(endpc);
362 : }
363 :
364 0 : if (sc) {
365 0 : const PCCounts* counts = sc->maybeGetPCCounts(script->pcToOffset(defaultpc));
366 0 : if (counts)
367 0 : defaultHits = counts->numExec();
368 : }
369 0 : defaultHits -= fallsThroughHits;
370 : }
371 :
372 0 : if (defaultHasOwnClause) {
373 0 : outBRDA_.printf("BRDA:%" PRIuSIZE ",%" PRIuSIZE ",%" PRIuSIZE ",",
374 0 : lineno, branchId, caseId);
375 0 : if (defaultHits)
376 0 : outBRDA_.printf("%" PRIu64 "\n", defaultHits);
377 : else
378 0 : outBRDA_.put("-\n", 2);
379 0 : numBranchesFound_++;
380 0 : numBranchesHit_ += !!defaultHits;
381 : }
382 :
383 : // Increment the branch identifier, and go to the next instruction.
384 0 : branchId++;
385 0 : tableswitchExitOffset = 0;
386 : }
387 : }
388 :
389 : // Report any new OOM.
390 0 : if (outFN_.hadOutOfMemory() ||
391 0 : outFNDA_.hadOutOfMemory() ||
392 0 : outBRDA_.hadOutOfMemory() ||
393 0 : outDA_.hadOutOfMemory())
394 : {
395 0 : return false;
396 : }
397 :
398 : // If this script is the top-level script, then record it such that we can
399 : // assume that the code coverage report is complete, as this script has
400 : // references on all inner scripts.
401 0 : if (script->isTopLevel())
402 0 : hasTopLevelScript_ = true;
403 :
404 0 : return true;
405 : }
406 :
407 315 : LCovCompartment::LCovCompartment()
408 : : alloc_(4096),
409 : outTN_(&alloc_),
410 315 : sources_(nullptr)
411 : {
412 315 : MOZ_ASSERT(alloc_.isEmpty());
413 315 : }
414 :
415 : void
416 0 : LCovCompartment::collectCodeCoverageInfo(JSCompartment* comp, JSScript* script, const char* name)
417 : {
418 : // Skip any operation if we already some out-of memory issues.
419 0 : if (outTN_.hadOutOfMemory())
420 0 : return;
421 :
422 0 : if (!script->code())
423 0 : return;
424 :
425 : // Get the existing source LCov summary, or create a new one.
426 0 : LCovSource* source = lookupOrAdd(comp, name);
427 0 : if (!source)
428 0 : return;
429 :
430 : // Write code coverage data into the LCovSource.
431 0 : if (!source->writeScript(script)) {
432 0 : outTN_.reportOutOfMemory();
433 0 : return;
434 : }
435 : }
436 :
437 : LCovSource*
438 0 : LCovCompartment::lookupOrAdd(JSCompartment* comp, const char* name)
439 : {
440 : // On the first call, write the compartment name, and allocate a LCovSource
441 : // vector in the LifoAlloc.
442 0 : if (!sources_) {
443 0 : if (!writeCompartmentName(comp))
444 0 : return nullptr;
445 :
446 0 : LCovSourceVector* raw = alloc_.pod_malloc<LCovSourceVector>();
447 0 : if (!raw) {
448 0 : outTN_.reportOutOfMemory();
449 0 : return nullptr;
450 : }
451 :
452 0 : sources_ = new(raw) LCovSourceVector(alloc_);
453 : } else {
454 : // Find the first matching source.
455 0 : for (LCovSource& source : *sources_) {
456 0 : if (source.match(name))
457 0 : return &source;
458 : }
459 : }
460 :
461 0 : char* source_name = js_strdup(name);
462 0 : if (!source_name) {
463 0 : outTN_.reportOutOfMemory();
464 0 : return nullptr;
465 : }
466 :
467 : // Allocate a new LCovSource for the current top-level.
468 0 : if (!sources_->append(Move(LCovSource(&alloc_, source_name)))) {
469 0 : outTN_.reportOutOfMemory();
470 0 : return nullptr;
471 : }
472 :
473 0 : return &sources_->back();
474 : }
475 :
476 : void
477 0 : LCovCompartment::exportInto(GenericPrinter& out, bool* isEmpty) const
478 : {
479 0 : if (!sources_ || outTN_.hadOutOfMemory())
480 0 : return;
481 :
482 : // If we only have cloned function, then do not serialize anything.
483 0 : bool someComplete = false;
484 0 : for (const LCovSource& sc : *sources_) {
485 0 : if (sc.isComplete()) {
486 0 : someComplete = true;
487 0 : break;
488 : };
489 : }
490 :
491 0 : if (!someComplete)
492 0 : return;
493 :
494 0 : *isEmpty = false;
495 0 : outTN_.exportInto(out);
496 0 : for (const LCovSource& sc : *sources_) {
497 0 : if (sc.isComplete())
498 0 : sc.exportInto(out);
499 : }
500 : }
501 :
502 : bool
503 0 : LCovCompartment::writeCompartmentName(JSCompartment* comp)
504 : {
505 0 : JSContext* cx = TlsContext.get();
506 :
507 : // lcov trace files are starting with an optional test case name, that we
508 : // recycle to be a compartment name.
509 : //
510 : // Note: The test case name has some constraint in terms of valid character,
511 : // thus we escape invalid chracters with a "_" symbol in front of its
512 : // hexadecimal code.
513 0 : outTN_.put("TN:");
514 0 : if (cx->runtime()->compartmentNameCallback) {
515 : char name[1024];
516 : {
517 : // Hazard analysis cannot tell that the callback does not GC.
518 0 : JS::AutoSuppressGCAnalysis nogc;
519 0 : (*cx->runtime()->compartmentNameCallback)(cx, comp, name, sizeof(name));
520 : }
521 0 : for (char *s = name; s < name + sizeof(name) && *s; s++) {
522 0 : if (('a' <= *s && *s <= 'z') ||
523 0 : ('A' <= *s && *s <= 'Z') ||
524 0 : ('0' <= *s && *s <= '9'))
525 : {
526 0 : outTN_.put(s, 1);
527 0 : continue;
528 : }
529 0 : outTN_.printf("_%p", (void*) size_t(*s));
530 : }
531 0 : outTN_.put("\n", 1);
532 : } else {
533 0 : outTN_.printf("Compartment_%p%p\n", (void*) size_t('_'), comp);
534 : }
535 :
536 0 : return !outTN_.hadOutOfMemory();
537 : }
538 :
539 4 : LCovRuntime::LCovRuntime()
540 : : out_(),
541 4 : pid_(getpid()),
542 8 : isEmpty_(false)
543 : {
544 4 : }
545 :
546 0 : LCovRuntime::~LCovRuntime()
547 : {
548 0 : if (out_.isInitialized())
549 0 : finishFile();
550 0 : }
551 :
552 : bool
553 4 : LCovRuntime::fillWithFilename(char *name, size_t length)
554 : {
555 4 : const char* outDir = getenv("JS_CODE_COVERAGE_OUTPUT_DIR");
556 4 : if (!outDir || *outDir == 0)
557 4 : return false;
558 :
559 0 : int64_t timestamp = static_cast<double>(PRMJ_Now()) / PRMJ_USEC_PER_SEC;
560 : static mozilla::Atomic<size_t> globalRuntimeId(0);
561 0 : size_t rid = globalRuntimeId++;
562 :
563 0 : int len = snprintf(name, length, "%s/%" PRId64 "-%" PRIu32 "-%" PRIuSIZE ".info",
564 0 : outDir, timestamp, pid_, rid);
565 0 : if (len < 0 || size_t(len) >= length) {
566 0 : fprintf(stderr, "Warning: LCovRuntime::init: Cannot serialize file name.");
567 0 : return false;
568 : }
569 :
570 0 : return true;
571 : }
572 :
573 : void
574 4 : LCovRuntime::init()
575 : {
576 : char name[1024];
577 4 : if (!fillWithFilename(name, sizeof(name)))
578 4 : return;
579 :
580 : // If we cannot open the file, report a warning.
581 0 : if (!out_.init(name))
582 0 : fprintf(stderr, "Warning: LCovRuntime::init: Cannot open file named '%s'.", name);
583 0 : isEmpty_ = true;
584 : }
585 :
586 : void
587 0 : LCovRuntime::finishFile()
588 : {
589 0 : MOZ_ASSERT(out_.isInitialized());
590 0 : out_.finish();
591 :
592 0 : if (isEmpty_) {
593 : char name[1024];
594 0 : if (!fillWithFilename(name, sizeof(name)))
595 0 : return;
596 0 : remove(name);
597 : }
598 : }
599 :
600 : void
601 0 : LCovRuntime::writeLCovResult(LCovCompartment& comp)
602 : {
603 0 : if (!out_.isInitialized())
604 0 : return;
605 :
606 0 : uint32_t p = getpid();
607 0 : if (pid_ != p) {
608 0 : pid_ = p;
609 0 : finishFile();
610 0 : init();
611 0 : if (!out_.isInitialized())
612 0 : return;
613 : }
614 :
615 0 : comp.exportInto(out_, &isEmpty_);
616 0 : out_.flush();
617 : }
618 :
619 : } // namespace coverage
620 : } // namespace js
|