0

gin: Updates v8-platform based on v8 changes

This change (and the v8 one) are motivated to improve the performance
of Date.now, which shows up in a MotionMark story.

The v8 change:
https://chromium-review.googlesource.com/c/v8/v8/+/4246257

Adds two time functions (the goal is to eventually remove the
existing one):
  virtual int64_t CurrentClockTimeMilliseconds();
  virtual double CurrentClockTimeMillisecondsHighResolution();

Both return millisecond accuracy, with the former returning an
integer value to make it clear precision is milliseconds.

The expensive part of Date.now() is the jitter code, which for
millisecond precision is only necessary when within kResolutionMillis
of a millisecond boundary. This patch updates the code to only
call to jitter when it actually matters.

This patch also converts TimeClamper to use integer values, and it
moves it into its own class to make it easy to test.

Bug: 1414615

Change-Id: Icff8637d45bfc39d544016f1236ed1cd16b4a0fb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4242478
Reviewed-by: Leszek Swirski <leszeks@chromium.org>
Commit-Queue: Scott Violet <sky@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1107174}
This commit is contained in:
Scott Violet
2023-02-18 15:29:32 +00:00
committed by Chromium LUCI CQ
parent 86c5707fde
commit 787d18548d
6 changed files with 251 additions and 68 deletions

@ -55,6 +55,8 @@ component("gin") {
"runner.h",
"shell_runner.cc",
"shell_runner.h",
"time_clamper.cc",
"time_clamper.h",
"try_catch.cc",
"try_catch.h",
"v8_foreground_task_runner.cc",
@ -193,6 +195,7 @@ test("gin_unittests") {
"per_context_data_unittest.cc",
"shell_runner_unittest.cc",
"test/run_all_unittests.cc",
"time_clamper_unittest.cc",
"v8_isolate_memory_dump_provider_unittest.cc",
"v8_platform_unittest.cc",
"v8_shared_memory_dump_provider_unittest.cc",

@ -9,6 +9,7 @@
#include "base/compiler_specific.h"
#include "base/lazy_instance.h"
#include "gin/gin_export.h"
#include "gin/time_clamper.h"
#include "gin/v8_platform_page_allocator.h"
#include "v8/include/v8-platform.h"
@ -49,6 +50,8 @@ class GIN_EXPORT V8Platform : public v8::Platform {
bool IdleTasksEnabled(v8::Isolate* isolate) override;
double MonotonicallyIncreasingTime() override;
double CurrentClockTimeMillis() override;
int64_t CurrentClockTimeMilliseconds() override;
double CurrentClockTimeMillisecondsHighResolution() override;
StackTracePrinter GetStackTracePrinter() override;
v8::TracingController* GetTracingController() override;
@ -60,6 +63,7 @@ class GIN_EXPORT V8Platform : public v8::Platform {
class TracingControllerImpl;
std::unique_ptr<TracingControllerImpl> tracing_controller_;
TimeClamper time_clamper_;
};
} // namespace gin

20
gin/time_clamper.cc Normal file

@ -0,0 +1,20 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "gin/time_clamper.h"
#include "build/build_config.h"
namespace gin {
// As site isolation is enabled on desktop platforms, we can safely provide
// more timing resolution. Jittering is still enabled everywhere.
#if BUILDFLAG(IS_ANDROID)
// static
const int64_t TimeClamper::kResolutionMicros = 100;
#else
const int64_t TimeClamper::kResolutionMicros = 5;
#endif
} // namespace gin

116
gin/time_clamper.h Normal file

@ -0,0 +1,116 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef GIN_TIME_CLAMPER_H_
#define GIN_TIME_CLAMPER_H_
#include <algorithm>
#include "base/rand_util.h"
#include "base/time/time.h"
#include "gin/gin_export.h"
namespace gin {
// This class adds some amount of jitter to time. That is, for every
// `kResolutionMicros` microseconds it calculates a threshold (using a hash)
// that once exceeded advances to the next threshold. This is done so that
// time jumps slightly and does not move smoothly.
//
// NOTE: the implementation assumes it's used for servicing calls from JS,
// which uses the unix-epoch at time 0.
// TODO(skyostil): Deduplicate this with the clamper in Blink.
class GIN_EXPORT TimeClamper {
public:
// Public for tests.
static const int64_t kResolutionMicros;
TimeClamper() : secret_(base::RandUint64()) {}
// This constructor should only be used in tests.
explicit TimeClamper(uint64_t secret) : secret_(secret) {}
TimeClamper(const TimeClamper&) = delete;
TimeClamper& operator=(const TimeClamper&) = delete;
~TimeClamper() = default;
// Clamps a time to millisecond precision. The return value is in milliseconds
// relative to unix-epoch (which is what JS uses).
inline int64_t ClampToMillis(base::Time time) const {
// Adding jitter is non-trivial, only use it if necessary.
// ClampTimeResolution() adjusts the time to land on `kResolutionMicros`
// boundaries, and either uses the current `kResolutionMicros` boundary, or
// the next one. Because `kResolutionMicros` is smaller than 1ms, and this
// function returns millisecond accuracy, ClampTimeResolution() is only
// necessary when within `kResolutionMicros` of the next millisecond.
const int64_t now_micros =
(time - base::Time::UnixEpoch()).InMicroseconds();
const int64_t micros = now_micros % 1000;
// abs() is necessary for devices with times before unix-epoch (most likely
// configured incorrectly).
if (abs(micros) + kResolutionMicros < 1000) {
return now_micros / 1000;
}
return ClampTimeResolution(now_micros) / 1000;
}
// Clamps the time, giving microsecond precision. The return value is in
// milliseconds relative to unix-epoch (which is what JS uses).
inline double ClampToMillisHighResolution(base::Time now) const {
const int64_t clamped_time =
ClampTimeResolution((now - base::Time::UnixEpoch()).InMicroseconds());
return static_cast<double>(clamped_time) / 1000.0;
}
private:
inline int64_t ClampTimeResolution(int64_t time_micros) const {
if (time_micros < 0) {
return -ClampTimeResolutionPositiveValue(-time_micros);
}
return ClampTimeResolutionPositiveValue(time_micros);
}
inline int64_t ClampTimeResolutionPositiveValue(int64_t time_micros) const {
DCHECK_GE(time_micros, 0u);
// For each clamped time interval, compute a pseudorandom transition
// threshold. The reported time will either be the start of that interval or
// the next one depending on which side of the threshold |time_seconds| is.
const int64_t interval = time_micros / kResolutionMicros;
const int64_t clamped_time_micros = interval * kResolutionMicros;
const int64_t tick_threshold = ThresholdFor(clamped_time_micros);
if (time_micros - clamped_time_micros < tick_threshold) {
return clamped_time_micros;
}
return clamped_time_micros + kResolutionMicros;
}
inline int64_t ThresholdFor(int64_t clamped_time) const {
// Returns a random value between 0 and kResolutionMicros. The distribution
// is not necessarily equal, but for a random value it's good enough.
// Avoid floating-point math by rewriting:
// (random_value * 1.0 / UINT64_MAX) * kResolutionMicros
// into:
// random_value / (UINT64_MAX / kResolutionMicros)
// where we avoid integer overflow by dividing instead of multiplying.
const uint64_t random_value = MurmurHash3(clamped_time ^ secret_);
return std::min(static_cast<int64_t>(random_value /
(std::numeric_limits<uint64_t>::max() /
kResolutionMicros)),
kResolutionMicros);
}
static inline uint64_t MurmurHash3(uint64_t value) {
value ^= value >> 33;
value *= uint64_t{0xFF51AFD7ED558CCD};
value ^= value >> 33;
value *= uint64_t{0xC4CEB9FE1A85EC53};
value ^= value >> 33;
return value;
}
const uint64_t secret_;
};
} // namespace gin
#endif // GIN_TIME_CLAMPER_H_

@ -0,0 +1,99 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "gin/time_clamper.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace gin {
TEST(TimeClamperTest, CurrentClockTimeMilliseconds) {
TimeClamper clamper(1);
EXPECT_EQ(0u, clamper.ClampToMillis(base::Time::UnixEpoch()));
// 1 us after unix epoch should round to 0.
base::Time time = base::Time::UnixEpoch() + base::Microseconds(1);
EXPECT_EQ(0u, clamper.ClampToMillis(time));
time = base::Time::UnixEpoch() + base::Milliseconds(1) -
base::Microseconds(TimeClamper::kResolutionMicros - 1);
EXPECT_EQ(0u, clamper.ClampToMillis(time));
time = base::Time::UnixEpoch() + base::Milliseconds(1);
EXPECT_EQ(1u, clamper.ClampToMillis(time));
}
#if !BUILDFLAG(IS_ANDROID)
TEST(TimeClamperTest, CurrentClockTimeMillisecondsThreshold) {
// This test assumes a time clamp of 5. If the clamp changes the time values
// will need to be adjusted.
ASSERT_EQ(5, TimeClamper::kResolutionMicros);
TimeClamper clamper(1);
base::Time time = base::Time::UnixEpoch() + base::Milliseconds(1) -
base::Microseconds(TimeClamper::kResolutionMicros);
EXPECT_EQ(0u, clamper.ClampToMillis(time));
time = base::Time::UnixEpoch() + base::Milliseconds(1) -
base::Microseconds(TimeClamper::kResolutionMicros) +
base::Microseconds(1);
EXPECT_EQ(0u, clamper.ClampToMillis(time));
time = base::Time::UnixEpoch() + base::Milliseconds(1) -
base::Microseconds(TimeClamper::kResolutionMicros) +
base::Microseconds(2);
EXPECT_EQ(1u, clamper.ClampToMillis(time));
}
TEST(TimeClamperTest, CurrentClockTimeMillisecondsThresholdNegative) {
// This test assumes a time clamp of 5. If the clamp changes the time values
// will need to be adjusted.
// be adjusted.
ASSERT_EQ(5, TimeClamper::kResolutionMicros);
TimeClamper clamper(1);
base::Time time = base::Time::UnixEpoch() + base::Milliseconds(-1) +
base::Microseconds(TimeClamper::kResolutionMicros);
EXPECT_EQ(0u, clamper.ClampToMillis(time));
time = base::Time::UnixEpoch() + base::Milliseconds(-1) +
base::Microseconds(TimeClamper::kResolutionMicros) -
base::Microseconds(1);
EXPECT_EQ(0u, clamper.ClampToMillis(time));
time = base::Time::UnixEpoch() + base::Milliseconds(-1) +
base::Microseconds(TimeClamper::kResolutionMicros) -
base::Microseconds(2);
EXPECT_EQ(static_cast<int64_t>(-1), clamper.ClampToMillis(time));
}
TEST(TimeClamperTest, CurrentClockTimeMillisecondsHighResolution) {
TimeClamper clamper(1);
EXPECT_EQ(0, clamper.ClampToMillisHighResolution(base::Time::UnixEpoch()));
base::Time time = base::Time::UnixEpoch() + base::Microseconds(1);
EXPECT_EQ(0, clamper.ClampToMillisHighResolution(time));
time = base::Time::UnixEpoch() +
base::Microseconds(TimeClamper::kResolutionMicros);
EXPECT_EQ(0.005, clamper.ClampToMillisHighResolution(time));
time = base::Time::UnixEpoch() + base::Milliseconds(1) -
base::Microseconds(TimeClamper::kResolutionMicros - 1);
EXPECT_EQ(0.995, clamper.ClampToMillisHighResolution(time));
time = base::Time::UnixEpoch() + base::Milliseconds(1) -
base::Microseconds(TimeClamper::kResolutionMicros - 3);
EXPECT_EQ(1, clamper.ClampToMillisHighResolution(time));
time = base::Time::UnixEpoch() + base::Milliseconds(1);
EXPECT_EQ(1, clamper.ClampToMillisHighResolution(time));
time =
base::Time::UnixEpoch() + base::Milliseconds(1) + base::Microseconds(1);
EXPECT_EQ(1.005, clamper.ClampToMillisHighResolution(time));
}
#endif
} // namespace gin

@ -14,7 +14,6 @@
#include "base/location.h"
#include "base/memory/nonscannable_memory.h"
#include "base/memory/raw_ptr.h"
#include "base/rand_util.h"
#include "base/system/sys_info.h"
#include "base/task/post_job.h"
#include "base/task/task_traits.h"
@ -123,71 +122,6 @@ base::LazyInstance<EnabledStateObserverImpl>::Leaky g_trace_state_dispatcher =
LAZY_INSTANCE_INITIALIZER;
#endif // !BUILDFLAG(USE_PERFETTO_CLIENT_LIBRARY)
// TODO(skyostil): Deduplicate this with the clamper in Blink.
class TimeClamper {
public:
// As site isolation is enabled on desktop platforms, we can safely provide
// more timing resolution. Jittering is still enabled everywhere.
#if BUILDFLAG(IS_ANDROID)
static constexpr double kResolutionSeconds = 100e-6;
#else
static constexpr double kResolutionSeconds = 5e-6;
#endif
TimeClamper() : secret_(base::RandUint64()) {}
TimeClamper(const TimeClamper&) = delete;
TimeClamper& operator=(const TimeClamper&) = delete;
double ClampTimeResolution(double time_seconds) const {
bool was_negative = false;
if (time_seconds < 0) {
was_negative = true;
time_seconds = -time_seconds;
}
// For each clamped time interval, compute a pseudorandom transition
// threshold. The reported time will either be the start of that interval or
// the next one depending on which side of the threshold |time_seconds| is.
double interval = floor(time_seconds / kResolutionSeconds);
double clamped_time = interval * kResolutionSeconds;
double tick_threshold = ThresholdFor(clamped_time);
if (time_seconds >= tick_threshold)
clamped_time = (interval + 1) * kResolutionSeconds;
if (was_negative)
clamped_time = -clamped_time;
return clamped_time;
}
private:
inline double ThresholdFor(double clamped_time) const {
uint64_t time_hash =
MurmurHash3(base::bit_cast<int64_t>(clamped_time) ^ secret_);
return clamped_time + kResolutionSeconds * ToDouble(time_hash);
}
static inline double ToDouble(uint64_t value) {
// Exponent for double values for [1.0 .. 2.0]
static const uint64_t kExponentBits = uint64_t{0x3FF0000000000000};
static const uint64_t kMantissaMask = uint64_t{0x000FFFFFFFFFFFFF};
uint64_t random = (value & kMantissaMask) | kExponentBits;
return base::bit_cast<double>(random) - 1;
}
static inline uint64_t MurmurHash3(uint64_t value) {
value ^= value >> 33;
value *= uint64_t{0xFF51AFD7ED558CCD};
value ^= value >> 33;
value *= uint64_t{0xC4CEB9FE1A85EC53};
value ^= value >> 33;
return value;
}
const uint64_t secret_;
};
base::LazyInstance<TimeClamper>::Leaky g_time_clamper =
LAZY_INSTANCE_INITIALIZER;
#if BUILDFLAG(USE_PARTITION_ALLOC)
base::LazyInstance<gin::PageAllocator>::Leaky g_page_allocator =
@ -480,8 +414,15 @@ double V8Platform::MonotonicallyIncreasingTime() {
}
double V8Platform::CurrentClockTimeMillis() {
double now_seconds = base::Time::Now().ToJsTime() / 1000;
return g_time_clamper.Get().ClampTimeResolution(now_seconds) * 1000;
return static_cast<double>(time_clamper_.ClampToMillis(base::Time::Now()));
}
int64_t V8Platform::CurrentClockTimeMilliseconds() {
return time_clamper_.ClampToMillis(base::Time::Now());
}
double V8Platform::CurrentClockTimeMillisecondsHighResolution() {
return time_clamper_.ClampToMillisHighResolution(base::Time::Now());
}
v8::TracingController* V8Platform::GetTracingController() {