diff options
Diffstat (limited to 'js/src/vm/Stopwatch.cpp')
-rw-r--r-- | js/src/vm/Stopwatch.cpp | 655 |
1 files changed, 655 insertions, 0 deletions
diff --git a/js/src/vm/Stopwatch.cpp b/js/src/vm/Stopwatch.cpp new file mode 100644 index 000000000..28632c2a1 --- /dev/null +++ b/js/src/vm/Stopwatch.cpp @@ -0,0 +1,655 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "vm/Stopwatch.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/IntegerTypeTraits.h" +#include "mozilla/Unused.h" + +#if defined(XP_WIN) +#include <processthreadsapi.h> +#include <windows.h> +#endif // defined(XP_WIN) + +#include "jscompartment.h" + +#include "gc/Zone.h" +#include "vm/Runtime.h" + +namespace js { + +bool +PerformanceMonitoring::addRecentGroup(PerformanceGroup* group) +{ + if (group->isUsedInThisIteration()) + return true; + + group->setIsUsedInThisIteration(true); + return recentGroups_.append(group); +} + +void +PerformanceMonitoring::reset() +{ + // All ongoing measures are dependent on the current iteration#. + // By incrementing it, we mark all data as stale. Stale data will + // be overwritten progressively during the execution. + ++iteration_; + recentGroups_.clear(); + + // Every so often, we will be rescheduled to another CPU. If this + // happens, we may end up with an entirely unsynchronized + // timestamp counter. If we do not reset + // `highestTimestampCounter_`, we could end up ignoring entirely + // valid sets of measures just because we are on a CPU that has a + // lower RDTSC. + highestTimestampCounter_ = 0; +} + +void +PerformanceMonitoring::start() +{ + if (!isMonitoringJank_) + return; + + if (iteration_ == startedAtIteration_) { + // The stopwatch is already started for this iteration. + return; + } + + startedAtIteration_ = iteration_; + if (stopwatchStartCallback) + stopwatchStartCallback(iteration_, stopwatchStartClosure); +} + +// Commit the data that has been collected during the iteration +// into the actual `PerformanceData`. +// +// We use the proportion of cycles-spent-in-group over +// cycles-spent-in-toplevel-group as an approximation to allocate +// system (kernel) time and user (CPU) time to each group. Note +// that cycles are not an exact measure: +// +// 1. if the computer has gone to sleep, the clock may be reset to 0; +// 2. if the process is moved between CPUs/cores, it may end up on a CPU +// or core with an unsynchronized clock; +// 3. the mapping between clock cycles and walltime varies with the current +// frequency of the CPU; +// 4. other threads/processes using the same CPU will also increment +// the counter. +// +// ** Effect of 1. (computer going to sleep) +// +// We assume that this will happen very seldom. Since the final numbers +// are bounded by the CPU time and Kernel time reported by `getresources`, +// the effect will be contained to a single iteration of the event loop. +// +// ** Effect of 2. (moving between CPUs/cores) +// +// On platforms that support it, we only measure the number of cycles +// if we start and end execution of a group on the same +// CPU/core. While there is a small window (a few cycles) during which +// the thread can be migrated without us noticing, we expect that this +// will happen rarely enough that this won't affect the statistics +// meaningfully. +// +// On other platforms, assuming that the probability of jumping +// between CPUs/cores during a given (real) cycle is constant, and +// that the distribution of differences between clocks is even, the +// probability that the number of cycles reported by a measure is +// modified by X cycles should be a gaussian distribution, with groups +// with longer execution having a larger amplitude than groups with +// shorter execution. Since we discard measures that result in a +// negative number of cycles, this distribution is actually skewed +// towards over-estimating the number of cycles of groups that already +// have many cycles and under-estimating the number of cycles that +// already have fewer cycles. +// +// Since the final numbers are bounded by the CPU time and Kernel time +// reported by `getresources`, we accept this bias. +// +// ** Effect of 3. (mapping between clock cycles and walltime) +// +// Assuming that this is evenly distributed, we expect that this will +// eventually balance out. +// +// ** Effect of 4. (cycles increase with system activity) +// +// Assuming that, within an iteration of the event loop, this happens +// unformly over time, this will skew towards over-estimating the number +// of cycles of groups that already have many cycles and under-estimating +// the number of cycles that already have fewer cycles. +// +// Since the final numbers are bounded by the CPU time and Kernel time +// reported by `getresources`, we accept this bias. +// +// ** Big picture +// +// Computing the number of cycles is fast and should be accurate +// enough in practice. Alternatives (such as calling `getresources` +// all the time or sampling from another thread) are very expensive +// in system calls and/or battery and not necessarily more accurate. +bool +PerformanceMonitoring::commit() +{ +#if !defined(MOZ_HAVE_RDTSC) + // The AutoStopwatch is only executed if `MOZ_HAVE_RDTSC`. + return false; +#endif // !defined(MOZ_HAVE_RDTSC) + + if (!isMonitoringJank_) { + // Either we have not started monitoring or monitoring has + // been cancelled during the iteration. + return true; + } + + if (startedAtIteration_ != iteration_) { + // No JS code has been monitored during this iteration. + return true; + } + + PerformanceGroupVector recentGroups; + recentGroups_.swap(recentGroups); + + bool success = true; + if (stopwatchCommitCallback) + success = stopwatchCommitCallback(iteration_, recentGroups, stopwatchCommitClosure); + + // Reset immediately, to make sure that we're not hit by the end + // of a nested event loop (which would cause `commit` to be called + // twice in succession). + reset(); + return success; +} + +uint64_t +PerformanceMonitoring::monotonicReadTimestampCounter() +{ +#if defined(MOZ_HAVE_RDTSC) + const uint64_t hardware = ReadTimestampCounter(); + if (highestTimestampCounter_ < hardware) + highestTimestampCounter_ = hardware; + return highestTimestampCounter_; +#else + return 0; +#endif // defined(MOZ_HAVE_RDTSC) +} + +void +PerformanceMonitoring::dispose(JSRuntime* rt) +{ + reset(); + for (CompartmentsIter c(rt, SkipAtoms); !c.done(); c.next()) { + c->performanceMonitoring.unlink(); + } +} + +PerformanceGroupHolder::~PerformanceGroupHolder() +{ + unlink(); +} + +void +PerformanceGroupHolder::unlink() +{ + initialized_ = false; + groups_.clear(); +} + +const PerformanceGroupVector* +PerformanceGroupHolder::getGroups(JSContext* cx) +{ + if (initialized_) + return &groups_; + + if (!runtime_->performanceMonitoring.getGroupsCallback) + return nullptr; + + if (!runtime_->performanceMonitoring.getGroupsCallback(cx, groups_, runtime_->performanceMonitoring.getGroupsClosure)) + return nullptr; + + initialized_ = true; + return &groups_; +} + +AutoStopwatch::AutoStopwatch(JSContext* cx MOZ_GUARD_OBJECT_NOTIFIER_PARAM_IN_IMPL) + : cx_(cx) + , iteration_(0) + , isMonitoringJank_(false) + , isMonitoringCPOW_(false) + , cyclesStart_(0) + , CPOWTimeStart_(0) +{ + MOZ_GUARD_OBJECT_NOTIFIER_INIT; + + JSCompartment* compartment = cx_->compartment(); + if (compartment->scheduledForDestruction) + return; + + JSRuntime* runtime = cx_->runtime(); + iteration_ = runtime->performanceMonitoring.iteration(); + + const PerformanceGroupVector* groups = compartment->performanceMonitoring.getGroups(cx); + if (!groups) { + // Either the embedding has not provided any performance + // monitoring logistics or there was an error that prevents + // performance monitoring. + return; + } + for (auto group = groups->begin(); group < groups->end(); group++) { + auto acquired = acquireGroup(*group); + if (acquired) { + if (!groups_.append(acquired)) + MOZ_CRASH(); + } + } + if (groups_.length() == 0) { + // We are not in charge of monitoring anything. + return; + } + + // Now that we are sure that JS code is being executed, + // initialize the stopwatch for this iteration, lazily. + runtime->performanceMonitoring.start(); + enter(); +} + +AutoStopwatch::~AutoStopwatch() +{ + if (groups_.length() == 0) { + // We are not in charge of monitoring anything. + return; + } + + JSCompartment* compartment = cx_->compartment(); + if (compartment->scheduledForDestruction) + return; + + JSRuntime* runtime = cx_->runtime(); + if (iteration_ != runtime->performanceMonitoring.iteration()) { + // We have entered a nested event loop at some point. + // Any information we may have is obsolete. + return; + } + + mozilla::Unused << exit(); // Sadly, there is nothing we can do about an error at this point. + + for (auto group = groups_.begin(); group < groups_.end(); group++) + releaseGroup(*group); +} + +void +AutoStopwatch::enter() +{ + JSRuntime* runtime = cx_->runtime(); + + if (runtime->performanceMonitoring.isMonitoringCPOW()) { + CPOWTimeStart_ = runtime->performanceMonitoring.totalCPOWTime; + isMonitoringCPOW_ = true; + } + + if (runtime->performanceMonitoring.isMonitoringJank()) { + cyclesStart_ = this->getCycles(runtime); + cpuStart_ = this->getCPU(); + isMonitoringJank_ = true; + } +} + +bool +AutoStopwatch::exit() +{ + JSRuntime* runtime = cx_->runtime(); + + uint64_t cyclesDelta = 0; + if (isMonitoringJank_ && runtime->performanceMonitoring.isMonitoringJank()) { + // We were monitoring jank when we entered and we still are. + + // If possible, discard results when we don't end on the + // same CPU as we started. Note that we can be + // rescheduled to another CPU beween `getCycles()` and + // `getCPU()`. We hope that this will happen rarely + // enough that the impact on our statistics will remain + // limited. + const cpuid_t cpuEnd = this->getCPU(); + if (isSameCPU(cpuStart_, cpuEnd)) { + const uint64_t cyclesEnd = getCycles(runtime); + cyclesDelta = cyclesEnd - cyclesStart_; // Always >= 0 by definition of `getCycles`. + } +#if WINVER >= 0x600 + updateTelemetry(cpuStart_, cpuEnd); +#elif defined(__linux__) + updateTelemetry(cpuStart_, cpuEnd); +#endif // WINVER >= 0x600 || _linux__ + } + + uint64_t CPOWTimeDelta = 0; + if (isMonitoringCPOW_ && runtime->performanceMonitoring.isMonitoringCPOW()) { + // We were monitoring CPOW when we entered and we still are. + const uint64_t CPOWTimeEnd = runtime->performanceMonitoring.totalCPOWTime; + CPOWTimeDelta = getDelta(CPOWTimeEnd, CPOWTimeStart_); + } + return addToGroups(cyclesDelta, CPOWTimeDelta); +} + +void +AutoStopwatch::updateTelemetry(const cpuid_t& cpuStart_, const cpuid_t& cpuEnd) +{ + JSRuntime* runtime = cx_->runtime(); + + if (isSameCPU(cpuStart_, cpuEnd)) + runtime->performanceMonitoring.testCpuRescheduling.stayed += 1; + else + runtime->performanceMonitoring.testCpuRescheduling.moved += 1; +} + +PerformanceGroup* +AutoStopwatch::acquireGroup(PerformanceGroup* group) +{ + MOZ_ASSERT(group); + + if (group->isAcquired(iteration_)) + return nullptr; + + if (!group->isActive()) + return nullptr; + + group->acquire(iteration_, this); + return group; +} + +void +AutoStopwatch::releaseGroup(PerformanceGroup* group) +{ + MOZ_ASSERT(group); + group->release(iteration_, this); +} + +bool +AutoStopwatch::addToGroups(uint64_t cyclesDelta, uint64_t CPOWTimeDelta) +{ + JSRuntime* runtime = cx_->runtime(); + + for (auto group = groups_.begin(); group < groups_.end(); ++group) { + if (!addToGroup(runtime, cyclesDelta, CPOWTimeDelta, *group)) + return false; + } + return true; +} + +bool +AutoStopwatch::addToGroup(JSRuntime* runtime, uint64_t cyclesDelta, uint64_t CPOWTimeDelta, PerformanceGroup* group) +{ + MOZ_ASSERT(group); + MOZ_ASSERT(group->isAcquired(iteration_, this)); + + if (!runtime->performanceMonitoring.addRecentGroup(group)) + return false; + group->addRecentTicks(iteration_, 1); + group->addRecentCycles(iteration_, cyclesDelta); + group->addRecentCPOW(iteration_, CPOWTimeDelta); + return true; +} + +uint64_t +AutoStopwatch::getDelta(const uint64_t end, const uint64_t start) const +{ + if (start >= end) + return 0; + return end - start; +} + +uint64_t +AutoStopwatch::getCycles(JSRuntime* runtime) const +{ + return runtime->performanceMonitoring.monotonicReadTimestampCounter(); +} + +cpuid_t inline +AutoStopwatch::getCPU() const +{ +#if defined(XP_WIN) && WINVER >= _WIN32_WINNT_VISTA + PROCESSOR_NUMBER proc; + GetCurrentProcessorNumberEx(&proc); + + cpuid_t result(proc.Group, proc.Number); + return result; +#else + return {}; +#endif // defined(XP_WIN) +} + +bool inline +AutoStopwatch::isSameCPU(const cpuid_t& a, const cpuid_t& b) const +{ +#if defined(XP_WIN) && WINVER >= _WIN32_WINNT_VISTA + return a.group_ == b.group_ && a.number_ == b.number_; +#else + return true; +#endif +} + +PerformanceGroup::PerformanceGroup() + : recentCycles_(0) + , recentTicks_(0) + , recentCPOW_(0) + , iteration_(0) + , isActive_(false) + , isUsedInThisIteration_(false) + , owner_(nullptr) + , refCount_(0) +{ } + +uint64_t +PerformanceGroup::iteration() const +{ + return iteration_; +} + + +bool +PerformanceGroup::isAcquired(uint64_t it) const +{ + return owner_ != nullptr && iteration_ == it; +} + +bool +PerformanceGroup::isAcquired(uint64_t it, const AutoStopwatch* owner) const +{ + return owner_ == owner && iteration_ == it; +} + +void +PerformanceGroup::acquire(uint64_t it, const AutoStopwatch* owner) +{ + if (iteration_ != it) { + // Any data that pretends to be recent is actually bound + // to an older iteration and therefore stale. + resetRecentData(); + } + iteration_ = it; + owner_ = owner; +} + +void +PerformanceGroup::release(uint64_t it, const AutoStopwatch* owner) +{ + if (iteration_ != it) + return; + + MOZ_ASSERT(owner == owner_ || owner_ == nullptr); + owner_ = nullptr; +} + +void +PerformanceGroup::resetRecentData() +{ + recentCycles_ = 0; + recentTicks_ = 0; + recentCPOW_ = 0; + isUsedInThisIteration_ = false; +} + + +uint64_t +PerformanceGroup::recentCycles(uint64_t iteration) const +{ + MOZ_ASSERT(iteration == iteration_); + return recentCycles_; +} + +void +PerformanceGroup::addRecentCycles(uint64_t iteration, uint64_t cycles) +{ + MOZ_ASSERT(iteration == iteration_); + recentCycles_ += cycles; +} + +uint64_t +PerformanceGroup::recentTicks(uint64_t iteration) const +{ + MOZ_ASSERT(iteration == iteration_); + return recentTicks_; +} + +void +PerformanceGroup::addRecentTicks(uint64_t iteration, uint64_t ticks) +{ + MOZ_ASSERT(iteration == iteration_); + recentTicks_ += ticks; +} + + +uint64_t +PerformanceGroup::recentCPOW(uint64_t iteration) const +{ + MOZ_ASSERT(iteration == iteration_); + return recentCPOW_; +} + +void +PerformanceGroup::addRecentCPOW(uint64_t iteration, uint64_t CPOW) +{ + MOZ_ASSERT(iteration == iteration_); + recentCPOW_ += CPOW; +} + + +bool +PerformanceGroup::isActive() const +{ + return isActive_; +} + +void +PerformanceGroup::setIsActive(bool value) +{ + isActive_ = value; +} + +void +PerformanceGroup::setIsUsedInThisIteration(bool value) +{ + isUsedInThisIteration_ = value; +} +bool +PerformanceGroup::isUsedInThisIteration() const +{ + return isUsedInThisIteration_; +} + +void +PerformanceGroup::AddRef() +{ + ++refCount_; +} + +void +PerformanceGroup::Release() +{ + MOZ_ASSERT(refCount_ > 0); + --refCount_; + if (refCount_ > 0) + return; + + this->Delete(); +} + +JS_PUBLIC_API(bool) +SetStopwatchStartCallback(JSContext* cx, StopwatchStartCallback cb, void* closure) +{ + cx->performanceMonitoring.setStopwatchStartCallback(cb, closure); + return true; +} + +JS_PUBLIC_API(bool) +SetStopwatchCommitCallback(JSContext* cx, StopwatchCommitCallback cb, void* closure) +{ + cx->performanceMonitoring.setStopwatchCommitCallback(cb, closure); + return true; +} + +JS_PUBLIC_API(bool) +SetGetPerformanceGroupsCallback(JSContext* cx, GetGroupsCallback cb, void* closure) +{ + cx->performanceMonitoring.setGetGroupsCallback(cb, closure); + return true; +} + +JS_PUBLIC_API(bool) +FlushPerformanceMonitoring(JSContext* cx) +{ + return cx->performanceMonitoring.commit(); +} +JS_PUBLIC_API(void) +ResetPerformanceMonitoring(JSContext* cx) +{ + return cx->performanceMonitoring.reset(); +} +JS_PUBLIC_API(void) +DisposePerformanceMonitoring(JSContext* cx) +{ + return cx->performanceMonitoring.dispose(cx); +} + +JS_PUBLIC_API(bool) +SetStopwatchIsMonitoringJank(JSContext* cx, bool value) +{ + return cx->performanceMonitoring.setIsMonitoringJank(value); +} +JS_PUBLIC_API(bool) +GetStopwatchIsMonitoringJank(JSContext* cx) +{ + return cx->performanceMonitoring.isMonitoringJank(); +} + +JS_PUBLIC_API(bool) +SetStopwatchIsMonitoringCPOW(JSContext* cx, bool value) +{ + return cx->performanceMonitoring.setIsMonitoringCPOW(value); +} +JS_PUBLIC_API(bool) +GetStopwatchIsMonitoringCPOW(JSContext* cx) +{ + return cx->performanceMonitoring.isMonitoringCPOW(); +} + +JS_PUBLIC_API(void) +GetPerfMonitoringTestCpuRescheduling(JSContext* cx, uint64_t* stayed, uint64_t* moved) +{ + *stayed = cx->performanceMonitoring.testCpuRescheduling.stayed; + *moved = cx->performanceMonitoring.testCpuRescheduling.moved; +} + +JS_PUBLIC_API(void) +AddCPOWPerformanceDelta(JSContext* cx, uint64_t delta) +{ + cx->performanceMonitoring.totalCPOWTime += delta; +} + + +} // namespace js + |