[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:
parent
63b980f996
commit
058d7ab7f4
2
BUILD.gn
2
BUILD.gn
@ -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
144
src/debug/debug-coverage.cc
Normal 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
|
39
src/debug/debug-coverage.h
Normal file
39
src/debug/debug-coverage.h
Normal 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_
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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()");
|
109
test/mjsunit/code-coverage-ad-hoc.js
Normal file
109
test/mjsunit/code-coverage-ad-hoc.js
Normal 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]]
|
||||
`
|
||||
);
|
@ -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()); });
|
Loading…
Reference in New Issue
Block a user