52cfd29cbb
The RecordCoverage instruction has an A/E encoding. The A argument is the type of coverage being recorded, whereas the E argument is the logical index into the coverage array for updating whether that source position has been hit. Also adds new metadata to the bytecode component for the coverage arrays associated with bytecode containing RecordCoverage instructions and a new runtime entry for lazily allocate the coverage array for an interpreted function when needed. The type of coverage is encoded in the RecordCoverage instruction, despite being redundant with the information in the coverage array, so that checking whether that type of coverage is currently enabled at runtime doesn't require either accessing the coverage array (which may be lazily allocated), forcing allocation of the coverage array just to discover that type of coverage is currently disabled, or reading the serialized bytecode component to avoid that forced allocation. ------ Other changes: Source reporting now treats unexecuted interpreted functions when not forcing compilation as if they were uncompiled native functions, so that the source report from running the same code gives the same result whether using the interpreter or the native compiler. Bytecode closures are no longer skipped in source reports. Previously any closure without a context scope was skipped, but bytecode closures don't have those. TEST=vm/cc/SourceReport_Coverage Cq-Include-Trybots: luci.dart.try:vm-dyn-linux-debug-x64-try,vm-aot-dyn-linux-debug-x64-try,vm-aot-dyn-linux-product-x64-try Change-Id: I7557e5dd4c98331c7ca2f5c867dd5f6d03e9d756 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/501520 Reviewed-by: Alexander Markov <alexmarkov@google.com>
828 lines
29 KiB
C++
828 lines
29 KiB
C++
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
#include "vm/globals.h"
|
|
#if !defined(PRODUCT) && !defined(DART_PRECOMPILED_RUNTIME)
|
|
#include "vm/source_report.h"
|
|
|
|
#include "vm/bit_vector.h"
|
|
#include "vm/bytecode_reader.h"
|
|
#include "vm/closure_functions_cache.h"
|
|
#include "vm/compiler/jit/compiler.h"
|
|
#include "vm/isolate.h"
|
|
#include "vm/kernel_loader.h"
|
|
#include "vm/object.h"
|
|
#include "vm/object_store.h"
|
|
#include "vm/profiler.h"
|
|
#include "vm/profiler_service.h"
|
|
|
|
namespace dart {
|
|
|
|
const char* SourceReport::kCallSitesStr = "_CallSites";
|
|
const char* SourceReport::kCoverageStr = "Coverage";
|
|
const char* SourceReport::kPossibleBreakpointsStr = "PossibleBreakpoints";
|
|
const char* SourceReport::kProfileStr = "_Profile";
|
|
const char* SourceReport::kBranchCoverageStr = "BranchCoverage";
|
|
|
|
SourceReport::SourceReport(intptr_t report_set,
|
|
CompileMode compile_mode,
|
|
bool report_lines)
|
|
: report_set_(report_set),
|
|
compile_mode_(compile_mode),
|
|
report_lines_(report_lines),
|
|
library_filters_(GrowableObjectArray::Handle()),
|
|
libraries_already_compiled_(nullptr),
|
|
thread_(nullptr),
|
|
script_(nullptr),
|
|
start_pos_(TokenPosition::kMinSource),
|
|
end_pos_(TokenPosition::kMaxSource),
|
|
next_script_index_(0) {}
|
|
|
|
SourceReport::SourceReport(intptr_t report_set,
|
|
const GrowableObjectArray& library_filters,
|
|
ZoneCStringSet* libraries_already_compiled,
|
|
CompileMode compile_mode,
|
|
bool report_lines)
|
|
: report_set_(report_set),
|
|
compile_mode_(compile_mode),
|
|
report_lines_(report_lines),
|
|
library_filters_(library_filters),
|
|
libraries_already_compiled_(libraries_already_compiled),
|
|
thread_(nullptr),
|
|
script_(nullptr),
|
|
start_pos_(TokenPosition::kMinSource),
|
|
end_pos_(TokenPosition::kMaxSource),
|
|
next_script_index_(0) {}
|
|
|
|
SourceReport::~SourceReport() {
|
|
ClearScriptTable();
|
|
}
|
|
|
|
void SourceReport::ClearScriptTable() {
|
|
// Delete entries from script_table_ as it has the unfiltered list.
|
|
DirectChainedHashMap<ScriptTableTrait>::Iterator iter =
|
|
script_table_.GetIterator();
|
|
ScriptTableTrait::Pair* pair;
|
|
while ((pair = iter.Next()) != nullptr) {
|
|
delete ScriptTableTrait::ValueOf(*pair);
|
|
}
|
|
script_table_.Clear();
|
|
|
|
for (intptr_t i = 0; i < script_table_entries_.length(); i++) {
|
|
script_table_entries_[i] = nullptr;
|
|
}
|
|
script_table_entries_.Clear();
|
|
|
|
next_script_index_ = 0;
|
|
}
|
|
|
|
void SourceReport::Init(Thread* thread,
|
|
const Script* script,
|
|
TokenPosition start_pos,
|
|
TokenPosition end_pos) {
|
|
thread_ = thread;
|
|
script_ = script;
|
|
start_pos_ = TokenPosition::Max(start_pos, TokenPosition::kMinSource);
|
|
end_pos_ = TokenPosition::Min(end_pos, TokenPosition::kMaxSource);
|
|
ClearScriptTable();
|
|
if (IsReportRequested(kProfile)) {
|
|
// Build the profile.
|
|
SampleFilter samplesForIsolate(thread_->isolate()->main_port(),
|
|
Thread::kMutatorTask, -1, -1);
|
|
profile_.Build(thread, thread->isolate(), &samplesForIsolate,
|
|
Profiler::sample_block_buffer());
|
|
}
|
|
}
|
|
|
|
bool SourceReport::IsReportRequested(ReportKind report_kind) {
|
|
return (report_set_ & report_kind) != 0;
|
|
}
|
|
|
|
bool SourceReport::ShouldSkipFunction(const Function& func) {
|
|
// TODO(32315): Verify that the check is still needed after the issue is
|
|
// resolved.
|
|
if (!func.token_pos().IsReal() || !func.end_token_pos().IsReal()) {
|
|
// At least one of the token positions is not known.
|
|
return true;
|
|
}
|
|
|
|
if (script_ != nullptr && !script_->IsNull()) {
|
|
if (func.script() != script_->ptr()) {
|
|
// The function is from the wrong script.
|
|
return true;
|
|
}
|
|
if ((func.end_token_pos() < start_pos_) || (func.token_pos() > end_pos_)) {
|
|
// The function does not intersect with the requested token range.
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// These don't have unoptimized code and are only used for synthetic stubs.
|
|
if (func.ForceOptimize()) return true;
|
|
|
|
switch (func.kind()) {
|
|
case UntaggedFunction::kRegularFunction:
|
|
case UntaggedFunction::kClosureFunction:
|
|
case UntaggedFunction::kImplicitClosureFunction:
|
|
case UntaggedFunction::kImplicitStaticGetter:
|
|
case UntaggedFunction::kFieldInitializer:
|
|
case UntaggedFunction::kGetterFunction:
|
|
case UntaggedFunction::kSetterFunction:
|
|
case UntaggedFunction::kConstructor:
|
|
break;
|
|
default:
|
|
return true;
|
|
}
|
|
if (func.is_abstract() || func.IsImplicitConstructor() ||
|
|
func.is_synthetic() || func.IsRedirectingFactory()) {
|
|
return true;
|
|
}
|
|
if (func.IsNonImplicitClosureFunction() && !func.HasBytecode() &&
|
|
(func.context_scope() == ContextScope::null())) {
|
|
// TODO(iposva): This can arise if we attempt to compile an inner function
|
|
// before we have compiled its enclosing function or if the enclosing
|
|
// function failed to compile.
|
|
return true;
|
|
}
|
|
|
|
// There is an idiom where static utility classes are given a private
|
|
// constructor to prevent the class from being instantiated. Ignore these
|
|
// constructors so that they don't lower the coverage rate. See #47021.
|
|
SafepointReadRwLocker ml(thread_, thread_->isolate_group()->program_lock());
|
|
if (func.kind() == UntaggedFunction::kConstructor &&
|
|
func.NumParameters() == func.NumImplicitParameters() &&
|
|
func.IsPrivate()) {
|
|
// Check that the class has no non-static members and no subclasses.
|
|
Class& cls = Class::Handle(func.Owner());
|
|
GrowableObjectArray& subclasses =
|
|
GrowableObjectArray::Handle(cls.direct_subclasses());
|
|
if (cls.is_abstract() && !cls.HasInstanceFields() &&
|
|
(subclasses.IsNull() || subclasses.Length() == 0)) {
|
|
// Check that the constructor is the only non-static function.
|
|
Array& clsFuncs = Array::Handle(cls.functions());
|
|
Function& otherFunc = Function::Handle();
|
|
intptr_t numNonStaticFunctions = 0;
|
|
for (intptr_t i = 0; i < clsFuncs.Length(); ++i) {
|
|
otherFunc ^= clsFuncs.At(i);
|
|
if (!otherFunc.IsStaticFunction()) {
|
|
++numNonStaticFunctions;
|
|
}
|
|
}
|
|
if (numNonStaticFunctions == 1) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Enum constructors cannot be invoked by the user, so ignore them.
|
|
if (func.IsGenerativeConstructor()) {
|
|
Class& cls = Class::Handle(func.Owner());
|
|
if (cls.is_enum_class()) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool SourceReport::ShouldSkipField(const Field& field) {
|
|
if (!field.token_pos().IsReal() || !field.end_token_pos().IsReal()) {
|
|
// At least one of the token positions is not known.
|
|
return true;
|
|
}
|
|
|
|
if (script_ != nullptr && !script_->IsNull()) {
|
|
if (field.Script() != script_->ptr()) {
|
|
// The field is from the wrong script.
|
|
return true;
|
|
}
|
|
if ((field.end_token_pos() < start_pos_) ||
|
|
(field.token_pos() > end_pos_)) {
|
|
// The field does not intersect with the requested token range.
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool SourceReport::IsLibraryAlreadyCompiled(const Library& lib) {
|
|
if (libraries_already_compiled_ == nullptr) return false;
|
|
const char* url = String::ToCString(thread(), lib.url());
|
|
if (url == nullptr) return false;
|
|
return libraries_already_compiled_->Lookup(url) != nullptr;
|
|
}
|
|
|
|
bool SourceReport::ShouldFiltersIncludeUrl(const String& url) {
|
|
String& filter = String::Handle(zone());
|
|
const intptr_t num_filters = library_filters_.Length();
|
|
for (intptr_t i = 0; i < num_filters; ++i) {
|
|
filter ^= library_filters_.At(i);
|
|
if (url.StartsWith(filter)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool SourceReport::ShouldFiltersIncludeScript(const Script& script) {
|
|
if (library_filters_.IsNull()) return true;
|
|
String& url = String::Handle(zone(), script.url());
|
|
if (ShouldFiltersIncludeUrl(url)) return true;
|
|
const Library& lib = Library::Handle(zone(), script.FindLibrary());
|
|
url ^= lib.url();
|
|
return ShouldFiltersIncludeUrl(url);
|
|
}
|
|
|
|
intptr_t SourceReport::GetScriptIndex(const Script& script) {
|
|
ScriptTableEntry wrapper;
|
|
const String& url = String::Handle(zone(), script.url());
|
|
wrapper.key = &url;
|
|
wrapper.script = &Script::Handle(zone(), script.ptr());
|
|
ScriptTableEntry* pair = script_table_.LookupValue(&wrapper);
|
|
if (pair != nullptr) {
|
|
return pair->index;
|
|
}
|
|
ScriptTableEntry* tmp = new ScriptTableEntry();
|
|
tmp->key = &url;
|
|
tmp->script = wrapper.script;
|
|
if (ShouldFiltersIncludeScript(script)) {
|
|
tmp->index = next_script_index_++;
|
|
script_table_entries_.Add(tmp);
|
|
} else {
|
|
tmp->index = -1;
|
|
}
|
|
script_table_.Insert(tmp);
|
|
ASSERT(script_table_entries_.length() == next_script_index_);
|
|
#if defined(DEBUG)
|
|
VerifyScriptTable();
|
|
#endif
|
|
return tmp->index;
|
|
}
|
|
|
|
#if defined(DEBUG)
|
|
void SourceReport::VerifyScriptTable() {
|
|
for (intptr_t i = 0; i < script_table_entries_.length(); i++) {
|
|
const String* url = script_table_entries_[i]->key;
|
|
const Script* script = script_table_entries_[i]->script;
|
|
intptr_t index = script_table_entries_[i]->index;
|
|
ASSERT(i == index);
|
|
const String& url2 = String::Handle(zone(), script->url());
|
|
ASSERT(url2.Equals(*url));
|
|
ScriptTableEntry wrapper;
|
|
wrapper.key = &url2;
|
|
wrapper.script = &Script::Handle(zone(), script->ptr());
|
|
ScriptTableEntry* pair = script_table_.LookupValue(&wrapper);
|
|
ASSERT(i == pair->index);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
bool SourceReport::ScriptIsLoadedByLibrary(const Script& script,
|
|
const Library& lib) {
|
|
const Array& scripts = Array::Handle(zone(), lib.LoadedScripts());
|
|
for (intptr_t j = 0; j < scripts.Length(); j++) {
|
|
if (scripts.At(j) == script.ptr()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void SourceReport::PrintCallSitesData(JSONObject* jsobj,
|
|
const Function& function,
|
|
const Code& code) {
|
|
ASSERT(!code.IsNull());
|
|
const TokenPosition& begin_pos = function.token_pos();
|
|
const TokenPosition& end_pos = function.end_token_pos();
|
|
ZoneGrowableArray<const ICData*>* ic_data_array =
|
|
new (zone()) ZoneGrowableArray<const ICData*>();
|
|
function.RestoreICDataMap(ic_data_array, false /* clone ic-data */);
|
|
const PcDescriptors& descriptors =
|
|
PcDescriptors::Handle(zone(), code.pc_descriptors());
|
|
|
|
JSONArray sites(jsobj, "callSites");
|
|
|
|
PcDescriptors::Iterator iter(
|
|
descriptors,
|
|
UntaggedPcDescriptors::kIcCall | UntaggedPcDescriptors::kUnoptStaticCall);
|
|
while (iter.MoveNext()) {
|
|
HANDLESCOPE(thread());
|
|
ASSERT(iter.DeoptId() < ic_data_array->length());
|
|
const ICData* ic_data = (*ic_data_array)[iter.DeoptId()];
|
|
if (ic_data != nullptr) {
|
|
const TokenPosition& token_pos = iter.TokenPos();
|
|
if (!token_pos.IsWithin(begin_pos, end_pos)) {
|
|
// Does not correspond to a valid source position.
|
|
continue;
|
|
}
|
|
ic_data->PrintToJSONArray(sites, token_pos);
|
|
}
|
|
}
|
|
}
|
|
|
|
intptr_t SourceReport::GetTokenPosOrLine(const Script& script,
|
|
const TokenPosition& token_pos) {
|
|
if (!report_lines_) {
|
|
return token_pos.Pos();
|
|
}
|
|
intptr_t line = -1;
|
|
const bool found = script.GetTokenLocation(token_pos, &line);
|
|
ASSERT(found);
|
|
return line;
|
|
}
|
|
|
|
void SourceReport::PrintCoverageData(JSONObject* jsobj,
|
|
const Script& script,
|
|
intptr_t script_index,
|
|
const Function& function,
|
|
bool report_branch_coverage) {
|
|
const TokenPosition& begin_pos = function.token_pos();
|
|
const TokenPosition& end_pos = function.end_token_pos();
|
|
|
|
bool const_constructor_hit = false;
|
|
if (function.IsFunction() && function.is_const()) {
|
|
for (TokenPosition hit :
|
|
script_table_entries_[script_index]->const_constructor_hits) {
|
|
if (hit == begin_pos) {
|
|
const_constructor_hit = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
const int kCoverageNone = 0;
|
|
const int kCoverageMiss = 1;
|
|
const int kCoverageHit = 2;
|
|
|
|
intptr_t func_length = function.SourceSize() + 1;
|
|
GrowableArray<char> coverage(func_length);
|
|
coverage.SetLength(func_length);
|
|
for (int i = 0; i < func_length; i++) {
|
|
coverage[i] = kCoverageNone;
|
|
}
|
|
|
|
if (function.WasExecuted() || const_constructor_hit) {
|
|
coverage[0] = kCoverageHit;
|
|
} else {
|
|
coverage[0] = kCoverageMiss;
|
|
}
|
|
|
|
auto update_coverage = [&](TokenPosition token_pos, bool was_executed) {
|
|
if (!(token_pos.IsReal() && token_pos.IsWithin(begin_pos, end_pos))) {
|
|
return;
|
|
}
|
|
|
|
const intptr_t token_offset = token_pos.Pos() - begin_pos.Pos();
|
|
if (was_executed) {
|
|
coverage[token_offset] = kCoverageHit;
|
|
} else {
|
|
if (coverage[token_offset] == kCoverageNone) {
|
|
coverage[token_offset] = kCoverageMiss;
|
|
}
|
|
}
|
|
};
|
|
|
|
// Merge the coverage from coverage_array attached to the function.
|
|
const Array& coverage_array = Array::Handle(function.GetCoverageArray());
|
|
if (!coverage_array.IsNull()) {
|
|
for (intptr_t i = 0; i < coverage_array.Length(); i += 2) {
|
|
bool is_branch_coverage;
|
|
const TokenPosition token_pos = TokenPosition::DecodeCoveragePosition(
|
|
Smi::Value(Smi::RawCast(coverage_array.At(i))), &is_branch_coverage);
|
|
if (is_branch_coverage == report_branch_coverage) {
|
|
const bool was_executed =
|
|
Smi::Value(Smi::RawCast(coverage_array.At(i + 1))) != 0;
|
|
update_coverage(token_pos, was_executed || const_constructor_hit);
|
|
}
|
|
}
|
|
}
|
|
|
|
JSONObject cov(jsobj, report_branch_coverage ? "branchCoverage" : "coverage");
|
|
{
|
|
JSONArray hits(&cov, "hits");
|
|
TokenPosition pos = begin_pos;
|
|
for (int i = 0; i < func_length; i++) {
|
|
if (coverage[i] == kCoverageHit) {
|
|
// Add the token position or line number of the hit.
|
|
hits.AddValue(GetTokenPosOrLine(script, pos));
|
|
}
|
|
pos = pos.Next();
|
|
}
|
|
}
|
|
{
|
|
JSONArray misses(&cov, "misses");
|
|
TokenPosition pos = begin_pos;
|
|
for (int i = 0; i < func_length; i++) {
|
|
if (coverage[i] == kCoverageMiss) {
|
|
// Add the token position or line number of the miss.
|
|
misses.AddValue(GetTokenPosOrLine(script, pos));
|
|
}
|
|
pos = pos.Next();
|
|
}
|
|
}
|
|
}
|
|
|
|
void SourceReport::PrintPossibleBreakpointsData(JSONObject* jsobj,
|
|
const Script& script,
|
|
const Function& func,
|
|
const Code& code) {
|
|
const TokenPosition& begin_pos = func.token_pos();
|
|
const TokenPosition& end_pos = func.end_token_pos();
|
|
intptr_t func_length = func.SourceSize() + 1;
|
|
|
|
BitVector possible(zone(), func_length);
|
|
|
|
if (func.HasBytecode()) {
|
|
#if defined(DART_DYNAMIC_MODULES)
|
|
const auto& bytecode = Bytecode::Handle(zone(), func.GetBytecode());
|
|
// Currently, every source position is a possible breakpoint.
|
|
bytecode::BytecodeSourcePositionsIterator iter(zone(), bytecode);
|
|
while (iter.MoveNext()) {
|
|
const TokenPosition& token_pos = iter.TokenPos();
|
|
if (!token_pos.IsWithin(begin_pos, end_pos)) {
|
|
// Does not correspond to a valid source position.
|
|
continue;
|
|
}
|
|
intptr_t token_offset = token_pos.Pos() - begin_pos.Pos();
|
|
possible.Add(token_offset);
|
|
}
|
|
#else
|
|
UNREACHABLE();
|
|
#endif
|
|
} else {
|
|
ASSERT(!code.IsNull());
|
|
|
|
const uint8_t kSafepointKind = (UntaggedPcDescriptors::kIcCall |
|
|
UntaggedPcDescriptors::kUnoptStaticCall |
|
|
UntaggedPcDescriptors::kRuntimeCall);
|
|
|
|
const PcDescriptors& descriptors =
|
|
PcDescriptors::Handle(zone(), code.pc_descriptors());
|
|
|
|
PcDescriptors::Iterator iter(descriptors, kSafepointKind);
|
|
while (iter.MoveNext()) {
|
|
const TokenPosition& token_pos = iter.TokenPos();
|
|
if (!token_pos.IsWithin(begin_pos, end_pos)) {
|
|
// Does not correspond to a valid source position.
|
|
continue;
|
|
}
|
|
intptr_t token_offset = token_pos.Pos() - begin_pos.Pos();
|
|
possible.Add(token_offset);
|
|
}
|
|
}
|
|
JSONArray bpts(jsobj, "possibleBreakpoints");
|
|
TokenPosition pos = begin_pos;
|
|
for (int i = 0; i < func_length; i++) {
|
|
if (possible.Contains(i)) {
|
|
// Add the token position or line number.
|
|
bpts.AddValue(GetTokenPosOrLine(script, pos));
|
|
}
|
|
pos = pos.Next();
|
|
}
|
|
}
|
|
|
|
void SourceReport::PrintProfileData(JSONObject* jsobj,
|
|
ProfileFunction* profile_function) {
|
|
ASSERT(profile_function != nullptr);
|
|
ASSERT(profile_function->NumSourcePositions() > 0);
|
|
|
|
{
|
|
JSONObject profile(jsobj, "profile");
|
|
|
|
{
|
|
JSONObject profileData(&profile, "metadata");
|
|
profileData.AddProperty("sampleCount", profile_.sample_count());
|
|
}
|
|
|
|
// Positions.
|
|
{
|
|
JSONArray positions(&profile, "positions");
|
|
for (intptr_t i = 0; i < profile_function->NumSourcePositions(); i++) {
|
|
const ProfileFunctionSourcePosition& position =
|
|
profile_function->GetSourcePosition(i);
|
|
if (position.token_pos().IsReal()) {
|
|
// Add as an integer.
|
|
positions.AddValue(position.token_pos().Pos());
|
|
} else {
|
|
// Add as a string.
|
|
positions.AddValue(position.token_pos().ToCString());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Exclusive ticks.
|
|
{
|
|
JSONArray exclusiveTicks(&profile, "exclusiveTicks");
|
|
for (intptr_t i = 0; i < profile_function->NumSourcePositions(); i++) {
|
|
const ProfileFunctionSourcePosition& position =
|
|
profile_function->GetSourcePosition(i);
|
|
exclusiveTicks.AddValue(position.exclusive_ticks());
|
|
}
|
|
}
|
|
// Inclusive ticks.
|
|
{
|
|
JSONArray inclusiveTicks(&profile, "inclusiveTicks");
|
|
for (intptr_t i = 0; i < profile_function->NumSourcePositions(); i++) {
|
|
const ProfileFunctionSourcePosition& position =
|
|
profile_function->GetSourcePosition(i);
|
|
inclusiveTicks.AddValue(position.inclusive_ticks());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void SourceReport::PrintScriptTable(JSONArray* scripts) {
|
|
for (intptr_t i = 0; i < script_table_entries_.length(); i++) {
|
|
const Script* script = script_table_entries_[i]->script;
|
|
scripts->AddValue(*script);
|
|
}
|
|
}
|
|
|
|
void SourceReport::VisitCodeOrBytecode(JSONObject* jsobj,
|
|
const Script& script,
|
|
intptr_t script_index,
|
|
const Function& func,
|
|
const Code& code,
|
|
CompileMode compile_mode) {
|
|
ASSERT(!code.IsNull() || func.HasBytecode());
|
|
// TODO(sstrickl): Handle call site data for bytecode.
|
|
if (IsReportRequested(kCallSites) && !code.IsNull()) {
|
|
PrintCallSitesData(jsobj, func, code);
|
|
}
|
|
if (IsReportRequested(kCoverage)) {
|
|
PrintCoverageData(jsobj, script, script_index, func,
|
|
/* report_branch_coverage */ false);
|
|
}
|
|
if (IsReportRequested(kBranchCoverage)) {
|
|
PrintCoverageData(jsobj, script, script_index, func,
|
|
/* report_branch_coverage */ true);
|
|
}
|
|
if (IsReportRequested(kPossibleBreakpoints)) {
|
|
PrintPossibleBreakpointsData(jsobj, script, func, code);
|
|
}
|
|
if (IsReportRequested(kProfile)) {
|
|
if (auto* const profile_function = profile_.FindFunction(func)) {
|
|
if (profile_function->NumSourcePositions() > 0) {
|
|
PrintProfileData(jsobj, profile_function);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void SourceReport::VisitFunction(JSONArray* jsarr,
|
|
const Function& func,
|
|
CompileMode compile_mode) {
|
|
if (ShouldSkipFunction(func)) {
|
|
return;
|
|
}
|
|
|
|
const Script& script = Script::Handle(zone(), func.script());
|
|
const TokenPosition begin_pos = func.token_pos();
|
|
const TokenPosition end_pos = func.end_token_pos();
|
|
|
|
const intptr_t script_index = GetScriptIndex(script);
|
|
if (script_index < 0) {
|
|
return;
|
|
}
|
|
|
|
auto& code = Code::Handle(zone(), func.unoptimized_code());
|
|
auto& err = Error::Handle(zone());
|
|
bool is_compiled = !code.IsNull();
|
|
if (func.HasBytecode()) {
|
|
ASSERT(code.IsNull());
|
|
// We treat unexecuted bytecode as "uncompiled" unless force compilation was
|
|
// requested, to match the reports for compiled code.
|
|
is_compiled = func.WasExecuted() || compile_mode == kForceCompile;
|
|
} else if (code.IsNull() &&
|
|
(func.HasCode() || (compile_mode == kForceCompile))) {
|
|
err = Compiler::EnsureUnoptimizedCode(thread(), func);
|
|
if (err.IsNull()) {
|
|
is_compiled = true;
|
|
code = func.unoptimized_code();
|
|
} else {
|
|
// Emit an uncompiled range for this function with error information.
|
|
}
|
|
}
|
|
|
|
JSONObject range(jsarr);
|
|
range.AddProperty("scriptIndex", script_index);
|
|
range.AddProperty("startPos", begin_pos);
|
|
range.AddProperty("endPos", end_pos);
|
|
range.AddProperty("compiled", is_compiled);
|
|
if (!err.IsNull()) {
|
|
range.AddProperty("error", err);
|
|
}
|
|
if (is_compiled) {
|
|
VisitCodeOrBytecode(&range, script, script_index, func, code, compile_mode);
|
|
}
|
|
}
|
|
|
|
void SourceReport::VisitField(JSONArray* jsarr,
|
|
const Field& field,
|
|
CompileMode compile_mode) {
|
|
if (ShouldSkipField(field) || !field.HasInitializerFunction()) return;
|
|
const Function& func = Function::Handle(field.InitializerFunction());
|
|
VisitFunction(jsarr, func, compile_mode);
|
|
}
|
|
|
|
void SourceReport::VisitLibrary(JSONArray* jsarr, const Library& lib) {
|
|
Class& cls = Class::Handle(zone());
|
|
Array& functions = Array::Handle(zone());
|
|
Array& fields = Array::Handle(zone());
|
|
Function& func = Function::Handle(zone());
|
|
Field& field = Field::Handle(zone());
|
|
Script& script = Script::Handle(zone());
|
|
ClassDictionaryIterator it(lib, ClassDictionaryIterator::kIteratePrivate);
|
|
CompileMode compile_mode = compile_mode_;
|
|
if (compile_mode == kForceCompile && IsLibraryAlreadyCompiled(lib)) {
|
|
compile_mode = kNoCompile;
|
|
}
|
|
while (it.HasNext()) {
|
|
cls = it.GetNextClass();
|
|
if (!cls.is_finalized()) {
|
|
if (compile_mode == kForceCompile) {
|
|
Error& err = Error::Handle(cls.EnsureIsFinalized(thread()));
|
|
if (!err.IsNull()) {
|
|
// Emit an uncompiled range for this class with error information.
|
|
script = cls.script();
|
|
const intptr_t script_index = GetScriptIndex(script);
|
|
if (script_index < 0) {
|
|
continue;
|
|
}
|
|
JSONObject range(jsarr);
|
|
range.AddProperty("scriptIndex", script_index);
|
|
range.AddProperty("startPos", cls.token_pos());
|
|
range.AddProperty("endPos", cls.end_token_pos());
|
|
range.AddProperty("compiled", false);
|
|
range.AddProperty("error", err);
|
|
continue;
|
|
}
|
|
ASSERT(cls.is_finalized());
|
|
} else {
|
|
cls.EnsureDeclarationLoaded();
|
|
// Emit one range for the whole uncompiled class.
|
|
script = cls.script();
|
|
const intptr_t script_index = GetScriptIndex(script);
|
|
if (script_index < 0) {
|
|
continue;
|
|
}
|
|
JSONObject range(jsarr);
|
|
range.AddProperty("scriptIndex", script_index);
|
|
range.AddProperty("startPos", cls.token_pos());
|
|
range.AddProperty("endPos", cls.end_token_pos());
|
|
range.AddProperty("compiled", false);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
functions = cls.current_functions();
|
|
for (int i = 0; i < functions.Length(); i++) {
|
|
func ^= functions.At(i);
|
|
// Skip getter functions of static const field.
|
|
if (func.kind() == UntaggedFunction::kImplicitStaticGetter) {
|
|
field ^= func.accessor_field();
|
|
if (field.is_const() && field.is_static()) {
|
|
continue;
|
|
}
|
|
}
|
|
VisitFunction(jsarr, func, compile_mode);
|
|
}
|
|
|
|
fields = cls.fields();
|
|
for (intptr_t i = 0; i < fields.Length(); i++) {
|
|
field ^= fields.At(i);
|
|
VisitField(jsarr, field, compile_mode);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SourceReport::VisitClosures(JSONArray* jsarr) {
|
|
ClosureFunctionsCache::ForAllClosureFunctions([&](const Function& func) {
|
|
VisitFunction(jsarr, func, compile_mode_);
|
|
return true; // Continue iteration.
|
|
});
|
|
}
|
|
|
|
void SourceReport::PrintJSON(JSONStream* js,
|
|
const Script& script,
|
|
TokenPosition start_pos,
|
|
TokenPosition end_pos) {
|
|
Init(Thread::Current(), &script, start_pos, end_pos);
|
|
|
|
JSONObject report(js);
|
|
report.AddProperty("type", "SourceReport");
|
|
{
|
|
JSONArray ranges(&report, "ranges");
|
|
|
|
// Output constant coverage if coverage is requested.
|
|
if (IsReportRequested(kCoverage) || IsReportRequested(kBranchCoverage)) {
|
|
// Find all scripts. We need to go though all scripts because a script
|
|
// (even one we don't want) can add coverage to another library (i.e.
|
|
// potentially one we want).
|
|
DirectChainedHashMap<ScriptTableTrait> local_script_table;
|
|
GrowableArray<ScriptTableEntry*> local_script_table_entries;
|
|
CollectAllScripts(&local_script_table, &local_script_table_entries);
|
|
CollectConstConstructorCoverageFromScripts(&local_script_table_entries);
|
|
CleanupCollectedScripts(&local_script_table, &local_script_table_entries);
|
|
}
|
|
|
|
const GrowableObjectArray& libs = GrowableObjectArray::Handle(
|
|
zone(), thread()->isolate_group()->object_store()->libraries());
|
|
|
|
// We only visit the libraries which actually load the specified script.
|
|
Library& lib = Library::Handle(zone());
|
|
for (intptr_t i = 0; i < libs.Length(); i++) {
|
|
lib ^= libs.At(i);
|
|
if (script.IsNull() || ScriptIsLoadedByLibrary(script, lib)) {
|
|
VisitLibrary(&ranges, lib);
|
|
}
|
|
}
|
|
|
|
// Visit all closures for this isolate.
|
|
VisitClosures(&ranges);
|
|
}
|
|
|
|
// Print the script table.
|
|
JSONArray scripts(&report, "scripts");
|
|
PrintScriptTable(&scripts);
|
|
}
|
|
|
|
void SourceReport::CollectAllScripts(
|
|
DirectChainedHashMap<ScriptTableTrait>* local_script_table,
|
|
GrowableArray<ScriptTableEntry*>* local_script_table_entries) {
|
|
ScriptTableEntry wrapper;
|
|
const GrowableObjectArray& libs = GrowableObjectArray::Handle(
|
|
zone(), thread()->isolate_group()->object_store()->libraries());
|
|
Library& lib = Library::Handle(zone());
|
|
Script& scriptRef = Script::Handle(zone());
|
|
for (int i = 0; i < libs.Length(); i++) {
|
|
lib ^= libs.At(i);
|
|
const Array& scripts = Array::Handle(zone(), lib.LoadedScripts());
|
|
for (intptr_t j = 0; j < scripts.Length(); j++) {
|
|
scriptRef ^= scripts.At(j);
|
|
const String& url = String::Handle(zone(), scriptRef.url());
|
|
wrapper.key = &url;
|
|
wrapper.script = &Script::Handle(zone(), scriptRef.ptr());
|
|
ScriptTableEntry* pair = local_script_table->LookupValue(&wrapper);
|
|
if (pair != nullptr) {
|
|
// Existing one.
|
|
continue;
|
|
}
|
|
// New one. Insert.
|
|
ScriptTableEntry* tmp = new ScriptTableEntry();
|
|
tmp->key = &url;
|
|
tmp->index = -1;
|
|
tmp->script = wrapper.script;
|
|
local_script_table_entries->Add(tmp);
|
|
local_script_table->Insert(tmp);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SourceReport::CleanupCollectedScripts(
|
|
DirectChainedHashMap<ScriptTableTrait>* local_script_table,
|
|
GrowableArray<ScriptTableEntry*>* local_script_table_entries) {
|
|
for (intptr_t i = 0; i < local_script_table_entries->length(); i++) {
|
|
delete local_script_table_entries->operator[](i);
|
|
local_script_table_entries->operator[](i) = nullptr;
|
|
}
|
|
local_script_table_entries->Clear();
|
|
local_script_table->Clear();
|
|
}
|
|
|
|
void SourceReport::CollectConstConstructorCoverageFromScripts(
|
|
GrowableArray<ScriptTableEntry*>* local_script_table_entries) {
|
|
// Now output the wanted constant coverage.
|
|
for (intptr_t i = 0; i < local_script_table_entries->length(); i++) {
|
|
const Script* script = local_script_table_entries->At(i)->script;
|
|
|
|
// Whether we want *this* script or not we need to look at the constant
|
|
// constructor coverage. Any of those could be in a script we *do* want.
|
|
{
|
|
Script& scriptRef = Script::Handle(zone());
|
|
const Array& constructors =
|
|
Array::Handle(script->CollectConstConstructorCoverageFrom());
|
|
intptr_t constructors_count = constructors.Length();
|
|
Function& constructor = Function::Handle(zone());
|
|
for (intptr_t i = 0; i < constructors_count; i++) {
|
|
constructor ^= constructors.At(i);
|
|
// Check if we want coverage for this constructor.
|
|
if (ShouldSkipFunction(constructor)) {
|
|
continue;
|
|
}
|
|
scriptRef ^= constructor.script();
|
|
const intptr_t script_index = GetScriptIndex(scriptRef);
|
|
if (script_index < 0) {
|
|
continue;
|
|
}
|
|
|
|
script_table_entries_[script_index]->const_constructor_hits.Add(
|
|
constructor.token_pos());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace dart
|
|
#endif // !defined(PRODUCT) && !defined(DART_PRECOMPILED_RUNTIME)
|