#include "src/debug/debug-coverage.h"
#include "src/ast/ast-source-ranges.h"
#include "src/base/hashmap.h"
#include "src/common/assert-scope.h"
#include "src/common/globals.h"
#include "src/debug/debug.h"
#include "src/deoptimizer/deoptimizer.h"
#include "src/execution/frames-inl.h"
#include "src/execution/isolate.h"
#include "src/objects/objects.h"
#if V8_ENABLE_WEBASSEMBLY
#include "src/wasm/names-provider.h"
#include "src/wasm/string-builder.h"
#include "src/wasm/wasm-code-coverage.h"
#endif
namespace v8 {
namespace internal {
class SharedToCounterMap
: public base::TemplateHashMapImpl<Tagged<SharedFunctionInfo>, uint32_t,
base::KeyEqualityMatcher<Tagged<Object>>,
base::DefaultAllocationPolicy> {
public:
using Entry =
base::TemplateHashMapEntry<Tagged<SharedFunctionInfo>, uint32_t>;
inline void Add(Tagged<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(Tagged<SharedFunctionInfo> key) {
Entry* entry = Lookup(key, Hash(key));
if (entry == nullptr) return 0;
return entry->value;
}
private:
static uint32_t Hash(Tagged<SharedFunctionInfo> key) {
return static_cast<uint32_t>(key.ptr());
}
DISALLOW_GARBAGE_COLLECTION(no_gc)
};
namespace {
int StartPosition(Tagged<SharedFunctionInfo> info) {
int start = info->function_token_position();
if (start == kNoSourcePosition) start = info->StartPosition();
return start;
}
bool CompareCoverageBlock(const CoverageBlock& a, const CoverageBlock& b) {
DCHECK_NE(kNoSourcePosition, a.start);
DCHECK_NE(kNoSourcePosition, b.start);
if (a.start == b.start) return a.end > b.end;
return a.start < b.start;
}
void SortBlockData(std::vector<CoverageBlock>& v) {
std::sort(v.begin(), v.end(), CompareCoverageBlock);
}
std::vector<CoverageBlock> GetSortedBlockData(
Isolate* isolate, Tagged<SharedFunctionInfo> shared) {
DCHECK(shared->HasCoverageInfo(isolate));
Tagged<CoverageInfo> coverage_info =
Cast<CoverageInfo>(shared->GetDebugInfo(isolate)->coverage_info());
std::vector<CoverageBlock> result;
if (coverage_info->slot_count() == 0) return result;
for (int i = 0; i < coverage_info->slot_count(); i++) {
const int start_pos = coverage_info->slots_start_source_position(i);
const int until_pos = coverage_info->slots_end_source_position(i);
const int count = coverage_info->slots_block_count(i);
DCHECK_NE(kNoSourcePosition, start_pos);
result.emplace_back(start_pos, until_pos, count);
}
SortBlockData(result);
return result;
}
class CoverageBlockIterator final {
public:
explicit CoverageBlockIterator(CoverageFunction* function)
: function_(function) {
DCHECK(std::is_sorted(function_->blocks.begin(), function_->blocks.end(),
CompareCoverageBlock));
}
~CoverageBlockIterator() {
Finalize();
DCHECK(std::is_sorted(function_->blocks.begin(), function_->blocks.end(),
CompareCoverageBlock));
}
bool HasNext() const {
return read_index_ + 1 < static_cast<int>(function_->blocks.size());
}
bool Next() {
if (!HasNext()) {
if (!ended_) MaybeWriteCurrent();
ended_ = true;
return false;
}
MaybeWriteCurrent();
if (read_index_ == -1) {
nesting_stack_.emplace_back(function_->start, function_->end,
function_->count);
} else if (!delete_current_) {
nesting_stack_.emplace_back(GetBlock());
}
delete_current_ = false;
read_index_++;
DCHECK(IsActive());
CoverageBlock& block = GetBlock();
while (nesting_stack_.size() > 1 &&
nesting_stack_.back().end <= block.start) {
nesting_stack_.pop_back();
}
DCHECK_IMPLIES(block.start >= function_->end,
block.end == kNoSourcePosition);
DCHECK_NE(block.start, kNoSourcePosition);
DCHECK_LE(block.end, GetParent().end);
return true;
}
CoverageBlock& GetBlock() {
DCHECK(IsActive());
return function_->blocks[read_index_];
}
CoverageBlock& GetNextBlock() {
DCHECK(IsActive());
DCHECK(HasNext());
return function_->blocks[read_index_ + 1];
}
CoverageBlock& GetPreviousBlock() {
DCHECK(IsActive());
DCHECK_GT(read_index_, 0);
return function_->blocks[read_index_ - 1];
}
CoverageBlock& GetParent() {
DCHECK(IsActive());
return nesting_stack_.back();
}
bool HasSiblingOrChild() {
DCHECK(IsActive());
return HasNext() && GetNextBlock().start < GetParent().end;
}
CoverageBlock& GetSiblingOrChild() {
DCHECK(HasSiblingOrChild());
DCHECK(IsActive());
return GetNextBlock();
}
bool IsTopLevel() const { return nesting_stack_.size() == 1; }
void DeleteBlock() {
DCHECK(!delete_current_);
DCHECK(IsActive());
delete_current_ = true;
}
private:
void MaybeWriteCurrent() {
if (delete_current_) return;
if (read_index_ >= 0 && write_index_ != read_index_) {
function_->blocks[write_index_] = function_->blocks[read_index_];
}
write_index_++;
}
void Finalize() {
while (Next()) {
}
function_->blocks.resize(write_index_);
}
bool IsActive() const { return read_index_ >= 0 && !ended_; }
CoverageFunction* function_;
std::vector<CoverageBlock> nesting_stack_;
bool ended_ = false;
bool delete_current_ = false;
int read_index_ = -1;
int write_index_ = -1;
};
bool HaveSameSourceRange(const CoverageBlock& lhs, const CoverageBlock& rhs) {
return lhs.start == rhs.start && lhs.end == rhs.end;
}
void MergeDuplicateRanges(CoverageFunction* function) {
CoverageBlockIterator iter(function);
while (iter.Next() && iter.HasNext()) {
CoverageBlock& block = iter.GetBlock();
CoverageBlock& next_block = iter.GetNextBlock();
if (!HaveSameSourceRange(block, next_block)) continue;
DCHECK_NE(kNoSourcePosition, block.end);
next_block.count = std::max(block.count, next_block.count);
iter.DeleteBlock();
}
}
void RewritePositionSingletonsToRanges(CoverageFunction* function) {
CoverageBlockIterator iter(function);
while (iter.Next()) {
CoverageBlock& block = iter.GetBlock();
CoverageBlock& parent = iter.GetParent();
if (block.start >= function->end) {
DCHECK_EQ(block.end, kNoSourcePosition);
iter.DeleteBlock();
} else if (block.end == kNoSourcePosition) {
if (iter.HasSiblingOrChild()) {
block.end = iter.GetSiblingOrChild().start;
} else if (iter.IsTopLevel()) {
block.end = parent.end - 1;
} else {
block.end = parent.end;
}
}
}
}
void MergeConsecutiveRanges(CoverageFunction* function) {
CoverageBlockIterator iter(function);
while (iter.Next()) {
CoverageBlock& block = iter.GetBlock();
if (iter.HasSiblingOrChild()) {
CoverageBlock& sibling = iter.GetSiblingOrChild();
if (sibling.start == block.end && sibling.count == block.count) {
sibling.start = block.start;
iter.DeleteBlock();
}
}
}
}
void MergeNestedRanges(CoverageFunction* function) {
CoverageBlockIterator iter(function);
while (iter.Next()) {
CoverageBlock& block = iter.GetBlock();
CoverageBlock& parent = iter.GetParent();
if (parent.count == block.count) {
iter.DeleteBlock();
}
}
}
void RewriteFunctionScopeCounter(CoverageFunction* function) {
DCHECK(!function->blocks.empty());
CoverageBlockIterator iter(function);
if (iter.Next()) {
DCHECK(iter.IsTopLevel());
CoverageBlock& block = iter.GetBlock();
if (block.start == SourceRange::kFunctionLiteralSourcePosition &&
block.end == SourceRange::kFunctionLiteralSourcePosition) {
function->count = block.count;
iter.DeleteBlock();
}
}
}
void FilterAliasedSingletons(CoverageFunction* function) {
CoverageBlockIterator iter(function);
iter.Next();
while (iter.Next()) {
CoverageBlock& previous_block = iter.GetPreviousBlock();
CoverageBlock& block = iter.GetBlock();
bool is_singleton = block.end == kNoSourcePosition;
bool aliases_start = block.start == previous_block.start;
if (is_singleton && aliases_start) {
DCHECK_NE(previous_block.end, kNoSourcePosition);
DCHECK_IMPLIES(iter.HasNext(), iter.GetNextBlock().start != block.start);
iter.DeleteBlock();
}
}
}
void FilterUncoveredRanges(CoverageFunction* function) {
CoverageBlockIterator iter(function);
while (iter.Next()) {
CoverageBlock& block = iter.GetBlock();
CoverageBlock& parent = iter.GetParent();
if (block.count == 0 && parent.count == 0) iter.DeleteBlock();
}
}
void FilterEmptyRanges(CoverageFunction* function) {
CoverageBlockIterator iter(function);
while (iter.Next()) {
CoverageBlock& block = iter.GetBlock();
if (block.start == block.end) iter.DeleteBlock();
}
}
void ClampToBinary(CoverageFunction* function) {
CoverageBlockIterator iter(function);
while (iter.Next()) {
CoverageBlock& block = iter.GetBlock();
if (block.count > 0) block.count = 1;
}
}
void ResetAllBlockCounts(Isolate* isolate, Tagged<SharedFunctionInfo> shared) {
DCHECK(shared->HasCoverageInfo(isolate));
Tagged<CoverageInfo> coverage_info =
Cast<CoverageInfo>(shared->GetDebugInfo(isolate)->coverage_info());
for (int i = 0; i < coverage_info->slot_count(); i++) {
coverage_info->ResetBlockCount(i);
}
}
bool IsBlockMode(debug::CoverageMode mode) {
switch (mode) {
case debug::CoverageMode::kBlockBinary:
case debug::CoverageMode::kBlockCount:
return true;
default:
return false;
}
}
bool IsBinaryMode(debug::CoverageMode mode) {
switch (mode) {
case debug::CoverageMode::kBlockBinary:
case debug::CoverageMode::kPreciseBinary:
return true;
default:
return false;
}
}
void CollectBlockCoverageInternal(Isolate* isolate, CoverageFunction* function,
Tagged<SharedFunctionInfo> info,
debug::CoverageMode mode) {
DCHECK(IsBlockMode(mode));
if (!function->HasNonEmptySourceRange()) return;
function->has_block_coverage = true;
function->blocks = GetSortedBlockData(isolate, info);
if (mode == debug::CoverageMode::kBlockBinary) ClampToBinary(function);
RewriteFunctionScopeCounter(function);
if (!function->HasBlocks()) return;
FilterAliasedSingletons(function);
RewritePositionSingletonsToRanges(function);
MergeConsecutiveRanges(function);
SortBlockData(function->blocks);
MergeDuplicateRanges(function);
MergeNestedRanges(function);
MergeConsecutiveRanges(function);
FilterUncoveredRanges(function);
FilterEmptyRanges(function);
}
void CollectBlockCoverage(Isolate* isolate, CoverageFunction* function,
Tagged<SharedFunctionInfo> info,
debug::CoverageMode mode) {
CollectBlockCoverageInternal(isolate, function, info, mode);
ResetAllBlockCounts(isolate, info);
}
void PrintBlockCoverage(const CoverageFunction* function,
Tagged<SharedFunctionInfo> info,
bool has_nonempty_source_range,
bool function_is_relevant) {
DCHECK(v8_flags.trace_block_coverage);
std::unique_ptr<char[]> function_name = function->name->ToCString();
i::PrintF(
"Coverage for function='%s', SFI=%p, has_nonempty_source_range=%d, "
"function_is_relevant=%d\n",
function_name.get(), reinterpret_cast<void*>(info.ptr()),
has_nonempty_source_range, function_is_relevant);
i::PrintF("{start: %d, end: %d, count: %d}\n", function->start, function->end,
function->count);
for (const auto& block : function->blocks) {
i::PrintF("{start: %d, end: %d, count: %d}\n", block.start, block.end,
block.count);
}
}
void CollectAndMaybeResetCounts(Isolate* isolate,
SharedToCounterMap* counter_map,
v8::debug::CoverageMode coverage_mode) {
const bool reset_count =
coverage_mode != v8::debug::CoverageMode::kBestEffort;
switch (isolate->code_coverage_mode()) {
case v8::debug::CoverageMode::kBlockBinary:
case v8::debug::CoverageMode::kBlockCount:
case v8::debug::CoverageMode::kPreciseBinary:
case v8::debug::CoverageMode::kPreciseCount: {
DCHECK(IsArrayList(
*isolate->factory()->feedback_vectors_for_profiling_tools()));
auto list = Cast<ArrayList>(
isolate->factory()->feedback_vectors_for_profiling_tools());
for (int i = 0; i < list->length(); i++) {
Tagged<FeedbackVector> vector = Cast<FeedbackVector>(list->get(i));
Tagged<SharedFunctionInfo> shared = vector->shared_function_info();
DCHECK(shared->IsSubjectToDebugging());
uint32_t count = static_cast<uint32_t>(vector->invocation_count());
if (reset_count) vector->clear_invocation_count(kRelaxedStore);
counter_map->Add(shared, count);
}
break;
}
case v8::debug::CoverageMode::kBestEffort: {
DCHECK(!IsArrayList(
*isolate->factory()->feedback_vectors_for_profiling_tools()));
DCHECK_EQ(v8::debug::CoverageMode::kBestEffort, coverage_mode);
AllowGarbageCollection allow_gc;
HeapObjectIterator heap_iterator(isolate->heap());
for (Tagged<HeapObject> current_obj = heap_iterator.Next();
!current_obj.is_null(); current_obj = heap_iterator.Next()) {
if (!IsJSFunction(current_obj)) continue;
Tagged<JSFunction> func = Cast<JSFunction>(current_obj);
Tagged<SharedFunctionInfo> shared = func->shared();
if (!shared->IsSubjectToDebugging()) continue;
if (!(func->has_feedback_vector() ||
func->has_closure_feedback_cell_array())) {
continue;
}
uint32_t count = 0;
if (func->has_feedback_vector()) {
count = static_cast<uint32_t>(
func->feedback_vector()->invocation_count());
} else if (func->shared()->HasBytecodeArray() &&
func->raw_feedback_cell()->interrupt_budget() <
TieringManager::InterruptBudgetFor(isolate, func, {})) {
count = 1;
}
counter_map->Add(shared, count);
}
for (JavaScriptStackFrameIterator it(isolate); !it.done(); it.Advance()) {
Tagged<SharedFunctionInfo> shared = it.frame()->function()->shared();
if (counter_map->Get(shared) != 0) continue;
counter_map->Add(shared, 1);
}
break;
}
}
}
struct SharedFunctionInfoAndCount {
SharedFunctionInfoAndCount(Handle<SharedFunctionInfo> info, uint32_t count)
: info(info),
count(count),
start(StartPosition(*info)),
end(info->EndPosition()) {}
bool operator<(const SharedFunctionInfoAndCount& that) const {
if (this->start != that.start) return this->start < that.start;
if (this->end != that.end) return this->end > that.end;
if (this->info->is_toplevel() != that.info->is_toplevel()) {
return this->info->is_toplevel();
}
return this->count > that.count;
}
Handle<SharedFunctionInfo> info;
uint32_t count;
int start;
int end;
};
}
std::unique_ptr<Coverage> Coverage::CollectPrecise(Isolate* isolate) {
DCHECK(!isolate->is_best_effort_code_coverage());
std::unique_ptr<Coverage> result =
Collect(isolate, isolate->code_coverage_mode());
if (isolate->is_precise_binary_code_coverage() ||
isolate->is_block_binary_code_coverage()) {
isolate->SetFeedbackVectorsForProfilingTools(
ReadOnlyRoots(isolate).empty_array_list());
}
return result;
}
std::unique_ptr<Coverage> Coverage::CollectBestEffort(Isolate* isolate) {
return Collect(isolate, v8::debug::CoverageMode::kBestEffort);
}
#ifdef V8_ENABLE_WEBASSEMBLY
std::unique_ptr<Coverage> Coverage::CollectWasmData(Isolate* isolate) {
CHECK(!V8_JITLESS_BOOL);
std::unique_ptr<Coverage> result(new Coverage());
std::vector<std::string> function_names;
{
Script::Iterator script_it(isolate);
for (Tagged<Script> script = script_it.Next(); !script.is_null();
script = script_it.Next()) {
if (script->type() != Script::Type::kWasm) continue;
result->emplace_back(handle(script, isolate));
std::vector<CoverageFunction>* functions = &result->back().functions;
const wasm::NativeModule* native_module = script->wasm_native_module();
const wasm::WasmModule* wasm_module = native_module->module();
const wasm::WasmModuleCoverageData* coverage_data =
native_module->coverage_data().get();
for (int declared_function_index = 0;
declared_function_index <
static_cast<int>(coverage_data->function_count());
declared_function_index++) {
const int function_index =
declared_function_index + wasm_module->num_imported_functions;
wasm::StringBuilder sb;
wasm::NamesProvider names_provider(wasm_module,
native_module->wire_bytes());
names_provider.PrintFunctionName(sb, function_index,
wasm::NamesProvider::kDevTools);
function_names.emplace_back(sb.start(), sb.length());
CoverageFunction function(0, 0, 0, isolate->factory()->empty_string());
const wasm::WasmFunctionCoverageData* function_coverage =
coverage_data->GetFunctionCoverageData(declared_function_index);
if (function_coverage) {
const base::Vector<const wasm::WasmCodeRange> code_ranges =
function_coverage->code_ranges();
for (size_t i_code_range = 0; i_code_range < code_ranges.size();
i_code_range++) {
wasm::WasmCodeRange code_range = code_ranges[i_code_range];
const base::Vector<uint32_t> counters =
function_coverage->counters();
DCHECK_LT(i_code_range, counters.size());
function.blocks.emplace_back(code_range.start, code_range.end,
counters[i_code_range]);
}
}
functions->emplace_back(function);
}
if (functions->empty()) result->pop_back();
}
}
size_t i_name = 0;
for (CoverageScript& script : *result) {
for (CoverageFunction& function : script.functions) {
function.name = isolate->factory()->InternalizeString(
function_names[i_name].c_str(), function_names[i_name].length());
i_name++;
}
}
DCHECK_EQ(function_names.size(), i_name);
return result;
}
#endif
std::unique_ptr<Coverage> Coverage::Collect(
Isolate* isolate, v8::debug::CoverageMode collection_mode) {
CHECK(!V8_JITLESS_BOOL);
SharedToCounterMap counter_map;
CollectAndMaybeResetCounts(isolate, &counter_map, collection_mode);
std::unique_ptr<Coverage> result(new Coverage());
std::vector<Handle<Script>> scripts;
Script::Iterator script_it(isolate);
for (Tagged<Script> script = script_it.Next(); !script.is_null();
script = script_it.Next()) {
if (script->IsUserJavaScript()) scripts.push_back(handle(script, isolate));
}
for (Handle<Script> script : scripts) {
result->emplace_back(script);
std::vector<CoverageFunction>* functions = &result->back().functions;
std::vector<SharedFunctionInfoAndCount> sorted;
{
SharedFunctionInfo::ScriptIterator infos(isolate, *script);
for (Tagged<SharedFunctionInfo> info = infos.Next(); !info.is_null();
info = infos.Next()) {
sorted.emplace_back(handle(info, isolate), counter_map.Get(info));
}
std::sort(sorted.begin(), sorted.end());
}
std::vector<size_t> nesting;
for (const SharedFunctionInfoAndCount& v : sorted) {
DirectHandle<SharedFunctionInfo> info = v.info;
int start = v.start;
int end = v.end;
uint32_t count = v.count;
while (!nesting.empty() && functions->at(nesting.back()).end <= start) {
nesting.pop_back();
}
if (count != 0) {
switch (collection_mode) {
case v8::debug::CoverageMode::kBlockCount:
case v8::debug::CoverageMode::kPreciseCount:
break;
case v8::debug::CoverageMode::kBlockBinary:
case v8::debug::CoverageMode::kPreciseBinary:
count = info->has_reported_binary_coverage() ? 0 : 1;
info->set_has_reported_binary_coverage(true);
break;
case v8::debug::CoverageMode::kBestEffort:
count = 1;
break;
}
}
Handle<String> name = SharedFunctionInfo::DebugName(isolate, info);
CoverageFunction function(start, end, count, name);
if (IsBlockMode(collection_mode) && info->HasCoverageInfo(isolate)) {
CollectBlockCoverage(isolate, &function, *info, collection_mode);
}
bool is_covered = (count != 0);
bool parent_is_covered =
(!nesting.empty() && functions->at(nesting.back()).count != 0);
bool has_block_coverage = !function.blocks.empty();
bool function_is_relevant =
(is_covered || parent_is_covered || has_block_coverage);
bool has_nonempty_source_range = function.HasNonEmptySourceRange();
if (has_nonempty_source_range && function_is_relevant) {
nesting.push_back(functions->size());
functions->emplace_back(function);
}
if (v8_flags.trace_block_coverage) {
PrintBlockCoverage(&function, *info, has_nonempty_source_range,
function_is_relevant);
}
}
if (functions->empty()) result->pop_back();
}
return result;
}
void Coverage::SelectMode(Isolate* isolate, debug::CoverageMode mode) {
if (mode != isolate->code_coverage_mode()) {
isolate->CollectSourcePositionsForAllBytecodeArrays();
isolate->set_disable_bytecode_flushing(true);
}
switch (mode) {
case debug::CoverageMode::kBestEffort:
isolate->debug()->RemoveAllCoverageInfos();
isolate->SetFeedbackVectorsForProfilingTools(
ReadOnlyRoots(isolate).undefined_value());
break;
case debug::CoverageMode::kBlockBinary:
case debug::CoverageMode::kBlockCount:
case debug::CoverageMode::kPreciseBinary:
case debug::CoverageMode::kPreciseCount: {
HandleScope scope(isolate);
Deoptimizer::DeoptimizeAll(isolate);
std::vector<Handle<JSFunction>> funcs_needing_feedback_vector;
{
HeapObjectIterator heap_iterator(isolate->heap());
for (Tagged<HeapObject> o = heap_iterator.Next(); !o.is_null();
o = heap_iterator.Next()) {
if (IsJSFunction(o)) {
Tagged<JSFunction> func = Cast<JSFunction>(o);
if (func->has_closure_feedback_cell_array()) {
funcs_needing_feedback_vector.push_back(
Handle<JSFunction>(func, isolate));
}
} else if (IsBinaryMode(mode) && IsSharedFunctionInfo(o)) {
Tagged<SharedFunctionInfo> shared = Cast<SharedFunctionInfo>(o);
shared->set_has_reported_binary_coverage(false);
} else if (IsFeedbackVector(o)) {
Cast<FeedbackVector>(o)->clear_invocation_count(kRelaxedStore);
}
}
}
for (DirectHandle<JSFunction> func : funcs_needing_feedback_vector) {
IsCompiledScope is_compiled_scope(
func->shared()->is_compiled_scope(isolate));
CHECK(is_compiled_scope.is_compiled());
JSFunction::EnsureFeedbackVector(isolate, func, &is_compiled_scope);
}
isolate->MaybeInitializeVectorListFromHeap();
break;
}
}
isolate->set_code_coverage_mode(mode);
}
}
}