diff --git a/frameworks/bridge/declarative_frontend/state_mgmt/files_to_watch.gni b/frameworks/bridge/declarative_frontend/state_mgmt/files_to_watch.gni index 13d444b18a662ff040a2f596d74c7811c78b8b84..31ee2dd397b6b7858eff418b7d144c180dedb9a7 100644 --- a/frameworks/bridge/declarative_frontend/state_mgmt/files_to_watch.gni +++ b/frameworks/bridge/declarative_frontend/state_mgmt/files_to_watch.gni @@ -70,6 +70,7 @@ state_mgmt_release_files_to_watch = [ "//foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/puv2_common/puv2_updatefunc.ts", "//foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/puv2_common/puv2_view_native_base.d.ts", "//foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/puv2_common/puv2_view_base.ts", + "//foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/puv2_common/unified_view_update_controller.ts", "//foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/partial_update/pu_types_events.ts", "//foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/partial_update/pu_tracked_object.ts", "//foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/partial_update/pu_foreach.d.ts", diff --git a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/partial_update/pu_view.ts b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/partial_update/pu_view.ts index fe32b0319217f16b0751008d127cbd96921d60fb..950168c2e9a7727f62629f8124fee20c80240ebc 100644 --- a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/partial_update/pu_view.ts +++ b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/partial_update/pu_view.ts @@ -222,9 +222,6 @@ abstract class ViewPU extends PUV2ViewBase ViewBuildNodeBase.arkThemeScopeManager?.onViewPUCreate(this) - // Disable optimization when V1 is involved. - ObserveV2.getObserve().isParentChildOptimizable_ = false; - if (localStorage) { this.localStorage_ = localStorage; stateMgmtConsole.debug(`${this.debugInfo__()}: constructor: Using LocalStorage instance provided via @Entry or view instance creation.`); @@ -552,13 +549,17 @@ abstract class ViewPU extends PUV2ViewBase return; } - this.syncInstanceId(); - if (dependentElmtIds.size && !this.isFirstRender()) { if (!this.dirtDescendantElementIds_.size && !this.runReuse_) { // mark ComposedElement dirty when first elmtIds are added // do not need to do this every time - this.markNeedUpdate(); + if (ObserveV2.IS_PARENT_CHILD_OPTIMIZABLE) { + this.optimizedUpdate(); + } else { + this.syncInstanceId(); + this.markNeedUpdate(); + this.restoreInstanceId(); + } } stateMgmtConsole.debug(`${this.debugInfo__()}: viewPropertyHasChanged property: elmtIds that need re-render due to state variable change: ${this.debugInfoElmtIds(Array.from(dependentElmtIds))} .`); for (const elmtId of dependentElmtIds) { @@ -580,11 +581,24 @@ abstract class ViewPU extends PUV2ViewBase cb.call(this, varName); } - this.restoreInstanceId(); aceDebugTrace.end(); stateMgmtProfiler.end(); } + private optimizedUpdate() { + stateMgmtConsole.debug(`V1 optimizedUpdate() start, @Component '${this.constructor.name}' from parent '${this.getParent()?.constructor.name}'`); + const instanceId = this.getInstanceId(); + let viewMap = unifiedViewNeedUpdateMap.get(instanceId); + if (!viewMap) { + stateMgmtConsole.debug(' Create a new Map for instanceId: ' + instanceId); + viewMap = new Map>(); + unifiedViewNeedUpdateMap.set(instanceId, viewMap); + } + // For V1, we don't have element IDs, so just use a placeholder + viewMap.set(this, []); + + UnifiedViewUpdateController.getInstance().scheduleUpdateOnNextVSync(); + } /** * inform that UINode with given elmtId needs rerender @@ -634,7 +648,6 @@ abstract class ViewPU extends PUV2ViewBase stateMgmtProfiler.begin('ViewPU.performDelayedUpdate'); aceDebugTrace.begin('ViewPU.performDelayedUpdate', this.constructor.name); stateMgmtConsole.debug(`${this.debugInfo__()}: performDelayedUpdate start ...`); - this.syncInstanceId(); for (const stateLinkPropVar of this.ownObservedPropertiesStore_) { const changedElmtIds = stateLinkPropVar.moveElmtIdsForDelayedUpdate(); @@ -661,10 +674,10 @@ abstract class ViewPU extends PUV2ViewBase } this.elmtIdsDelayedUpdate.clear(); - this.restoreInstanceId(); - if (this.dirtDescendantElementIds_.size) { + this.syncInstanceId(); this.markNeedUpdate(); + this.restoreInstanceId(); } aceDebugTrace.end(); stateMgmtProfiler.end(); diff --git a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/puv2_common/puv2_view_base.ts b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/puv2_common/puv2_view_base.ts index e9be8d1e0b8c6e49b1dcd06211527481aba4042c..fc4ed30d08c189b224cc6cf588dc2424ab56d768 100644 --- a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/puv2_common/puv2_view_base.ts +++ b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/puv2_common/puv2_view_base.ts @@ -221,7 +221,14 @@ abstract class PUV2ViewBase extends ViewBuildNodeBase { public allowReusableV2Descendant(): boolean { return this.nativeViewPartialUpdate.allowReusableV2Descendant(); } - + + // Adds all given element IDs to the set of dirty descendant element IDs. + public addDirtyDescendantElementIds(elmtIds: Array): void { + for (const elmtId of elmtIds) { + this.dirtDescendantElementIds_.add(elmtId); + } + } + // globally unique id, this is different from compilerAssignedUniqueChildId! id__(): number { return this.id_; diff --git a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/puv2_common/unified_view_update_controller.ts b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/puv2_common/unified_view_update_controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..41ac62a084d060a30a0985d173cdc138a2a46073 --- /dev/null +++ b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/puv2_common/unified_view_update_controller.ts @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +// Common for both V1 and V2 views +// ViewPUs and ViewV2s Grouped by instance id (container id) +// This map will hold ViewPU or ViewV2 as keys and an array of element IDs that need update +// for V2, we will use element IDs, so we can track which elements need update +// for V1, we will use an empty array or a placeholder to indicate that all elements need update +const unifiedViewNeedUpdateMap = new Map>>(); + +// This class handles the scheduling and update of the dirty elements for both V1 and V2. +class UnifiedViewUpdateController { + private static instance_: UnifiedViewUpdateController; + + // To avoid multiple schedules on the same container + private scheduledContainerIds_: Set = new Set(); + + public static getInstance(): UnifiedViewUpdateController { + if (!this.instance_) { + this.instance_ = new UnifiedViewUpdateController(); + } + return this.instance_; + } + + // Schedule an update for each containerId (instance/container) in the map. + public scheduleUpdateOnNextVSync(): void { + for (const containerId of unifiedViewNeedUpdateMap.keys()) { + if (!this.scheduledContainerIds_.has(containerId)) { + stateMgmtConsole.debug(` scheduling update for containerId: ${containerId}`); + this.scheduledContainerIds_.add(containerId); + ViewStackProcessor.scheduleUpdateOnNextVSync(this.onVSyncUpdate.bind(this), containerId); + } + } + } + + private onVSyncUpdate(containerId: number): boolean { + stateMgmtConsole.debug('onVSyncUpdate() start: containerId: ' + containerId); + // Obtain and unregister the removed elmtIds + UINodeRegisterProxy.obtainDeletedElmtIds(); + UINodeRegisterProxy.unregisterElmtIdsFromIViews(); + + // Allow only 3 nested level updates to avoid infinite loop/blocking the UI thread. + // Refer PipelineContext::FlushDirtyNodeUpdate() + let maxFlushTimes = 3; + let returnVal = false; + + do { + this.updateDirtyV1V2Views(containerId); + } while (this.hasPendingUpdates(containerId) && --maxFlushTimes > 0); + + if (!this.hasPendingUpdates(containerId)) { + // If no more dirty views in the container, unregister the callback. + ViewStackProcessor.scheduleUpdateOnNextVSync(null, containerId); + stateMgmtConsole.debug(' scheduleUpdateOnNextVSync(null) containerId: ' + containerId); + + // Delete the containerId from scheduledContainerIds_ to ensure future updates can be scheduled. + this.scheduledContainerIds_.delete(containerId); + // returnVal = false; // Its already false by default + } else { + // There are still updates pending for this container. + returnVal = true; + } + + stateMgmtConsole.debug('onVSyncUpdate() end: containerId: ' + containerId + ' return: ' + returnVal); + return returnVal; + } + + private updateDirtyV1V2Views(containerId: number): void { + const viewMap = unifiedViewNeedUpdateMap.get(containerId); + unifiedViewNeedUpdateMap.delete(containerId); + if (!viewMap || viewMap.size === 0) { + stateMgmtConsole.debug('WARNING: updateDirtyV1V2Views() containerId: ' + containerId + ' NO views to update!'); + return; + } + + stateMgmtConsole.debug('updateDirtyV1V2Views() start: containerId: ' + containerId); + const sortedEntries = Array.from(viewMap.entries()).sort( + ([viewA], [viewB]) => viewA.id__() - viewB.id__() + ); + + for (const [view, elmtIds] of sortedEntries) { + if (view instanceof ViewPU) { + // Adding elmtIds to ViewPUs's dirtDescendantElementIds_ is needed to handle + // the case: ObserveV2.updateDirty2() -> updateUINodes() -> ViewPU.uiNodeNeedUpdateV2() + // In the main update path (ViewPU.viewPropertyHasChanged) this is not needed. dirtDescendantElementIds_ would be already updated. + if (elmtIds && elmtIds.length > 0) { + stateMgmtConsole.debug(' ViewPU addDirtyDescendantElementIds: ' + elmtIds); + view.addDirtyDescendantElementIds(elmtIds); + } + view.updateDirtyElements(); + } else if (view instanceof ViewV2) { + ObserveV2.getObserve().updateViewV2Elements(view, elmtIds); + } + } + + // There could be new dirty elements in the nested update cases + ObserveV2.getObserve().groupElementIdsByContainer(); + stateMgmtConsole.debug('updateDirtyV1V2Views() end: containerId: ' + containerId); + return; + } + + private hasPendingUpdates(containerId: number): boolean { + if (containerId === undefined) { + stateMgmtConsole.error('hasPendingUpdates() containerId is undefined!'); + return false; + } + + // Return true if there are dirty views in the unified map for the container, or + // there are pending monitor or computed property updates in ObserveV2 + const viewMap = unifiedViewNeedUpdateMap.get(containerId); + const observeV2 = ObserveV2.getObserve(); + return !!((viewMap && viewMap.size) || observeV2.hasPendingMonitorComputed()); + } + +} // End of class UnifiedViewUpdateController diff --git a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_change_observation.ts b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_change_observation.ts index 5ead899d122b1311f1fad747b9d321b8a6498e07..ed5e053885d2976b2a7ee38417479d77f42c2255 100644 --- a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_change_observation.ts +++ b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_change_observation.ts @@ -70,6 +70,9 @@ class ObserveV2 { // property of array, mark it as changed when array has changed. public static readonly OB_LENGTH = '___obj_length'; + // Enable or Disable the optimization for nested components. + public static readonly IS_PARENT_CHILD_OPTIMIZABLE: boolean = true; + private static setMapProxy: SetMapProxyHandler = new SetMapProxyHandler(); private static arrayProxy: ArrayProxyHandler = new ArrayProxyHandler(); private static objectProxy: ObjectProxyHandler = new ObjectProxyHandler(); @@ -114,12 +117,6 @@ class ObserveV2 { // to make sure the callback function will be executed only once public monitorFuncsToRun_: Set = new Set(); - // ViewV2s Grouped by instance id (container id) - private viewV2NeedUpdateMap_: Map>> = new Map(); - - // To avoid multiple schedules on the same container - private scheduledContainerIds_: Set = new Set(); - // avoid recursive execution of updateDirty // by state changes => fireChange while // UINode rerender or @monitor function execution @@ -136,9 +133,6 @@ class ObserveV2 { // use for mark current reuse id, ObserveV2.NO_REUSE(-1) mean no reuse on-going protected currentReuseId_: number = ObserveV2.NO_REUSE; - // flag to disable nested component optimization if V1 and V2 components are involved in the nested cases. - public isParentChildOptimizable_ : boolean = true; - private static obsInstance_: ObserveV2; public static getObserve(): ObserveV2 { @@ -597,13 +591,30 @@ class ObserveV2 { } } + /** + * Handles V2 view update logic from UnifiedViewUpdateController. + * Updates computed/monitors and the elements. + */ + public updateViewV2Elements(view: ViewV2, elmtIds: Array): void { + this.updateComputedAndMonitors(); + if (elmtIds && elmtIds.length > 0) { + const isActive = view.isViewActive(); // Cache the result + for (const elmtId of elmtIds) { + if (isActive) { + view.UpdateElement(elmtId); + } else { + view.scheduleDelayedUpdate(elmtId); + } + } + } + } + /** * Group elementIds by their containerId (instanceId). * Make sure view.getInstanceId() is called only once for each view., not for all elmtIds!!! */ - private groupElementIdsByContainer(): void { + public groupElementIdsByContainer(): void { stateMgmtConsole.debug('ObserveV2.groupElementIdsByContainer'); - // Create sorted array and clear set const elmtIds = Array.from(this.elmtIdsChanged_, Number).sort((a, b) => a - b); this.elmtIdsChanged_.clear(); @@ -627,10 +638,10 @@ class ObserveV2 { } // Get or create viewMap - let viewMap = this.viewV2NeedUpdateMap_.get(instanceId); + let viewMap = unifiedViewNeedUpdateMap.get(instanceId); if (!viewMap) { viewMap = new Map>(); - this.viewV2NeedUpdateMap_.set(instanceId, viewMap); + unifiedViewNeedUpdateMap.set(instanceId, viewMap); } // Get or create view's element array @@ -643,10 +654,11 @@ class ObserveV2 { elements.push(elmtId); stateMgmtConsole.debug(`groupElementIdsByContainer: elmtId=${elmtId}, view=${view.constructor.name}, instanceId=${instanceId}`); } -} + } + public updateDirty(): void { this.startDirty_ = true; - this.isParentChildOptimizable_ ? this.updateDirty2Optimized(): this.updateDirty2(false); + ObserveV2.IS_PARENT_CHILD_OPTIMIZABLE ? this.updateDirty2Optimized(): this.updateDirty2(false); this.startDirty_ = false; } @@ -671,76 +683,15 @@ class ObserveV2 { return; } - // Group elementIds before scheduling update + // Group the Views/elementIds before scheduling the update this.groupElementIdsByContainer(); - - // At this point, we have the viewV2NeedUpdateMap_ populated with the ViewV2/elementIds that need update - // For each containerId (instance/container) in the map, schedule an update. - for (const containerId of this.viewV2NeedUpdateMap_.keys()) { - if (!this.scheduledContainerIds_.has(containerId)) { - stateMgmtConsole.debug(` scheduling update for containerId: ${containerId}`); - this.scheduledContainerIds_.add(containerId); - ViewStackProcessor.scheduleUpdateOnNextVSync(this.onVSyncUpdate.bind(this), containerId); - } - } + // At this point, unifiedViewNeedUpdateMap would be populated with the ViewPU/ViewV2:elementIds that need update + UnifiedViewUpdateController.getInstance().scheduleUpdateOnNextVSync(); stateMgmtConsole.debug(`ObservedV2.updateDirty2Optimized() end`); } - // Callback from C++ on VSync - public onVSyncUpdate(containerId: number): boolean { - stateMgmtConsole.debug(`ObservedV2.flushDirtyViewsOnVSync containerId=${containerId} start`); - aceDebugTrace.begin(`ObservedV2.onVSyncUpdate`); - let maxFlushTimes = 3; // Refer PipelineContext::FlushDirtyNodeUpdate() - // Obtain and unregister the removed elmtIds - UINodeRegisterProxy.obtainDeletedElmtIds(); - UINodeRegisterProxy.unregisterElmtIdsFromIViews(); - - // Process updates in priority order: computed properties, monitors, UI nodes - do { - this.updateComputedAndMonitors(); - const viewV2Map = this.viewV2NeedUpdateMap_.get(containerId); - - // Clear the ViewV2 map for the current containerId - this.viewV2NeedUpdateMap_.delete(containerId); - - if (viewV2Map?.size) { - // Update elements, generating new elmtIds in elmtIdsChanged_ for nested updates - viewV2Map.forEach((elmtIds, view) => { - this.updateUINodesSynchronously(elmtIds, view); - }); - - if (this.elmtIdsChanged_.size) { - this.groupElementIdsByContainer(); - } - } else { - stateMgmtConsole.error(`No views to update for containerId=${containerId}`); - break; // Exit loop early since no updates are possible - } - } while (this.hasPendingUpdates(containerId) && --maxFlushTimes > 0); - - // Check if more updates are needed - const viewV2Map = this.viewV2NeedUpdateMap_.get(containerId); - - if (!viewV2Map || viewV2Map.size === 0) { - if (viewV2Map?.size === 0) { - this.viewV2NeedUpdateMap_.delete(containerId); - } - - ViewStackProcessor.scheduleUpdateOnNextVSync(null, containerId); - // After all processing, remove from scheduled set - this.scheduledContainerIds_.delete(containerId); - return false; - } - aceDebugTrace.end(); - stateMgmtConsole.debug(`ObservedV2.onVSyncUpdate there are still views to be updated for containerId=${containerId}`); - return true; - } - - public hasPendingUpdates(containerId: number): boolean { - const viewV2Map = this.viewV2NeedUpdateMap_.get(containerId); - let ret = ((viewV2Map && viewV2Map.size > 0) || this.monitorIdsChanged_.size > 0 || this.computedPropIdsChanged_.size > 0); - stateMgmtConsole.debug(`hasPendingUpdates() containerId: ${containerId}, viewV2Map size: ${viewV2Map?.size}, ret: ${ret}`); - return ret; + public hasPendingMonitorComputed(): boolean { + return (this.monitorIdsChanged_.size > 0 || this.computedPropIdsChanged_.size > 0) } /** diff --git a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_view.ts b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_view.ts index faf341ca4645c7b30313ec5974519bbdcf78af4f..91ee3419e97a38a032bd46444db3a757f74d0729 100644 --- a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_view.ts +++ b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_view.ts @@ -61,10 +61,6 @@ abstract class ViewV2 extends PUV2ViewBase implements IView { super(parent, elmtId, extraInfo); this.setIsV2(true); ViewBuildNodeBase.arkThemeScopeManager?.onViewPUCreate(this); - if (parent instanceof ViewPU) { - stateMgmtConsole.debug(`Both V1 and V2 components are involved. Disabling Parent-Child optimization`) - ObserveV2.getObserve().isParentChildOptimizable_ = false; - } stateMgmtConsole.debug(`ViewV2 constructor: Creating @Component '${this.constructor.name}' from parent '${parent?.constructor.name}'`); } diff --git a/frameworks/bridge/declarative_frontend/state_mgmt/tsconfig.base.json b/frameworks/bridge/declarative_frontend/state_mgmt/tsconfig.base.json index 3a6b6ce74a605f1a87f83b809d682b23d6e783a2..759ad1a82abc16e1d0e1a90abfc2a317a0aeadf3 100644 --- a/frameworks/bridge/declarative_frontend/state_mgmt/tsconfig.base.json +++ b/frameworks/bridge/declarative_frontend/state_mgmt/tsconfig.base.json @@ -70,6 +70,7 @@ "src/lib/puv2_common/puv2_updatefunc.ts", "src/lib/puv2_common/puv2_view_native_base.d.ts", "src/lib/puv2_common/puv2_view_base.ts", + "src/lib/puv2_common/unified_view_update_controller.ts", // partial_update specific "src/lib/partial_update/pu_types_events.ts",