Skip to content

Commit

Permalink
perf(anim): Register only once to the JS animationframe callback
Browse files Browse the repository at this point in the history
  • Loading branch information
jeromelaban committed Jan 14, 2025
1 parent 57185e6 commit 6ad5fc1
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 203 deletions.
Original file line number Diff line number Diff line change
@@ -1,29 +1,49 @@
using System;
using System.Runtime.InteropServices.JavaScript;
using Uno.Disposables;
using Windows.UI.Core;

namespace __Microsoft.UI.Xaml.Media.Animation
namespace Microsoft.UI.Xaml.Media.Animation
{
internal partial class RenderingLoopAnimator
{
internal static partial class NativeMethods
private static WeakEventHelper.WeakEventCollection _frameHandlers = new();

internal static IDisposable RegisterFrameEvent(Action action)
{
[JSImport("globalThis.Microsoft.UI.Xaml.Media.Animation.RenderingLoopAnimator.createInstance")]
internal static partial void CreateInstance(IntPtr managedHandle, double id);
var disposable = WeakEventHelper.RegisterEvent(
_frameHandlers,
action,
(h, s, a) => (h as Action)?.Invoke());

[JSImport("globalThis.Microsoft.UI.Xaml.Media.Animation.RenderingLoopAnimator.destroyInstance")]
internal static partial void DestroyInstance(double jsHandle);
NativeMethods.SetEnabled(true);

[JSImport("globalThis.Microsoft.UI.Xaml.Media.Animation.RenderingLoopAnimator.disableFrameReporting")]
internal static partial void DisableFrameReporting(double jsHandle);
return Disposable.Create(() =>
{
disposable.Dispose();

if (_frameHandlers.IsEmpty)
{
NativeMethods.SetEnabled(false);
}
});
}

[JSImport("globalThis.Microsoft.UI.Xaml.Media.Animation.RenderingLoopAnimator.enableFrameReporting")]
internal static partial void EnableFrameReporting(double jsHandle);
[JSExport]
public static void OnFrame()
{
_frameHandlers.Invoke(null, null);

[JSImport("globalThis.Microsoft.UI.Xaml.Media.Animation.RenderingLoopAnimator.setAnimationFramesInterval")]
internal static partial void SetAnimationFramesInterval(double jsHandle);
if (_frameHandlers.IsEmpty)
{
NativeMethods.SetEnabled(false);
}
}

[JSImport("globalThis.Microsoft.UI.Xaml.Media.Animation.RenderingLoopAnimator.setStartFrameDelay")]
internal static partial void SetStartFrameDelay(double jsHandle, double delayMs);
internal static partial class NativeMethods
{
[JSImport("globalThis.Microsoft.UI.Xaml.Media.Animation.RenderingLoopAnimator.setEnabled")]
internal static partial void SetEnabled(bool enabled);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,130 +1,84 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Runtime.InteropServices.JavaScript;
using System.Threading;
using Uno.Disposables;
using Uno.Foundation;
using Uno.Foundation.Interop;
using Uno.Foundation.Logging;
using Uno.UI.__Resources;
using Windows.UI.Core;

using NativeMethods = __Microsoft.UI.Xaml.Media.Animation.RenderingLoopAnimator.NativeMethods;
namespace Microsoft.UI.Xaml.Media.Animation;

namespace Microsoft.UI.Xaml.Media.Animation
internal abstract class RenderingLoopAnimator<T> : CPUBoundAnimator<T> where T : struct
{
internal abstract class RenderingLoopAnimator<T> : CPUBoundAnimator<T>, IJSObject where T : struct
private bool _isEnabled;
private IDisposable _frameEvent;
private DispatcherTimer _delayRequest;

protected RenderingLoopAnimator(T from, T to)
: base(from, to)
{
}

protected override void EnableFrameReporting()
{
protected RenderingLoopAnimator(T from, T to)
: base(from, to)
if (_isEnabled)
{
Handle = JSObjectHandle.Create(this, Metadata.Instance);
return;
}

public JSObjectHandle Handle { get; }
_isEnabled = true;

protected override void EnableFrameReporting()
{
if (Handle.IsAlive)
{
NativeMethods.EnableFrameReporting(Handle.JSHandle);
}
else if (this.Log().IsEnabled(LogLevel.Debug))
{
this.Log().Debug("Cannot EnableFrameReporting as Handle is no longer alive.");
}
}
_frameEvent = RenderingLoopAnimator.RegisterFrameEvent(OnFrame);
}

protected override void DisableFrameReporting()
{
if (Handle.IsAlive)
{
NativeMethods.DisableFrameReporting(Handle.JSHandle);
}
else if (this.Log().IsEnabled(LogLevel.Debug))
{
this.Log().Debug("Cannot DisableFrameReporting as Handle is no longer alive.");
}
}
protected override void DisableFrameReporting()
{
_isEnabled = false;
UnscheduleFrame();
}

protected override void SetStartFrameDelay(long delayMs)
{
if (Handle.IsAlive)
{
NativeMethods.SetStartFrameDelay(Handle.JSHandle, delayMs);
}
else if (this.Log().IsEnabled(LogLevel.Debug))
{
this.Log().Debug("Cannot SetStartFrameDelay as Handle is no longer alive.");
}
}
protected override void SetStartFrameDelay(long delayMs)
{
UnscheduleFrame();

protected override void SetAnimationFramesInterval()
if (_isEnabled)
{
if (Handle.IsAlive)
{
NativeMethods.SetAnimationFramesInterval(Handle.JSHandle);
}
else if (this.Log().IsEnabled(LogLevel.Debug))
_delayRequest = new DispatcherTimer() { Interval = TimeSpan.FromMilliseconds(delayMs) };
_delayRequest.Tick += (s, e) =>
{
this.Log().Debug("Cannot SetAnimationFramesInterval as Handle is no longer alive.");
}
_delayRequest.Stop();
_delayRequest = null;
OnFrame();
};
}
}

private void OnFrame() => OnFrame(null, null);
protected override void SetAnimationFramesInterval()
{
UnscheduleFrame();

/// <inheritdoc />
public override void Dispose()
if (_isEnabled)
{
// WARNING: If the Dispose is invoked by the GC, it has most probably already disposed the Handle,
// which means that we have already lost ability to dispose/stop the native object!

base.Dispose();
Handle.Dispose();

GC.SuppressFinalize(this);
OnFrame();
}
}

~RenderingLoopAnimator()
private void UnscheduleFrame()
{
if (_delayRequest != null)
{
Dispose();
_delayRequest.Stop();
_delayRequest = null;
}

private class Metadata : IJSObjectMetadata
{
public static Metadata Instance { get; } = new Metadata();

private Metadata() { }

/// <inheritdoc />
public long CreateNativeInstance(IntPtr managedHandle)
{
var id = RenderingLoopAnimatorMetadataIdProvider.Next();

NativeMethods.CreateInstance(managedHandle, id);

return id;
}

/// <inheritdoc />
public string GetNativeInstance(IntPtr managedHandle, long jsHandle)
=> $"Microsoft.UI.Xaml.Media.Animation.RenderingLoopAnimator.getInstance(\"{jsHandle}\")";

/// <inheritdoc />
public void DestroyNativeInstance(IntPtr managedHandle, long jsHandle)
=> NativeMethods.DestroyInstance(jsHandle);

/// <inheritdoc />
public object InvokeManaged(object instance, string method, string parameters)
{
switch (method)
{
case "OnFrame":
((RenderingLoopAnimator<T>)instance).OnFrame();
break;

default:
throw new ArgumentOutOfRangeException(nameof(method));
}

return null;
}
}
_frameEvent?.Dispose();
}

private void OnFrame()
=> OnFrame(null, null);
}
113 changes: 24 additions & 89 deletions src/Uno.UI/ts/Windows/UI/Xaml/Animation/RenderingLoopAnimator.ts
Original file line number Diff line number Diff line change
@@ -1,111 +1,46 @@
namespace Microsoft.UI.Xaml.Media.Animation {
export class RenderingLoopAnimator {
private static activeInstances: { [jsHandle: number]: RenderingLoopAnimator} = {};

public static createInstance(managedHandle: number, jsHandle: number) {
RenderingLoopAnimator.activeInstances[jsHandle] = new RenderingLoopAnimator(managedHandle);
}

public static getInstance(jsHandle: number): RenderingLoopAnimator {
return RenderingLoopAnimator.activeInstances[jsHandle];
}
private static dispatchFrame: () => number;

public static destroyInstance(jsHandle: number) {
var instance = RenderingLoopAnimator.getInstance(jsHandle);
// If the JSObjectHandle is being disposed before the animator is stopped (GC collecting JSObjectHandle before the animator)
// we won't be able to DisableFrameReporting anymore.
if (instance) {
instance.DisableFrameReporting();
private static init() {
if (!RenderingLoopAnimator.dispatchFrame) {
if ((<any>globalThis).DotnetExports !== undefined) {
RenderingLoopAnimator.dispatchFrame = (<any>globalThis).DotnetExports.UnoUI.Microsoft.UI.Xaml.Media.Animation.RenderingLoopAnimator.OnFrame;

Check warning on line 9 in src/Uno.UI/ts/Windows/UI/Xaml/Animation/RenderingLoopAnimator.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/Uno.UI/ts/Windows/UI/Xaml/Animation/RenderingLoopAnimator.ts#L9

Exceeds maximum line length of 120
} else {
throw `Unable to find dotnet exports`;
}
}
delete RenderingLoopAnimator.activeInstances[jsHandle];
}

private constructor(private managedHandle: number) {
}

public static setStartFrameDelay(jsHandle: number, delay: number) {
RenderingLoopAnimator.getInstance(jsHandle).SetStartFrameDelay(delay);
}
public static setEnabled(enabled: boolean) {
RenderingLoopAnimator.init();

public SetStartFrameDelay(delay: number) {
this.unscheduleFrame();
RenderingLoopAnimator._isEnabled = enabled;

if (this._isEnabled) {
this.scheduleDelayedFrame(delay);
if (enabled) {
RenderingLoopAnimator.scheduleAnimationFrame();
} else if (RenderingLoopAnimator._frameRequestId != null) {
window.cancelAnimationFrame(RenderingLoopAnimator._frameRequestId);
}
}

public static setAnimationFramesInterval(jsHandle: number) {
RenderingLoopAnimator.getInstance(jsHandle).SetAnimationFramesInterval();
}

public SetAnimationFramesInterval() {
this.unscheduleFrame();

if (this._isEnabled) {
this.onFrame();
}
private static scheduleAnimationFrame() {
RenderingLoopAnimator._frameRequestId = window.requestAnimationFrame(() => {
RenderingLoopAnimator._frameRequestId = null;
RenderingLoopAnimator.onFrame();
});
}

public static enableFrameReporting(jsHandle: number) {
RenderingLoopAnimator.getInstance(jsHandle).EnableFrameReporting();
}
private static onFrame() {
RenderingLoopAnimator.dispatchFrame();

public EnableFrameReporting() {
if (this._isEnabled) {
return;
}

this._isEnabled = true;
this.scheduleAnimationFrame();
}

public static disableFrameReporting(jsHandle: number) {
RenderingLoopAnimator.getInstance(jsHandle).DisableFrameReporting();
}

public DisableFrameReporting() {
this._isEnabled = false;
this.unscheduleFrame();
}

private onFrame() {
Uno.Foundation.Interop.ManagedObject.dispatch(this.managedHandle, "OnFrame", null);

// Schedule a new frame only if still enabled and no frame was scheduled by the managed OnFrame
if (this._isEnabled && this._frameRequestId == null && this._delayRequestId == null) {
this.scheduleAnimationFrame();
}
}

private unscheduleFrame() {
if (this._delayRequestId != null) {
clearTimeout(this._delayRequestId);
this._delayRequestId = null;
}
if (this._frameRequestId != null) {
window.cancelAnimationFrame(this._frameRequestId);
this._frameRequestId = null;
}
}

private scheduleDelayedFrame(delay: number) {
this._delayRequestId = setTimeout(() => {
this._delayRequestId = null;
this.onFrame();
},
delay);
}

private scheduleAnimationFrame() {
this._frameRequestId = window.requestAnimationFrame(() => {
this._frameRequestId = null;
this.onFrame();
});
}

private _delayRequestId?: number;
private _frameRequestId?: number;
private _isEnabled = false;
private static _frameRequestId?: number;
private static _isEnabled = false;
}
}
Loading

0 comments on commit 6ad5fc1

Please sign in to comment.