From e0df80f91029b80aef27e354dbdca1f6def6fc1b Mon Sep 17 00:00:00 2001 From: Aleksei Saveliev Date: Fri, 29 Jul 2022 23:41:05 +0800 Subject: [PATCH] Navigation perf profiling --- Example/android/app/build.gradle | 5 +- .../java/tonlabs/uikit/MainApplication.java | 8 +- .../uikit/PatchedReanimatedPackage.java | 112 ++ .../src/main/java/tonlabs/uikit/TraceLog.java | 11 + .../TraceNativeViewHierarchyManager.java | 48 + .../main/java/tonlabs/uikit/TracePackage.java | 49 + .../tonlabs/uikit/TraceUIImplementation.java | 58 + .../tonlabs/uikit/TraceUIManagerModule.java | 302 +++++ .../uikit/TraceUIViewOperationQueue.java | 1128 +++++++++++++++++ .../ios/Quiver UI.xcodeproj/project.pbxproj | 4 +- .../src/Pressable/Pressable.native.tsx | 118 +- .../src/UIBoxButton/BoxButtonContent.tsx | 3 +- 12 files changed, 1807 insertions(+), 39 deletions(-) create mode 100644 Example/android/app/src/main/java/tonlabs/uikit/PatchedReanimatedPackage.java create mode 100644 Example/android/app/src/main/java/tonlabs/uikit/TraceLog.java create mode 100644 Example/android/app/src/main/java/tonlabs/uikit/TraceNativeViewHierarchyManager.java create mode 100644 Example/android/app/src/main/java/tonlabs/uikit/TracePackage.java create mode 100644 Example/android/app/src/main/java/tonlabs/uikit/TraceUIImplementation.java create mode 100644 Example/android/app/src/main/java/tonlabs/uikit/TraceUIManagerModule.java create mode 100644 Example/android/app/src/main/java/tonlabs/uikit/TraceUIViewOperationQueue.java diff --git a/Example/android/app/build.gradle b/Example/android/app/build.gradle index c17e18f66..704ca49f0 100644 --- a/Example/android/app/build.gradle +++ b/Example/android/app/build.gradle @@ -78,11 +78,12 @@ import com.android.build.OutputFile */ project.ext.react = [ - cliPath: "../node_modules/react-native/cli.js", // path is relative to `process.cwd` - composeSourceMapsPath: "../node_modules/react-native/scripts/compose-source-maps.js", // path is relative to `process.cwd` + cliPath: "../../../node_modules/react-native/cli.js", // path is relative to `process.cwd` + composeSourceMapsPath: "../../../node_modules/react-native/scripts/compose-source-maps.js", // path is relative to `process.cwd` enableHermes: true, // clean and rebuild if changing entryFile: "index.js", // path is relative to `process.cwd` hermesCommand: "../../../node_modules/hermes-engine/osx-bin/hermesc", +// bundleInDebug: true, ] project.ext.themes = [ diff --git a/Example/android/app/src/main/java/tonlabs/uikit/MainApplication.java b/Example/android/app/src/main/java/tonlabs/uikit/MainApplication.java index 41dc23df8..d1a928217 100644 --- a/Example/android/app/src/main/java/tonlabs/uikit/MainApplication.java +++ b/Example/android/app/src/main/java/tonlabs/uikit/MainApplication.java @@ -17,6 +17,8 @@ import java.util.List; import com.facebook.react.bridge.JSIModulePackage; +import com.swmansion.reanimated.ReanimatedPackage; + import tonlabs.uikit.keyboard.UIKitKeyboardJSIModulePackage; public class MainApplication extends Application implements ReactApplication { @@ -35,7 +37,11 @@ protected List getPackages() { // Packages that cannot be autolinked yet can be added manually here, for example: // packages.add(new MyReactNativePackage()); - return packages; + packages = PatchedReanimatedPackage.patchPackages(packages); + + packages.add(new TracePackage()); + + return packages; } @Override diff --git a/Example/android/app/src/main/java/tonlabs/uikit/PatchedReanimatedPackage.java b/Example/android/app/src/main/java/tonlabs/uikit/PatchedReanimatedPackage.java new file mode 100644 index 000000000..950bdd26c --- /dev/null +++ b/Example/android/app/src/main/java/tonlabs/uikit/PatchedReanimatedPackage.java @@ -0,0 +1,112 @@ +package tonlabs.uikit; +import static com.facebook.react.bridge.ReactMarkerConstants.CREATE_UI_MANAGER_MODULE_END; +import static com.facebook.react.bridge.ReactMarkerConstants.CREATE_UI_MANAGER_MODULE_START; + +import com.facebook.react.ReactApplication; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.ReactPackage; +import com.facebook.react.TurboReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactMarker; +import com.facebook.react.module.annotations.ReactModule; +import com.facebook.react.module.model.ReactModuleInfo; +import com.facebook.react.module.model.ReactModuleInfoProvider; +import com.facebook.react.turbomodule.core.interfaces.TurboModule; +import com.facebook.react.uimanager.ReanimatedUIManager; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.systrace.Systrace; +import com.swmansion.reanimated.ReanimatedModule; +import com.swmansion.reanimated.ReanimatedPackage; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class PatchedReanimatedPackage extends TurboReactPackage implements ReactPackage { + + static List patchPackages(List packages) { + int reaPackageIndex = -1; + for (ReactPackage _package : packages) { + if (_package instanceof ReanimatedPackage) { + reaPackageIndex = packages.indexOf(_package); + } + } + if (reaPackageIndex > 0) { + packages.remove(reaPackageIndex); + } + + packages.add(new PatchedReanimatedPackage()); + + return packages; + } + + @Override + public NativeModule getModule(String name, ReactApplicationContext reactContext) { + if (name.equals(ReanimatedModule.NAME)) { + return new ReanimatedModule(reactContext); + } + return null; + } + + @Override + public ReactModuleInfoProvider getReactModuleInfoProvider() { + Class[] moduleList = + new Class[] { + ReanimatedModule.class, + }; + + final Map reactModuleInfoMap = new HashMap<>(); + for (Class moduleClass : moduleList) { + ReactModule reactModule = moduleClass.getAnnotation(ReactModule.class); + + reactModuleInfoMap.put( + reactModule.name(), + new ReactModuleInfo( + reactModule.name(), + moduleClass.getName(), + true, + reactModule.needsEagerInit(), + reactModule.hasConstants(), + reactModule.isCxxModule(), + TurboModule.class.isAssignableFrom(moduleClass))); + } + + return new ReactModuleInfoProvider() { + @Override + public Map getReactModuleInfos() { + return reactModuleInfoMap; + } + }; + } + + private UIManagerModule createUIManager(final ReactApplicationContext reactContext) { + ReactMarker.logMarker(CREATE_UI_MANAGER_MODULE_START); + Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "createUIManagerModule"); + final ReactInstanceManager reactInstanceManager = getReactInstanceManager(reactContext); + int minTimeLeftInFrameForNonBatchedOperationMs = -1; + try { + return new ReanimatedUIManager( + reactContext, + reactInstanceManager.getOrCreateViewManagers(reactContext), + minTimeLeftInFrameForNonBatchedOperationMs); + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + ReactMarker.logMarker(CREATE_UI_MANAGER_MODULE_END); + } + } + + /** + * Get the {@link ReactInstanceManager} used by this app. By default, assumes {@link + * ReactApplicationContext#getApplicationContext()} is an instance of {@link ReactApplication} and + * calls {@link ReactApplication#getReactNativeHost().getReactInstanceManager()}. Override this + * method if your application class does not implement {@code ReactApplication} or you simply have + * a different mechanism for storing a {@code ReactInstanceManager}, e.g. as a static field + * somewhere. + */ + public ReactInstanceManager getReactInstanceManager(ReactApplicationContext reactContext) { + return ((ReactApplication) reactContext.getApplicationContext()) + .getReactNativeHost() + .getReactInstanceManager(); + } +} \ No newline at end of file diff --git a/Example/android/app/src/main/java/tonlabs/uikit/TraceLog.java b/Example/android/app/src/main/java/tonlabs/uikit/TraceLog.java new file mode 100644 index 000000000..41fb9c236 --- /dev/null +++ b/Example/android/app/src/main/java/tonlabs/uikit/TraceLog.java @@ -0,0 +1,11 @@ +package tonlabs.uikit; + +import android.util.Log; + +public class TraceLog { + static final String TAG = "RNSTEST"; + + public static void log(String payload) { + Log.d(TAG, payload + " : " + System.currentTimeMillis()); + } +} diff --git a/Example/android/app/src/main/java/tonlabs/uikit/TraceNativeViewHierarchyManager.java b/Example/android/app/src/main/java/tonlabs/uikit/TraceNativeViewHierarchyManager.java new file mode 100644 index 000000000..e17e4b6f8 --- /dev/null +++ b/Example/android/app/src/main/java/tonlabs/uikit/TraceNativeViewHierarchyManager.java @@ -0,0 +1,48 @@ +package tonlabs.uikit; + +import androidx.annotation.Nullable; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.uimanager.NativeViewHierarchyManager; +import com.facebook.react.uimanager.ReactStylesDiffMap; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.TraceUIViewOperationQueue; +import com.facebook.react.uimanager.ViewAtIndex; +import com.facebook.react.uimanager.ViewGroupManager; +import com.facebook.react.uimanager.ViewManagerRegistry; + +public class TraceNativeViewHierarchyManager extends NativeViewHierarchyManager { + public TraceUIViewOperationQueue mTraceUIViewOperationQueue; + + public TraceNativeViewHierarchyManager( + ViewManagerRegistry viewManagers) { + super(viewManagers); + } + + @Override + public synchronized void createView(ThemedReactContext themedContext, int tag, String className, @Nullable ReactStylesDiffMap initialProps) { + TraceLog.log("TraceNativeViewHierarchyManager createView " + className); + if (className.equals("RNSScreen")) { + mTraceUIViewOperationQueue.printableDoFrames = 10; + } + +// long t = System.currentTimeMillis(); + super.createView(themedContext, tag, className, initialProps); +// TraceLog.log("TraceNativeViewHierarchyManager createView - " + (System.currentTimeMillis() - t) + " " + className); + } + + @Override + public synchronized void manageChildren(int tag, @Nullable int[] indicesToRemove, @Nullable ViewAtIndex[] viewsToAdd, @Nullable int[] tagsToDelete) { + final ViewGroupManager viewManager = (ViewGroupManager)this.resolveViewManager(tag); + TraceLog.log("TraceNativeViewHierarchyManager manageChildren " + viewManager.getName() + " (calls addView)"); + + super.manageChildren(tag, indicesToRemove, viewsToAdd, tagsToDelete); + } + + @Override + public synchronized void setChildren(int tag, ReadableArray childrenTags) { + TraceLog.log("TraceNativeViewHierarchyManager setChildren (calls addView)"); + super.setChildren(tag, childrenTags); + } +} diff --git a/Example/android/app/src/main/java/tonlabs/uikit/TracePackage.java b/Example/android/app/src/main/java/tonlabs/uikit/TracePackage.java new file mode 100644 index 000000000..651696005 --- /dev/null +++ b/Example/android/app/src/main/java/tonlabs/uikit/TracePackage.java @@ -0,0 +1,49 @@ +package tonlabs.uikit; + +import androidx.annotation.NonNull; + +import com.facebook.react.ReactApplication; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.TraceUIManagerModule; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.ViewManager; + +import java.util.ArrayList; +import java.util.List; + +public class TracePackage implements ReactPackage { + + @NonNull + @Override + public List createNativeModules(@NonNull ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + modules.add(createUIManager(reactContext)); + return modules; + } + + @NonNull + @Override + public List createViewManagers(@NonNull ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + return modules; + } + + private UIManagerModule createUIManager(final ReactApplicationContext reactContext) { + final ReactInstanceManager reactInstanceManager = getReactInstanceManager(reactContext); + int minTimeLeftInFrameForNonBatchedOperationMs = -1; + + return new TraceUIManagerModule( + reactContext, + reactInstanceManager.getOrCreateViewManagers(reactContext), + minTimeLeftInFrameForNonBatchedOperationMs); + } + + public ReactInstanceManager getReactInstanceManager(ReactApplicationContext reactContext) { + return ((ReactApplication) reactContext.getApplicationContext()) + .getReactNativeHost() + .getReactInstanceManager(); + } +} diff --git a/Example/android/app/src/main/java/tonlabs/uikit/TraceUIImplementation.java b/Example/android/app/src/main/java/tonlabs/uikit/TraceUIImplementation.java new file mode 100644 index 000000000..75b3d0e5b --- /dev/null +++ b/Example/android/app/src/main/java/tonlabs/uikit/TraceUIImplementation.java @@ -0,0 +1,58 @@ +package tonlabs.uikit; + +import androidx.annotation.Nullable; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.uimanager.TraceUIViewOperationQueue; +import com.facebook.react.uimanager.UIImplementation; +import com.facebook.react.uimanager.UIViewOperationQueue; +import com.facebook.react.uimanager.ViewManager; +import com.facebook.react.uimanager.ViewManagerRegistry; +import com.facebook.react.uimanager.ViewManagerResolver; +import com.facebook.react.uimanager.events.EventDispatcher; + +public class TraceUIImplementation extends UIImplementation { + public TraceUIImplementation( + ReactApplicationContext reactContext, + ViewManagerRegistry viewManagerRegistry, + EventDispatcher eventDispatcher, + int minTimeLeftInFrameForNonBatchedOperationMs) { + super( + reactContext, + viewManagerRegistry, + new TraceUIViewOperationQueue( + reactContext, + new TraceNativeViewHierarchyManager(viewManagerRegistry), + minTimeLeftInFrameForNonBatchedOperationMs), + eventDispatcher); + } + + public void createView(int tag, String className, int rootViewTag, ReadableMap props) { + TraceLog.log("TraceUIImplementation createView " + className); +// long t = System.currentTimeMillis(); + super.createView(tag, className, rootViewTag, props); +// TraceLog.log("TraceUIManagerModule creationTime - " + (System.currentTimeMillis() - t) + " " + className); + } + + public void updateView(final int tag, final String className, final ReadableMap props) { + TraceLog.log("TraceUIImplementation updateView " + className); + long t = System.currentTimeMillis(); + super.updateView(tag, className, props); + TraceLog.log("TraceUIManagerModule updateTime - " + (System.currentTimeMillis() - t) + " " + className); + } + + public void manageChildren( + int viewTag, + @Nullable ReadableArray moveFrom, + @Nullable ReadableArray moveTo, + @Nullable ReadableArray addChildTags, + @Nullable ReadableArray addAtIndices, + @Nullable ReadableArray removeFrom) { + TraceLog.log("TraceUIImplementation manageChildren1"); + super.manageChildren(viewTag, moveFrom, moveTo, addChildTags, addAtIndices, removeFrom); + TraceLog.log("TraceUIImplementation manageChildren2"); + } +} diff --git a/Example/android/app/src/main/java/tonlabs/uikit/TraceUIManagerModule.java b/Example/android/app/src/main/java/tonlabs/uikit/TraceUIManagerModule.java new file mode 100644 index 000000000..93703babf --- /dev/null +++ b/Example/android/app/src/main/java/tonlabs/uikit/TraceUIManagerModule.java @@ -0,0 +1,302 @@ +package com.facebook.react.uimanager; + +import androidx.annotation.Nullable; + +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.Dynamic; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.module.annotations.ReactModule; +import com.facebook.react.uimanager.events.EventDispatcher; + +import java.util.List; + +import tonlabs.uikit.TraceLog; +import tonlabs.uikit.TraceUIImplementation; + +class TraceUIImplementationProvider extends UIImplementationProvider { + @Override + UIImplementation createUIImplementation( + ReactApplicationContext reactContext, + ViewManagerRegistry viewManagerRegistry, + EventDispatcher eventDispatcher, + int minTimeLeftInFrameForNonBatchedOperationMs) { + TraceLog.log("TraceUIImplementationProvider"); + return new TraceUIImplementation( + reactContext, + viewManagerRegistry, + eventDispatcher, + minTimeLeftInFrameForNonBatchedOperationMs); + } +} + +@ReactModule(name = UIManagerModule.NAME) +public class TraceUIManagerModule extends UIManagerModule { + public TraceUIManagerModule( + ReactApplicationContext reactContext, + List viewManagersList, + int minTimeLeftInFrameForNonBatchedOperationMs) { + super( + reactContext, + viewManagersList, + new TraceUIImplementationProvider(), + minTimeLeftInFrameForNonBatchedOperationMs); + } + + @Override + public boolean canOverrideExistingModule() { + return true; + } + + public void onBatchComplete() { + super.onBatchComplete(); + } + + @ReactMethod(isBlockingSynchronousMethod = true) + public @Nullable WritableMap getConstantsForViewManager(@Nullable String viewManagerName) { + return super.getConstantsForViewManager(viewManagerName); + } + + @ReactMethod(isBlockingSynchronousMethod = true) + public WritableMap getDefaultEventTypes() { + return super.getDefaultEventTypes(); + } + + /** Unregisters a new root view. */ + @ReactMethod + public void removeRootView(int rootViewTag) { + super.removeRootView(rootViewTag); + } + + @ReactMethod + public void createView(int tag, String className, int rootViewTag, ReadableMap props) { +// TraceLog.log("TraceUIManagerModule createView " + className); +// long t = System.currentTimeMillis(); + super.createView(tag, className, rootViewTag, props); +// TraceLog.log("TraceUIManagerModule creationTime - " + (System.currentTimeMillis() - t) + " " + className); + } + + @ReactMethod + public void updateView(final int tag, final String className, final ReadableMap props) { +// TraceLog.log("TraceUIManagerModule updateView " + className); +// long t = System.currentTimeMillis(); + super.updateView(tag, className, props); +// TraceLog.log("TraceUIManagerModule updateTime - " + (System.currentTimeMillis() - t) + " " + className); + } + + /** + * Interface for adding/removing/moving views within a parent view from JS. + * + * @param viewTag the view tag of the parent view + * @param moveFrom a list of indices in the parent view to move views from + * @param moveTo parallel to moveFrom, a list of indices in the parent view to move views to + * @param addChildTags a list of tags of views to add to the parent + * @param addAtIndices parallel to addChildTags, a list of indices to insert those children at + * @param removeFrom a list of indices of views to permanently remove. The memory for the + * corresponding views and data structures should be reclaimed. + */ + @ReactMethod + public void manageChildren( + int viewTag, + @Nullable ReadableArray moveFrom, + @Nullable ReadableArray moveTo, + @Nullable ReadableArray addChildTags, + @Nullable ReadableArray addAtIndices, + @Nullable ReadableArray removeFrom) { + super.manageChildren(viewTag, moveFrom, moveTo, addChildTags, addAtIndices, removeFrom); + } + + /** + * Interface for fast tracking the initial adding of views. Children view tags are assumed to be + * in order + * + * @param viewTag the view tag of the parent view + * @param childrenTags An array of tags to add to the parent in order + */ + @ReactMethod + public void setChildren(int viewTag, ReadableArray childrenTags) { + super.setChildren(viewTag, childrenTags); + } + + /** + * Replaces the View specified by oldTag with the View specified by newTag within oldTag's parent. + * This resolves to a simple {@link #manageChildren} call, but React doesn't have enough info in + * JS to formulate it itself. + * + * @deprecated This method will not be available in Fabric UIManager class. + */ + @ReactMethod + @Deprecated + public void replaceExistingNonRootView(int oldTag, int newTag) { + super.replaceExistingNonRootView(oldTag, newTag); + } + + /** + * Method which takes a container tag and then releases all subviews for that container upon + * receipt. + * + * @param containerTag the tag of the container for which the subviews must be removed + * @deprecated This method will not be available in Fabric UIManager class. + */ + @ReactMethod + @Deprecated + public void removeSubviewsFromContainerWithID(int containerTag) { + super.removeSubviewsFromContainerWithID(containerTag); + } + + /** + * Determines the location on screen, width, and height of the given view and returns the values + * via an async callback. + */ + @ReactMethod + public void measure(int reactTag, Callback callback) { + super.measure(reactTag, callback); + } + + /** + * Determines the location on screen, width, and height of the given view relative to the device + * screen and returns the values via an async callback. This is the absolute position including + * things like the status bar + */ + @ReactMethod + public void measureInWindow(int reactTag, Callback callback) { + super.measureInWindow(reactTag, callback); + } + + /** + * Measures the view specified by tag relative to the given ancestorTag. This means that the + * returned x, y are relative to the origin x, y of the ancestor view. Results are stored in the + * given outputBuffer. We allow ancestor view and measured view to be the same, in which case the + * position always will be (0, 0) and method will only measure the view dimensions. + * + *

NB: Unlike {@link #measure}, this will measure relative to the view layout, not the visible + * window which can cause unexpected results when measuring relative to things like ScrollViews + * that can have offset content on the screen. + */ + @ReactMethod + public void measureLayout( + int tag, int ancestorTag, Callback errorCallback, Callback successCallback) { + super.measureLayout(tag, ancestorTag, errorCallback, successCallback); + } + + /** + * Like {@link #measure} and {@link #measureLayout} but measures relative to the immediate parent. + * + *

NB: Unlike {@link #measure}, this will measure relative to the view layout, not the visible + * window which can cause unexpected results when measuring relative to things like ScrollViews + * that can have offset content on the screen. + * + * @deprecated this method will not be available in FabricUIManager class. + */ + @ReactMethod + @Deprecated + public void measureLayoutRelativeToParent( + int tag, Callback errorCallback, Callback successCallback) { + super.measureLayoutRelativeToParent(tag, errorCallback, successCallback); + } + + /** + * Find the touch target child native view in the supplied root view hierarchy, given a react + * target location. + * + *

This method is currently used only by Element Inspector DevTool. + * + * @param reactTag the tag of the root view to traverse + * @param point an array containing both X and Y target location + * @param callback will be called if with the identified child view react ID, and measurement + * info. If no view was found, callback will be invoked with no data. + */ + @ReactMethod + public void findSubviewIn( + final int reactTag, final ReadableArray point, final Callback callback) { + super.findSubviewIn(reactTag, point, callback); + } + + /** + * Check if the first shadow node is the descendant of the second shadow node + * + * @deprecated this method will not be available in FabricUIManager class. + */ + @ReactMethod + @Deprecated + public void viewIsDescendantOf( + final int reactTag, final int ancestorReactTag, final Callback callback) { + super.viewIsDescendantOf(reactTag, ancestorReactTag, callback); + } + + @ReactMethod + public void setJSResponder(int reactTag, boolean blockNativeResponder) { + super.setJSResponder(reactTag, blockNativeResponder); + } + + @ReactMethod + public void clearJSResponder() { + super.clearJSResponder(); + } + + @ReactMethod + public void dispatchViewManagerCommand( + int reactTag, Dynamic commandId, @Nullable ReadableArray commandArgs) { + super.dispatchViewManagerCommand(reactTag, commandId, commandArgs); + } + + /** + * Show a PopupMenu. + * + * @param reactTag the tag of the anchor view (the PopupMenu is displayed next to this view); this + * needs to be the tag of a native view (shadow views can not be anchors) + * @param items the menu items as an array of strings + * @param error will be called if there is an error displaying the menu + * @param success will be called with the position of the selected item as the first argument, or + * no arguments if the menu is dismissed + */ + @ReactMethod + public void showPopupMenu(int reactTag, ReadableArray items, Callback error, Callback success) { + super.showPopupMenu(reactTag, items, error, success); + } + + @ReactMethod + public void dismissPopupMenu() { + super.dismissPopupMenu(); + } + + /** + * LayoutAnimation API on Android is currently experimental. Therefore, it needs to be enabled + * explicitly in order to avoid regression in existing application written for iOS using this API. + * + *

Warning : This method will be removed in future version of React Native, and layout + * animation will be enabled by default, so always check for its existence before invoking it. + * + *

TODO(9139831) : remove this method once layout animation is fully stable. + * + * @param enabled whether layout animation is enabled or not + */ + @ReactMethod + public void setLayoutAnimationEnabledExperimental(boolean enabled) { + super.setLayoutAnimationEnabledExperimental(enabled); + } + + /** + * Configure an animation to be used for the native layout changes, and native views creation. The + * animation will only apply during the current batch operations. + * + *

TODO(7728153) : animating view deletion is currently not supported. + * + * @param config the configuration of the animation for view addition/removal/update. + * @param success will be called when the animation completes, or when the animation get + * interrupted. In this case, callback parameter will be false. + * @param error will be called if there was an error processing the animation + */ + @ReactMethod + public void configureNextLayoutAnimation(ReadableMap config, Callback success, Callback error) { + super.configureNextLayoutAnimation(config, success, error); + } + + @ReactMethod + public void sendAccessibilityEvent(int tag, int eventType) { + super.sendAccessibilityEvent(tag, eventType); + } +} diff --git a/Example/android/app/src/main/java/tonlabs/uikit/TraceUIViewOperationQueue.java b/Example/android/app/src/main/java/tonlabs/uikit/TraceUIViewOperationQueue.java new file mode 100644 index 000000000..836a7ccd1 --- /dev/null +++ b/Example/android/app/src/main/java/tonlabs/uikit/TraceUIViewOperationQueue.java @@ -0,0 +1,1128 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.uimanager; + +import android.os.SystemClock; +import android.view.View; +import androidx.annotation.GuardedBy; +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; +import com.facebook.common.logging.FLog; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.GuardedRunnable; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReactNoCrashSoftException; +import com.facebook.react.bridge.ReactSoftExceptionLogger; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.RetryableMountingLayerException; +import com.facebook.react.bridge.SoftAssertions; +import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.common.ReactConstants; +import com.facebook.react.modules.core.ReactChoreographer; +import com.facebook.react.uimanager.debug.NotThreadSafeViewHierarchyUpdateDebugListener; +import com.facebook.systrace.Systrace; +import com.facebook.systrace.SystraceMessage; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import tonlabs.uikit.TraceLog; +import tonlabs.uikit.TraceNativeViewHierarchyManager; + + +public class TraceUIViewOperationQueue extends UIViewOperationQueue { + + public static final int DEFAULT_MIN_TIME_LEFT_IN_FRAME_FOR_NONBATCHED_OPERATION_MS = 8; + private static final String TAG = UIViewOperationQueue.class.getSimpleName(); + + private final int[] mMeasureBuffer = new int[4]; + + /** A mutation or animation operation on the view hierarchy. */ + public interface UIOperation { + + void execute(); + } + + /** A spec for an operation on the native View hierarchy. */ + private abstract class ViewOperation implements UIOperation { + + public int mTag; + + public ViewOperation(int tag) { + mTag = tag; + } + } + + private final class RemoveRootViewOperation extends ViewOperation { + + public RemoveRootViewOperation(int tag) { + super(tag); + } + + @Override + public void execute() { + mNativeViewHierarchyManager.removeRootView(mTag); + } + } + + private final class UpdatePropertiesOperation extends ViewOperation { + + private final ReactStylesDiffMap mProps; + + private UpdatePropertiesOperation(int tag, ReactStylesDiffMap props) { + super(tag); + mProps = props; + } + + @Override + public void execute() { + mNativeViewHierarchyManager.updateProperties(mTag, mProps); + } + } + + private final class EmitOnLayoutEventOperation extends ViewOperation { + + private final int mScreenX; + private final int mScreenY; + private final int mScreenWidth; + private final int mScreenHeight; + + public EmitOnLayoutEventOperation( + int tag, int screenX, int screenY, int screenWidth, int screenHeight) { + super(tag); + mScreenX = screenX; + mScreenY = screenY; + mScreenWidth = screenWidth; + mScreenHeight = screenHeight; + } + + @Override + public void execute() { + UIManagerModule uiManager = mReactApplicationContext.getNativeModule(UIManagerModule.class); + + if (uiManager != null) { + uiManager + .getEventDispatcher() + .dispatchEvent( + OnLayoutEvent.obtain( + -1 /* SurfaceId not used in classic renderer */, + mTag, + mScreenX, + mScreenY, + mScreenWidth, + mScreenHeight)); + } + } + } + + private final class UpdateInstanceHandleOperation extends ViewOperation { + + private final long mInstanceHandle; + + private UpdateInstanceHandleOperation(int tag, long instanceHandle) { + super(tag); + mInstanceHandle = instanceHandle; + } + + @Override + public void execute() { + mNativeViewHierarchyManager.updateInstanceHandle(mTag, mInstanceHandle); + } + } + + /** + * Operation for updating native view's position and size. The operation is not created directly + * by a {@link UIManagerModule} call from JS. Instead it gets inflated using computed position and + * size values by CSSNodeDEPRECATED hierarchy. + */ + private final class UpdateLayoutOperation extends ViewOperation { + + private final int mParentTag, mX, mY, mWidth, mHeight; + + public UpdateLayoutOperation(int parentTag, int tag, int x, int y, int width, int height) { + super(tag); + mParentTag = parentTag; + mX = x; + mY = y; + mWidth = width; + mHeight = height; + Systrace.startAsyncFlow(Systrace.TRACE_TAG_REACT_VIEW, "updateLayout", mTag); + } + + @Override + public void execute() { + Systrace.endAsyncFlow(Systrace.TRACE_TAG_REACT_VIEW, "updateLayout", mTag); + mNativeViewHierarchyManager.updateLayout(mParentTag, mTag, mX, mY, mWidth, mHeight); + } + } + + private final class CreateViewOperation extends ViewOperation { + + private final ThemedReactContext mThemedContext; + private final String mClassName; + private final @Nullable ReactStylesDiffMap mInitialProps; + + public CreateViewOperation( + ThemedReactContext themedContext, + int tag, + String className, + @Nullable ReactStylesDiffMap initialProps) { + super(tag); + mThemedContext = themedContext; + mClassName = className; + mInitialProps = initialProps; + Systrace.startAsyncFlow(Systrace.TRACE_TAG_REACT_VIEW, "createView", mTag); + } + + @Override + public void execute() { + Systrace.endAsyncFlow(Systrace.TRACE_TAG_REACT_VIEW, "createView", mTag); + mNativeViewHierarchyManager.createView(mThemedContext, mTag, mClassName, mInitialProps); + } + } + + private final class ManageChildrenOperation extends ViewOperation { + + private final @Nullable int[] mIndicesToRemove; + private final @Nullable ViewAtIndex[] mViewsToAdd; + private final @Nullable int[] mTagsToDelete; + + public ManageChildrenOperation( + int tag, + @Nullable int[] indicesToRemove, + @Nullable ViewAtIndex[] viewsToAdd, + @Nullable int[] tagsToDelete) { + super(tag); + mIndicesToRemove = indicesToRemove; + mViewsToAdd = viewsToAdd; + mTagsToDelete = tagsToDelete; + } + + @Override + public void execute() { + mNativeViewHierarchyManager.manageChildren( + mTag, mIndicesToRemove, mViewsToAdd, mTagsToDelete); + } + } + + private final class SetChildrenOperation extends ViewOperation { + + private final ReadableArray mChildrenTags; + + public SetChildrenOperation(int tag, ReadableArray childrenTags) { + super(tag); + mChildrenTags = childrenTags; + } + + @Override + public void execute() { + mNativeViewHierarchyManager.setChildren(mTag, mChildrenTags); + } + } + + private final class UpdateViewExtraData extends ViewOperation { + + private final Object mExtraData; + + public UpdateViewExtraData(int tag, Object extraData) { + super(tag); + mExtraData = extraData; + } + + @Override + public void execute() { + mNativeViewHierarchyManager.updateViewExtraData(mTag, mExtraData); + } + } + + private final class ChangeJSResponderOperation extends ViewOperation { + + private final int mInitialTag; + private final boolean mBlockNativeResponder; + private final boolean mClearResponder; + + public ChangeJSResponderOperation( + int tag, int initialTag, boolean clearResponder, boolean blockNativeResponder) { + super(tag); + mInitialTag = initialTag; + mClearResponder = clearResponder; + mBlockNativeResponder = blockNativeResponder; + } + + @Override + public void execute() { + if (!mClearResponder) { + mNativeViewHierarchyManager.setJSResponder(mTag, mInitialTag, mBlockNativeResponder); + } else { + mNativeViewHierarchyManager.clearJSResponder(); + } + } + } + + /** + * This is a common interface for View Command operations. Once we delete the deprecated {@link + * DispatchCommandOperation}, we can delete this interface too. It provides a set of common + * operations to simplify generic operations on all types of ViewCommands. + */ + private interface DispatchCommandViewOperation { + + /** + * Like the execute function, but throws real exceptions instead of logging soft errors and + * returning silently. + */ + void executeWithExceptions(); + + /** Increment retry counter. */ + void incrementRetries(); + + /** Get retry counter. */ + int getRetries(); + } + + @Deprecated + private final class DispatchCommandOperation extends ViewOperation + implements DispatchCommandViewOperation { + + private final int mCommand; + private final @Nullable ReadableArray mArgs; + + private int numRetries = 0; + + public DispatchCommandOperation(int tag, int command, @Nullable ReadableArray args) { + super(tag); + mCommand = command; + mArgs = args; + } + + @Override + public void execute() { + try { + mNativeViewHierarchyManager.dispatchCommand(mTag, mCommand, mArgs); + } catch (Throwable e) { + ReactSoftExceptionLogger.logSoftException( + TAG, new RuntimeException("Error dispatching View Command", e)); + } + } + + @Override + public void executeWithExceptions() { + mNativeViewHierarchyManager.dispatchCommand(mTag, mCommand, mArgs); + } + + @Override + @UiThread + public void incrementRetries() { + numRetries++; + } + + @Override + @UiThread + public int getRetries() { + return numRetries; + } + } + + private final class DispatchStringCommandOperation extends ViewOperation + implements DispatchCommandViewOperation { + + private final String mCommand; + private final @Nullable ReadableArray mArgs; + private int numRetries = 0; + + public DispatchStringCommandOperation(int tag, String command, @Nullable ReadableArray args) { + super(tag); + mCommand = command; + mArgs = args; + } + + @Override + public void execute() { + try { + mNativeViewHierarchyManager.dispatchCommand(mTag, mCommand, mArgs); + } catch (Throwable e) { + ReactSoftExceptionLogger.logSoftException( + TAG, new RuntimeException("Error dispatching View Command", e)); + } + } + + @Override + @UiThread + public void executeWithExceptions() { + mNativeViewHierarchyManager.dispatchCommand(mTag, mCommand, mArgs); + } + + @Override + @UiThread + public void incrementRetries() { + numRetries++; + } + + @Override + public int getRetries() { + return numRetries; + } + } + + private final class ShowPopupMenuOperation extends ViewOperation { + + private final ReadableArray mItems; + private final Callback mError; + private final Callback mSuccess; + + public ShowPopupMenuOperation(int tag, ReadableArray items, Callback error, Callback success) { + super(tag); + mItems = items; + mError = error; + mSuccess = success; + } + + @Override + public void execute() { + mNativeViewHierarchyManager.showPopupMenu(mTag, mItems, mSuccess, mError); + } + } + + private final class DismissPopupMenuOperation implements UIOperation { + @Override + public void execute() { + mNativeViewHierarchyManager.dismissPopupMenu(); + } + } + + /** A spec for animation operations (add/remove) */ + private abstract static class AnimationOperation implements UIViewOperationQueue.UIOperation { + + protected final int mAnimationID; + + public AnimationOperation(int animationID) { + mAnimationID = animationID; + } + } + + private class SetLayoutAnimationEnabledOperation implements UIOperation { + private final boolean mEnabled; + + private SetLayoutAnimationEnabledOperation(final boolean enabled) { + mEnabled = enabled; + } + + @Override + public void execute() { + mNativeViewHierarchyManager.setLayoutAnimationEnabled(mEnabled); + } + } + + private class ConfigureLayoutAnimationOperation implements UIOperation { + private final ReadableMap mConfig; + private final Callback mAnimationComplete; + + private ConfigureLayoutAnimationOperation( + final ReadableMap config, final Callback animationComplete) { + mConfig = config; + mAnimationComplete = animationComplete; + } + + @Override + public void execute() { + mNativeViewHierarchyManager.configureLayoutAnimation(mConfig, mAnimationComplete); + } + } + + private final class MeasureOperation implements UIOperation { + + private final int mReactTag; + private final Callback mCallback; + + private MeasureOperation(final int reactTag, final Callback callback) { + super(); + mReactTag = reactTag; + mCallback = callback; + } + + @Override + public void execute() { + try { + mNativeViewHierarchyManager.measure(mReactTag, mMeasureBuffer); + } catch (NoSuchNativeViewException e) { + // Invoke with no args to signal failure and to allow JS to clean up the callback + // handle. + mCallback.invoke(); + return; + } + + float x = PixelUtil.toDIPFromPixel(mMeasureBuffer[0]); + float y = PixelUtil.toDIPFromPixel(mMeasureBuffer[1]); + float width = PixelUtil.toDIPFromPixel(mMeasureBuffer[2]); + float height = PixelUtil.toDIPFromPixel(mMeasureBuffer[3]); + mCallback.invoke(0, 0, width, height, x, y); + } + } + + private final class MeasureInWindowOperation implements UIOperation { + + private final int mReactTag; + private final Callback mCallback; + + private MeasureInWindowOperation(final int reactTag, final Callback callback) { + super(); + mReactTag = reactTag; + mCallback = callback; + } + + @Override + public void execute() { + try { + mNativeViewHierarchyManager.measureInWindow(mReactTag, mMeasureBuffer); + } catch (NoSuchNativeViewException e) { + // Invoke with no args to signal failure and to allow JS to clean up the callback + // handle. + mCallback.invoke(); + return; + } + + float x = PixelUtil.toDIPFromPixel(mMeasureBuffer[0]); + float y = PixelUtil.toDIPFromPixel(mMeasureBuffer[1]); + float width = PixelUtil.toDIPFromPixel(mMeasureBuffer[2]); + float height = PixelUtil.toDIPFromPixel(mMeasureBuffer[3]); + mCallback.invoke(x, y, width, height); + } + } + + private final class FindTargetForTouchOperation implements UIOperation { + + private final int mReactTag; + private final float mTargetX; + private final float mTargetY; + private final Callback mCallback; + + private FindTargetForTouchOperation( + final int reactTag, final float targetX, final float targetY, final Callback callback) { + super(); + mReactTag = reactTag; + mTargetX = targetX; + mTargetY = targetY; + mCallback = callback; + } + + @Override + public void execute() { + try { + mNativeViewHierarchyManager.measure(mReactTag, mMeasureBuffer); + } catch (IllegalViewOperationException e) { + mCallback.invoke(); + return; + } + + // Because React coordinates are relative to root container, and measure() operates + // on screen coordinates, we need to offset values using root container location. + final float containerX = (float) mMeasureBuffer[0]; + final float containerY = (float) mMeasureBuffer[1]; + + final int touchTargetReactTag = + mNativeViewHierarchyManager.findTargetTagForTouch(mReactTag, mTargetX, mTargetY); + + try { + mNativeViewHierarchyManager.measure(touchTargetReactTag, mMeasureBuffer); + } catch (IllegalViewOperationException e) { + mCallback.invoke(); + return; + } + + float x = PixelUtil.toDIPFromPixel(mMeasureBuffer[0] - containerX); + float y = PixelUtil.toDIPFromPixel(mMeasureBuffer[1] - containerY); + float width = PixelUtil.toDIPFromPixel(mMeasureBuffer[2]); + float height = PixelUtil.toDIPFromPixel(mMeasureBuffer[3]); + mCallback.invoke(touchTargetReactTag, x, y, width, height); + } + } + + private final class LayoutUpdateFinishedOperation implements UIOperation { + + private final ReactShadowNode mNode; + private final UIImplementation.LayoutUpdateListener mListener; + + private LayoutUpdateFinishedOperation( + ReactShadowNode node, UIImplementation.LayoutUpdateListener listener) { + mNode = node; + mListener = listener; + } + + @Override + public void execute() { + mListener.onLayoutUpdated(mNode); + } + } + + private class UIBlockOperation implements UIOperation { + private final UIBlock mBlock; + + public UIBlockOperation(UIBlock block) { + mBlock = block; + } + + @Override + public void execute() { + mBlock.execute(mNativeViewHierarchyManager); + } + } + + private final class SendAccessibilityEvent extends ViewOperation { + + private final int mEventType; + + private SendAccessibilityEvent(int tag, int eventType) { + super(tag); + mEventType = eventType; + } + + @Override + public void execute() { + mNativeViewHierarchyManager.sendAccessibilityEvent(mTag, mEventType); + } + } + + private final NativeViewHierarchyManager mNativeViewHierarchyManager; + private final Object mDispatchRunnablesLock = new Object(); + private final Object mNonBatchedOperationsLock = new Object(); + private final DispatchUIFrameCallback mDispatchUIFrameCallback; + private final ReactApplicationContext mReactApplicationContext; + + private ArrayList mViewCommandOperations = new ArrayList<>(); + + // Only called from the UIManager queue? + private ArrayList mOperations = new ArrayList<>(); + + @GuardedBy("mDispatchRunnablesLock") + private ArrayList mDispatchUIRunnables = new ArrayList<>(); + + @GuardedBy("mNonBatchedOperationsLock") + private ArrayDeque mNonBatchedOperations = new ArrayDeque<>(); + + private @Nullable NotThreadSafeViewHierarchyUpdateDebugListener mViewHierarchyUpdateDebugListener; + private boolean mIsDispatchUIFrameCallbackEnqueued = false; + private boolean mIsInIllegalUIState = false; + private boolean mIsProfilingNextBatch = false; + private long mNonBatchedExecutionTotalTime; + private long mProfiledBatchCommitStartTime; + private long mProfiledBatchCommitEndTime; + private long mProfiledBatchLayoutTime; + private long mProfiledBatchDispatchViewUpdatesTime; + private long mProfiledBatchRunStartTime; + private long mProfiledBatchRunEndTime; + private long mProfiledBatchBatchedExecutionTime; + private long mProfiledBatchNonBatchedExecutionTime; + private long mThreadCpuTime; + private long mCreateViewCount; + private long mUpdatePropertiesOperationCount; + + public int printableDoFrames = 0; + + public TraceUIViewOperationQueue( + ReactApplicationContext reactContext, + NativeViewHierarchyManager nativeViewHierarchyManager, + int minTimeLeftInFrameForNonBatchedOperationMs) { + super(reactContext, nativeViewHierarchyManager, minTimeLeftInFrameForNonBatchedOperationMs); + + ((TraceNativeViewHierarchyManager) nativeViewHierarchyManager).mTraceUIViewOperationQueue = this; + + mNativeViewHierarchyManager = nativeViewHierarchyManager; + mDispatchUIFrameCallback = + new DispatchUIFrameCallback( + reactContext, + minTimeLeftInFrameForNonBatchedOperationMs == -1 + ? DEFAULT_MIN_TIME_LEFT_IN_FRAME_FOR_NONBATCHED_OPERATION_MS + : minTimeLeftInFrameForNonBatchedOperationMs); + mReactApplicationContext = reactContext; + } + + /*package*/ NativeViewHierarchyManager getNativeViewHierarchyManager() { + return mNativeViewHierarchyManager; + } + + public void setViewHierarchyUpdateDebugListener( + @Nullable NotThreadSafeViewHierarchyUpdateDebugListener listener) { + mViewHierarchyUpdateDebugListener = listener; + } + + public void profileNextBatch() { + mIsProfilingNextBatch = true; + mProfiledBatchCommitStartTime = 0; + mCreateViewCount = 0; + mUpdatePropertiesOperationCount = 0; + } + + public Map getProfiledBatchPerfCounters() { + Map perfMap = new HashMap<>(); + perfMap.put("CommitStartTime", mProfiledBatchCommitStartTime); + perfMap.put("CommitEndTime", mProfiledBatchCommitEndTime); + perfMap.put("LayoutTime", mProfiledBatchLayoutTime); + perfMap.put("DispatchViewUpdatesTime", mProfiledBatchDispatchViewUpdatesTime); + perfMap.put("RunStartTime", mProfiledBatchRunStartTime); + perfMap.put("RunEndTime", mProfiledBatchRunEndTime); + perfMap.put("BatchedExecutionTime", mProfiledBatchBatchedExecutionTime); + perfMap.put("NonBatchedExecutionTime", mProfiledBatchNonBatchedExecutionTime); + perfMap.put("NativeModulesThreadCpuTime", mThreadCpuTime); + perfMap.put("CreateViewCount", mCreateViewCount); + perfMap.put("UpdatePropsCount", mUpdatePropertiesOperationCount); + return perfMap; + } + + public boolean isEmpty() { + return mOperations.isEmpty() && mViewCommandOperations.isEmpty(); + } + + public void addRootView(final int tag, final View rootView) { + mNativeViewHierarchyManager.addRootView(tag, rootView); + } + + /** + * Enqueues a UIOperation to be executed in UI thread. This method should only be used by a + * subclass to support UIOperations not provided by UIViewOperationQueue. + */ + protected void enqueueUIOperation(UIOperation operation) { + SoftAssertions.assertNotNull(operation); + mOperations.add(operation); + } + + public void enqueueRemoveRootView(int rootViewTag) { + mOperations.add(new RemoveRootViewOperation(rootViewTag)); + } + + public void enqueueSetJSResponder(int tag, int initialTag, boolean blockNativeResponder) { + mOperations.add( + new ChangeJSResponderOperation( + tag, initialTag, false /*clearResponder*/, blockNativeResponder)); + } + + public void enqueueClearJSResponder() { + // Tag is 0 because JSResponderHandler doesn't need one in order to clear the responder. + mOperations.add(new ChangeJSResponderOperation(0, 0, true /*clearResponder*/, false)); + } + + @Deprecated + public void enqueueDispatchCommand( + int reactTag, int commandId, @Nullable ReadableArray commandArgs) { + final DispatchCommandOperation command = + new DispatchCommandOperation(reactTag, commandId, commandArgs); + mViewCommandOperations.add(command); + } + + public void enqueueDispatchCommand( + int reactTag, String commandId, @Nullable ReadableArray commandArgs) { + final DispatchStringCommandOperation command = + new DispatchStringCommandOperation(reactTag, commandId, commandArgs); + mViewCommandOperations.add(command); + } + + public void enqueueUpdateExtraData(int reactTag, Object extraData) { + mOperations.add(new UpdateViewExtraData(reactTag, extraData)); + } + + public void enqueueShowPopupMenu( + int reactTag, ReadableArray items, Callback error, Callback success) { + mOperations.add(new ShowPopupMenuOperation(reactTag, items, error, success)); + } + + public void enqueueDismissPopupMenu() { + mOperations.add(new DismissPopupMenuOperation()); + } + + public void enqueueCreateView( + ThemedReactContext themedContext, + int viewReactTag, + String viewClassName, + @Nullable ReactStylesDiffMap initialProps) { + synchronized (mNonBatchedOperationsLock) { + mCreateViewCount++; + mNonBatchedOperations.addLast( + new CreateViewOperation(themedContext, viewReactTag, viewClassName, initialProps)); + } + } + + public void enqueueUpdateInstanceHandle(int reactTag, long instanceHandle) { + mOperations.add(new UpdateInstanceHandleOperation(reactTag, instanceHandle)); + } + + public void enqueueUpdateProperties(int reactTag, String className, ReactStylesDiffMap props) { + mUpdatePropertiesOperationCount++; + mOperations.add(new UpdatePropertiesOperation(reactTag, props)); + } + + public void enqueueOnLayoutEvent( + int tag, int screenX, int screenY, int screenWidth, int screenHeight) { + mOperations.add( + new EmitOnLayoutEventOperation(tag, screenX, screenY, screenWidth, screenHeight)); + } + + public void enqueueUpdateLayout( + int parentTag, int reactTag, int x, int y, int width, int height) { + mOperations.add(new UpdateLayoutOperation(parentTag, reactTag, x, y, width, height)); + } + + public void enqueueManageChildren( + int reactTag, + @Nullable int[] indicesToRemove, + @Nullable ViewAtIndex[] viewsToAdd, + @Nullable int[] tagsToDelete) { + mOperations.add( + new ManageChildrenOperation(reactTag, indicesToRemove, viewsToAdd, tagsToDelete)); + } + + public void enqueueSetChildren(int reactTag, ReadableArray childrenTags) { + mOperations.add(new SetChildrenOperation(reactTag, childrenTags)); + } + + public void enqueueSetLayoutAnimationEnabled(final boolean enabled) { + mOperations.add(new SetLayoutAnimationEnabledOperation(enabled)); + } + + public void enqueueConfigureLayoutAnimation( + final ReadableMap config, final Callback onAnimationComplete) { + mOperations.add(new ConfigureLayoutAnimationOperation(config, onAnimationComplete)); + } + + public void enqueueMeasure(final int reactTag, final Callback callback) { + mOperations.add(new MeasureOperation(reactTag, callback)); + } + + public void enqueueMeasureInWindow(final int reactTag, final Callback callback) { + mOperations.add(new MeasureInWindowOperation(reactTag, callback)); + } + + public void enqueueFindTargetForTouch( + final int reactTag, final float targetX, final float targetY, final Callback callback) { + mOperations.add(new FindTargetForTouchOperation(reactTag, targetX, targetY, callback)); + } + + public void enqueueSendAccessibilityEvent(int tag, int eventType) { + mOperations.add(new SendAccessibilityEvent(tag, eventType)); + } + + public void enqueueLayoutUpdateFinished( + ReactShadowNode node, UIImplementation.LayoutUpdateListener listener) { + mOperations.add(new LayoutUpdateFinishedOperation(node, listener)); + } + + public void enqueueUIBlock(UIBlock block) { + mOperations.add(new UIBlockOperation(block)); + } + + public void prependUIBlock(UIBlock block) { + mOperations.add(0, new UIBlockOperation(block)); + } + + public void dispatchViewUpdates( + final int batchId, final long commitStartTime, final long layoutTime) { + SystraceMessage.beginSection( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "UIViewOperationQueue.dispatchViewUpdates") + .arg("batchId", batchId) + .flush(); + try { + final long dispatchViewUpdatesTime = SystemClock.uptimeMillis(); + final long nativeModulesThreadCpuTime = SystemClock.currentThreadTimeMillis(); + + // Store the current operation queues to dispatch and create new empty ones to continue + // receiving new operations + final ArrayList viewCommandOperations; + if (!mViewCommandOperations.isEmpty()) { + viewCommandOperations = mViewCommandOperations; + mViewCommandOperations = new ArrayList<>(); + } else { + viewCommandOperations = null; + } + + final ArrayList batchedOperations; + if (!mOperations.isEmpty()) { + batchedOperations = mOperations; + mOperations = new ArrayList<>(); + } else { + batchedOperations = null; + } + + final ArrayDeque nonBatchedOperations; + synchronized (mNonBatchedOperationsLock) { + if (!mNonBatchedOperations.isEmpty()) { + nonBatchedOperations = mNonBatchedOperations; + mNonBatchedOperations = new ArrayDeque<>(); + } else { + nonBatchedOperations = null; + } + } + + if (mViewHierarchyUpdateDebugListener != null) { + mViewHierarchyUpdateDebugListener.onViewHierarchyUpdateEnqueued(); + } + + Runnable runOperations = + new Runnable() { + @Override + public void run() { + SystraceMessage.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "DispatchUI") + .arg("BatchId", batchId) + .flush(); + TraceLog.log("dispatchViewUpdates runOperations"); + try { + long runStartTime = SystemClock.uptimeMillis(); + + // All ViewCommands should be executed first as a perf optimization. + // This entire block is only executed if there's at least one ViewCommand queued. + if (viewCommandOperations != null) { + for (DispatchCommandViewOperation op : viewCommandOperations) { + try { + op.executeWithExceptions(); + } catch (RetryableMountingLayerException e) { + // Catch errors in DispatchCommands. We allow all commands to be retried + // exactly once, after the current batch of other mountitems. If the second + // attempt fails, then we log a soft error. This will still crash only in + // debug. We do this because it is a ~relatively common pattern to dispatch a + // command during render, for example, to scroll to the bottom of a ScrollView + // in render. This dispatches the command before that View is even mounted. By + // retrying once, we can still dispatch the vast majority of commands faster, + // avoid errors, and still operate correctly for most commands even when + // they're executed too soon. + if (op.getRetries() == 0) { + op.incrementRetries(); + mViewCommandOperations.add(op); + } else { + // Retryable exceptions should be logged, but never crash in debug. + ReactSoftExceptionLogger.logSoftException( + TAG, new ReactNoCrashSoftException(e)); + } + } catch (Throwable e) { + // Non-retryable exceptions should be logged in prod, and crash in Debug. + ReactSoftExceptionLogger.logSoftException(TAG, e); + } + } + } + + // All nonBatchedOperations should be executed before regular operations as + // regular operations may depend on them + if (nonBatchedOperations != null) { + for (UIOperation op : nonBatchedOperations) { + op.execute(); + } + } + + if (batchedOperations != null) { + for (UIOperation op : batchedOperations) { + op.execute(); + } + } + + if (mIsProfilingNextBatch && mProfiledBatchCommitStartTime == 0) { + mProfiledBatchCommitStartTime = commitStartTime; + mProfiledBatchCommitEndTime = SystemClock.uptimeMillis(); + mProfiledBatchLayoutTime = layoutTime; + mProfiledBatchDispatchViewUpdatesTime = dispatchViewUpdatesTime; + mProfiledBatchRunStartTime = runStartTime; + mProfiledBatchRunEndTime = mProfiledBatchCommitEndTime; + mThreadCpuTime = nativeModulesThreadCpuTime; + + Systrace.beginAsyncSection( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, + "delayBeforeDispatchViewUpdates", + 0, + mProfiledBatchCommitStartTime * 1000000); + Systrace.endAsyncSection( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, + "delayBeforeDispatchViewUpdates", + 0, + mProfiledBatchDispatchViewUpdatesTime * 1000000); + Systrace.beginAsyncSection( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, + "delayBeforeBatchRunStart", + 0, + mProfiledBatchDispatchViewUpdatesTime * 1000000); + Systrace.endAsyncSection( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, + "delayBeforeBatchRunStart", + 0, + mProfiledBatchRunStartTime * 1000000); + } + + // Clear layout animation, as animation only apply to current UI operations batch. + mNativeViewHierarchyManager.clearLayoutAnimation(); + + if (mViewHierarchyUpdateDebugListener != null) { + mViewHierarchyUpdateDebugListener.onViewHierarchyUpdateFinished(); + } + } catch (Exception e) { + mIsInIllegalUIState = true; + throw e; + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + } + }; + + SystraceMessage.beginSection( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "acquiring mDispatchRunnablesLock") + .arg("batchId", batchId) + .flush(); + synchronized (mDispatchRunnablesLock) { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + mDispatchUIRunnables.add(runOperations); + } + + // In the case where the frame callback isn't enqueued, the UI isn't being displayed or is + // being + // destroyed. In this case it's no longer important to align to frames, but it is important to + // make + // sure any late-arriving UI commands are executed. + if (!mIsDispatchUIFrameCallbackEnqueued) { + UiThreadUtil.runOnUiThread( + new GuardedRunnable(mReactApplicationContext) { + @Override + public void runGuarded() { + mFlushPendingBatchesCaller = "UiThreadUtil.runOnUiThread flushPendingBatches"; + flushPendingBatches(); + } + }); + } + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + } + + /* package */ void resumeFrameCallback() { + mIsDispatchUIFrameCallbackEnqueued = true; + ReactChoreographer.getInstance() + .postFrameCallback(ReactChoreographer.CallbackType.DISPATCH_UI, mDispatchUIFrameCallback); + } + + /* package */ void pauseFrameCallback() { + mIsDispatchUIFrameCallbackEnqueued = false; + ReactChoreographer.getInstance() + .removeFrameCallback(ReactChoreographer.CallbackType.DISPATCH_UI, mDispatchUIFrameCallback); + mFlushPendingBatchesCaller = "pauseFrameCallback flushPendingBatches"; + flushPendingBatches(); + } + + String mFlushPendingBatchesCaller = "none"; + + private void flushPendingBatches() { + if (mIsInIllegalUIState) { + FLog.w( + ReactConstants.TAG, + "Not flushing pending UI operations because of previously thrown Exception"); + return; + } + + final ArrayList runnables; + synchronized (mDispatchRunnablesLock) { + if (!mDispatchUIRunnables.isEmpty()) { + runnables = mDispatchUIRunnables; + mDispatchUIRunnables = new ArrayList<>(); + } else { + return; + } + } + + TraceLog.log("flushPendingBatches " + mFlushPendingBatchesCaller + " " + runnables.size()); + + final long batchedExecutionStartTime = SystemClock.uptimeMillis(); + for (Runnable runnable : runnables) { + TraceLog.log("flushPendingBatches runnable"); + runnable.run(); + } + + if (mIsProfilingNextBatch) { + mProfiledBatchBatchedExecutionTime = SystemClock.uptimeMillis() - batchedExecutionStartTime; + mProfiledBatchNonBatchedExecutionTime = mNonBatchedExecutionTotalTime; + mIsProfilingNextBatch = false; + + Systrace.beginAsyncSection( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, + "batchedExecutionTime", + 0, + batchedExecutionStartTime * 1000000); + Systrace.endAsyncSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "batchedExecutionTime", 0); + } + mNonBatchedExecutionTotalTime = 0; + } + + private class DispatchUIFrameCallback extends GuardedFrameCallback { + + private static final int FRAME_TIME_MS = 16; + private final int mMinTimeLeftInFrameForNonBatchedOperationMs; + + private DispatchUIFrameCallback( + ReactContext reactContext, int minTimeLeftInFrameForNonBatchedOperationMs) { + super(reactContext); + mMinTimeLeftInFrameForNonBatchedOperationMs = minTimeLeftInFrameForNonBatchedOperationMs; + } + + @Override + public void doFrameGuarded(long frameTimeNanos) { + if (mIsInIllegalUIState) { + FLog.w( + ReactConstants.TAG, + "Not flushing pending UI operations because of previously thrown Exception"); + return; + } + + Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "dispatchNonBatchedUIOperations"); + try { + dispatchPendingNonBatchedOperations(frameTimeNanos); + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + + mFlushPendingBatchesCaller = "DispatchUIFrameCallback doFrameGuarded"; +// TraceLog.log("DispatchUIFrameCallback doFrameGuarded"); + if (printableDoFrames > 0) { + StringWriter sw = new StringWriter(); + new Throwable("").printStackTrace(new PrintWriter(sw)); + String stackTrace = sw.toString(); + + TraceLog.log(stackTrace); + + --printableDoFrames; + } + + flushPendingBatches(); + + ReactChoreographer.getInstance() + .postFrameCallback(ReactChoreographer.CallbackType.DISPATCH_UI, this); + } + + private void dispatchPendingNonBatchedOperations(long frameTimeNanos) { + while (true) { + long timeLeftInFrame = FRAME_TIME_MS - ((System.nanoTime() - frameTimeNanos) / 1000000); + if (timeLeftInFrame < mMinTimeLeftInFrameForNonBatchedOperationMs) { + break; + } + + UIOperation nextOperation; + synchronized (mNonBatchedOperationsLock) { + if (mNonBatchedOperations.isEmpty()) { + break; + } + + nextOperation = mNonBatchedOperations.pollFirst(); + } + + try { + long nonBatchedExecutionStartTime = SystemClock.uptimeMillis(); + nextOperation.execute(); + mNonBatchedExecutionTotalTime += + SystemClock.uptimeMillis() - nonBatchedExecutionStartTime; + } catch (Exception e) { + mIsInIllegalUIState = true; + throw e; + } + } + } + } +} \ No newline at end of file diff --git a/Example/ios/Quiver UI.xcodeproj/project.pbxproj b/Example/ios/Quiver UI.xcodeproj/project.pbxproj index ee96e9183..12b86c3a2 100644 --- a/Example/ios/Quiver UI.xcodeproj/project.pbxproj +++ b/Example/ios/Quiver UI.xcodeproj/project.pbxproj @@ -760,7 +760,7 @@ "FB_SONARKIT_ENABLED=1", ); INFOPLIST_FILE = "Quiver UI/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; OTHER_LDFLAGS = ( "$(inherited)", @@ -787,7 +787,7 @@ DEVELOPMENT_TEAM = S87M7476F9; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; INFOPLIST_FILE = "Quiver UI/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; OTHER_LDFLAGS = ( "$(inherited)", diff --git a/kit/controls/src/Pressable/Pressable.native.tsx b/kit/controls/src/Pressable/Pressable.native.tsx index c8281ba84..dd846a151 100644 --- a/kit/controls/src/Pressable/Pressable.native.tsx +++ b/kit/controls/src/Pressable/Pressable.native.tsx @@ -1,12 +1,32 @@ import * as React from 'react'; -import { runOnJS, useSharedValue } from 'react-native-reanimated'; -import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -import { View } from 'react-native'; +import Animated, { + runOnJS, + useAnimatedGestureHandler, + useSharedValue, +} from 'react-native-reanimated'; +import { + Gesture, + GestureDetector, + GestureEvent, + NativeViewGestureHandlerPayload, + NativeViewGestureHandlerProps, + RawButton as GHRawButton, + RawButtonProps, +} from 'react-native-gesture-handler'; +import { processColor, StyleProp, View, ViewStyle } from 'react-native'; import { maxPressDistance, PressableStateContext } from './constants'; import type { PressableProps } from './types'; import { usePressableState } from './hooks'; import { useConvertToAnimatedValue } from './hooks/useConvertToAnimatedValue'; +export const RawButton = Animated.createAnimatedComponent< + RawButtonProps & + NativeViewGestureHandlerProps & { + testID?: string; + style?: StyleProp; + } +>(GHRawButton); + /** * It is necessary to simplify the creation of new buttons. * It provides a context from which to get the current state of the component. @@ -30,46 +50,78 @@ export function Pressable({ const pressableState = usePressableState(isDisabled, isLoading, isPressed, isHovered); - let tap = Gesture.Tap() - .onEnd((_e, success: boolean) => { - success && onPress && runOnJS(onPress)(); - }) - .enabled(!(disabled || loading)); - - if (waitFor != null) { - tap = tap.requireExternalGestureToFail(waitFor); + const onPressRef = React.useRef(onPress); + if (onPress !== onPressRef.current) { + onPressRef.current = onPress; } + const onPressIfNeeded = React.useCallback(function onPressIfNeeded() { + if (onPressRef.current != null) { + onPressRef.current(); + } + }, []); - let longPress = Gesture.LongPress() - .maxDistance(maxPressDistance) - .shouldCancelWhenOutside(true) - .onBegin(() => { - isPressed.value = true; - }) - .onFinalize(() => { - isPressed.value = false; - }) - .onStart(() => { - onLongPress && runOnJS(onLongPress)(); - }) - .onEnd((_e, success: boolean) => { - !onLongPress && success && onPress && runOnJS(onPress)(); - }) - .enabled(!(disabled || loading)); + // Reanimated event mapping somehow freeze main thread on Android + // It cann be seen in Logcat that there might be up to 4-5 sec + // freeze in dev mode - if (waitFor != null) { - longPress = longPress.requireExternalGestureToFail(waitFor); - } + // const tap = Gesture.Tap() + // .onEnd((_e, success: boolean) => { + // success && runOnJS(onPressIfNeeded)(); + // }) + // .enabled(!(disabled || loading)); - const tapGestures = Gesture.Simultaneous(longPress, tap); + // const longPress = Gesture.LongPress() + // .maxDistance(maxPressDistance) + // .shouldCancelWhenOutside(true) + // .onBegin(() => { + // // isPressed.value = true; + // }) + // .onFinalize(() => { + // // isPressed.value = false; + // }) + // .onStart(() => { + // // onLongPress && runOnJS(onLongPress)(); + // }) + // .onEnd((_e, success: boolean) => { + // // !onLongPress && success && onPress && runOnJS(onPress)(); + // !onLongPress && success && runOnJS(onPressIfNeeded)(); + // }) + // .enabled(!(disabled || loading)); + + // const tapGestures = Gesture.Simultaneous(longPress, tap); + + const gestureHandler = useAnimatedGestureHandler>( + { + onStart: () => { + console.log('pressed'); + isPressed.value = true; + }, + onFinish: () => { + runOnJS(onPressIfNeeded)(); + }, + onCancel: () => { + isPressed.value = false; + }, + onEnd: () => { + console.log('unpressed'); + isPressed.value = false; + }, + }, + ); return ( - + {/* */} + {children} - + + {/* */} ); } diff --git a/kit/controls/src/UIBoxButton/BoxButtonContent.tsx b/kit/controls/src/UIBoxButton/BoxButtonContent.tsx index 1adf58db2..7eece1129 100644 --- a/kit/controls/src/UIBoxButton/BoxButtonContent.tsx +++ b/kit/controls/src/UIBoxButton/BoxButtonContent.tsx @@ -46,7 +46,8 @@ export const BoxButtonContent = ({ }); const animatedBackgroundOverlayStyle = useAnimatedStyle(() => { - if (type === UIBoxButtonType.Primary) { + // if (type === UIBoxButtonType.Primary) { + if (type === 'Primary') { return { backgroundColor: backgroundOverlayColor.value, };