Repository navigation
Conversation
Undoing a type or catalog change puts the component back at its position, through the new SurfaceComponentsModel.insertComponent, and an undone deleteSurface keeps the order of the replayed components. The data model of a deleted surface is written straight back instead of replayed as one updateDataModel, which the depth limit could reject, and is no longer copied when recorded. A failing undo no longer stops the rest of a rollback, and the error that caused the rollback propagates. Signed-off-by: Nan Yu <nanyu@google.com>
There was a problem hiding this comment.
Code Review
This pull request enhances the rollback mechanism in RendererState to preserve the original order of components when they are recreated or when a deleted surface is restored. It introduces SurfaceComponentsModel.insertComponent to support inserting components at specific positions and ensures that errors thrown during an undo operation do not halt the remaining rollback process. The feedback suggests retaining explicit type annotations on loop variables when iterating over non-literal collections in both renderer_state.dart and component_model.dart to comply with Dart lints.
a2ui_agent uses SurfaceComponentsModel.insertComponent, checkPathsAndRecursion and checkDuplicateComponentIds, none of which a2ui_core 0.3.0 has. The workspace cannot resolve a constraint above the version in a2ui_core's pubspec, which only a release changes, so the constraint stays ^0.3.0 and the release raises it to ^0.3.1. Also list the checkDuplicateComponentIds export in the a2ui_core CHANGELOG. Signed-off-by: Nan Yu <nanyu@google.com>
Signed-off-by: Nan Yu <nanyu@google.com>
polina-c
left a comment
There was a problem hiding this comment.
I reviewed f462c4f. The fixes for both comments from #3276 work. The three repro tests fail on main and pass here, and so do the two new order tests. dart analyze, dart format and dart test (including -P performance) pass in a2ui_agent and a2ui_core.
I also fuzzed rollback, comparing the state before and after each rollback, including component order and catalogIds:
- v0.9, 3,000 cases: no differences other than data model key order (below). At main the same fuzz finds 980.
- v1.0 with two catalogs that both declare
Box, 20,000 cases: 126 differences. Every one comes from the replay in the first inline comment.
Main points, explained inline:
- Undoing a
deleteSurfacestill replays its components as oneupdateComponents. The processor can reject that message even though it accepted the same components one at a time. The undo error is now dropped, so the surface comes back empty. - Two performance regressions: recording a message that may change a component's type, and rolling back many type changes.
- Two new branches have no tests.
Smaller things:
- A rolled-back delete of a data model key puts the key back at the end of its map. With
{a: 1, b: 2}, a rolled-backupdateDataModelthat sets/ato null leaves{b: 2, a: 1}. Main already did this, but #3278 now puts the key back where it was, so Dart and TypeScript differ here. - The description says reading the positions "costs O(surface), the same as the processor's own handling of a recreated component". The measurements in the inline comment show about 7 times the processor's cost at 16,000 components. Could the description show the measurement as a line naming it and the size, then a before/after list?
🤖 Agent-generated
|
A clarification on my review (#3347 (review)). "Two new branches have no tests" means two code paths in
I checked each one at f462c4f. With the line simplified to 🤖 Agent-generated |
Signed-off-by: Nan Yu <nanyu@google.com>
Undoing a deleteSurface puts the same SurfaceModel back, through the new SurfaceGroupModel.detachSurface, instead of replaying its components through the processor, which could reject them as one message. Detached surfaces are disposed of once the outermost frame ends. Positions of components an update may recreate are found in one walk that stops at the last one needed, and a rollback puts recreated components back with the new SurfaceComponentsModel.insertComponents in one pass. A rolled-back delete of a data model key puts the key back at its place in its map. Signed-off-by: Nan Yu <nanyu@google.com>
|
Thanks for clarifying. Both code paths have tests at cc879f7:
|
Follow-up to #3276. It fixes the two rollback bugs polina-c found in the
RendererStateundo log (comment 1, comment 2), and the points from her review of this PR. The Python counterpart is #3268 and the TypeScript one is #3278.Problem
deleteSurfacereplayed the surface through the processor. The data model went in as oneupdateDataModelat/, which the depth limit of 50 can reject even though each write that built it was accepted. The components went in as oneupdateComponents, which the processor can reject even though it accepted them one message at a time: a type's reference fields come from the first catalog that declares the type, in component order. When the replay failed, the surface came back incomplete or empty. Recording the surface also deep-copied it, and that copy threwTypeErroron a map with non-string keys.catalogIds, which follows component order, so a rolled-back state could accept a cycle that an untouched state rejects.Fix
New
a2ui_coreAPIs. The processor calls none of them, so forward behavior is unchanged:SurfaceGroupModel.detachSurface(id)takes a surface out of the group without disposing of it and returns it. To the group's listeners it is the same asdeleteSurface:onSurfaceDeletedfires, and the surface's actions no longer reachonAction.addSurfaceputs the same object back and firesonSurfaceCreated.SurfaceComponentsModel.insertComponents(pairs)adds components at given positions, passed as(index, component)pairs in ascending order, in one pass over the model. LikeaddComponent, it bumps the change counter and emitsonCreated, and it resets the cachedcatalogIdsso they follow the new order.insertComponent(index, component)inserts one component, for parity with Python and TypeScript.a2ui_agent,RendererState:applyor transaction is open detaches it instead of disposing of it, then runs the message through the processor, which now finds no surface and only checks it. The undo puts the sameSurfaceModelback, with its components in their order, its data model, and everything the processor set on it. Nothing is copied or checked again, and recording a delete is O(1). Surfaces still detached when the outermost frame ends are disposed of then.updateComponentsmay recreate a component (it names another type, or another catalog than the component has), the record stores the component's position. The ids that need a position are collected first, thenmodel.keys.indexedis walked only until all of them are found. Plain updates skip this. On undo, components that kept their type and catalog are restored in place, and recreated components go back to their old positions with oneinsertComponentscall.updateDataModeldeletes a key from a map, the record stores the key's position, and the undo puts the key back there. Other writes are unchanged._revertkeeps undoing the rest of the log when one step throws, and the error that caused the rollback propagates._deepCopy, now used only for component properties and metadata, keeps maps with non-string keys instead of throwing.a2ui_agentusesa2ui_coreAPIs that 0.3.0 lacks:insertComponent,insertComponentsanddetachSurfacefrom this PR, andcheckPathsAndRecursionandcheckDuplicateComponentIds, which Fix performance and create conformance tests. #3314 exported. Itsa2ui_core: ^0.3.0constraint stays as it is here. The repository is a pub workspace, and pub resolvesa2ui_corefrom the workspace at the version in itspubspec.yaml, which only a release changes (see thea2ui-dart-versioningskill). A^0.3.1constraint makesflutter pub getfail, and pub rejects overriding a workspace package. Thea2ui_agentCHANGELOG notes that it needs the nexta2ui_corerelease, and that the release raises the constraint to^0.3.1along with thea2ui_coreversion.Measurements
Median of 7 runs on one machine. main is before this PR, and f462c4f is the head polina-c reviewed.
1,000 type-change messages near the end of a 1,000-component surface, in one transaction:
The same at 4,000 components:
The same at 16,000 components:
Rolling back one message that changes the type of the first 8,000 components of a 16,000-component surface:
Recording 500 type changes on an 8,000-component surface, as a ratio to a bare
MessageProcessorapplying the same messages (new timing test, fails above 2.5):Applying and rolling back a type change of 4,000 components of an 8,000-component surface, as a ratio to only applying it (new timing test, fails above 3):
Tests
renderer_state_test.dartfor every repro in the reviews. They fail at the code they report on and pass here: the deep data model, the catalog change that must keep rejecting the cycle, the map with non-string keys, the deleted surface that oneupdateComponentscould not rebuild, the component moved to the surface's catalog, and a component property holding a map with non-string keys. Other tests cover: several recreated components in one message keep their order; the order is kept after an undonedeleteSurface; a rollback puts back the same surface object, and a committed delete disposes of it; a deleted data model key goes back to its place.a2ui_core: tests forinsertComponent,insertComponents(positions,catalogIds, events, and rejected batches leaving the model unchanged), anddetachSurface(events, action forwarding, the same object coming back).renderer_state_timing_test.dart: two performance-tagged tests, the two ratios above.dart/a2ui_core,dart/a2ui_agentanddart/a2ui_flutter:dart format --set-exit-if-changedandflutter analyze --fatal-infosare clean, andflutter testpasses in all three (core 1831, agent 1123, flutter 309).dart test -P performancepasses for core (5) and agent (14).