Use idle time to make progress on scheduled compilation jobs

BUG=v8:5215
R=rmcilroy@chromium.org,marja@chromium.org,vogelheim@chromium.org

Review-Url: https://codereview.chromium.org/2573493002
Cr-Commit-Position: refs/heads/master@{#41767}
This commit is contained in:
jochen 2016-12-16 06:40:32 -08:00 committed by Commit bot
parent c0d6939836
commit 692ba84f4f
9 changed files with 346 additions and 42 deletions

View File

@ -263,5 +263,36 @@ void CompilerDispatcherJob::ResetOnMainThread() {
status_ = CompileJobStatus::kInitial;
}
double CompilerDispatcherJob::EstimateRuntimeOfNextStepInMs() const {
switch (status_) {
case CompileJobStatus::kInitial:
return tracer_->EstimatePrepareToParseInMs();
case CompileJobStatus::kReadyToParse:
return tracer_->EstimateParseInMs(parse_info_->end_position() -
parse_info_->start_position());
case CompileJobStatus::kParsed:
return tracer_->EstimateFinalizeParsingInMs();
case CompileJobStatus::kReadyToAnalyse:
return tracer_->EstimatePrepareToCompileInMs();
case CompileJobStatus::kReadyToCompile:
return tracer_->EstimateCompileInMs(
parse_info_->literal()->ast_node_count());
case CompileJobStatus::kCompiled:
return tracer_->EstimateFinalizeCompilingInMs();
case CompileJobStatus::kFailed:
case CompileJobStatus::kDone:
return 0.0;
}
UNREACHABLE();
return 0.0;
}
} // namespace internal
} // namespace v8

View File

@ -83,6 +83,9 @@ class V8_EXPORT_PRIVATE CompilerDispatcherJob {
// Transition from any state to kInitial and free all resources.
void ResetOnMainThread();
// Estimate how long the next step will take using the tracer.
double EstimateRuntimeOfNextStepInMs() const;
private:
FRIEND_TEST(CompilerDispatcherJobTest, ScopeChain);

View File

@ -16,6 +16,8 @@ double MonotonicallyIncreasingTimeInMs() {
static_cast<double>(base::Time::kMillisecondsPerSecond);
}
const double kEstimatedRuntimeWithoutData = 1.0;
} // namespace
CompilerDispatcherTracer::Scope::Scope(CompilerDispatcherTracer* tracer,
@ -129,22 +131,23 @@ double CompilerDispatcherTracer::EstimateParseInMs(size_t source_length) const {
return Estimate(parse_events_, source_length);
}
double CompilerDispatcherTracer::EstimateFinalizeParsingInMs() {
double CompilerDispatcherTracer::EstimateFinalizeParsingInMs() const {
base::LockGuard<base::Mutex> lock(&mutex_);
return Average(finalize_parsing_events_);
}
double CompilerDispatcherTracer::EstimatePrepareToCompileInMs() {
double CompilerDispatcherTracer::EstimatePrepareToCompileInMs() const {
base::LockGuard<base::Mutex> lock(&mutex_);
return Average(prepare_compile_events_);
}
double CompilerDispatcherTracer::EstimateCompileInMs(size_t ast_size_in_bytes) {
double CompilerDispatcherTracer::EstimateCompileInMs(
size_t ast_size_in_bytes) const {
base::LockGuard<base::Mutex> lock(&mutex_);
return Estimate(compile_events_, ast_size_in_bytes);
}
double CompilerDispatcherTracer::EstimateFinalizeCompilingInMs() {
double CompilerDispatcherTracer::EstimateFinalizeCompilingInMs() const {
base::LockGuard<base::Mutex> lock(&mutex_);
return Average(finalize_compiling_events_);
}
@ -158,7 +161,7 @@ double CompilerDispatcherTracer::Average(
double CompilerDispatcherTracer::Estimate(
const base::RingBuffer<std::pair<size_t, double>>& buffer, size_t num) {
if (buffer.Count() == 0) return 0.0;
if (buffer.Count() == 0) return kEstimatedRuntimeWithoutData;
std::pair<size_t, double> sum = buffer.Sum(
[](std::pair<size_t, double> a, std::pair<size_t, double> b) {
return std::make_pair(a.first + b.first, a.second + b.second);

View File

@ -69,10 +69,10 @@ class V8_EXPORT_PRIVATE CompilerDispatcherTracer {
double EstimatePrepareToParseInMs() const;
double EstimateParseInMs(size_t source_length) const;
double EstimateFinalizeParsingInMs();
double EstimatePrepareToCompileInMs();
double EstimateCompileInMs(size_t ast_size_in_bytes);
double EstimateFinalizeCompilingInMs();
double EstimateFinalizeParsingInMs() const;
double EstimatePrepareToCompileInMs() const;
double EstimateCompileInMs(size_t ast_size_in_bytes) const;
double EstimateFinalizeCompilingInMs() const;
private:
static double Average(const base::RingBuffer<double>& buffer);

View File

@ -4,6 +4,10 @@
#include "src/compiler-dispatcher/compiler-dispatcher.h"
#include "include/v8-platform.h"
#include "include/v8.h"
#include "src/base/platform/time.h"
#include "src/cancelable-task.h"
#include "src/compiler-dispatcher/compiler-dispatcher-job.h"
#include "src/compiler-dispatcher/compiler-dispatcher-tracer.h"
#include "src/objects-inl.h"
@ -13,7 +17,12 @@ namespace internal {
namespace {
bool DoNextStepOnMainThread(CompilerDispatcherJob* job) {
enum class ExceptionHandling { kSwallow, kThrow };
bool DoNextStepOnMainThread(Isolate* isolate, CompilerDispatcherJob* job,
ExceptionHandling exception_handling) {
DCHECK(ThreadId::Current().Equals(isolate->thread_id()));
v8::TryCatch try_catch(reinterpret_cast<v8::Isolate*>(isolate));
switch (job->status()) {
case CompileJobStatus::kInitial:
job->PrepareToParseOnMainThread();
@ -44,6 +53,11 @@ bool DoNextStepOnMainThread(CompilerDispatcherJob* job) {
break;
}
if (exception_handling == ExceptionHandling::kThrow &&
try_catch.HasCaught()) {
DCHECK(job->status() == CompileJobStatus::kFailed);
try_catch.ReThrow();
}
return job->status() != CompileJobStatus::kFailed;
}
@ -52,14 +66,48 @@ bool IsFinished(CompilerDispatcherJob* job) {
job->status() == CompileJobStatus::kFailed;
}
// Theoretically we get 50ms of idle time max, however it's unlikely that
// we'll get all of it so try to be a conservative.
const double kMaxIdleTimeToExpectInMs = 40;
} // namespace
CompilerDispatcher::CompilerDispatcher(Isolate* isolate, size_t max_stack_size)
: isolate_(isolate),
max_stack_size_(max_stack_size),
tracer_(new CompilerDispatcherTracer(isolate_)) {}
class CompilerDispatcher::IdleTask : public CancelableIdleTask {
public:
IdleTask(Isolate* isolate, CompilerDispatcher* dispatcher);
~IdleTask() override;
CompilerDispatcher::~CompilerDispatcher() {}
// CancelableIdleTask implementation.
void RunInternal(double deadline_in_seconds) override;
private:
CompilerDispatcher* dispatcher_;
DISALLOW_COPY_AND_ASSIGN(IdleTask);
};
CompilerDispatcher::IdleTask::IdleTask(Isolate* isolate,
CompilerDispatcher* dispatcher)
: CancelableIdleTask(isolate), dispatcher_(dispatcher) {}
CompilerDispatcher::IdleTask::~IdleTask() {}
void CompilerDispatcher::IdleTask::RunInternal(double deadline_in_seconds) {
dispatcher_->DoIdleWork(deadline_in_seconds);
}
CompilerDispatcher::CompilerDispatcher(Isolate* isolate, Platform* platform,
size_t max_stack_size)
: isolate_(isolate),
platform_(platform),
max_stack_size_(max_stack_size),
tracer_(new CompilerDispatcherTracer(isolate_)),
idle_task_scheduled_(false) {}
CompilerDispatcher::~CompilerDispatcher() {
// To avoid crashing in unit tests due to unfished jobs.
AbortAll(BlockingBehavior::kBlock);
}
bool CompilerDispatcher::Enqueue(Handle<SharedFunctionInfo> function) {
// We only handle functions (no eval / top-level code / wasm) that are
@ -75,6 +123,7 @@ bool CompilerDispatcher::Enqueue(Handle<SharedFunctionInfo> function) {
std::pair<int, int> key(Script::cast(function->script())->id(),
function->function_literal_id());
jobs_.insert(std::make_pair(key, std::move(job)));
ScheduleIdleTaskIfNeeded();
return true;
}
@ -89,9 +138,11 @@ bool CompilerDispatcher::FinishNow(Handle<SharedFunctionInfo> function) {
// TODO(jochen): Check if there's an in-flight background task working on this
// job.
while (!IsFinished(job->second.get())) {
DoNextStepOnMainThread(job->second.get());
DoNextStepOnMainThread(isolate_, job->second.get(),
ExceptionHandling::kThrow);
}
bool result = job->second->status() != CompileJobStatus::kFailed;
job->second->ResetOnMainThread();
jobs_.erase(job);
return result;
}
@ -104,6 +155,7 @@ void CompilerDispatcher::Abort(Handle<SharedFunctionInfo> function,
// TODO(jochen): Check if there's an in-flight background task working on this
// job.
job->second->ResetOnMainThread();
jobs_.erase(job);
}
@ -111,6 +163,9 @@ void CompilerDispatcher::AbortAll(BlockingBehavior blocking) {
USE(blocking);
// TODO(jochen): Check if there's an in-flight background task working on this
// job.
for (auto& kv : jobs_) {
kv.second->ResetOnMainThread();
}
jobs_.clear();
}
@ -126,5 +181,54 @@ CompilerDispatcher::JobMap::const_iterator CompilerDispatcher::GetJobFor(
return jobs_.end();
}
void CompilerDispatcher::ScheduleIdleTaskIfNeeded() {
v8::Isolate* v8_isolate = reinterpret_cast<v8::Isolate*>(isolate_);
if (!platform_->IdleTasksEnabled(v8_isolate)) return;
if (idle_task_scheduled_) return;
if (jobs_.empty()) return;
idle_task_scheduled_ = true;
platform_->CallIdleOnForegroundThread(v8_isolate,
new IdleTask(isolate_, this));
}
void CompilerDispatcher::DoIdleWork(double deadline_in_seconds) {
idle_task_scheduled_ = false;
// Number of jobs that are unlikely to make progress during any idle callback
// due to their estimated duration.
size_t too_long_jobs = 0;
// Iterate over all available jobs & remaining time. For each job, decide
// whether to 1) skip it (if it would take too long), 2) erase it (if it's
// finished), or 3) make progress on it.
double idle_time_in_seconds =
deadline_in_seconds - platform_->MonotonicallyIncreasingTime();
for (auto job = jobs_.begin();
job != jobs_.end() && idle_time_in_seconds > 0.0;
idle_time_in_seconds =
deadline_in_seconds - platform_->MonotonicallyIncreasingTime()) {
double estimate_in_ms = job->second->EstimateRuntimeOfNextStepInMs();
if (idle_time_in_seconds <
(estimate_in_ms /
static_cast<double>(base::Time::kMillisecondsPerSecond))) {
// If there's not enough time left, try to estimate whether we would
// have managed to finish the job in a large idle task to assess
// whether we should ask for another idle callback.
if (estimate_in_ms > kMaxIdleTimeToExpectInMs) ++too_long_jobs;
++job;
} else if (IsFinished(job->second.get())) {
job->second->ResetOnMainThread();
job = jobs_.erase(job);
break;
} else {
// Do one step, and keep processing the job (as we don't advance the
// iterator).
DoNextStepOnMainThread(isolate_, job->second.get(),
ExceptionHandling::kSwallow);
}
}
if (jobs_.size() > too_long_jobs) ScheduleIdleTaskIfNeeded();
}
} // namespace internal
} // namespace v8

View File

@ -11,8 +11,12 @@
#include "src/base/macros.h"
#include "src/globals.h"
#include "testing/gtest/include/gtest/gtest_prod.h"
namespace v8 {
class Platform;
namespace internal {
class CompilerDispatcherJob;
@ -23,11 +27,14 @@ class SharedFunctionInfo;
template <typename T>
class Handle;
// The CompilerDispatcher uses a combination of idle tasks and background tasks
// to parse and compile lazily parsed functions.
class V8_EXPORT_PRIVATE CompilerDispatcher {
public:
enum class BlockingBehavior { kBlock, kDontBlock };
CompilerDispatcher(Isolate* isolate, size_t max_stack_size);
CompilerDispatcher(Isolate* isolate, Platform* platform,
size_t max_stack_size);
~CompilerDispatcher();
// Returns true if a job was enqueued.
@ -47,15 +54,24 @@ class V8_EXPORT_PRIVATE CompilerDispatcher {
void AbortAll(BlockingBehavior blocking);
private:
FRIEND_TEST(CompilerDispatcherTest, IdleTaskSmallIdleTime);
typedef std::multimap<std::pair<int, int>,
std::unique_ptr<CompilerDispatcherJob>>
JobMap;
class IdleTask;
JobMap::const_iterator GetJobFor(Handle<SharedFunctionInfo> shared) const;
void ScheduleIdleTaskIfNeeded();
void DoIdleWork(double deadline_in_seconds);
Isolate* isolate_;
Platform* platform_;
size_t max_stack_size_;
std::unique_ptr<CompilerDispatcherTracer> tracer_;
bool idle_task_scheduled_;
// Mapping from (script id, function literal id) to job. We use a multimap,
// as script id is not necessarily unique.
JobMap jobs_;

View File

@ -2518,7 +2518,8 @@ bool Isolate::Init(Deserializer* des) {
cpu_profiler_ = new CpuProfiler(this);
heap_profiler_ = new HeapProfiler(heap());
interpreter_ = new interpreter::Interpreter(this);
compiler_dispatcher_ = new CompilerDispatcher(this, FLAG_stack_size);
compiler_dispatcher_ =
new CompilerDispatcher(this, V8::GetCurrentPlatform(), FLAG_stack_size);
// Enable logging before setting up the heap
logger_->SetUp(this);

View File

@ -8,16 +8,16 @@
namespace v8 {
namespace internal {
TEST(CompilerDispatcherTracerTest, EstimateZeroWithoutSamples) {
TEST(CompilerDispatcherTracerTest, EstimateWithoutSamples) {
CompilerDispatcherTracer tracer(nullptr);
EXPECT_EQ(0.0, tracer.EstimatePrepareToParseInMs());
EXPECT_EQ(0.0, tracer.EstimateParseInMs(0));
EXPECT_EQ(0.0, tracer.EstimateParseInMs(42));
EXPECT_EQ(1.0, tracer.EstimateParseInMs(0));
EXPECT_EQ(1.0, tracer.EstimateParseInMs(42));
EXPECT_EQ(0.0, tracer.EstimateFinalizeParsingInMs());
EXPECT_EQ(0.0, tracer.EstimatePrepareToCompileInMs());
EXPECT_EQ(0.0, tracer.EstimateCompileInMs(0));
EXPECT_EQ(0.0, tracer.EstimateCompileInMs(42));
EXPECT_EQ(1.0, tracer.EstimateCompileInMs(0));
EXPECT_EQ(1.0, tracer.EstimateCompileInMs(42));
EXPECT_EQ(0.0, tracer.EstimateFinalizeCompilingInMs());
}
@ -36,7 +36,7 @@ TEST(CompilerDispatcherTracerTest, Average) {
TEST(CompilerDispatcherTracerTest, SizeBasedAverage) {
CompilerDispatcherTracer tracer(nullptr);
EXPECT_EQ(0.0, tracer.EstimateParseInMs(100));
EXPECT_EQ(1.0, tracer.EstimateParseInMs(100));
// All three samples parse 100 units/ms.
tracer.RecordParse(1.0, 100);

View File

@ -2,9 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include "src/compiler-dispatcher/compiler-dispatcher.h"
#include "include/v8-platform.h"
#include "src/compiler-dispatcher/compiler-dispatcher-job.h"
#include "src/flags.h"
#include "src/handles.h"
#include "src/objects-inl.h"
@ -17,45 +18,190 @@ namespace internal {
typedef TestWithContext CompilerDispatcherTest;
namespace {
class MockPlatform : public v8::Platform {
public:
MockPlatform() : task_(nullptr), time_(0.0), time_step_(0.0) {}
~MockPlatform() override = default;
void CallOnBackgroundThread(Task* task,
ExpectedRuntime expected_runtime) override {
UNREACHABLE();
}
void CallOnForegroundThread(v8::Isolate* isolate, Task* task) override {
UNREACHABLE();
}
void CallDelayedOnForegroundThread(v8::Isolate* isolate, Task* task,
double delay_in_seconds) override {
UNREACHABLE();
}
void CallIdleOnForegroundThread(v8::Isolate* isolate,
IdleTask* task) override {
task_ = task;
}
bool IdleTasksEnabled(v8::Isolate* isolate) override { return true; }
double MonotonicallyIncreasingTime() override {
time_ += time_step_;
return time_;
}
void RunIdleTask(double deadline_in_seconds, double time_step) {
ASSERT_TRUE(task_ != nullptr);
time_step_ = time_step;
IdleTask* task = task_;
task_ = nullptr;
task->Run(deadline_in_seconds);
delete task;
}
bool IdleTaskPending() const { return !!task_; }
private:
IdleTask* task_;
double time_;
double time_step_;
DISALLOW_COPY_AND_ASSIGN(MockPlatform);
};
} // namespace
TEST_F(CompilerDispatcherTest, Construct) {
std::unique_ptr<CompilerDispatcher> dispatcher(
new CompilerDispatcher(i_isolate(), FLAG_stack_size));
MockPlatform platform;
CompilerDispatcher dispatcher(i_isolate(), &platform, FLAG_stack_size);
}
TEST_F(CompilerDispatcherTest, IsEnqueued) {
std::unique_ptr<CompilerDispatcher> dispatcher(
new CompilerDispatcher(i_isolate(), FLAG_stack_size));
MockPlatform platform;
CompilerDispatcher dispatcher(i_isolate(), &platform, FLAG_stack_size);
const char script[] =
"function g() { var y = 1; function f(x) { return x * y }; return f; } "
"function g() { var y = 1; function f1(x) { return x * y }; return f1; } "
"g();";
Handle<JSFunction> f = Handle<JSFunction>::cast(RunJS(isolate(), script));
Handle<SharedFunctionInfo> shared(f->shared(), i_isolate());
ASSERT_FALSE(dispatcher->IsEnqueued(shared));
ASSERT_TRUE(dispatcher->Enqueue(shared));
ASSERT_TRUE(dispatcher->IsEnqueued(shared));
dispatcher->Abort(shared, CompilerDispatcher::BlockingBehavior::kBlock);
ASSERT_FALSE(dispatcher->IsEnqueued(shared));
ASSERT_FALSE(dispatcher.IsEnqueued(shared));
ASSERT_TRUE(dispatcher.Enqueue(shared));
ASSERT_TRUE(dispatcher.IsEnqueued(shared));
dispatcher.Abort(shared, CompilerDispatcher::BlockingBehavior::kBlock);
ASSERT_FALSE(dispatcher.IsEnqueued(shared));
}
TEST_F(CompilerDispatcherTest, FinishNow) {
std::unique_ptr<CompilerDispatcher> dispatcher(
new CompilerDispatcher(i_isolate(), FLAG_stack_size));
MockPlatform platform;
CompilerDispatcher dispatcher(i_isolate(), &platform, FLAG_stack_size);
const char script[] =
"function g() { var y = 1; function f(x) { return x * y }; return f; } "
"function g() { var y = 1; function f2(x) { return x * y }; return f2; } "
"g();";
Handle<JSFunction> f = Handle<JSFunction>::cast(RunJS(isolate(), script));
Handle<SharedFunctionInfo> shared(f->shared(), i_isolate());
ASSERT_FALSE(shared->HasBaselineCode());
ASSERT_TRUE(dispatcher->Enqueue(shared));
ASSERT_TRUE(dispatcher->FinishNow(shared));
ASSERT_TRUE(dispatcher.Enqueue(shared));
ASSERT_TRUE(dispatcher.FinishNow(shared));
// Finishing removes the SFI from the queue.
ASSERT_FALSE(dispatcher->IsEnqueued(shared));
ASSERT_FALSE(dispatcher.IsEnqueued(shared));
ASSERT_TRUE(shared->HasBaselineCode());
}
TEST_F(CompilerDispatcherTest, IdleTask) {
MockPlatform platform;
CompilerDispatcher dispatcher(i_isolate(), &platform, FLAG_stack_size);
const char script[] =
"function g() { var y = 1; function f3(x) { return x * y }; return f3; } "
"g();";
Handle<JSFunction> f = Handle<JSFunction>::cast(RunJS(isolate(), script));
Handle<SharedFunctionInfo> shared(f->shared(), i_isolate());
ASSERT_FALSE(platform.IdleTaskPending());
ASSERT_TRUE(dispatcher.Enqueue(shared));
ASSERT_TRUE(platform.IdleTaskPending());
// Since time doesn't progress on the MockPlatform, this is enough idle time
// to finish compiling the function.
platform.RunIdleTask(1000.0, 0.0);
ASSERT_FALSE(dispatcher.IsEnqueued(shared));
ASSERT_TRUE(shared->HasBaselineCode());
}
TEST_F(CompilerDispatcherTest, IdleTaskSmallIdleTime) {
MockPlatform platform;
CompilerDispatcher dispatcher(i_isolate(), &platform, FLAG_stack_size);
const char script[] =
"function g() { var y = 1; function f4(x) { return x * y }; return f4; } "
"g();";
Handle<JSFunction> f = Handle<JSFunction>::cast(RunJS(isolate(), script));
Handle<SharedFunctionInfo> shared(f->shared(), i_isolate());
ASSERT_FALSE(platform.IdleTaskPending());
ASSERT_TRUE(dispatcher.Enqueue(shared));
ASSERT_TRUE(platform.IdleTaskPending());
// The job should be scheduled for the main thread.
ASSERT_EQ(dispatcher.jobs_.size(), 1u);
ASSERT_TRUE(dispatcher.jobs_.begin()->second->status() ==
CompileJobStatus::kInitial);
// Only grant a little idle time and have time advance beyond it in one step.
platform.RunIdleTask(2.0, 1.0);
ASSERT_TRUE(dispatcher.IsEnqueued(shared));
ASSERT_FALSE(shared->HasBaselineCode());
ASSERT_TRUE(platform.IdleTaskPending());
// The job should be still scheduled for the main thread, but ready for
// parsing.
ASSERT_EQ(dispatcher.jobs_.size(), 1u);
ASSERT_TRUE(dispatcher.jobs_.begin()->second->status() ==
CompileJobStatus::kReadyToParse);
// Only grant a lot of idle time and freeze time.
platform.RunIdleTask(1000.0, 0.0);
ASSERT_FALSE(dispatcher.IsEnqueued(shared));
ASSERT_TRUE(shared->HasBaselineCode());
ASSERT_FALSE(platform.IdleTaskPending());
}
TEST_F(CompilerDispatcherTest, IdleTaskException) {
MockPlatform platform;
CompilerDispatcher dispatcher(i_isolate(), &platform, 50);
std::string script("function g() { function f5(x) { var a = ");
for (int i = 0; i < 1000; i++) {
script += "'x' + ";
}
script += " 'x'; }; return f5; } g();";
Handle<JSFunction> f =
Handle<JSFunction>::cast(RunJS(isolate(), script.c_str()));
Handle<SharedFunctionInfo> shared(f->shared(), i_isolate());
ASSERT_FALSE(platform.IdleTaskPending());
ASSERT_TRUE(dispatcher.Enqueue(shared));
ASSERT_TRUE(platform.IdleTaskPending());
// Idle tasks shouldn't leave exceptions behind.
v8::TryCatch try_catch(isolate());
// Since time doesn't progress on the MockPlatform, this is enough idle time
// to finish compiling the function.
platform.RunIdleTask(1000.0, 0.0);
ASSERT_FALSE(dispatcher.IsEnqueued(shared));
ASSERT_FALSE(shared->HasBaselineCode());
ASSERT_FALSE(try_catch.HasCaught());
}
} // namespace internal
} // namespace v8