com.facebook.litho.dataflow.DataFlowGraph.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.litho.dataflow.DataFlowGraph.java

Source

/**
 * 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);
            }
        }
    }
}