19 \$cache functions using DGAsyncNodeOnInput

Parameterless $cache functions are treated specially because there is no need for a cache map. Instead they are more like an individual async dependent variable.

Consider the following example:


@import "Ceda/cxObject/DGAsyncNode.h"

$struct X isa ceda::IObject
{
    $cache <<async>> int y() const
        with int x = 7;
    {
        return x;
    }
};

Xcpp generates the following C++ code:

struct X : public ceda::IObjectBaseMixin<X,ceda::EmptyBase>
{
    typedef ceda::IObjectBaseMixin<X,ceda::EmptyBase> BaseClass;

    void _calc_y_input(int& x) const
    {
        x = 7;
    }

    int _calc_y(int x) const
    {
        return x;
    }

    struct _depnode_y : public ceda::DGAsyncNodeOnInput<_depnode_y,X,int,int>
    {
        typedef ceda::DGAsyncNodeOnInput<_depnode_y,X,int,int> base;
        using typename base::output_type;
        using typename base::input_type;
        static void CalcOutput(const X* _self, output_type& _output, const input_type& _input)
        {
            _output = _self->_calc_y(_input);
        }
        static void CalcInput(const X* _self, input_type& _input)
        {
            _self->_calc_y_input(_input);
        }
        X const* _GetSelf() const { return CONST_ATTRIB_CAST(X,_dn_y); }
        X const* _GetFinalSelf() const { return _GetSelf(); }
        virtual ceda::xstring Name() const { return "X::y()"; }
    } _dn_y;

    int const& y() const
    {
        return _dn_y.read();
    }

    void EvictDgsNodes() const
    {
        BaseClass::EvictDgsNodes();
        _dn_y.TryEvict(false);
    }
};

There is a nested struct named _depnode_y which represents an asynchronously calculated variable.

_depnode_y is a subclass of ceda::DGAsyncNodeOnInput which is defined in DGAsyncNode.h

Policy

We implement the following policy (there are others that may be appropriate in situations but let's do this one):

  • We never abort an async calculation.
  • We always apply the new output when the async calculation is finished.
  • We don't allow two async calculations to occur in parallel.
  • When finishing the async calculation we check whether we are dirty and if so we recalculate again.

This is a good default because it has the following nice features:

  • there is no starvation (which can happen if tasks are aborted before they are finished, and they never get a chance to complete)
  • no aborting tasks
  • no risk of creating thousands of tasks
  • only one input needs to be created and visited

DGAsyncNodeOnInput

template <typename FinalClass, typename Self, typename Input, typename Output>
class DGAsyncNodeOnInput : public DGDepNode
{
public:
    typedef Input input_type;
    typedef Output output_type;

    DGAsyncNodeOnInput() :
        DGDepNode(DF_ASYNC)
    {
    }

    virtual ssize_t ByteSize() const
    {
        // Note that we don't add CacheValueAdditionalSize(input_) because we clear the input
        // at the time the output is updated
        return CacheValueAdditionalSize(_output);
    }

    virtual void VisitContainingObject(IObjectVisitor& _v) const
    {
        _v << input_ << _output << static_cast<const FinalClass*>(this)->_GetFinalSelf();
    }

    virtual void OnEvict() const
    {
        OnEvictCacheValue(_output);
        ClearCacheValue(_output);
    }

    virtual bool IsEvictable() const
    {
        return !Enabled(DF_ASYNC_RUNNING) && CacheValueIsEvictable(_output);
    }

    virtual bool RecalcCache() const
    {
        FinalClass::CalcInput(static_cast<const FinalClass*>(this)->_GetSelf(), input_);
        return false;   // Pretend input not changed to avoid propagating dirtiness into the out-nodes
    }

    virtual void OnInvalidate() const
    {
        if (Enabled(DF_ASYNC_RUNNING))
        {
            // Already running an async task, don't post another
        }
        else
        {
            SetFlag(DF_ASYNC_RUNNING);
            PostAsyncTask(GetThreadPtr<CSpace>(), [this]()
            {
                PrepareThreadLocalStorage(static_cast<const FinalClass*>(this)->_GetFinalSelf());

                // Calculate the inputs
                {
                    CSpaceTxn txn;
                    cxAssert(Enabled(DF_ASYNC_RUNNING));
                    ReadBarrierDep();               // This cleans this node
                }

                while(1)
                {
                    // Calculate output from input without a CSpace lock, hold output in a local variable
                    output_type localOutput;
                    FinalClass::CalcOutput(static_cast<const FinalClass*>(this)->_GetSelf(), localOutput, input_);

                    // Apply the output and invalidate out-nodes
                    {
                        CSpaceTxn txn;
                        cxAssert(Enabled(DF_ASYNC_RUNNING));

                        UpdateOutput(_output, localOutput);
                        input_ = input_type();          // Clear the input - might allow some memory reclamation

                        MarkOutNodesAsSoftAndHardDirty();

                        if (!Enabled(DF_SOFT_DIRTY))
                        {
                            // Finished
                            ClearFlag(DF_ASYNC_RUNNING);
                            if (TypeHasAdditionalSize<output_type>()) UpdateByteSize();
                            return;
                        }
                        else
                        {
                            // This DGS node is dirty indicating that the inputs have changed again, so we need to recalculate again

                            // Recalculate the input. This cleans this node
                            ReadBarrierDep();

                            // continue in the while loop
                        }
                    }
                }
            });
        }
    }

    const output_type& read() const
    {
        if (!Enabled(DF_CALLED_READ))
        {
            SetFlag(DF_CALLED_READ);
            OnInvalidate();
        }
        ReadBarrierIndep();      // Attach out-nodes
        return _output;
    }

protected:
    ///////////////////////////// Protected by a CSpace lock
    mutable input_type input_;
    mutable output_type _output;
};

Comments

An async dep node is kind of like an dep+indep pair, where the dep node has no out-nodes. Of course the indep node has in-nodes. This suggests statewise it makes sense to use one object for both. Indeed it is like a normal dep node except

  • from the point of view of callers it is more like an indep node. For that purpose we have a ReadBarrierIndep() function which is like an indep node read barrier and which is used to attach the out-nodes.
  • When updating the output it is like an indep node, in that we call MarkOutNodesAsSoftAndHardDirty() to invalidate the out-nodes.
  • from the point of view of the async task which wants to calculate the inputs in a CSpaceLock, it behaves more like a dep node, in that it allows for attaching in-nodes.
  • Unlike a normal dep node, when we are invalidated because the inputs change we don't propagate dirtiness into the out-nodes.

This is what needs to happen:

  • The containing class has a task executer member for posting tasks for this object. The task executer is closed when the containing object destructs. This usually happens without a CSpace lock to avoid dead-locks.
  • The cache map is initially empty. There is no concept of "kickstarting" by posting any tasks
  • If foo(a,b) is called for the first time then
    • a subclass of DGDepNode is inserted into the map. This holds the initial output value which is None.
    • Invoke ReadBarrierIndep() in the manner of an indep node, in order to attach the out-nodes.
    • Call OnInvalidate() to post a task, just as though the inputs had changed
    • return _output
  • When the task runs, ReadBarrierDep() calls RecalcCache() which calculates the inputs, and the in-nodes are attached as required.
  • If the node is invalidated then unlike a normal dep node we don't propagate dirtiness into the out-nodes.