From 3ae841137433aca17baa7c6bf3674a0935b0b41c Mon Sep 17 00:00:00 2001 From: Denis Pikalov Date: Thu, 31 Jul 2025 10:38:10 +0300 Subject: [PATCH 1/9] Add applySync(), flushUpdates(), flushUiUpdates() Signed-off-by: Denis Pikalov Change-Id: I619877d2af1147361b75e8f74391bc012126c80a --- .../engine/jsStateManagement.js | 40 ++++++++++++ .../state_mgmt/src/lib/sdk/ui_utils.ts | 12 ++++ .../src/lib/v2/v2_change_observation.ts | 61 +++++++++++++++++-- 3 files changed, 107 insertions(+), 6 deletions(-) diff --git a/frameworks/bridge/declarative_frontend/engine/jsStateManagement.js b/frameworks/bridge/declarative_frontend/engine/jsStateManagement.js index 839ecc7b813..d9eda3401b5 100644 --- a/frameworks/bridge/declarative_frontend/engine/jsStateManagement.js +++ b/frameworks/bridge/declarative_frontend/engine/jsStateManagement.js @@ -294,6 +294,46 @@ class UIUtils { static clearMonitor(target, path, monitorCallback) { UIUtils.uiUtilsImpl_.clearMonitor(target, path, monitorCallback) ; } + + /** + * Runs a task and processes all resulting updates immediately + * + * @param { object } task to be executed + * @static + * @syscap SystemCapability.ArkUI.ArkUI.Full + * @crossplatform + * @atomicservice + * @returns { T } The value returned by the task + */ + static applySync(task) { + return UIUtils.uiUtilsImpl_.applySync(task); + } + + /** + * Immediately processes all updates + * + * @static + * @syscap SystemCapability.ArkUI.ArkUI.Full + * @crossplatform + * @atomicservice + */ + static flushUpdates() { + return UIUtils.uiUtilsImpl_.flushUpdates(); + } + + + /** + * Immediately rerenders UINodes that need updates + * + * @static + * @syscap SystemCapability.ArkUI.ArkUI.Full + * @crossplatform + * @atomicservice + */ + static flushUiUpdates() { + return UIUtils.uiUtilsImpl_.flushUiUpdates(); + } + } UIUtils.uiUtilsImpl_ = UIUtilsImpl.instance(); diff --git a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/sdk/ui_utils.ts b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/sdk/ui_utils.ts index 44c49328c23..18e41b8fe81 100644 --- a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/sdk/ui_utils.ts +++ b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/sdk/ui_utils.ts @@ -97,6 +97,18 @@ class UIUtilsImpl { ObserveV2.getObserve().clearMonitorPath(target, path, monitorFunc); } + public static applySync(task : () => T) : T { + return ObserveV2.getObserve().applySync(task); + } + + public static flushUpdates() { + ObserveV2.getObserve().flushUpdates(); + } + + public static flushUiUpdates() { + ObserveV2.getObserve().flushUiUpdates(); + } + public static instance(): UIUtilsImpl { if (UIUtilsImpl.instance_) { return UIUtilsImpl.instance_; 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 f8acc2f8060..bbda4eab530 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 @@ -141,6 +141,9 @@ class ObserveV2 { // flag to disable nested component optimization if V1 and V2 components are involved in the nested cases. public isParentChildOptimizable_ : boolean = true; + // counter for nested applySync() calls + private applySyncCallDepth_: number = 0; + private static obsInstance_: ObserveV2; public static getObserve(): ObserveV2 { @@ -490,8 +493,7 @@ class ObserveV2 { return ret; } - - + /** * mark view model object 'target' property 'attrName' as changed * notify affected watchIds and elmtIds but exclude given elmtIds @@ -1317,6 +1319,57 @@ class ObserveV2 { public setCurrentReuseId(elmtId: number): void { this.currentReuseId_ = elmtId; } + + // Runs a task and processes all resulting updates immediately + public applySync(task: () => T) : T { + this.applySyncCallDepth_++; + + const names = [ + 'elmtIdsChanged_', + 'computedPropIdsChanged_', + 'monitorIdsChanged_', 'monitorIdsChangedForAddMonitor_', 'monitorSyncIdsChangedForAddMonitor_', + 'persistenceChanged_' + ]; + + // Save current ID sets and clear the originals + const snapshot = names.map(name => this[name]); + names.forEach(name => this[name] = new Set()); + + // Run the task, which will collect the IDs needing immediate update + const retValue = task(); + + // Exclude task-added IDs from the snapshot + names.forEach((name, i) => + this[name].forEach(id => snapshot[i].delete(id)) + ); + + // Process new IDs, update UI synchronously + this.updateDirty2(true); + + // Restore unprocessed IDs from the snapshot + names.forEach((name, i) => this[name] = snapshot[i]); + + this.applySyncCallDepth_--; + return retValue; + } + + // Immediately processes all updates + public flushUpdates() { + if (this.applySyncCallDepth_) { + return; + } + this.updateDirty2(true); + } + + // Update all UINodes that currently need update + public flushUiUpdates() { + if (this.applySyncCallDepth_) { + return; + } + const elmtIds = Array.from(this.elmtIdsChanged_).sort((id1, id2) => id1 - id2); + this.updateUINodes(elmtIds); + } + } // class ObserveV2 const trackInternal = ( @@ -1351,7 +1404,3 @@ const trackInternal = ( target[ObserveV2.V2_DECO_META] ??= {}; }; // trackInternal -// used to manually mark dirty v2 before animateTo -function __updateDirty2Immediately_V2_Change_Observation(): void { - ObserveV2.getObserve().updateDirty2(); -} \ No newline at end of file -- Gitee From ff23d6741b6bae092bb0b932c9c11e39371cf59e Mon Sep 17 00:00:00 2001 From: Denis Pikalov Date: Thu, 7 Aug 2025 12:11:52 +0300 Subject: [PATCH 2/9] Fix flushUiUpdates(): update nodes synchronously Signed-off-by: Denis Pikalov Change-Id: I9e0b441e98870fca8f43bafb5652d72567fffb8e --- .../state_mgmt/src/lib/v2/v2_change_observation.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 bbda4eab530..9245b3b9b26 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 @@ -1367,7 +1367,8 @@ class ObserveV2 { return; } const elmtIds = Array.from(this.elmtIdsChanged_).sort((id1, id2) => id1 - id2); - this.updateUINodes(elmtIds); + this.updateUINodesSynchronously(elmtIds); + this.elmtIdsChanged_.clear(); } } // class ObserveV2 -- Gitee From 2d57bd4070179f2af44404038cd27b648a2d720f Mon Sep 17 00:00:00 2001 From: Denis Pikalov Date: Thu, 7 Aug 2025 16:28:27 +0300 Subject: [PATCH 3/9] Fix. Update ui asynchronously Signed-off-by: Denis Pikalov Change-Id: I9b95bfb699493bb55116f8b3d5a1b5f45ae4a758 --- .../state_mgmt/src/lib/v2/v2_change_observation.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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 9245b3b9b26..c89831c0baf 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 @@ -1343,8 +1343,8 @@ class ObserveV2 { this[name].forEach(id => snapshot[i].delete(id)) ); - // Process new IDs, update UI synchronously - this.updateDirty2(true); + // Process new IDs, trigger UI node updates + this.updateDirty2(false); // Restore unprocessed IDs from the snapshot names.forEach((name, i) => this[name] = snapshot[i]); @@ -1358,7 +1358,7 @@ class ObserveV2 { if (this.applySyncCallDepth_) { return; } - this.updateDirty2(true); + this.updateDirty2(false); } // Update all UINodes that currently need update @@ -1367,8 +1367,7 @@ class ObserveV2 { return; } const elmtIds = Array.from(this.elmtIdsChanged_).sort((id1, id2) => id1 - id2); - this.updateUINodesSynchronously(elmtIds); - this.elmtIdsChanged_.clear(); + this.updateUINodes(elmtIds); } } // class ObserveV2 -- Gitee From 9d9f5c202f812baaa1ec4b751a6a05e3b23faccf Mon Sep 17 00:00:00 2001 From: Denis Pikalov Date: Fri, 8 Aug 2025 15:34:22 +0300 Subject: [PATCH 4/9] Add @Monitor/@Computed skip updates logic Signed-off-by: Denis Pikalov Change-Id: I7772602877864173ec46719db24d9988f08e7b50 --- .../src/lib/v2/v2_change_observation.ts | 26 +++++++++++++++++-- .../state_mgmt/src/lib/v2/v2_computed.ts | 4 +++ .../state_mgmt/src/lib/v2/v2_monitor.ts | 5 ++++ 3 files changed, 33 insertions(+), 2 deletions(-) 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 c89831c0baf..0b050370f6a 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 @@ -1322,6 +1322,16 @@ class ObserveV2 { // Runs a task and processes all resulting updates immediately public applySync(task: () => T) : T { + if (MonitorV2.runningCount) { + // Execute the task() without any special treatment + return task(); + } + if (ComputedV2.runningCount) { + const error = `applySync() is not allowed inside @Computed`; + stateMgmtConsole.applicationError(error); + throw new Error(error); + } + this.applySyncCallDepth_++; const names = [ @@ -1355,7 +1365,13 @@ class ObserveV2 { // Immediately processes all updates public flushUpdates() { - if (this.applySyncCallDepth_) { + if (ComputedV2.runningCount) { + const error = 'flushUpdates() is not allowed inside @Computed'; + stateMgmtConsole.applicationError(error); + throw new Error(error); + } + // Skip updates if @Monitor or applySync() are running + if (MonitorV2.runningCount || this.applySyncCallDepth_) { return; } this.updateDirty2(false); @@ -1363,7 +1379,13 @@ class ObserveV2 { // Update all UINodes that currently need update public flushUiUpdates() { - if (this.applySyncCallDepth_) { + if (ComputedV2.runningCount) { + const error = 'flushUiUpdates() is not allowed inside @Computed'; + stateMgmtConsole.applicationError(error); + throw new Error(error); + } + // Skip updates if @Monitor or applySync() are running + if (MonitorV2.runningCount || this.applySyncCallDepth_) { return; } const elmtIds = Array.from(this.elmtIdsChanged_).sort((id1, id2) => id1 - id2); diff --git a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_computed.ts b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_computed.ts index e176fde0a60..c093edef524 100644 --- a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_computed.ts +++ b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_computed.ts @@ -46,6 +46,8 @@ class ComputedV2 { public static readonly COMPUTED_PREFIX = '___comp_'; public static readonly COMPUTED_CACHED_PREFIX = '___comp_cached_'; + // count of currently running @Computed functions + public static runningCount = 0; constructor(target: object, prop: string, func: (...args: any[]) => any) { this.target_ = target; @@ -105,6 +107,7 @@ class ComputedV2 { let ret; try { + ComputedV2.runningCount++; ret = this.propertyComputeFunc_.call(this.target_); } catch (e) { stateMgmtConsole.applicationError(`@Computed Exception caught for ${this.propertyComputeFunc_.name}`, e.toString()); @@ -112,6 +115,7 @@ class ComputedV2 { throw e; } finally { ObserveV2.getObserve().stopRecordDependencies(); + ComputedV2.runningCount--; } return ret; diff --git a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_monitor.ts b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_monitor.ts index 0e174d71435..5109a4c6a0e 100644 --- a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_monitor.ts +++ b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_monitor.ts @@ -109,6 +109,8 @@ class MonitorV2 { public static nextWatchId_ = MonitorV2.MIN_WATCH_ID; public static nextWatchApiId_ = MonitorV2.MIN_WATCH_FROM_API_ID; public static nextSyncWatchApiId_ = MonitorV2.MIN_SYNC_WATCH_FROM_API_ID; + // count of currently running @Monitor functions + public static runningCount = 0; private values_: Map> = new Map>(); private target_: object; // @Monitor function 'this': data object or ViewV2 @@ -246,18 +248,21 @@ class MonitorV2 { // Executes the monitor function. public runMonitorFunction(): void { stateMgmtConsole.debug(`@Monitor function '${this.monitorFunction.name}' exec ...`); + if (this.dirty.length === 0) { stateMgmtConsole.debug(`No dirty values! not firing!`); return; } try { // exec @Monitor/AddMonitor function + MonitorV2.runningCount++; this.monitorFunction.call(this.target_, this); } catch (e) { stateMgmtConsole.applicationError(`AddMonitor exception caught for ${this.monitorFunction.name}`, e.toString()); throw e; } finally { this.resetMonitor(); + MonitorV2.runningCount--; } } -- Gitee From 58baa34d6e8e631081ffb6c1163c46436e248964 Mon Sep 17 00:00:00 2001 From: Denis Pikalov Date: Mon, 11 Aug 2025 12:23:46 +0300 Subject: [PATCH 5/9] Gather IDs processed by updateDirty2 during applySync() Signed-off-by: Denis Pikalov Change-Id: I3e0ef951ae7350dfbd9f9514632b858cedcf3e02 --- .../src/lib/v2/v2_change_observation.ts | 38 ++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) 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 0b050370f6a..4ade0103bef 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 @@ -761,23 +761,28 @@ class ObserveV2 { * */ - public updateDirty2(updateUISynchronously: boolean = false, isReuse: boolean = false): void { + public updateDirty2(updateUISynchronously: boolean = false, isReuse: boolean = false): Set { aceDebugTrace.begin('updateDirty2'); stateMgmtConsole.debug(`ObservedV2.updateDirty2 updateUISynchronously=${updateUISynchronously} ... `); // obtain and unregister the removed elmtIds UINodeRegisterProxy.obtainDeletedElmtIds(); UINodeRegisterProxy.unregisterElmtIdsFromIViews(); + // collect processed ids here + const processedIds = new Set(); + // priority order of processing: // 1- update computed properties until no more need computed props update // 2- update monitors until no more monitors and no more computed props // 3- update UINodes until no more monitors, no more computed props, and no more UINodes // FIXME prevent infinite loops do { - this.updateComputedAndMonitors(); + const ids = this.updateComputedAndMonitors(); + ids.forEach(Set.prototype.add, processedIds); if (this.elmtIdsChanged_.size) { const elmtIds = Array.from(this.elmtIdsChanged_).sort((elmtId1, elmtId2) => elmtId1 - elmtId2); + this.elmtIdsChanged_.forEach(Set.prototype.add, processedIds); this.elmtIdsChanged_ = new Set(); updateUISynchronously ? isReuse ? this.updateUINodesForReuse(elmtIds) : this.updateUINodesSynchronously(elmtIds) : this.updateUINodes(elmtIds); } @@ -785,6 +790,7 @@ class ObserveV2 { aceDebugTrace.end(); stateMgmtConsole.debug(`ObservedV2.updateDirty2 updateUISynchronously=${updateUISynchronously} - DONE `); + return processedIds; } /** @@ -796,25 +802,30 @@ class ObserveV2 { * process @Computed until no more @Computed need update * process @Monitor until no more @Computed and @Monitor */ - private updateComputedAndMonitors(): void { + private updateComputedAndMonitors(): Set { + const processedIds = new Set(); + do { while (this.computedPropIdsChanged_.size) { // sort the ids and update in ascending order // If a @Computed property depends on other @Computed properties, their // ids will be smaller as they are defined first. const computedProps = Array.from(this.computedPropIdsChanged_).sort((id1, id2) => id1 - id2); + this.computedPropIdsChanged_.forEach(Set.prototype.add, processedIds); this.computedPropIdsChanged_ = new Set(); this.updateDirtyComputedProps(computedProps); } if (this.persistenceChanged_.size) { const persistKeys: Array = Array.from(this.persistenceChanged_); + this.persistenceChanged_.forEach(Set.prototype.add, processedIds); this.persistenceChanged_ = new Set(); PersistenceV2Impl.instance().onChangeObserved(persistKeys); } if (this.monitorIdsChanged_.size) { const monitors = this.monitorIdsChanged_; + this.monitorIdsChanged_.forEach(Set.prototype.add, processedIds); this.monitorIdsChanged_ = new Set(); this.updateDirtyMonitors(monitors); } @@ -835,6 +846,8 @@ class ObserveV2 { } } while (this.monitorIdsChanged_.size + this.persistenceChanged_.size + this.computedPropIdsChanged_.size + this.monitorIdsChangedForAddMonitor_.size + this.monitorFuncsToRun_.size > 0); + + return processedIds; } public updateDirtyComputedProps(computed: Array): void { @@ -896,7 +909,7 @@ class ObserveV2 { // monitor notifyChange delayed if target is a View that is not active monitorTarget.addDelayedMonitorIds(watchId); } else { - monitor.notifyChange(); + monitor.notifyChange(); // can delete this.elmtIdsChanged_ } } }); @@ -1341,20 +1354,25 @@ class ObserveV2 { 'persistenceChanged_' ]; - // Save current ID sets and clear the originals + // Take a snapshot of the current pending IDs and reset them to empty sets const snapshot = names.map(name => this[name]); names.forEach(name => this[name] = new Set()); - // Run the task, which will collect the IDs needing immediate update + // Execute the task — any state changes here will be recorded in the cleared sets const retValue = task(); - // Exclude task-added IDs from the snapshot + // Remove IDs added by task() from the original snapshot names.forEach((name, i) => this[name].forEach(id => snapshot[i].delete(id)) ); - // Process new IDs, trigger UI node updates - this.updateDirty2(false); + // Process the new changes immediately (may produce further changes) + const processedIds = this.updateDirty2(false); + + // Remove IDs added by updateDirty2() from the original snapshot + processedIds.forEach(id => { + names.forEach((name, i) => snapshot[i].delete(id)); + }) // Restore unprocessed IDs from the snapshot names.forEach((name, i) => this[name] = snapshot[i]); @@ -1372,6 +1390,7 @@ class ObserveV2 { } // Skip updates if @Monitor or applySync() are running if (MonitorV2.runningCount || this.applySyncCallDepth_) { + stateMgmtConsole.log('Skip flushUpdate()'); return; } this.updateDirty2(false); @@ -1386,6 +1405,7 @@ class ObserveV2 { } // Skip updates if @Monitor or applySync() are running if (MonitorV2.runningCount || this.applySyncCallDepth_) { + stateMgmtConsole.log('Skip flushUiUpdate()'); return; } const elmtIds = Array.from(this.elmtIdsChanged_).sort((id1, id2) => id1 - id2); -- Gitee From 530ec3a3ee29da559e53f2f82d5ca8fbcdcf4525 Mon Sep 17 00:00:00 2001 From: Denis Pikalov Date: Tue, 12 Aug 2025 11:05:42 +0300 Subject: [PATCH 6/9] Add applySyncPartial() - wrap animateTo lambda into applySync() Signed-off-by: Denis Pikalov Change-Id: Idbd5ddf5dbdae0270aefb45772395a4930fda2e2 --- .../jsview/js_view_context.cpp | 15 +++++++++++---- .../partial_update/pu_uinode_registry_proxy.ts | 7 +++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/frameworks/bridge/declarative_frontend/jsview/js_view_context.cpp b/frameworks/bridge/declarative_frontend/jsview/js_view_context.cpp index 0dd7b33db08..63c4c291403 100644 --- a/frameworks/bridge/declarative_frontend/jsview/js_view_context.cpp +++ b/frameworks/bridge/declarative_frontend/jsview/js_view_context.cpp @@ -30,6 +30,7 @@ #include "bridge/declarative_frontend/engine/functions/js_animation_on_finish_function.h" #include "bridge/declarative_frontend/engine/js_converter.h" #include "bridge/declarative_frontend/jsview/js_view_abstract.h" +#include "bridge/declarative_frontend/jsview/js_view_common_def.h" #include "bridge/declarative_frontend/jsview/js_tabs_feature.h" #include "bridge/declarative_frontend/jsview/models/view_context_model_impl.h" #include "core/animation/animation_pub.h" @@ -791,6 +792,13 @@ void JSViewContext::AnimateToInner(const JSCallbackInfo& info, bool immediately) TAG_LOGW(AceLogTag::ACE_FORM, "[Form animation] Form animation SetDuration: %{public}lld ms", static_cast(DEFAULT_DURATION - GetFormAnimationTimeInterval(pipelineContext))); } + + // Wrap info[1] with applySyncPartial for use as the animateTo lambda + const auto globalObject = JSRef::Make(JSNApi::GetGlobalObject(info.GetVm())); + const JSRef applySyncPartial = globalObject->GetProperty("applySyncPartial"); + JSRef argv[] = { JSRef::Cast(info[1]) }; + auto lambda = JSRef::Cast(applySyncPartial)->Call(JSRef(), 1, argv); + if (SystemProperties::GetRosenBackendEnabled()) { bool usingSharedRuntime = container->GetSettings().usingSharedRuntime; if (usingSharedRuntime) { @@ -811,16 +819,15 @@ void JSViewContext::AnimateToInner(const JSCallbackInfo& info, bool immediately) TaskExecutor::TaskType::UI, "ArkUIAnimateToForStageMode", PriorityType::IMMEDIATE); return; } - StartAnimationForStageMode(pipelineContext, option, JSRef::Cast(info[1]), count, immediately); + StartAnimationForStageMode(pipelineContext, option, lambda, count, immediately); } else { - StartAnimateToForFaMode(pipelineContext, option, JSRef::Cast(info[1]), count, immediately); + StartAnimateToForFaMode(pipelineContext, option, lambda, count, immediately); } } else { pipelineContext->FlushBuild(); pipelineContext->SaveExplicitAnimationOption(option); // Execute the function. - JSRef jsAnimateToFunc = JSRef::Cast(info[1]); - jsAnimateToFunc->Call(info[1]); + JSRef::Cast(lambda)->Call(JSRef(), 0, nullptr); pipelineContext->FlushBuild(); pipelineContext->CreateExplicitAnimator(onFinishEvent); pipelineContext->ClearExplicitAnimationOption(); diff --git a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/partial_update/pu_uinode_registry_proxy.ts b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/partial_update/pu_uinode_registry_proxy.ts index 4023a23e0f8..71b15de9f62 100644 --- a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/partial_update/pu_uinode_registry_proxy.ts +++ b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/partial_update/pu_uinode_registry_proxy.ts @@ -54,6 +54,13 @@ function uiNodeCleanUpIdleTask(maxTimeInMs: number): void { ObserveV2.getObserve().runIdleTasks(deadline); } +// Creates a zero-argument function by partially applying applySync with one argument +function applySyncPartial(task: any) { + return () => { + task && ObserveV2.getObserve().applySync(task); + } +} + class UINodeRegisterProxy { public static readonly notRecordingDependencies : number = -1; public static readonly monitorIllegalV1V2StateAccess : number = -2; -- Gitee From 9ecd0e733ddfead45684053a17863a5d5954c140 Mon Sep 17 00:00:00 2001 From: Denis Pikalov Date: Tue, 12 Aug 2025 16:51:29 +0300 Subject: [PATCH 7/9] Update @Monitor/@Computed skip updates logic. Minor renames Signed-off-by: Denis Pikalov Change-Id: Ia7a228a3fb8c6f0c8ac7b77244bd11113dc3c387 --- .../pu_uinode_registry_proxy.ts | 4 +-- .../src/lib/v2/v2_change_observation.ts | 36 +++++++------------ 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/partial_update/pu_uinode_registry_proxy.ts b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/partial_update/pu_uinode_registry_proxy.ts index 71b15de9f62..ca5446c1c6e 100644 --- a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/partial_update/pu_uinode_registry_proxy.ts +++ b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/partial_update/pu_uinode_registry_proxy.ts @@ -55,9 +55,9 @@ function uiNodeCleanUpIdleTask(maxTimeInMs: number): void { } // Creates a zero-argument function by partially applying applySync with one argument -function applySyncPartial(task: any) { +function applySyncPartial(task: () => void) { return () => { - task && ObserveV2.getObserve().applySync(task); + ObserveV2.getObserve().applySync(task); } } 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 4ade0103bef..6ddafc87e1e 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 @@ -142,7 +142,7 @@ class ObserveV2 { public isParentChildOptimizable_ : boolean = true; // counter for nested applySync() calls - private applySyncCallDepth_: number = 0; + private applySyncRunningCount_: number = 0; private static obsInstance_: ObserveV2; @@ -1335,17 +1335,11 @@ class ObserveV2 { // Runs a task and processes all resulting updates immediately public applySync(task: () => T) : T { - if (MonitorV2.runningCount) { - // Execute the task() without any special treatment - return task(); - } if (ComputedV2.runningCount) { - const error = `applySync() is not allowed inside @Computed`; - stateMgmtConsole.applicationError(error); - throw new Error(error); + throw new Error('applySync() is not allowed inside @Computed'); } - this.applySyncCallDepth_++; + this.applySyncRunningCount_++; const names = [ 'elmtIdsChanged_', @@ -1377,20 +1371,17 @@ class ObserveV2 { // Restore unprocessed IDs from the snapshot names.forEach((name, i) => this[name] = snapshot[i]); - this.applySyncCallDepth_--; + this.applySyncRunningCount_--; return retValue; } // Immediately processes all updates public flushUpdates() { - if (ComputedV2.runningCount) { - const error = 'flushUpdates() is not allowed inside @Computed'; - stateMgmtConsole.applicationError(error); - throw new Error(error); + if (ComputedV2.runningCount || MonitorV2.runningCount) { + throw new Error('flushUpdates() is not allowed inside @Computed and @Monitor'); } - // Skip updates if @Monitor or applySync() are running - if (MonitorV2.runningCount || this.applySyncCallDepth_) { - stateMgmtConsole.log('Skip flushUpdate()'); + if (this.applySyncRunningCount_) { + stateMgmtConsole.log('flushUpdates() skipped inside applySync()'); return; } this.updateDirty2(false); @@ -1398,14 +1389,11 @@ class ObserveV2 { // Update all UINodes that currently need update public flushUiUpdates() { - if (ComputedV2.runningCount) { - const error = 'flushUiUpdates() is not allowed inside @Computed'; - stateMgmtConsole.applicationError(error); - throw new Error(error); + if (ComputedV2.runningCount || MonitorV2.runningCount) { + throw new Error('flushUiUpdates() is not allowed inside @Computed and @Monitor'); } - // Skip updates if @Monitor or applySync() are running - if (MonitorV2.runningCount || this.applySyncCallDepth_) { - stateMgmtConsole.log('Skip flushUiUpdate()'); + if (this.applySyncRunningCount_) { + stateMgmtConsole.log('flushUiUpdates() skipped inside applySync()'); return; } const elmtIds = Array.from(this.elmtIdsChanged_).sort((id1, id2) => id1 - id2); -- Gitee From fe953cd125dc6f3b0abd0ce30192adf015a2628c Mon Sep 17 00:00:00 2001 From: Denis Pikalov Date: Thu, 14 Aug 2025 13:00:03 +0300 Subject: [PATCH 8/9] Override animateTo in TS side. Revert changes in JSViewContext Signed-off-by: Denis Pikalov Change-Id: I88537bfbdf8ab72c007e9572c750dfae0f86cd37 --- .../declarative_frontend/jsview/js_view_context.cpp | 13 ++++--------- .../lib/partial_update/pu_uinode_registry_proxy.ts | 7 ------- .../state_mgmt/src/lib/v2/v2_change_observation.ts | 8 ++++++++ 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/frameworks/bridge/declarative_frontend/jsview/js_view_context.cpp b/frameworks/bridge/declarative_frontend/jsview/js_view_context.cpp index 63c4c291403..6b08ea2baff 100644 --- a/frameworks/bridge/declarative_frontend/jsview/js_view_context.cpp +++ b/frameworks/bridge/declarative_frontend/jsview/js_view_context.cpp @@ -793,12 +793,6 @@ void JSViewContext::AnimateToInner(const JSCallbackInfo& info, bool immediately) static_cast(DEFAULT_DURATION - GetFormAnimationTimeInterval(pipelineContext))); } - // Wrap info[1] with applySyncPartial for use as the animateTo lambda - const auto globalObject = JSRef::Make(JSNApi::GetGlobalObject(info.GetVm())); - const JSRef applySyncPartial = globalObject->GetProperty("applySyncPartial"); - JSRef argv[] = { JSRef::Cast(info[1]) }; - auto lambda = JSRef::Cast(applySyncPartial)->Call(JSRef(), 1, argv); - if (SystemProperties::GetRosenBackendEnabled()) { bool usingSharedRuntime = container->GetSettings().usingSharedRuntime; if (usingSharedRuntime) { @@ -819,15 +813,16 @@ void JSViewContext::AnimateToInner(const JSCallbackInfo& info, bool immediately) TaskExecutor::TaskType::UI, "ArkUIAnimateToForStageMode", PriorityType::IMMEDIATE); return; } - StartAnimationForStageMode(pipelineContext, option, lambda, count, immediately); + StartAnimationForStageMode(pipelineContext, option, JSRef::Cast(info[1]), count, immediately); } else { - StartAnimateToForFaMode(pipelineContext, option, lambda, count, immediately); + StartAnimateToForFaMode(pipelineContext, option, JSRef::Cast(info[1]), count, immediately); } } else { pipelineContext->FlushBuild(); pipelineContext->SaveExplicitAnimationOption(option); // Execute the function. - JSRef::Cast(lambda)->Call(JSRef(), 0, nullptr); + JSRef jsAnimateToFunc = JSRef::Cast(info[1]); + jsAnimateToFunc->Call(info[1]); pipelineContext->FlushBuild(); pipelineContext->CreateExplicitAnimator(onFinishEvent); pipelineContext->ClearExplicitAnimationOption(); diff --git a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/partial_update/pu_uinode_registry_proxy.ts b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/partial_update/pu_uinode_registry_proxy.ts index ca5446c1c6e..4023a23e0f8 100644 --- a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/partial_update/pu_uinode_registry_proxy.ts +++ b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/partial_update/pu_uinode_registry_proxy.ts @@ -54,13 +54,6 @@ function uiNodeCleanUpIdleTask(maxTimeInMs: number): void { ObserveV2.getObserve().runIdleTasks(deadline); } -// Creates a zero-argument function by partially applying applySync with one argument -function applySyncPartial(task: () => void) { - return () => { - ObserveV2.getObserve().applySync(task); - } -} - class UINodeRegisterProxy { public static readonly notRecordingDependencies : number = -1; public static readonly monitorIllegalV1V2StateAccess : number = -2; 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 6ddafc87e1e..61ae4289636 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 @@ -1434,3 +1434,11 @@ const trackInternal = ( target[ObserveV2.V2_DECO_META] ??= {}; }; // trackInternal +// Override animateTo() to run its callback via ObserveV2.applySync() +(() => { + const animateTo = globalThis.Context.animateTo; + globalThis.Context.animateTo = (value: object, event: () => void) => + animateTo.call(globalThis.Context, value, () => { + ObserveV2.getObserve().applySync(event) + }); +})(); -- Gitee From 40cd6d04a77f2b9ebfae1144a40725da4af01344 Mon Sep 17 00:00:00 2001 From: Denis Pikalov Date: Tue, 19 Aug 2025 19:14:26 +0300 Subject: [PATCH 9/9] Optimization: pass snapshot to updateDirty2 Signed-off-by: Denis Pikalov Change-Id: I447db553a73fe25595a2149ed701d58812445f43 --- .../src/lib/v2/v2_change_observation.ts | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) 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 61ae4289636..a1be8af0f44 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 @@ -758,31 +758,31 @@ class ObserveV2 { * process UINode update until no more @Computed and @Monitor and UINode rerender * * @param updateUISynchronously should be set to true if called during VSYNC only + * @param snapshot optional, used by applySync() to track processed IDs for optimization * */ - public updateDirty2(updateUISynchronously: boolean = false, isReuse: boolean = false): Set { + public updateDirty2(updateUISynchronously: boolean = false, isReuse: boolean = false, snapshot?: Object) { aceDebugTrace.begin('updateDirty2'); stateMgmtConsole.debug(`ObservedV2.updateDirty2 updateUISynchronously=${updateUISynchronously} ... `); // obtain and unregister the removed elmtIds UINodeRegisterProxy.obtainDeletedElmtIds(); UINodeRegisterProxy.unregisterElmtIdsFromIViews(); - // collect processed ids here - const processedIds = new Set(); - // priority order of processing: // 1- update computed properties until no more need computed props update // 2- update monitors until no more monitors and no more computed props // 3- update UINodes until no more monitors, no more computed props, and no more UINodes // FIXME prevent infinite loops do { - const ids = this.updateComputedAndMonitors(); - ids.forEach(Set.prototype.add, processedIds); + // update and remove processed ids from the snapshot + this.updateComputedAndMonitors(snapshot); if (this.elmtIdsChanged_.size) { const elmtIds = Array.from(this.elmtIdsChanged_).sort((elmtId1, elmtId2) => elmtId1 - elmtId2); - this.elmtIdsChanged_.forEach(Set.prototype.add, processedIds); + if (snapshot) { + this.elmtIdsChanged_.forEach(Set.prototype.delete, snapshot['elmtIdsChanged_']); + } this.elmtIdsChanged_ = new Set(); updateUISynchronously ? isReuse ? this.updateUINodesForReuse(elmtIds) : this.updateUINodesSynchronously(elmtIds) : this.updateUINodes(elmtIds); } @@ -790,7 +790,6 @@ class ObserveV2 { aceDebugTrace.end(); stateMgmtConsole.debug(`ObservedV2.updateDirty2 updateUISynchronously=${updateUISynchronously} - DONE `); - return processedIds; } /** @@ -801,31 +800,37 @@ class ObserveV2 { * three nested loops, means: * process @Computed until no more @Computed need update * process @Monitor until no more @Computed and @Monitor + * + * @param snapshot optional, used by applySync() to track processed IDs for optimization */ - private updateComputedAndMonitors(): Set { - const processedIds = new Set(); - + private updateComputedAndMonitors(snapshot?: Object) { do { while (this.computedPropIdsChanged_.size) { // sort the ids and update in ascending order // If a @Computed property depends on other @Computed properties, their // ids will be smaller as they are defined first. const computedProps = Array.from(this.computedPropIdsChanged_).sort((id1, id2) => id1 - id2); - this.computedPropIdsChanged_.forEach(Set.prototype.add, processedIds); + if (snapshot) { + this.computedPropIdsChanged_.forEach(Set.prototype.delete, snapshot['computedPropIdsChanged_']); + } this.computedPropIdsChanged_ = new Set(); this.updateDirtyComputedProps(computedProps); } if (this.persistenceChanged_.size) { const persistKeys: Array = Array.from(this.persistenceChanged_); - this.persistenceChanged_.forEach(Set.prototype.add, processedIds); + if (snapshot) { + this.persistenceChanged_.forEach(Set.prototype.delete, snapshot['persistenceChanged_']); + } this.persistenceChanged_ = new Set(); PersistenceV2Impl.instance().onChangeObserved(persistKeys); } if (this.monitorIdsChanged_.size) { const monitors = this.monitorIdsChanged_; - this.monitorIdsChanged_.forEach(Set.prototype.add, processedIds); + if (snapshot) { + this.monitorIdsChanged_.forEach(Set.prototype.delete, snapshot['monitorIdsChanged_']); + } this.monitorIdsChanged_ = new Set(); this.updateDirtyMonitors(monitors); } @@ -846,8 +851,6 @@ class ObserveV2 { } } while (this.monitorIdsChanged_.size + this.persistenceChanged_.size + this.computedPropIdsChanged_.size + this.monitorIdsChangedForAddMonitor_.size + this.monitorFuncsToRun_.size > 0); - - return processedIds; } public updateDirtyComputedProps(computed: Array): void { @@ -1349,27 +1352,24 @@ class ObserveV2 { ]; // Take a snapshot of the current pending IDs and reset them to empty sets - const snapshot = names.map(name => this[name]); + const snapshot = {}; + names.forEach(name => snapshot[name] = this[name]); names.forEach(name => this[name] = new Set()); // Execute the task — any state changes here will be recorded in the cleared sets const retValue = task(); // Remove IDs added by task() from the original snapshot - names.forEach((name, i) => - this[name].forEach(id => snapshot[i].delete(id)) + names.forEach((name) => + this[name].forEach(Set.prototype.delete, snapshot[name]) ); - // Process the new changes immediately (may produce further changes) - const processedIds = this.updateDirty2(false); - - // Remove IDs added by updateDirty2() from the original snapshot - processedIds.forEach(id => { - names.forEach((name, i) => snapshot[i].delete(id)); - }) + // Process the new changes immediately (may produce further changes) and + // remove IDs added by updateDirty2 from the original snapshot + this.updateDirty2(false, false, snapshot); // Restore unprocessed IDs from the snapshot - names.forEach((name, i) => this[name] = snapshot[i]); + names.forEach((name) => this[name] = snapshot[name]); this.applySyncRunningCount_--; return retValue; -- Gitee