-
Notifications
You must be signed in to change notification settings - Fork 753
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
perf(anim): Register only once to the JS animationframe callback
- Loading branch information
1 parent
57185e6
commit 6ad5fc1
Showing
4 changed files
with
123 additions
and
203 deletions.
There are no files selected for viewing
48 changes: 34 additions & 14 deletions
48
src/Uno.UI/UI/Xaml/Media/Animation/Animators/RenderingLoopAnimator.Interop.wasm.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
154 changes: 54 additions & 100 deletions
154
src/Uno.UI/UI/Xaml/Media/Animation/Animators/RenderingLoopAnimator.wasm.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
113
src/Uno.UI/ts/Windows/UI/Xaml/Animation/RenderingLoopAnimator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} 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; | ||
} | ||
} |
Oops, something went wrong.