Commit a2fe4a0a authored by Simeon Anfinrud's avatar Simeon Anfinrud

[Chromecast] Extract Sequencer from Controller.

This will allow other Observables and potentially others to use
Sequencer. Added tests.

Improved the efficiency of Sequencer. The old version used
recursion heavily, which meant that the stack could grow
arbitrarily large. The new version flattens the stack and
avoids wrapping Runnables in other Runnables, saving both
stack and heap space.

Bug: None
Test: cast_base_junit_tests
Change-Id: I401a6fac6f2029e7d2744a459e03c64dd7a311ee
Reviewed-on: https://chromium-review.googlesource.com/1187633Reviewed-by: default avatarSimeon Anfinrud <sanfin@chromium.org>
Cr-Commit-Position: refs/heads/master@{#594418}
parent 52a86740
......@@ -324,6 +324,7 @@ if (is_android) {
"$java_src_dir/org/chromium/chromecast/base/Scopes.java",
"$java_src_dir/org/chromium/chromecast/base/Predicate.java",
"$java_src_dir/org/chromium/chromecast/base/Subscription.java",
"$java_src_dir/org/chromium/chromecast/base/Sequencer.java",
"$java_src_dir/org/chromium/chromecast/base/Supplier.java",
"$java_src_dir/org/chromium/chromecast/base/Unit.java",
......@@ -367,6 +368,7 @@ if (is_android) {
"$java_test_dir/org/chromium/chromecast/base/ObservableNotTest.java",
"$java_test_dir/org/chromium/chromecast/base/ReactiveRecorderTest.java",
"$java_test_dir/org/chromium/chromecast/base/ObserversTest.java",
"$java_test_dir/org/chromium/chromecast/base/SequencerTest.java",
"$java_test_dir/org/chromium/chromecast/base/UnitTest.java",
]
deps = [
......
......@@ -4,7 +4,6 @@
package org.chromium.chromecast.base;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
......@@ -98,31 +97,4 @@ public class Controller<T> extends Observable<T> {
mScopeMap.remove(observer);
scope.close();
}
// TODO(sanfin): make this its own public class and add tests.
private static class Sequencer {
private boolean mIsRunning;
private final ArrayDeque<Runnable> mMessageQueue = new ArrayDeque<>();
/**
* Runs the task synchronously, or, if a sequence()d task is already running, posts the task
* to a queue, whose items will be run synchronously when the current task is finished.
*/
public void sequence(Runnable impl) {
if (mIsRunning) {
mMessageQueue.add(() -> sequence(impl));
return;
}
mIsRunning = true;
impl.run();
mIsRunning = false;
while (!mMessageQueue.isEmpty()) {
mMessageQueue.removeFirst().run();
}
}
public boolean inSequence() {
return mIsRunning;
}
}
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chromecast.base;
import java.util.ArrayDeque;
/**
* Schedules tasks to run one after another.
*
* If nothing is running, scheduled tasks execute synchronously. Any tasks posted by a scheduled
* task are deferred until the current task finishes. This grants a limited notion of single-
* threaded atomicity. Note that this is NOT thread-safe!
*/
public class Sequencer {
private static final int MAX_RECURSION_DEPTH = 65536;
private boolean mIsRunning;
private final ArrayDeque<Runnable> mMessageQueue = new ArrayDeque<>();
/**
* Runs the task synchronously, or, if a sequence()d task is already running, posts the task
* to a queue, whose items will be run synchronously when the current task is finished.
*/
public void sequence(Runnable task) {
mMessageQueue.add(task);
if (mIsRunning) return;
for (int count = 0; !mMessageQueue.isEmpty(); count++) {
if (count == MAX_RECURSION_DEPTH) {
throw new InceptionException(
"Too many nested sequenced tasks posted from one sequenced call! "
+ "Is there an infinite loop?");
}
sequenceInternal(mMessageQueue.removeFirst());
}
}
private void sequenceInternal(Runnable task) {
mIsRunning = true;
task.run();
mIsRunning = false;
}
public boolean inSequence() {
return mIsRunning;
}
/**
* Thrown if a task in a task in a task... seems to go on forever. Of course we can't detect
* whether it actually goes on forever because of the halting problem, so we give up after a
* threshold is exceeded.
*/
public static class InceptionException extends RuntimeException {
public InceptionException(String message) {
super(message);
}
}
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chromecast.base;
import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.assertThat;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.BlockJUnit4ClassRunner;
import java.util.ArrayList;
import java.util.List;
/**
* Tests for Sequencer.
*/
@RunWith(BlockJUnit4ClassRunner.class)
public class SequencerTest {
@Test
public void testPostedTaskIsRun() {
List<String> result = new ArrayList<>();
Sequencer s = new Sequencer();
s.sequence(() -> result.add("a"));
assertThat(result, contains("a"));
}
@Test
public void testTaskPostedByTaskIsRunAfterFirstTaskFinishes() {
List<String> result = new ArrayList<>();
Sequencer s = new Sequencer();
s.sequence(() -> {
s.sequence(() -> result.add("b"));
result.add("a");
});
assertThat(result, contains("a", "b"));
}
@Test
public void testTaskThatEnqueuesManyTasks() {
List<Integer> result = new ArrayList<>();
Sequencer s = new Sequencer();
s.sequence(() -> {
for (int i = 0; i < 10; i++) {
final int toAdd = i;
s.sequence(() -> result.add(toAdd));
}
});
assertThat(result, contains(0, 1, 2, 3, 4, 5, 6, 7, 8, 9));
}
@Test(expected = Sequencer.InceptionException.class)
public void testThrowsExceptionEventuallyOnInfiniteLoop() {
Sequencer s = new Sequencer();
Runnable runception = new Runnable() {
@Override
public void run() {
s.sequence(this);
}
};
s.sequence(runception);
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment