mirror of
https://github.com/google/blockly.git
synced 2026-01-12 03:17:09 +01:00
Implement Blockly.Events.filter in linear time (#1205)
* Implement Blockly.Events.filter in linear time For large App Inventor projects (order 1k+ blocks, 100+ top-level blocks), the O(n^2) behavior of Blockly.Event.filter was causing performance issues when rearranging blocks or pasting from the backpack. This commit provides a linear merge implementation using a key that uniquely identifies a block so that multiple events targeting the same block are merged. This change benefits from O(1) amortized lookup using an object as a key-value store. * Add event filter unit tests and fix logic bugs * Update Blockly.Events.filter unit tests
This commit is contained in:
committed by
Rachel Fenichel
parent
a3586db023
commit
e1e94271c4
@@ -174,45 +174,37 @@ Blockly.Events.filter = function(queueIn, forward) {
|
||||
// Undo is merged in reverse order.
|
||||
queue.reverse();
|
||||
}
|
||||
// Merge duplicates. O(n^2), but n should be very small.
|
||||
for (var i = 0, event1; event1 = queue[i]; i++) {
|
||||
for (var j = i + 1, event2; event2 = queue[j]; j++) {
|
||||
if (event1.type == event2.type &&
|
||||
event1.blockId == event2.blockId &&
|
||||
event1.workspaceId == event2.workspaceId) {
|
||||
if (event1.type == Blockly.Events.MOVE) {
|
||||
// Merge move events.
|
||||
event1.newParentId = event2.newParentId;
|
||||
event1.newInputName = event2.newInputName;
|
||||
event1.newCoordinate = event2.newCoordinate;
|
||||
queue.splice(j, 1);
|
||||
j--;
|
||||
} else if (event1.type == Blockly.Events.CHANGE &&
|
||||
event1.element == event2.element &&
|
||||
event1.name == event2.name) {
|
||||
// Merge change events.
|
||||
event1.newValue = event2.newValue;
|
||||
queue.splice(j, 1);
|
||||
j--;
|
||||
} else if (event1.type == Blockly.Events.UI &&
|
||||
event2.element == 'click' &&
|
||||
(event1.element == 'commentOpen' ||
|
||||
event1.element == 'mutatorOpen' ||
|
||||
event1.element == 'warningOpen')) {
|
||||
// Merge change events.
|
||||
event1.newValue = event2.newValue;
|
||||
queue.splice(j, 1);
|
||||
j--;
|
||||
}
|
||||
var mergedQueue = [];
|
||||
var hash = Object.create(null);
|
||||
// Merge duplicates.
|
||||
for (var i = 0, event; event = queue[i]; i++) {
|
||||
if (!event.isNull()) {
|
||||
var key = [event.type, event.blockId, event.workspaceId].join(' ');
|
||||
var lastEvent = hash[key];
|
||||
if (!lastEvent) {
|
||||
hash[key] = event;
|
||||
mergedQueue.push(event);
|
||||
} else if (event.type == Blockly.Events.MOVE) {
|
||||
// Merge move events.
|
||||
lastEvent.newParentId = event.newParentId;
|
||||
lastEvent.newInputName = event.newInputName;
|
||||
lastEvent.newCoordinate = event.newCoordinate;
|
||||
} else if (event.type == Blockly.Events.CHANGE &&
|
||||
event.element == lastEvent.element &&
|
||||
event.name == lastEvent.name) {
|
||||
// Merge change events.
|
||||
lastEvent.newValue = event.newValue;
|
||||
} else if (event.type == Blockly.Events.UI &&
|
||||
event.element == 'click' &&
|
||||
(lastEvent.element == 'commentOpen' ||
|
||||
lastEvent.element == 'mutatorOpen' ||
|
||||
lastEvent.element == 'warningOpen')) {
|
||||
// Merge click events.
|
||||
lastEvent.newValue = event.newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Remove null events.
|
||||
for (var i = queue.length - 1; i >= 0; i--) {
|
||||
if (queue[i].isNull()) {
|
||||
queue.splice(i, 1);
|
||||
}
|
||||
}
|
||||
queue = mergedQueue;
|
||||
if (!forward) {
|
||||
// Restore undo order.
|
||||
queue.reverse();
|
||||
|
||||
@@ -392,3 +392,136 @@ function test_varBackard_runForward() {
|
||||
checkVariableValues(workspace, 'name1', 'type1', 'id1');
|
||||
eventTest_tearDown();
|
||||
}
|
||||
|
||||
function test_events_filter() {
|
||||
eventTest_setUpWithMockBlocks();
|
||||
var block1 = workspace.newBlock('field_variable_test_block', '1');
|
||||
var events = [
|
||||
new Blockly.Events.BlockCreate(block1),
|
||||
new Blockly.Events.BlockMove(block1),
|
||||
new Blockly.Events.BlockChange(block1, 'field', 'VAR', 'item', 'item1'),
|
||||
new Blockly.Events.Ui(block1, 'click')
|
||||
];
|
||||
var filteredEvents = Blockly.Events.filter(events, true);
|
||||
assertEquals(4, filteredEvents.length); // no event should have been removed.
|
||||
// test that the order hasn't changed
|
||||
assertTrue(filteredEvents[0] instanceof Blockly.Events.BlockCreate);
|
||||
assertTrue(filteredEvents[1] instanceof Blockly.Events.BlockMove);
|
||||
assertTrue(filteredEvents[2] instanceof Blockly.Events.BlockChange);
|
||||
assertTrue(filteredEvents[3] instanceof Blockly.Events.Ui);
|
||||
}
|
||||
|
||||
function test_events_filterForward() {
|
||||
eventTest_setUpWithMockBlocks();
|
||||
var block1 = workspace.newBlock('field_variable_test_block', '1');
|
||||
var events = [
|
||||
new Blockly.Events.BlockCreate(block1),
|
||||
];
|
||||
helper_addMoveEvent(events, block1, 1, 1);
|
||||
helper_addMoveEvent(events, block1, 2, 2);
|
||||
helper_addMoveEvent(events, block1, 3, 3);
|
||||
var filteredEvents = Blockly.Events.filter(events, true);
|
||||
assertEquals(2, filteredEvents.length); // duplicate moves should have been removed.
|
||||
// test that the order hasn't changed
|
||||
assertTrue(filteredEvents[0] instanceof Blockly.Events.BlockCreate);
|
||||
assertTrue(filteredEvents[1] instanceof Blockly.Events.BlockMove);
|
||||
assertEquals(3, filteredEvents[1].newCoordinate.x);
|
||||
assertEquals(3, filteredEvents[1].newCoordinate.y);
|
||||
eventTest_tearDownWithMockBlocks();
|
||||
}
|
||||
|
||||
function test_events_filterBackward() {
|
||||
eventTest_setUpWithMockBlocks();
|
||||
var block1 = workspace.newBlock('field_variable_test_block', '1');
|
||||
var events = [
|
||||
new Blockly.Events.BlockCreate(block1),
|
||||
];
|
||||
helper_addMoveEvent(events, block1, 1, 1);
|
||||
helper_addMoveEvent(events, block1, 2, 2);
|
||||
helper_addMoveEvent(events, block1, 3, 3);
|
||||
var filteredEvents = Blockly.Events.filter(events, false);
|
||||
assertEquals(2, filteredEvents.length); // duplicate event should have been removed.
|
||||
// test that the order hasn't changed
|
||||
assertTrue(filteredEvents[0] instanceof Blockly.Events.BlockCreate);
|
||||
assertTrue(filteredEvents[1] instanceof Blockly.Events.BlockMove);
|
||||
assertEquals(1, filteredEvents[1].newCoordinate.x);
|
||||
assertEquals(1, filteredEvents[1].newCoordinate.y);
|
||||
eventTest_tearDownWithMockBlocks();
|
||||
}
|
||||
|
||||
function test_events_filterDifferentBlocks() {
|
||||
eventTest_setUpWithMockBlocks();
|
||||
var block1 = workspace.newBlock('field_variable_test_block', '1');
|
||||
var block2 = workspace.newBlock('field_variable_test_block', '2');
|
||||
var events = [
|
||||
new Blockly.Events.BlockCreate(block1),
|
||||
new Blockly.Events.BlockMove(block1),
|
||||
new Blockly.Events.BlockCreate(block2),
|
||||
new Blockly.Events.BlockMove(block2)
|
||||
];
|
||||
var filteredEvents = Blockly.Events.filter(events, true);
|
||||
assertEquals(4, filteredEvents.length); // no event should have been removed.
|
||||
eventTest_tearDownWithMockBlocks();
|
||||
}
|
||||
|
||||
function test_events_mergeMove() {
|
||||
eventTest_setUpWithMockBlocks();
|
||||
var block1 = workspace.newBlock('field_variable_test_block', '1');
|
||||
var events = [];
|
||||
helper_addMoveEvent(events, block1, 0, 0);
|
||||
helper_addMoveEvent(events, block1, 1, 1);
|
||||
var filteredEvents = Blockly.Events.filter(events, true);
|
||||
assertEquals(1, filteredEvents.length); // second move event merged into first
|
||||
assertEquals(1, filteredEvents[0].newCoordinate.x);
|
||||
assertEquals(1, filteredEvents[0].newCoordinate.y);
|
||||
eventTest_tearDownWithMockBlocks();
|
||||
}
|
||||
|
||||
function test_events_mergeChange() {
|
||||
eventTest_setUpWithMockBlocks();
|
||||
var block1 = workspace.newBlock('field_variable_test_block', '1');
|
||||
var events = [
|
||||
new Blockly.Events.Change(block1, 'field', 'VAR', 'item', 'item1'),
|
||||
new Blockly.Events.Change(block1, 'field', 'VAR', 'item1', 'item2')
|
||||
];
|
||||
var filteredEvents = Blockly.Events.filter(events, true);
|
||||
assertEquals(1, filteredEvents.length); // second change event merged into first
|
||||
assertEquals('item', filteredEvents[0].oldValue);
|
||||
assertEquals('item2', filteredEvents[0].newValue);
|
||||
eventTest_tearDownWithMockBlocks();
|
||||
}
|
||||
|
||||
function test_events_mergeUi() {
|
||||
eventTest_setUpWithMockBlocks();
|
||||
var block1 = workspace.newBlock('field_variable_test_block', '1');
|
||||
var block2 = workspace.newBlock('field_variable_test_block', '2');
|
||||
var block3 = workspace.newBlock('field_variable_test_block', '3');
|
||||
var events = [
|
||||
new Blockly.Events.Ui(block1, 'commentOpen', 'false', 'true'),
|
||||
new Blockly.Events.Ui(block1, 'click', 'false', 'true'),
|
||||
new Blockly.Events.Ui(block2, 'mutatorOpen', 'false', 'true'),
|
||||
new Blockly.Events.Ui(block2, 'click', 'false', 'true'),
|
||||
new Blockly.Events.Ui(block3, 'warningOpen', 'false', 'true'),
|
||||
new Blockly.Events.Ui(block3, 'click', 'false', 'true')
|
||||
];
|
||||
var filteredEvents = Blockly.Events.filter(events, true);
|
||||
assertEquals(3, filteredEvents.length); // click event merged into corresponding *Open event
|
||||
assertEquals('commentOpen', filteredEvents[0].element);
|
||||
assertEquals('mutatorOpen', filteredEvents[1].element);
|
||||
assertEquals('warningOpen', filteredEvents[2].element);
|
||||
eventTest_tearDownWithMockBlocks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to simulate block move events.
|
||||
*
|
||||
* @param {!Array.<Blockly.Events.Abstract>} events a queue of events.
|
||||
* @param {!Blockly.Block} block the block to be moved
|
||||
* @param {number} newX new X coordinate of the block
|
||||
* @param {number} newY new Y coordinate of the block
|
||||
*/
|
||||
function helper_addMoveEvent(events, block, newX, newY) {
|
||||
events.push(new Blockly.Events.BlockMove(block));
|
||||
block.xy_ = new goog.math.Coordinate(newX, newY);
|
||||
events[events.length-1].recordNew();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user