/* 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/. */

#ifndef mozilla_dom_SharedScriptCache_h
#define mozilla_dom_SharedScriptCache_h

#include "PLDHashTable.h"            // PLDHashEntryHdr
#include "js/TypeDecls.h"            // JSContext, JS::MutableHandle, JS::Value
#include "js/loader/LoadedScript.h"  // JS::loader::LoadedScript
#include "js/loader/ScriptFetchOptions.h"    // JS::loader::ScriptFetchOptions
#include "js/loader/ScriptKind.h"            // JS::loader::ScriptKind
#include "js/loader/ScriptLoadRequest.h"     // JS::loader::ScriptLoadRequest
#include "mozilla/CORSMode.h"                // mozilla::CORSMode
#include "mozilla/HashTable.h"               // mozilla::HashMap
#include "mozilla/MemoryReporting.h"         // MallocSizeOf
#include "mozilla/Mutex.h"                   // Mutex, GUARDED_BY, MutexAutoLock
#include "mozilla/RefPtr.h"                  // RefPtr
#include "mozilla/SharedSubResourceCache.h"  // SharedSubResourceCache, SharedSubResourceCacheLoadingValueBase, SubResourceNetworkMetadataHolder
#include "mozilla/ThreadSafety.h"            // MOZ_GUARDED_BY
#include "mozilla/UniquePtr.h"               // mozilla::UniquePtr
#include "mozilla/WeakPtr.h"                 // SupportsWeakPtr
#include "mozilla/dom/CacheExpirationTime.h"  // CacheExpirationTime
#include "nsIMemoryReporter.h"  // nsIMemoryReporter, NS_DECL_NSIMEMORYREPORTER
#include "nsIObserver.h"        // nsIObserver
#include "nsIPrincipal.h"       // nsIPrincipal
#include "nsISupports.h"        // nsISupports, NS_DECL_ISUPPORTS
#include "nsStringFwd.h"        // nsACString

namespace mozilla {
namespace dom {

class ScriptLoader;
class ScriptLoadData;

class ScriptHashKey : public PLDHashEntryHdr {
 public:
  using KeyType = const ScriptHashKey&;
  using KeyTypePointer = const ScriptHashKey*;

  explicit ScriptHashKey(const ScriptHashKey& aKey)
      : PLDHashEntryHdr(),
        mURI(aKey.mURI),
        mPartitionPrincipal(aKey.mPartitionPrincipal),
        mLoaderPrincipal(aKey.mLoaderPrincipal),
        mKind(aKey.mKind),
        mCORSMode(aKey.mCORSMode),
        mReferrerPolicy(aKey.mReferrerPolicy),
        mHintCharset(aKey.mHintCharset) {
    MOZ_COUNT_CTOR(ScriptHashKey);
  }

  explicit ScriptHashKey(const ScriptHashKey* aKey) : ScriptHashKey(*aKey) {}

  ScriptHashKey(ScriptHashKey&& aKey)
      : PLDHashEntryHdr(),
        mURI(std::move(aKey.mURI)),
        mPartitionPrincipal(std::move(aKey.mPartitionPrincipal)),
        mLoaderPrincipal(std::move(aKey.mLoaderPrincipal)),
        mKind(std::move(aKey.mKind)),
        mCORSMode(std::move(aKey.mCORSMode)),
        mReferrerPolicy(std::move(aKey.mReferrerPolicy)),
        mHintCharset(std::move(aKey.mHintCharset)) {
    MOZ_COUNT_CTOR(ScriptHashKey);
  }

  ScriptHashKey(ScriptLoader* aLoader,
                const JS::loader::ScriptLoadRequest* aRequest,
                mozilla::dom::ReferrerPolicy aReferrerPolicy,
                const JS::loader::ScriptFetchOptions* aFetchOptions,
                const nsCOMPtr<nsIURI> aURI);
  explicit ScriptHashKey(const ScriptLoadData& aLoadData);

  // Create a key which can be used only for lookup.
  // aKey is the result of ToStringForLookup.
  static Maybe<ScriptHashKey> FromStringsForLookup(
      const nsACString& aKey, const nsACString& aURI,
      const nsACString& aHintCharset);

 private:
  ScriptHashKey(nsIURI* aURI, nsIPrincipal* aPartitionPrincipal,
                JS::loader::ScriptKind aKind, CORSMode aCORSMode,
                mozilla::dom::ReferrerPolicy aReferrerPolicy,
                const nsString& aHintCharset)
      : PLDHashEntryHdr(),
        mURI(aURI),
        mPartitionPrincipal(aPartitionPrincipal),
        mLoaderPrincipal(nullptr),
        mKind(aKind),
        mCORSMode(aCORSMode),
        mReferrerPolicy(aReferrerPolicy),
        mHintCharset(aHintCharset) {
    MOZ_COUNT_CTOR(ScriptHashKey);
  }

 public:
  MOZ_COUNTED_DTOR(ScriptHashKey)

  const ScriptHashKey& GetKey() const { return *this; }
  const ScriptHashKey* GetKeyPointer() const { return this; }

  bool KeyEquals(const ScriptHashKey* aKey) const { return KeyEquals(*aKey); }

  bool KeyEquals(const ScriptHashKey&) const;

  static const ScriptHashKey* KeyToPointer(const ScriptHashKey& aKey) {
    return &aKey;
  }
  static PLDHashNumber HashKey(const ScriptHashKey* aKey) {
    return nsURIHashKey::HashKey(aKey->mURI);
  }

  nsIPrincipal* LoaderPrincipal() const { return mLoaderPrincipal; }
  nsIPrincipal* PartitionPrincipal() const { return mPartitionPrincipal; }

  nsIURI* URI() const { return mURI; }

  enum { ALLOW_MEMMOVE = true };

  // Stringifies this key's information for the aKey parameter for the
  // FromStringsForLookup.
  // This stringifies a subset of the fields, which cannot be directly
  // extracted from the channel.
  void ToStringForLookup(nsACString& aResult);

 protected:
  // Order the fields from the most important one as much as possible, while
  // packing them, in order to use the same order between the definition and
  // the KeyEquals implementation.

  // The script's URI.  This should distinguish the cache entry in most case.
  const nsCOMPtr<nsIURI> mURI;

  // If single content process has multiple principals, mPartitionPrincipal
  // should distinguish them.
  const nsCOMPtr<nsIPrincipal> mPartitionPrincipal;

  // NOTE: mLoaderPrincipal is only for SharedSubResourceCache logic,
  //       and not part of KeyEquals.
  const nsCOMPtr<nsIPrincipal> mLoaderPrincipal;

  // Other fields should be unique per each script in general.
  const JS::loader::ScriptKind mKind;
  const CORSMode mCORSMode;
  const mozilla::dom::ReferrerPolicy mReferrerPolicy;

  // charset attribute for classic script.
  // module always use UTF-8.
  nsString mHintCharset;
};

class ScriptLoadData final
    : public SupportsWeakPtr,
      public nsISupports,
      public SharedSubResourceCacheLoadingValueBase<ScriptLoadData> {
 protected:
  ~ScriptLoadData() = default;

 public:
  ScriptLoadData(ScriptLoader* aLoader, JS::loader::ScriptLoadRequest* aRequest,
                 CacheExpirationTime aExpirationTime,
                 JS::loader::LoadedScript* aLoadedScript);

  NS_DECL_ISUPPORTS

  // Only completed loads are used for the cache.
  bool IsLoading() const override { return false; }
  bool IsCancelled() const override { return false; }
  bool IsSyncLoad() const override { return true; }

  SubResourceNetworkMetadataHolder* GetNetworkMetadata() const override {
    return mNetworkMetadata.get();
  }

  void StartLoading() override {}
  void SetLoadCompleted() override {}
  void OnCoalescedTo(const ScriptLoadData& aExistingLoad) override {}
  void Cancel() override {}

  void DidCancelLoad() {}

  bool ShouldDefer() const { return false; }

  JS::loader::LoadedScript* ValueForCache() const {
    return mLoadedScript.get();
  }

  const CacheExpirationTime& ExpirationTime() const { return mExpirationTime; }

  ScriptLoader& Loader() { return *mLoader; }

  const ScriptHashKey& CacheKey() const { return mKey; }

 private:
  CacheExpirationTime mExpirationTime = CacheExpirationTime::Never();
  ScriptLoader* mLoader;
  ScriptHashKey mKey;
  RefPtr<JS::loader::LoadedScript> mLoadedScript;
  RefPtr<SubResourceNetworkMetadataHolder> mNetworkMetadata;
};

struct SharedScriptCacheTraits {
  using Loader = ScriptLoader;
  using Key = ScriptHashKey;
  using Value = JS::loader::LoadedScript;
  using LoadingValue = ScriptLoadData;

  static ScriptHashKey KeyFromLoadingValue(const LoadingValue& aValue) {
    return ScriptHashKey(aValue);
  }
};

class SharedScriptCache final
    : public SharedSubResourceCache<SharedScriptCacheTraits, SharedScriptCache>,
      public nsIMemoryReporter,
      public nsIObserver {
 public:
  using Base =
      SharedSubResourceCache<SharedScriptCacheTraits, SharedScriptCache>;

  NS_DECL_ISUPPORTS
  NS_DECL_NSIMEMORYREPORTER

  SharedScriptCache();
  void Init();

  NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic,
                     const char16_t* aData) override {
    if (strcmp(aTopic, "ipc:content-shutdown") == 0) {
      OnContentShutdown(aSubject);
      return NS_OK;
    }
    if (strcmp(aTopic, "profile-before-change") == 0) {
      OnProfileBeforeChange();
      return NS_OK;
    }
    return Base::DoObserve(aSubject, aTopic, aData);
  }

  bool MaybeScheduleUpdateDiskCache();
  void UpdateDiskCache();

  void EncodeAndCompress();
  void SaveToDiskCache();

  void InvalidateInProcess();

  void OnEntryInserted();
  void OnEntryEverHit();

  void UpdateEverHitTelemetry();
  static void RecvUpdateEverHitTelemetry(const uint64_t& aChildId,
                                         const uint32_t& aRate);

  // This has to be static because it's also called for loaders that don't have
  // a sheet cache (loaders that are not owned by a document).
  static void LoadCompleted(SharedScriptCache*, ScriptLoadData&);
  using Base::LoadCompleted;
  static void Clear(const Maybe<bool>& aChrome = Nothing(),
                    const Maybe<nsCOMPtr<nsIPrincipal>>& aPrincipal = Nothing(),
                    const Maybe<nsCString>& aSchemelessSite = Nothing(),
                    const Maybe<OriginAttributesPattern>& aPattern = Nothing(),
                    const Maybe<nsCString>& aURL = Nothing());

  static void Invalidate();

  static bool GetCachedScriptSource(JSContext* aCx, const nsACString& aKey,
                                    const nsACString& aURI,
                                    const nsACString& aHintCharset,
                                    JS::MutableHandle<JS::Value> aRetval);

  static void PrepareForLastCC();

 protected:
  ~SharedScriptCache();

  bool ShouldIgnoreMemoryPressure() override;

  void ClearInProcessForMemoryPressure() override;

 private:
  bool EnsureEverHitMap();
  void OnContentShutdown(nsISupports* aSubject);
  void OnProfileBeforeChange();
  void AccumulateEverHitTelemetry(uint32_t aRate);

  void SetDiskCacheTimer();
  void ClearDiskCacheTimer();
  void OnDiskCacheTimer();

  class EncodeItem {
   public:
    EncodeItem(JS::Stencil* aStencil, JS::TranscodeBuffer&& aSRI,
               JS::loader::LoadedScript* aLoadedScript)
        : mStencil(aStencil),
          mSRI(std::move(aSRI)),
          mLoadedScript(aLoadedScript) {}

    // These fields can be touched from multiple threads.
    RefPtr<JS::Stencil> mStencil;
    JS::TranscodeBuffer mSRI;
    Vector<uint8_t> mCompressed;

    // This can be dereferenced only from the main thread.
    // Reading the pointer itself is allowed also off main thread.
    RefPtr<JS::loader::LoadedScript> mLoadedScript;
  };

  // Set to true if the telemetry data is sent from the content process and
  // the preparation for the telemetry accumulation/submission is done.
  // This is set to true even if the preparation fails (mEverHitMap == nullptr),
  // to avoid retrying the preparation again and again.
  //
  // This field is used only on the parent process.
  bool mPreparedEverHitMap = false;

  // True if the disk cache timer should be rescheduled.
  // This is set to true if another activity happens during the timer
  // is set.
  bool mRetryDiskCacheTimer = false;

  // The initial value for mLastEverHitRatio, which is outside of the
  // valid range.
  static constexpr uint32_t NOT_YET_REPORTED = 101;

  // The cache-hit ratio value that's sent to the parent process.
  // Used to avoid performing unnecessary IPC when UpdateEverHitTelemetry is
  // successively called without the cache-hit ratio changed.
  uint32_t mLastEverHitRatio = NOT_YET_REPORTED;

  // The number of times each cache entry is ever hit.
  // Used with mEntryInserted to calculate the cache-hit ratio, for the
  // dom.script_memory_cache_ever_hit telemetry.
  size_t mEntryEverHit = 0;

  // The number of times a new cache entry is inserted.
  size_t mEntryInserted = 0;

  // A map from content process ID to the cache-hit ratio, in [0,100] range.
  // The parent process uses CONTENT_PROCESS_ID_MAIN as the ID.
  //
  // This is the telemetry data which is eventually be reflected to the
  // dom.script_memory_cache_ever_hit telemetry probe.
  //
  // This field is used only on the parent process.
  //
  // Content processes periodically send the data to the parent process,
  // and the parent process keeps the latest value, until the process shutdown.
  using EverHitMapType = HashMap<uint64_t, uint32_t>;
  UniquePtr<EverHitMapType> mEverHitMap;

  Mutex mEncodeMutex{"SharedScriptCache::mEncodeMutex"};
  Vector<EncodeItem> mEncodeItems MOZ_GUARDED_BY(mEncodeMutex);

  nsCOMPtr<nsITimer> mDiskCacheTimer;
};

}  // namespace dom
}  // namespace mozilla

#endif  // mozilla_dom_SharedScriptCache_h
