Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

[WIP][iOS] infer dynamic refresh rate via heuristics. #29692

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion shell/common/animator.cc
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ Animator::Animator(Delegate& delegate,
: 2)),
#endif // SHELL_ENABLE_METAL
pending_frame_semaphore_(1),
weak_factory_(this) {
weak_factory_(this),
dynamic_frr_provider_(std::make_unique<DynamicFrameRateRangeProvider>()),
current_frame_rate_range_(FrameRateRange()) {
// Set initial frame rate range.
waiter_->UpdateFrameRateRange(current_frame_rate_range_);
}

Animator::~Animator() = default;
Expand Down Expand Up @@ -76,6 +80,12 @@ void Animator::EnqueueTraceFlowId(uint64_t trace_flow_id) {
});
}

void Animator::RecordFrameDuration(const int64_t frame_duration) {
TRACE_EVENT0("flutter", "Engine::RecordFrameDuration");
FML_DCHECK(frame_duration >= 0);
dynamic_frr_provider_->Record(frame_duration);
}

// This Parity is used by the timeline component to correctly align
// GPU Workloads events with their respective Framework Workload.
const char* Animator::FrameParity() {
Expand All @@ -94,6 +104,8 @@ static fml::TimePoint FxlToDartOrEarlier(fml::TimePoint time) {

void Animator::BeginFrame(
std::unique_ptr<FrameTimingsRecorder> frame_timings_recorder) {
UpdateFrameRateRangeIfChanged();

TRACE_EVENT_ASYNC_END0("flutter", "Frame Request Pending",
frame_request_number_);
frame_request_number_++;
Expand Down Expand Up @@ -148,6 +160,8 @@ void Animator::BeginFrame(
}

if (!frame_scheduled_ && has_rendered_) {
dynamic_frr_provider_->Reset();
UpdateFrameRateRangeIfChanged();
// Under certain workloads (such as our parent view resizing us, which is
// communicated to us by repeat viewport metrics events), we won't
// actually have a frame scheduled yet, despite the fact that we *will* be
Expand Down Expand Up @@ -298,4 +312,12 @@ void Animator::ScheduleMaybeClearTraceFlowIds() {
});
}

void Animator::UpdateFrameRateRangeIfChanged() {
FrameRateRange new_frame_rate_range = dynamic_frr_provider_->Provide();
if (current_frame_rate_range_ != new_frame_rate_range) {
current_frame_rate_range_ = new_frame_rate_range;
waiter_->UpdateFrameRateRange(current_frame_rate_range_);
}
}

} // namespace flutter
14 changes: 13 additions & 1 deletion shell/common/animator.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ class Animator final {
// active rendering.
void EnqueueTraceFlowId(uint64_t trace_flow_id);

//----------------------------------------------------------------------------
/// @brief Record the frame duration of the last frame.
///
/// @param[in] frame_duration The delta between raster end time and build
/// start time, in milliseconds. This value must
/// be positive
///
void RecordFrameDuration(const int64_t frame_duration);

private:
using LayerTreePipeline = Pipeline<flutter::LayerTree>;

Expand All @@ -100,6 +109,8 @@ class Animator final {
// Clear |trace_flow_ids_| if |frame_scheduled_| is false.
void ScheduleMaybeClearTraceFlowIds();

void UpdateFrameRateRangeIfChanged();

Delegate& delegate_;
TaskRunners task_runners_;
std::shared_ptr<VsyncWaiter> waiter_;
Expand All @@ -118,8 +129,9 @@ class Animator final {
SkISize last_layer_tree_size_ = {0, 0};
std::deque<uint64_t> trace_flow_ids_;
bool has_rendered_ = false;

fml::WeakPtrFactory<Animator> weak_factory_;
std::unique_ptr<DynamicFrameRateRangeProvider> dynamic_frr_provider_;
FrameRateRange current_frame_rate_range_;

friend class testing::ShellTest;

Expand Down
6 changes: 6 additions & 0 deletions shell/common/engine.cc
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,12 @@ void Engine::ReportTimings(std::vector<int64_t> timings) {
runtime_controller_->ReportTimings(std::move(timings));
}

void Engine::RecordFrameDuration(const int64_t frame_duration) {
TRACE_EVENT0("flutter", "Engine::RecordFrameDuration");
FML_DCHECK(frame_duration >= 0);
animator_->RecordFrameDuration(frame_duration);
}

void Engine::NotifyIdle(int64_t deadline) {
auto trace_event = std::to_string(deadline - Dart_TimelineGetMicros());
TRACE_EVENT1("flutter", "Engine::NotifyIdle", "deadline_now_delta",
Expand Down
9 changes: 9 additions & 0 deletions shell/common/engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,15 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate {
///
void ReportTimings(std::vector<int64_t> timings);

//----------------------------------------------------------------------------
/// @brief Record the frame duration of the last frame.
///
/// @param[in] frame_duration The delta between raster end time and build
/// start time, in milliseconds. This value must
/// be positive
///
void RecordFrameDuration(const int64_t frame_duration);

//----------------------------------------------------------------------------
/// @brief Gets the main port of the root isolate. Since the isolate is
/// created immediately in the constructor of the engine, it is
Expand Down
6 changes: 6 additions & 0 deletions shell/common/shell.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1361,6 +1361,12 @@ void Shell::OnFrameRasterized(const FrameTiming& timing) {
settings_.frame_rasterized_callback(timing);
}

int64_t frame_duration_ms = (timing.Get(FrameTiming::Phase::kRasterFinish) -
timing.Get(FrameTiming::Phase::kBuildStart))
.ToMilliseconds();

engine_->RecordFrameDuration(frame_duration_ms);

if (!needs_report_timings_) {
return;
}
Expand Down
65 changes: 65 additions & 0 deletions shell/common/vsync_waiter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@

namespace flutter {

static constexpr const int64_t kFrameRateMin = 30;
static constexpr const int64_t kFrameRateMedium = 60;
static constexpr const int64_t kFrameRateHigh = 120;

static constexpr const char* kVsyncFlowName = "VsyncFlow";

#if defined(OS_FUCHSIA)
Expand Down Expand Up @@ -170,4 +174,65 @@ void VsyncWaiter::ResumeDartMicroTasks(fml::TaskQueueId ui_task_queue_id) {
task_queues->ResumeSecondarySource(ui_task_queue_id);
}

// FrameRageRange

FrameRateRange::FrameRateRange(const int64_t min,
const int64_t preferred,
const int64_t max)
: min_(min), preferred_(preferred), max_(max) {}

FrameRateRange::FrameRateRange()
: min_(kFrameRateMin),
preferred_(kFrameRateMedium),
max_(kFrameRateMedium) {}

int64_t FrameRateRange::GetMin() const {
return min_;
}

int64_t FrameRateRange::GetPreferred() const {
return preferred_;
}

int64_t FrameRateRange::GetMax() const {
return max_;
}

// DynamicFrameRateRangeProvider

DynamicFrameRateRangeProvider::DynamicFrameRateRangeProvider(size_t size)
: size_(size) {}

void DynamicFrameRateRangeProvider::Record(int64_t frame_duration) {
FML_DCHECK(frame_durations_.size() <= size_);
if (frame_durations_.size() == size_) {
frame_durations_.erase(frame_durations_.begin());
}
FML_LOG(ERROR) << ">>> record frame duration " << frame_duration;
frame_durations_.push_back(frame_duration);
}

FrameRateRange DynamicFrameRateRangeProvider::Provide() {
int64_t preferred = kFrameRateMedium;
if (frame_durations_.size() == size_) {
int64_t sum = 0;
for (auto& duration : frame_durations_) {
// TODO(cyanglaz): int overflow.
sum += duration;
}
int64_t average = sum / size_;
if (average > 16) {
preferred = kFrameRateMin;
} else if (average < 8) {
preferred = kFrameRateHigh;
}
}
int64_t min = preferred / 2;
return FrameRateRange(min, preferred, preferred);
}

void DynamicFrameRateRangeProvider::Reset() {
frame_durations_.clear();
}

} // namespace flutter
65 changes: 65 additions & 0 deletions shell/common/vsync_waiter.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,68 @@

namespace flutter {

// Represents a dynamic frame rate range.
struct FrameRateRange {
public:
// Constructor with default values:
// min = kFrameRateMin,
// preferred = kFrameRateHigh,
// max = kFrameRateHigh
FrameRateRange();
// Constructor with custom values.
FrameRateRange(const int64_t min, const int64_t preferred, const int64_t max);

// Get the min frame rate.
int64_t GetMin() const;

// Get the preferred frame rate.
int64_t GetPreferred() const;

// Get the max frame rate.
int64_t GetMax() const;

friend bool operator==(const FrameRateRange& lhs, const FrameRateRange& rhs) {
return lhs.min_ == rhs.min_ && lhs.preferred_ == rhs.preferred_ &&
lhs.max_ == rhs.max_;
}

friend bool operator!=(const FrameRateRange& lhs, const FrameRateRange& rhs) {
return lhs.min_ != rhs.min_ || lhs.preferred_ != rhs.preferred_ ||
lhs.max_ != rhs.max_;
}

private:
int64_t min_;
int64_t preferred_;
int64_t max_;
};

static const size_t kDefaultFRRProviderSize = 10;

// This object can provide the preferred |FrameRateRange| to use
// based on the performance.
class DynamicFrameRateRangeProvider {
public:
// Constuctor.
// The `size` is used to determine how many frame durations are stored.
DynamicFrameRateRangeProvider(size_t size = kDefaultFRRProviderSize);

// Record a frame_duration for the current frame.
// The `frame_duration` must be the current frame's duration.
void Record(int64_t frame_duration);

// Provides a |FrameRateRange| based on the recorded frame_durations.
FrameRateRange Provide();

// Lose all memory about the recent frame performances, resets to inital
// state.
void Reset();

private:
std::vector<int64_t> frame_durations_;
size_t size_;
};

/// Abstract Base Class that represents a platform specific mechanism for
/// getting callbacks when a vsync event happens.
class VsyncWaiter : public std::enable_shared_from_this<VsyncWaiter> {
Expand All @@ -32,6 +94,9 @@ class VsyncWaiter : public std::enable_shared_from_this<VsyncWaiter> {
/// |Animator::ScheduleMaybeClearTraceFlowIds|.
void ScheduleSecondaryCallback(uintptr_t id, const fml::closure& callback);

// Tell vsync waiter to update the frame rate range.
virtual void UpdateFrameRateRange(const FrameRateRange& frame_rate_range){};

protected:
// On some backends, the |FireCallback| needs to be made from a static C
// method.
Expand Down
4 changes: 4 additions & 0 deletions shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@

- (void)invalidate;

- (void)updateFrameRateRangeWithMin:(int64_t)min max:(int64_t)max preferred:(int64_t)preferred;

@end

namespace flutter {
Expand All @@ -44,6 +46,8 @@ class VsyncWaiterIOS final : public VsyncWaiter {

~VsyncWaiterIOS() override;

void UpdateFrameRateRange(const FrameRateRange& frame_rate_range) override;

private:
fml::scoped_nsobject<VSyncClient> client_;

Expand Down
38 changes: 38 additions & 0 deletions shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@
[client_.get() await];
}

void VsyncWaiterIOS::UpdateFrameRateRange(const FrameRateRange& frame_rate_range) {
[client_.get() updateFrameRateRangeWithMin:frame_rate_range.GetMin()
max:frame_rate_range.GetMax()
preferred:frame_rate_range.GetPreferred()];
}

} // namespace flutter

@implementation VSyncClient {
Expand All @@ -56,6 +62,7 @@ - (instancetype)initWithTaskRunner:(fml::RefPtr<fml::TaskRunner>)task_runner
[[CADisplayLink displayLinkWithTarget:self selector:@selector(onDisplayLink:)] retain]
};
display_link_.get().paused = YES;
;

task_runner->PostTask([client = [self retain]]() {
[client->display_link_.get() addToRunLoop:[NSRunLoop currentRunLoop]
Expand Down Expand Up @@ -97,6 +104,37 @@ - (void)invalidate {
[display_link_.get() invalidate];
}

- (void)updateFrameRateRangeWithMin:(int64_t)min max:(int64_t)max preferred:(int64_t)preferred {
float minRate = (float)min;
double displayRefreshRate = [DisplayLinkManager displayRefreshRate];
float preferredRate = fminf(preferred, displayRefreshRate);
float maxRate = fminf(max, displayRefreshRate);
if (@available(iOS 15, *)) {
// preferredFrameRateRange is only available in iOS 15 and above.
display_link_.get().preferredFrameRateRange =
CAFrameRateRangeMake(minRate, maxRate, preferredRate);
} else if (@available(iOS 10, *)) {
// preferredFramesPerSecond is only available in iOS 10 and above, and deprecated at iOS 15.
display_link_.get().preferredFramesPerSecond = preferredRate;
}
// No-op as setting the dynamic frame rate is not possible in lower iOS versions.
// TODO(cyanglaz) remove logs
FML_LOG(ERROR) << "====================================================";
FML_LOG(ERROR) << ">>> engine preferred rate " << preferred;
FML_LOG(ERROR) << ">>> engine max rate " << max;
FML_LOG(ERROR) << ">>> system preferred rate " << displayRefreshRate;
if (@available(iOS 10, *)) {
double systemFrameDuration =
display_link_.get().targetTimestamp - display_link_.get().timestamp;
FML_LOG(ERROR) << ">>> system actual frame duration " << systemFrameDuration;
double systemFrameRate = 1 / systemFrameDuration;
FML_LOG(ERROR) << ">>> system actual frame rate " << systemFrameRate;
}
FML_LOG(ERROR) << ">>> min rate " << minRate;
FML_LOG(ERROR) << ">>> final preferred rate " << preferredRate;
FML_LOG(ERROR) << ">>> final max rate " << maxRate;
}

- (void)dealloc {
[self invalidate];

Expand Down