/*
 * Copyright (C) 2017 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "MarkingConstraintSolver.h"

#include "JSCInlines.h"

namespace JSC {

MarkingConstraintSolver::MarkingConstraintSolver(MarkingConstraintSet& set)
    : m_heap(set.m_heap)
    , m_mainVisitor(m_heap.collectorSlotVisitor())
    , m_set(set)
{
    m_heap.forEachSlotVisitor(
        [&] (SlotVisitor& visitor) {
            m_visitCounters.append(VisitCounter(visitor));
        });
}

MarkingConstraintSolver::~MarkingConstraintSolver()
{
}

bool MarkingConstraintSolver::didVisitSomething() const
{
    for (const VisitCounter& visitCounter : m_visitCounters) {
        if (visitCounter.visitCount())
            return true;
    }
    return false;
}

void MarkingConstraintSolver::execute(SchedulerPreference preference, ScopedLambda<std::optional<unsigned>()> pickNext)
{
    m_pickNextIsStillActive = true;
    RELEASE_ASSERT(!m_numThreadsThatMayProduceWork);

    if (Options::useParallelMarkingConstraintSolver()) {
        if (Options::logGC())
            dataLog(preference == ParallelWorkFirst ? "P" : "N", "<");

        m_heap.runFunctionInParallel(
            [&] (SlotVisitor& visitor) { runExecutionThread(visitor, preference, pickNext); });

        if (Options::logGC())
            dataLog(">");
    } else
        runExecutionThread(m_mainVisitor, preference, pickNext);

    RELEASE_ASSERT(!m_pickNextIsStillActive);
    RELEASE_ASSERT(!m_numThreadsThatMayProduceWork);

    if (!m_toExecuteSequentially.isEmpty()) {
        for (unsigned indexToRun : m_toExecuteSequentially)
            execute(*m_set.m_set[indexToRun]);
        m_toExecuteSequentially.clear();
    }

    RELEASE_ASSERT(m_toExecuteInParallel.isEmpty());
}

void MarkingConstraintSolver::drain(BitVector& unexecuted)
{
    auto iter = unexecuted.begin();
    auto end = unexecuted.end();
    if (iter == end)
        return;
    auto pickNext = scopedLambda<std::optional<unsigned>()>(
        [&] () -> std::optional<unsigned> {
            if (iter == end)
                return std::nullopt;
            return *iter++;
        });
    execute(NextConstraintFirst, pickNext);
    unexecuted.clearAll();
}

void MarkingConstraintSolver::converge(const Vector<MarkingConstraint*>& order)
{
    if (didVisitSomething())
        return;

    if (order.isEmpty())
        return;

    size_t index = 0;

    // We want to execute the first constraint sequentially if we think it will quickly give us a
    // result. If we ran it in parallel to other constraints, then we might end up having to wait for
    // those other constraints to finish, which would be a waste of time since during convergence it's
    // empirically most optimal to return to draining as soon as a constraint generates work. Most
    // constraints don't generate any work most of the time, and when they do generate work, they tend
    // to generate enough of it to feed a decent draining cycle. Therefore, pause times are lowest if
    // we get the heck out of here as soon as a constraint generates work. I think that part of what
    // makes this optimal is that we also never abort running a constraint early, so when we do run
    // one, it has an opportunity to generate as much work as it possibly can.
    if (order[index]->quickWorkEstimate(m_mainVisitor) > 0.) {
        execute(*order[index++]);

        if (m_toExecuteInParallel.isEmpty()
            && (order.isEmpty() || didVisitSomething()))
            return;
    }

    auto pickNext = scopedLambda<std::optional<unsigned>()>(
        [&] () -> std::optional<unsigned> {
            if (didVisitSomething())
                return std::nullopt;

            if (index >= order.size())
                return std::nullopt;

            MarkingConstraint& constraint = *order[index++];
            return constraint.index();
        });

    execute(ParallelWorkFirst, pickNext);
}

void MarkingConstraintSolver::execute(MarkingConstraint& constraint)
{
    if (m_executed.get(constraint.index()))
        return;

    constraint.prepareToExecute(NoLockingNecessary, m_mainVisitor);
    constraint.execute(m_mainVisitor);
    m_executed.set(constraint.index());
}

void MarkingConstraintSolver::addParallelTask(RefPtr<SharedTask<void(SlotVisitor&)>> task, MarkingConstraint& constraint)
{
    auto locker = holdLock(m_lock);
    m_toExecuteInParallel.append(TaskWithConstraint(WTFMove(task), &constraint));
}

void MarkingConstraintSolver::runExecutionThread(SlotVisitor& visitor, SchedulerPreference preference, ScopedLambda<std::optional<unsigned>()> pickNext)
{
    for (;;) {
        bool doParallelWorkMode;
        MarkingConstraint* constraint = nullptr;
        unsigned indexToRun = UINT_MAX;
        TaskWithConstraint task;
        {
            auto locker = holdLock(m_lock);

            for (;;) {
                auto tryParallelWork = [&] () -> bool {
                    if (m_toExecuteInParallel.isEmpty())
                        return false;

                    task = m_toExecuteInParallel.first();
                    constraint = task.constraint;
                    doParallelWorkMode = true;
                    return true;
                };

                auto tryNextConstraint = [&] () -> bool {
                    if (!m_pickNextIsStillActive)
                        return false;

                    for (;;) {
                        std::optional<unsigned> pickResult = pickNext();
                        if (!pickResult) {
                            m_pickNextIsStillActive = false;
                            return false;
                        }

                        if (m_executed.get(*pickResult))
                            continue;

                        MarkingConstraint& candidateConstraint = *m_set.m_set[*pickResult];
                        if (candidateConstraint.concurrency() == ConstraintConcurrency::Sequential) {
                            m_toExecuteSequentially.append(*pickResult);
                            continue;
                        }
                        if (candidateConstraint.parallelism() == ConstraintParallelism::Parallel)
                            m_numThreadsThatMayProduceWork++;
                        indexToRun = *pickResult;
                        constraint = &candidateConstraint;
                        doParallelWorkMode = false;
                        constraint->prepareToExecute(locker, visitor);
                        return true;
                    }
                };

                if (preference == ParallelWorkFirst) {
                    if (tryParallelWork() || tryNextConstraint())
                        break;
                } else {
                    if (tryNextConstraint() || tryParallelWork())
                        break;
                }

                // This means that we have nothing left to run. The only way for us to have more work is
                // if someone is running a constraint that may produce parallel work.

                if (!m_numThreadsThatMayProduceWork)
                    return;

                // FIXME: Any waiting could be replaced with just running the SlotVisitor.
                // I wonder if that would be profitable.
                m_condition.wait(m_lock);
            }
        }

        if (doParallelWorkMode)
            constraint->doParallelWork(visitor, *task.task);
        else {
            if (constraint->parallelism() == ConstraintParallelism::Parallel) {
                visitor.m_currentConstraint = constraint;
                visitor.m_currentSolver = this;
            }

            constraint->execute(visitor);

            visitor.m_currentConstraint = nullptr;
            visitor.m_currentSolver = nullptr;
        }

        {
            auto locker = holdLock(m_lock);

            if (doParallelWorkMode) {
                if (!m_toExecuteInParallel.isEmpty()
                    && task == m_toExecuteInParallel.first())
                    m_toExecuteInParallel.takeFirst();
                else
                    ASSERT(!m_toExecuteInParallel.contains(task));
            } else {
                if (constraint->parallelism() == ConstraintParallelism::Parallel)
                    m_numThreadsThatMayProduceWork--;
                m_executed.set(indexToRun);
            }

            m_condition.notifyAll();
        }
    }
}

} // namespace JSC

