diff --git a/Polyfills/Performance/Source/Performance.cpp b/Polyfills/Performance/Source/Performance.cpp index 6fa142d6..b2e4a729 100644 --- a/Polyfills/Performance/Source/Performance.cpp +++ b/Polyfills/Performance/Source/Performance.cpp @@ -6,16 +6,6 @@ namespace { constexpr const char* JS_INSTANCE_NAME{"performance"}; - - // Store the start time when the polyfill is initialized - std::chrono::high_resolution_clock::time_point g_startTime; - - Napi::Value Now(const Napi::CallbackInfo& info) - { - auto now = std::chrono::high_resolution_clock::now(); - auto duration = std::chrono::duration(now - g_startTime); - return Napi::Number::New(info.Env(), duration.count()); - } } namespace Babylon::Polyfills::Performance @@ -24,9 +14,6 @@ namespace Babylon::Polyfills::Performance { Napi::HandleScope scope{env}; - // Initialize the start time - g_startTime = std::chrono::high_resolution_clock::now(); - auto performance = env.Global().Get(JS_INSTANCE_NAME).As(); if (!performance.IsUndefined()) { @@ -36,6 +23,13 @@ namespace Babylon::Polyfills::Performance performance = Napi::Object::New(env); env.Global().Set(JS_INSTANCE_NAME, performance); - performance.Set("now", Napi::Function::New(env, Now, "now")); + // The time origin belongs to this environment, as it does to each browsing context and + // worker: a process-wide origin would be reset by every runtime (worker) that initializes + // the polyfill, moving performance.now() backwards elsewhere and racing with it. + const auto timeOrigin = std::chrono::steady_clock::now(); + performance.Set("now", Napi::Function::New(env, [timeOrigin](const Napi::CallbackInfo& info) { + const auto elapsed = std::chrono::duration(std::chrono::steady_clock::now() - timeOrigin); + return Napi::Number::New(info.Env(), elapsed.count()); + }, "now")); } } diff --git a/Tests/UnitTests/CMakeLists.txt b/Tests/UnitTests/CMakeLists.txt index 69cc6cac..d5705a9f 100644 --- a/Tests/UnitTests/CMakeLists.txt +++ b/Tests/UnitTests/CMakeLists.txt @@ -28,6 +28,7 @@ set(SOURCES "Source/Tests.DelayedTaskScheduler.cpp" "Source/Tests.JavaScript.cpp" "Source/Tests.NodeApi.cpp" + "Source/Tests.Performance.cpp" "Source/Tests.Scheduling.cpp" "Source/Tests.StandardStreamLogger.cpp" "Source/Tests.TimeoutDispatcher.cpp") diff --git a/Tests/UnitTests/Source/Android/app/src/main/cpp/CMakeLists.txt b/Tests/UnitTests/Source/Android/app/src/main/cpp/CMakeLists.txt index aa933dba..7bdc9f11 100644 --- a/Tests/UnitTests/Source/Android/app/src/main/cpp/CMakeLists.txt +++ b/Tests/UnitTests/Source/Android/app/src/main/cpp/CMakeLists.txt @@ -27,6 +27,7 @@ add_library(UnitTestsJNI SHARED ${UNIT_TESTS_DIR}/Source/Tests.NodeApi.cpp ${UNIT_TESTS_DIR}/Source/Tests.Scheduling.cpp ${UNIT_TESTS_DIR}/Source/Tests.StandardStreamLogger.cpp + ${UNIT_TESTS_DIR}/Source/Tests.Performance.cpp ${UNIT_TESTS_DIR}/Source/Tests.TimeoutDispatcher.cpp) if(NAPI_JAVASCRIPT_ENGINE STREQUAL "V8") diff --git a/Tests/UnitTests/Source/Tests.Performance.cpp b/Tests/UnitTests/Source/Tests.Performance.cpp new file mode 100644 index 00000000..eb226894 --- /dev/null +++ b/Tests/UnitTests/Source/Tests.Performance.cpp @@ -0,0 +1,43 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +TEST(Performance, TimeOriginIsPerEnvironment) +{ + // Regression: performance.now() measured from one process-wide start time that every + // Initialize() rewrote, so bringing up a second runtime moved the first one's clock back to + // zero. Each environment keeps its own origin now, so the first clock stays monotonic. + const auto initialize = [](Babylon::AppRuntime& runtime) { + std::promise done; + runtime.Dispatch([&done](Napi::Env env) { + Babylon::Polyfills::Performance::Initialize(env); + done.set_value(); + }); + done.get_future().get(); + }; + const auto now = [](Babylon::AppRuntime& runtime) { + std::promise value; + runtime.Dispatch([&value](Napi::Env env) { + auto performance = env.Global().Get("performance").As(); + value.set_value(performance.Get("now").As().Call(performance, {}).As().DoubleValue()); + }); + return value.get_future().get(); + }; + + Babylon::AppRuntime first{}; + initialize(first); + std::this_thread::sleep_for(std::chrono::milliseconds{50}); + const double before = now(first); + + Babylon::AppRuntime second{}; + initialize(second); + const double after = now(first); + + EXPECT_GE(after, before); +}