[debugger] implement per-function code coverage.

Collect code coverage from the available invocation counts.
The granularity is at function level, and invocation counts may
be lost to GC.

Coverage::Collect returns a std::vector of Coverage::ScriptData.
Each ScriptData contains a script ID and a std::vector of
Coverage::RangeEntry.
Each RangeEntry consists of a end position and the invocation
count. The start position is implicit from the end position of
the previous RangeEntry, or 0 if it's the first RangeEntry.

R=jgruber@chromium.org
BUG=v8:5808

Review-Url: https://codereview.chromium.org/2689493002
Cr-Commit-Position: refs/heads/master@{#43072}
This commit is contained in:
yangguo 2017-02-09 11:00:49 -08:00 committed by Commit bot
parent 63b980f996
commit 058d7ab7f4
9 changed files with 341 additions and 24 deletions

View File

@ -1333,6 +1333,8 @@ v8_source_set("v8_base") {
"src/dateparser-inl.h",
"src/dateparser.cc",
"src/dateparser.h",
"src/debug/debug-coverage.cc",
"src/debug/debug-coverage.h",
"src/debug/debug-evaluate.cc",
"src/debug/debug-evaluate.h",
"src/debug/debug-frames.cc",

144
src/debug/debug-coverage.cc Normal file
View File

@ -0,0 +1,144 @@
// Copyright 2017 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/debug/debug-coverage.h"
#include "src/base/hashmap.h"
#include "src/objects-inl.h"
#include "src/objects.h"
namespace v8 {
namespace internal {
class SharedToCounterMap
: public base::TemplateHashMapImpl<SharedFunctionInfo*, uint32_t,
base::KeyEqualityMatcher<void*>,
base::DefaultAllocationPolicy> {
public:
typedef base::TemplateHashMapEntry<SharedFunctionInfo*, uint32_t> Entry;
inline void Add(SharedFunctionInfo* key, uint32_t count) {
Entry* entry = LookupOrInsert(key, Hash(key), []() { return 0; });
uint32_t old_count = entry->value;
if (UINT32_MAX - count < old_count) {
entry->value = UINT32_MAX;
} else {
entry->value = old_count + count;
}
}
inline uint32_t Get(SharedFunctionInfo* key) {
Entry* entry = Lookup(key, Hash(key));
if (entry == nullptr) return 0;
return entry->value;
}
private:
static uint32_t Hash(SharedFunctionInfo* key) {
return static_cast<uint32_t>(reinterpret_cast<intptr_t>(key));
}
};
class ScriptDataBuilder {
public:
void Add(int end_position, uint32_t count) {
DCHECK(entries_.empty() || entries_.back().end_position <= end_position);
if (entries_.empty()) {
if (end_position > 0) entries_.push_back({end_position, count});
} else if (entries_.back().count == count) {
// Extend last range.
entries_.back().end_position = end_position;
} else if (entries_.back().end_position < end_position) {
// Add new range.
entries_.push_back({end_position, count});
}
}
std::vector<Coverage::RangeEntry> Finish() {
std::vector<Coverage::RangeEntry> result;
std::swap(result, entries_);
return result;
}
private:
std::vector<Coverage::RangeEntry> entries_;
};
std::vector<Coverage::ScriptData> Coverage::Collect(Isolate* isolate) {
SharedToCounterMap counter_map;
// Iterate the heap to find all feedback vectors and accumulate the
// invocation counts into the map for each shared function info.
HeapIterator heap_iterator(isolate->heap());
HeapObject* current_obj;
while ((current_obj = heap_iterator.next())) {
if (!current_obj->IsFeedbackVector()) continue;
FeedbackVector* vector = FeedbackVector::cast(current_obj);
SharedFunctionInfo* shared = vector->shared_function_info();
if (!shared->IsSubjectToDebugging()) continue;
uint32_t count = static_cast<uint32_t>(vector->invocation_count());
counter_map.Add(shared, count);
}
// Make sure entries in the counter map is not invalidated by GC.
DisallowHeapAllocation no_gc;
// Stack to track nested functions.
struct FunctionNode {
FunctionNode(int s, int e, uint32_t c) : start(s), end(e), count(c) {}
int start;
int end;
uint32_t count;
};
std::vector<FunctionNode> stack;
// Iterate shared function infos of every script and build a mapping
// between source ranges and invocation counts.
std::vector<Coverage::ScriptData> result;
Script::Iterator scripts(isolate);
while (Script* script = scripts.Next()) {
// Dismiss non-user scripts.
if (script->type() != Script::TYPE_NORMAL) continue;
DCHECK(stack.empty());
int script_end = String::cast(script->source())->length();
// If not rooted, the top-level function is likely no longer alive. Set the
// outer-most count to 1 to indicate that the script has run at least once.
stack.push_back({0, script_end, 1});
ScriptDataBuilder builder;
// Iterate through the list of shared function infos, reconstruct the
// nesting, and compute the ranges covering different invocation counts.
HandleScope scope(isolate);
SharedFunctionInfo::ScriptIterator infos(Handle<Script>(script, isolate));
while (SharedFunctionInfo* info = infos.Next()) {
int start = info->function_token_position();
if (start == kNoSourcePosition) start = info->start_position();
int end = info->end_position();
uint32_t count = counter_map.Get(info);
// The shared function infos are sorted by start.
DCHECK_LE(stack.back().start, start);
// If the start are the same, the outer function comes before the inner.
DCHECK(stack.back().start < start || stack.back().end >= end);
// Drop the stack to the outer function.
while (start > stack.back().end) {
// Write out rest of function being dropped.
builder.Add(stack.back().end, stack.back().count);
stack.pop_back();
}
// Write out outer function up to the start of new function.
builder.Add(start, stack.back().count);
// New nested function.
DCHECK_LE(end, stack.back().end);
stack.emplace_back(start, end, count);
}
// Drop the stack to the script level.
while (!stack.empty()) {
// Write out rest of function being dropped.
builder.Add(stack.back().end, stack.back().count);
stack.pop_back();
}
result.emplace_back(script->id(), builder.Finish());
}
return result;
}
} // namespace internal
} // namespace v8

View File

@ -0,0 +1,39 @@
// Copyright 2017 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef V8_DEBUG_DEBUG_COVERAGE_H_
#define V8_DEBUG_DEBUG_COVERAGE_H_
#include <vector>
#include "src/allocation.h"
#include "src/base/macros.h"
namespace v8 {
namespace internal {
// Forward declaration.
class Isolate;
class Coverage : public AllStatic {
public:
struct RangeEntry {
int end_position;
uint32_t count;
};
struct ScriptData {
ScriptData(int s, std::vector<RangeEntry> e)
: script_id(s), entries(std::move(e)) {}
int script_id;
std::vector<RangeEntry> entries;
};
static std::vector<ScriptData> Collect(Isolate* isolate);
};
} // namespace internal
} // namespace v8
#endif // V8_DEBUG_DEBUG_COVERAGE_H_

View File

@ -6,6 +6,7 @@
#include "src/arguments.h"
#include "src/compiler.h"
#include "src/debug/debug-coverage.h"
#include "src/debug/debug-evaluate.h"
#include "src/debug/debug-frames.h"
#include "src/debug/debug-scopes.h"
@ -1244,10 +1245,6 @@ RUNTIME_FUNCTION(Runtime_DebugGetLoadedScripts) {
HandleScope scope(isolate);
DCHECK_EQ(0, args.length());
// This runtime function is used by the debugger to determine whether the
// debugger is active or not. Hence we fail gracefully here and don't crash.
if (!isolate->debug()->is_active()) return isolate->ThrowIllegalOperation();
Handle<FixedArray> instances;
{
DebugScope debug_scope(isolate->debug());
@ -1896,5 +1893,47 @@ RUNTIME_FUNCTION(Runtime_DebugBreakInOptimizedCode) {
return NULL;
}
RUNTIME_FUNCTION(Runtime_DebugCollectCoverage) {
HandleScope scope(isolate);
// Collect coverage data.
std::vector<Coverage::ScriptData> scripts = Coverage::Collect(isolate);
Factory* factory = isolate->factory();
// Turn the returned data structure into JavaScript.
// Create an array of scripts.
int num_scripts = static_cast<int>(scripts.size());
// Prepare property keys.
Handle<FixedArray> scripts_array = factory->NewFixedArray(num_scripts);
Handle<String> id_string = factory->NewStringFromStaticChars("script_id");
Handle<String> entries_string = factory->NewStringFromStaticChars("entries");
Handle<String> end_string = factory->NewStringFromStaticChars("end_position");
Handle<String> count_string = factory->NewStringFromStaticChars("count");
for (int i = 0; i < num_scripts; i++) {
// Create an object for each script, containing the script id and entries.
const auto& script = scripts[i];
HandleScope inner_scope(isolate);
int num_entries = static_cast<int>(script.entries.size());
Handle<FixedArray> entries_array = factory->NewFixedArray(num_entries);
for (int j = 0; j < num_entries; j++) {
// Create an object for each entry, containing the end position and count.
const auto& entry = script.entries[j];
Handle<JSObject> entry_obj = factory->NewJSObjectWithNullProto();
JSObject::AddProperty(entry_obj, end_string,
factory->NewNumberFromInt(entry.end_position),
NONE);
JSObject::AddProperty(entry_obj, count_string,
factory->NewNumberFromUint(entry.count), NONE);
entries_array->set(j, *entry_obj);
}
Handle<JSObject> script_obj = factory->NewJSObjectWithNullProto();
JSObject::AddProperty(script_obj, id_string,
factory->NewNumberFromInt(script.script_id), NONE);
JSObject::AddProperty(
script_obj, entries_string,
factory->NewJSArrayWithElements(entries_array, FAST_ELEMENTS), NONE);
scripts_array->set(i, *script_obj);
}
return *factory->NewJSArrayWithElements(scripts_array, FAST_ELEMENTS);
}
} // namespace internal
} // namespace v8

View File

@ -202,7 +202,8 @@ namespace internal {
F(DebugAsyncEventEnqueueRecurring, 2, 1) \
F(DebugAsyncFunctionPromiseCreated, 1, 1) \
F(DebugIsActive, 0, 1) \
F(DebugBreakInOptimizedCode, 0, 1)
F(DebugBreakInOptimizedCode, 0, 1) \
F(DebugCollectCoverage, 0, 1)
#define FOR_EACH_INTRINSIC_ERROR(F) F(ErrorToString, 1, 1)

View File

@ -837,6 +837,8 @@
'dateparser-inl.h',
'dateparser.cc',
'dateparser.h',
'debug/debug-coverage.cc',
'debug/debug-coverage.h',
'debug/debug-evaluate.cc',
'debug/debug-evaluate.h',
'debug/debug-interface.h',

View File

@ -1,12 +0,0 @@
// Copyright 2015 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
Debug = debug.Debug;
Debug.disable();
assertThrows("Debug.scripts()");
Debug.enable();
assertDoesNotThrow("Debug.scripts()");

View File

@ -0,0 +1,109 @@
// Copyright 2017 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --allow-natives-syntax --no-always-opt
// Test code coverage without explicitly activating it upfront.
function GetCoverage(source) {
var scripts = %DebugGetLoadedScripts();
for (var script of scripts) {
if (script.source == source) {
var coverage = %DebugCollectCoverage();
for (var data of coverage) {
if (data.script_id == script.id) return data.entries;
}
}
}
return undefined;
}
function ApplyCoverageToSource(source, coverage) {
var result = "";
var cursor = 0;
for (var entry of coverage) {
var chunk = source.substring(cursor, entry.end_position);
cursor = entry.end_position;
result += `[${chunk}[${entry.count}]]`;
}
return result;
}
function TestCoverage(name, source, expectation) {
source = source.trim();
expectation = expectation.trim();
eval(source);
var coverage = GetCoverage(source);
var result = ApplyCoverageToSource(source, coverage);
print(result);
assertEquals(expectation, result, name + " failed");
}
TestCoverage(
"call simple function twice",
`
function f() {}
f();
f();
`,
`
[function f() {}[2]][
f();
f();[1]]
`
);
TestCoverage(
"call arrow function twice",
`
var f = () => 1;
f();
f();
`,
`
[var f = [1]][() => 1[2]][;
f();
f();[1]]
`
);
TestCoverage(
"call nested function",
`
function f() {
function g() {}
g();
g();
}
f();
f();
`,
`
[function f() {
[2]][function g() {}[4]][
g();
g();
}[2]][
f();
f();[1]]
`
);
TestCoverage(
"call recursive function",
`
function fib(x) {
if (x < 2) return 1;
return fib(x-1) + fib(x-2);
}
fib(5);
`,
`
[function fib(x) {
if (x < 2) return 1;
return fib(x-1) + fib(x-2);
}[15]][
fib(5);[1]]
`
);

View File

@ -1,7 +0,0 @@
// Copyright 2015 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --allow-natives-syntax
assertThrows(function() { JSON.stringify(%DebugGetLoadedScripts()); });