/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "vm/InvalidatingFuse.h"

#include "gc/PublicIterators.h"
#include "jit/Invalidation.h"
#include "jit/JitSpewer.h"
#include "vm/JSContext.h"
#include "vm/JSScript.h"
#include "vm/Logging.h"

#include "gc/StableCellHasher-inl.h"
#include "vm/JSScript-inl.h"

js::FuseDependentIonScriptSet::FuseDependentIonScriptSet(JSContext* cx,
                                                         InvalidatingFuse* fuse)
    : associatedFuse(fuse), ionScripts(cx->runtime()) {}

bool js::InvalidatingRuntimeFuse::addFuseDependency(
    JSContext* cx, const jit::IonScriptKey& ionScript) {
  MOZ_ASSERT(ionScript.script()->zone() == cx->zone());

  auto* scriptSet =
      cx->zone()->fuseDependencies.getOrCreateDependentScriptSet(cx, this);
  if (!scriptSet) {
    return false;
  }

  return scriptSet->addScriptForFuse(this, ionScript);
}

// This method will pop the fuse and iterates over its dependencies
// iff this fuse is still intact.
// This avoids a costly iteration over dependencies in subsequent calls.
void js::InvalidatingRuntimeFuse::popFuse(JSContext* cx) {
  if (!intact()) {
    return;
  }

  // Pop the fuse in the base class
  GuardFuse::popFuse(cx);
  JS_LOG(fuseInvalidation, Verbose, "Invalidating fuse popping: %s", name());
  // do invalidation.
  for (AllZonesIter z(cx->runtime()); !z.done(); z.next()) {
    // There's one dependent script set per fuse; just iterate over them all to
    // find the one we need (see comment on JS::Zone::fuseDependencies for
    // reasoning).
    for (auto& fd : z.get()->fuseDependencies) {
      fd.invalidateForFuse(cx, this);
    }
  }
}

void js::FuseDependentIonScriptSet::invalidateForFuse(JSContext* cx,
                                                      InvalidatingFuse* fuse) {
  if (associatedFuse != fuse) {
    return;
  }
  ionScripts.get().invalidateAndClear(cx, "fuse");
}

void js::jit::DependentIonScriptSet::invalidateAndClear(JSContext* cx,
                                                        const char* reason) {
  for (const auto& ionScriptKey : ionScripts_) {
    IonScript* ionScript = ionScriptKey.maybeIonScriptToInvalidate();
    if (ionScript) {
      JSScript* script = ionScriptKey.script();
      JitSpew(jit::JitSpew_IonInvalidate, "Invalidating ion script %p for %s",
              ionScript, reason);
      JS_LOG(fuseInvalidation, Debug,
             "Invalidating ion script %s:%d for reason %s", script->filename(),
             script->lineno(), reason);
    }
  }
  js::jit::Invalidate(cx, ionScripts_);
  ionScripts_.clearAndFree();
}

bool js::FuseDependentIonScriptSet::addScriptForFuse(
    InvalidatingFuse* fuse, const jit::IonScriptKey& ionScript) {
  MOZ_ASSERT(fuse == associatedFuse);
  return ionScripts.get().addToSet(ionScript);
}

js::FuseDependentIonScriptSet*
js::DependentIonScriptGroup::getOrCreateDependentScriptSet(
    JSContext* cx, js::InvalidatingFuse* fuse) {
  for (auto& dss : dependencies) {
    if (dss.associatedFuse == fuse) {
      return &dss;
    }
  }

  if (!dependencies.emplaceBack(cx, fuse)) {
    return nullptr;
  }

  auto& dss = dependencies.back();
  MOZ_ASSERT(dss.associatedFuse == fuse);
  return &dss;
}

bool js::jit::DependentIonScriptSet::addToSet(const IonScriptKey& ionScript) {
  MOZ_ASSERT(lengthAfterLastCompaction_ <= ionScripts_.length());

  // If `ionScript` is already in the vector, it must be the last entry.
  if (!ionScripts_.empty() && ionScripts_.back() == ionScript) {
    return true;
  }

  // Assert `ionScript` is not in the vector. Limit this to the last 8 entries
  // to not slow down debug builds too much.
#ifdef DEBUG
  size_t numToCheck = std::min<size_t>(ionScripts_.length(), 8);
  for (size_t i = 0; i < numToCheck; i++) {
    MOZ_ASSERT(ionScripts_[ionScripts_.length() - 1 - i] != ionScript);
  }
#endif

  // Compact the vector if its size doubled since the last compaction. This
  // isn't required for correctness but it avoids keeping a lot of stale
  // entries around between GCs.
  if (ionScripts_.length() / 2 > lengthAfterLastCompaction_) {
    ionScripts_.eraseIf([](const IonScriptKey& ionScript) {
      return ionScript.maybeIonScriptToInvalidate() == nullptr;
    });
    lengthAfterLastCompaction_ = ionScripts_.length();
  }

  return ionScripts_.append(ionScript);
}
