/**
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.litho.dataflow;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.concurrent.CopyOnWriteArrayList;
import android.support.annotation.VisibleForTesting;
import android.support.v4.util.SimpleArrayMap;
import com.facebook.litho.ComponentsPools;
import com.facebook.litho.internal.ArraySet;
/**
* A directed acyclic graph (DAG) created from one or more {@link GraphBinding}s. These component
* GraphBindings define how nodes in this graph are connected to each other: GraphBindings can add
* nodes and connections when they are 'activated' and can remove nodes and connections when they're
* deactivated.
*
* Data flows through the graph on each frame, from input nodes to output nodes.
*/
public class DataFlowGraph {
private static DataFlowGraph sInstance;
public static DataFlowGraph getInstance() {
if (sInstance == null) {
final ChoreographerTimingSource timingSource = new ChoreographerTimingSource();
sInstance = new DataFlowGraph(timingSource);
timingSource.setDataFlowGraph(sInstance);
}
return sInstance;
}
@VisibleForTesting
public static DataFlowGraph create(TimingSource timingSource) {
DataFlowGraph instance = new DataFlowGraph(timingSource);
timingSource.setDataFlowGraph(instance);
return instance;
}
private final TimingSource mTimingSource;
private final CopyOnWriteArrayList<GraphBinding> mBindings = new CopyOnWriteArrayList<>();
private final ArrayList<ValueNode> mSortedNodes = new ArrayList<>();
private final ArraySet<ValueNode> mFinishedNodes = new ArraySet<>();
private final ArraySet<ValueNode> mNodesWithFinishedInputs = new ArraySet<>();
private final SimpleArrayMap<GraphBinding, ArraySet<ValueNode>> mBindingToNodes =
new SimpleArrayMap<>();
private final ArraySet<GraphBinding> mFinishedBindings = new ArraySet<>();
private boolean mIsDirty = false;
private DataFlowGraph(TimingSource timingSource) {
mTimingSource = timingSource;
}
/**
* Adds an activated {@link GraphBinding}. This means that binding's nodes are added to the
* existing graph and data will flow through them on the next frame.
*/
public void register(GraphBinding binding) {
if (!binding.isActive()) {
throw new RuntimeException("Expected added GraphBinding to be active: " + binding);
}
mBindings.add(binding);
mBindingToNodes.put(binding, binding.getAllNodes());
if (mBindings.size() == 1) {
mTimingSource.start();
}
mIsDirty = true;
}
/**
* Removes a {@link GraphBinding}. This means any nodes that only belonged to that binding will
* be removed from the graph.
*/
public void unregister(GraphBinding binding) {
if (!mBindings.remove(binding)) {
throw new RuntimeException("Tried to unregister non-existent binding");
}
mBindingToNodes.remove(binding);
mFinishedBindings.remove(binding);
if (mBindings.isEmpty()) {
mTimingSource.stop();
}
mIsDirty = true;
}
void doFrame(long frameTimeNanos) {
if (mIsDirty) {
regenerateSortedNodes();
}
propagate(frameTimeNanos);
updateFinishedStates();
}
private void propagate(long frameTimeNanos) {
final int size = mSortedNodes.size();
for (int i = 0; i < size; i++) {
final ValueNode node = mSortedNodes.get(i);
node.doCalculateValue(frameTimeNanos);
}
}
private void regenerateSortedNodes() {
mSortedNodes.clear();
if (mBindings.size() == 0) {
return;
}
final ArraySet<ValueNode> leafNodes = ComponentsPools.acquireArraySet();
final SimpleArrayMap<ValueNode, Integer> nodesToOutputsLeft = new SimpleArrayMap<>();
for (int i = 0, bindingsSize = mBindingToNodes.size(); i < bindingsSize; i++) {
final ArraySet<ValueNode> nodes = mBindingToNodes.valueAt(i);
for (int j = 0, nodesSize = nodes.size(); j < nodesSize; j++) {
final ValueNode node = nodes.valueAt(j);
final int outputCount = node.getOutputCount();
if (outputCount == 0) {
leafNodes.add(node);
} else {
nodesToOutputsLeft.put(node, outputCount);
}
}
}
if (!nodesToOutputsLeft.isEmpty() && leafNodes.isEmpty()) {
throw new DetectedCycleException(
"Graph has nodes, but they represent a cycle with no leaf nodes!");
}
final ArrayDeque<ValueNode> nodesToProcess = ComponentsPools.acquireArrayDeque();
nodesToProcess.addAll(leafNodes);
while (!nodesToProcess.isEmpty()) {
final ValueNode next = nodesToProcess.pollFirst();
mSortedNodes.add(next);
for (int i = 0, count = next.getInputCount(); i < count; i++) {
final ValueNode input = next.getInputAt(i);
final int outputsLeft = nodesToOutputsLeft.get(input) - 1;
nodesToOutputsLeft.put(input, outputsLeft);
if (outputsLeft == 0) {
nodesToProcess.addLast(input);
} else if (outputsLeft < 0) {
throw new DetectedCycleException("Detected cycle.");
}
}
}
int expectedTotalNodes = nodesToOutputsLeft.size() + leafNodes.size();
if (mSortedNodes.size() != expectedTotalNodes) {
throw new DetectedCycleException(
"Had unreachable nodes in graph -- this likely means there was a cycle");
}
Collections.reverse(mSortedNodes);
mIsDirty = false;
ComponentsPools.release(nodesToProcess);
ComponentsPools.release(leafNodes);
}
private void updateFinishedStates() {
updateFinishedNodes();
notifyFinishedBindings();
}
private void updateFinishedNodes() {
for (int i = 0, size = mSortedNodes.size(); i < size; i++) {
final ValueNode node = mSortedNodes.get(i);
if (mFinishedNodes.contains(node)) {
continue;
}
final boolean wereInputsFinished = mNodesWithFinishedInputs.contains(node);
final boolean areInputsFinished = wereInputsFinished || areInputsFinished(node);
if (!wereInputsFinished && areInputsFinished) {
mNodesWithFinishedInputs.add(node);
if (node instanceof NodeCanFinish) {
((NodeCanFinish) node).onInputsFinished();
}
}
final boolean nodeIsNowFinished =
!(node instanceof NodeCanFinish) ||
((NodeCanFinish) node).isFinished();
if (nodeIsNowFinished) {
mFinishedNodes.add(node);
}
}
}
private boolean areInputsFinished(ValueNode node) {
for (int i = 0, inputCount = node.getInputCount(); i < inputCount; i++) {
if (!mFinishedNodes.contains(node.getInputAt(i))) {
return false;
}
}
return true;
}
private void notifyFinishedBindings() {
// Iterate in reverse order since notifying that a binding is finished might result in removing
// that binding.
for (int i = mBindingToNodes.size() - 1; i >= 0; i--) {
final GraphBinding binding = mBindingToNodes.keyAt(i);
if (mFinishedBindings.contains(binding)) {
continue;
}
boolean allAreFinished = true;
final ArraySet<ValueNode> nodesToCheck = mBindingToNodes.valueAt(i);
for (int j = 0, nodesSize = nodesToCheck.size(); j < nodesSize; j++) {
final ValueNode node = nodesToCheck.valueAt(j);
if (!mFinishedNodes.contains(node)) {
allAreFinished = false;
break;
}
}
if (allAreFinished) {
binding.notifyNodesHaveFinished();
mFinishedBindings.add(binding);
}
}
}
}