-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged events producing no-op event result in incorrect state after repeated undo/redo. #7026
Comments
@BeksOmega , Rachel said we got some of the merge/filter code from App Inventor. Can you check with them if they've noticed this and have a fix for it? |
@maribethb @BeksOmega So I checked out 67e192e, which was the last commit in Blockly from MIT App Inventor related to event merging. At least in the playground on that version I was unable to replicate the issue following the steps above by @johnnesky. At each undo/redo step I logged the respective stacks and they appear to have the correct information, no merging occurs. From there, I verified that indeed on develop the unexpected behavior occurs. A git bisect pointed to a commit that initially made no sense to me (switching to hosting the playground via http-server). This appears to be due to some issues on my end getting blockly_uncompressed.js to update correctly even with npm run build. Anyway, at a high level, earlier commits in the bisect that appear good have a unique groupdId per field change event, which is triggered on every keystroke. Later commits have a groupId for the entirety of the keystroke sequence. The change in semantics here seems to cause the problem, but I haven't dug into why, and our optimizations were mainly to reduce the computational complexity of merging events from O(n^2) to O(n), so the resulting semantics should be the same, just faster (we sometimes have workspaces on order 1000 or more blocks, so quadratic operation, like clean up blocks, can be millions of operations). |
Thanks, Evan! I don't have time to look into this right at the moment but your message rung a bell and I found this PR #4952 which may be related. In that one, we say it's a bug that the events didn't group related edits. Haven't thought ahead further than that yet. |
I did mention to Beka in a separate channel that we may need to adjust the event merging code to treat runs of events on the same field as a collapsed event if the semantics around how individual keystores are reported has changed. In App Inventor, we've gone further in our interpretation and we don't consider individual keystrokes to be block-event worthy and don't even report them. Only once the user has hit enter do we consider this a change to the field, at which point an event is generated. This is mainly due to our auto-saving code keying off the onWorkspaceChange event, and we don't want to save a workspace while the user in the middle of editing a field and AFAIK Blockly at the time we most recently incorporated changes (circa 2017) had no way to do this. |
Ok, so I looked into this a bit more and it seems like the main reason that the logic is broken is that events within the same group are:
1 is problematic because the filter operation merges events and drops them if the merged result is a null operation. However, since each event fires when the keystroke occurs, but they all have the same group ID, the individual events aren't merged into an event that says the field of the block goes from 123 to 123 (which is null and is dropped by filter). So 3 events end up on the undo stack rather than 0. 2 is problematic partly due to 1 but also undo/redo seem to not make use of the semantics of filter. More specifically here the events are pushed onto the opposing stack before filter is called, so even though filter is trying to merge and delete redundant events in the undo stack (resulting in the bug reported in this thread), its efforts are for naught. I can see that this would make sense if 1 was implemented because a group of events on the undo stack really ought to not result in null changes (i.e., if every change on the undo stack is significant than filtering again should be redundant, but there is a series of events on the undo stack that indeed represent a null operation). The simplest fix might be to modify the undo function to filter the event list before pushing the events onto the opposing stack. However, this will still have the effect that if you repeat the sequence of events above, you will have to undo twice to remove the block from the workspace (the first undo will merge the change events into a null event, which will get removed from the undo/redo stacks, and then the second undo would remove the block) but only then be able to redo once. A harder fix would be fixing 1 since the operations within a group are happening at different times on the run loop but filter has never been updated to accommodate this. |
I have a suggested fix: Use the output of the filtering/merging process to transfer events from the undo queue to the redo queue and vice versa, instead of transferring the original events. The merging behavior makes sense in the context of the output, but the fact that the unmerged events remain in the undo/redo queue alongside the modified/merged event is what causes problems. I would also suggest testing to see if the output of filtering/merging contains any non-null events. If not, then the undo/redo behavior should be repeated, trying the next group of events, until something actually changes, otherwise it feels weird to press undo and nothing happens. |
Use of the filter function in Workspace.prototype.undo has caused problems with repeated undo/redo (see issue google#7026), the originally-chosen fix for which was the addition (in PR google#7069) of code to fireNow to post-filter the .undoStack_ and .redoStack_ of any workspace that had just been involved in dispatching events. This apparently resolved the issue but added considerable additional complexity and made it difficlut to reason about how events are processed for undo/redo. Instead, since this filtering typically does nothing (at least nothing desirable), simply don't re-filter events on the undo stack before replaying them.
Use of the filter function in Workspace.prototype.undo has caused problems with repeated undo/redo (see issue google#7026), the originally-chosen fix for which was the addition (in PR google#7069) of code to fireNow to post-filter the .undoStack_ and .redoStack_ of any workspace that had just been involved in dispatching events. This apparently resolved the issue but added considerable additional complexity and made it difficlut to reason about how events are processed for undo/redo. Instead, since this filtering typically does nothing (at least nothing desirable), simply don't re-filter events on the undo stack before replaying them.
Use of the filter function in Workspace.prototype.undo has caused problems with repeated undo/redo (see issue google#7026), the originally-chosen fix for which was the addition (in PR google#7069) of code to fireNow to post-filter the .undoStack_ and .redoStack_ of any workspace that had just been involved in dispatching events. This apparently resolved the issue but added considerable additional complexity and made it difficult to reason about how events are processed for undo/redo. Instead, since this filtering typically does nothing (at least nothing desirable), simply don't re-filter events on the undo stack before replaying them.
Use of the filter function in Workspace.prototype.undo has caused problems with repeated undo/redo (see issue google#7026), the originally-chosen fix for which was the addition (in PR google#7069) of code to fireNow to post-filter the .undoStack_ and .redoStack_ of any workspace that had just been involved in dispatching events. This apparently resolved the issue but added considerable additional complexity and made it difficult to reason about how events are processed for undo/redo. Instead, since this filtering typically does nothing (at least nothing desirable), simply don't re-filter events on the undo stack before replaying them.
Use of the filter function in Workspace.prototype.undo has caused problems with repeated undo/redo (see issue #7026), the originally-chosen fix for which was the addition (in PR #7069) of code to fireNow to post-filter the .undoStack_ and .redoStack_ of any workspace that had just been involved in dispatching events. This apparently resolved the issue but added considerable additional complexity and made it difficult to reason about how events are processed for undo/redo. Instead, since this filtering typically does nothing (at least nothing desirable), simply don't re-filter events on the undo stack before replaying them.
Description
eventUtils.filter(...) attempts to merge related events into a single change for broadcast purposes, but in doing so it modifies the underlying events which remain in the undo/redo history. Those modified events can become no-op events even if they weren't before, reporting true for isNull(). The next time filtering is performed, these now no-op events are skipped by the merging process and important data is lost, resulting in an incorrect state in the workspace after performing undo + redo + undo.
Reproduction steps
The text was updated successfully, but these errors were encountered: