<commit_before>package com.programmaticallyspeaking.ncd.nashorn

import java.util.concurrent.LinkedBlockingQueue

import com.programmaticallyspeaking.ncd.host._
import com.programmaticallyspeaking.ncd.infra.{IdGenerator, PathUtils, ScriptURL}

import scala.collection.concurrent.TrieMap

class Scripts {
  import scala.collection.JavaConverters._
  private val scriptIdGenerator = new IdGenerator("ndx") // special prefix for replacement scripts

  private val _scripts = TrieMap[ScriptURL, Script]()
  private val _anonScripts = new LinkedBlockingQueue[Script]()

  def scripts: Seq[Script] = _scripts.values.groupBy(_.id).flatMap(_._2.headOption).toSeq ++ _anonScripts.asScala

  private def isAnonymousScript(s: Script) = s.url.toString == ""

  def suggest(script: Script): Script = {
    require(byId(ScriptIdentity.fromId(script.id)).isEmpty, s"Script with ID ${script.id} has already been added")

    if (isAnonymousScript(script)) {
      _anonScripts.add(script)
      return script
    }

    // For a recompilation, we will (most likely) already have the original script that was recompiled (recompilation
    // happens for example when a function inside the eval script is called with known types). We find the original
    // script by comparing contents hashes. If we find the original script, we just discard the new one and use the
    // original.
    _scripts.values.find(_.contentsHash() == script.contentsHash()) match {
      case Some(scriptWithSameSource) =>
        // Note that we add a map entry for the original script with the new URL as key. This way we'll find our
        // reused script using all its "alias URLs".
        // Note 2: I worry that comparing contents hashes isn't enough - that we need to verify no overlapping
        // line locations also. But we don't have locations here, and I don't want to do too much defensive coding.
        _scripts += script.url -> scriptWithSameSource
        scriptWithSameSource
      case None =>
        _scripts.get(script.url) match {
          case Some(_) =>
            // This is a new script with new contents but with the same URL as an old one - it is likely a script
            // that has been reloaded via Nashorn's 'load' function.
            // The choice of using the ID as suffix is pretty arbitrary - it could also be a sequence number.
            val newId = scriptIdGenerator.next
            //TODO: Don't use PathUtils here, it's URL now
            val newURLString = PathUtils.insertNameSuffix(script.url.toString, "_" + newId)
            val newURL = ScriptURL.create(newURLString)

            val replacement = ScriptImpl.fromSource(newURL, script.contents, newId)
            _scripts += newURL -> replacement
            replacement

          case None =>
            _scripts += script.url -> script
            script
        }
    }
  }

  def byId(id: ScriptIdentity): Option[Script] = {
    id match {
      case u: URLBasedScriptIdentity => _scripts.get(u.scriptURL)
      case other => scripts.find(other.matchesScript)
    }
  }
}<commit_msg>Scripts: Track scripts by ID also for faster lookup<commit_after>package com.programmaticallyspeaking.ncd.nashorn

import java.util.concurrent.LinkedBlockingQueue

import com.programmaticallyspeaking.ncd.host._
import com.programmaticallyspeaking.ncd.infra.{IdGenerator, PathUtils, ScriptURL}

import scala.collection.concurrent.TrieMap

class Scripts {
  private val scriptIdGenerator = new IdGenerator("ndx") // special prefix for replacement scripts

  private val _scriptByUrl = TrieMap[ScriptURL, Script]()
  private val _scriptById = TrieMap[String, Script]()
  private val _anonScripts = new LinkedBlockingQueue[Script]()

  def scripts: Seq[Script] = _scriptById.values.toSeq

  private def isAnonymousScript(s: Script) = s.url.toString == ""

  def suggest(script: Script): Script = {
    require(byId(ScriptIdentity.fromId(script.id)).isEmpty, s"Script with ID ${script.id} has already been added")

    // No URL checking for an anonymous script
    if (isAnonymousScript(script)) {
      _scriptById += script.id -> script
      return script
    }

    // For a recompilation, we will (most likely) already have the original script that was recompiled (recompilation
    // happens for example when a function inside the eval script is called with known types). We find the original
    // script by comparing contents hashes. If we find the original script, we just discard the new one and use the
    // original.
    _scriptByUrl.values.find(_.contentsHash() == script.contentsHash()) match {
      case Some(scriptWithSameSource) =>
        // Note that we add a map entry for the original script with the new URL as key. This way we'll find our
        // reused script using all its "alias URLs".
        // Note 2: I worry that comparing contents hashes isn't enough - that we need to verify no overlapping
        // line locations also. But we don't have locations here, and I don't want to do too much defensive coding.
        _scriptByUrl += script.url -> scriptWithSameSource
        // Don't add an entry to _scriptById since we ignore the new script
        scriptWithSameSource
      case None =>
        _scriptByUrl.get(script.url) match {
          case Some(_) =>
            // This is a new script with new contents but with the same URL as an old one - it is likely a script
            // that has been reloaded via Nashorn's 'load' function.
            // The choice of using the ID as suffix is pretty arbitrary - it could also be a sequence number.
            val newId = scriptIdGenerator.next
            //TODO: Don't use PathUtils here, it's URL now
            val newURLString = PathUtils.insertNameSuffix(script.url.toString, "_" + newId)
            val newURL = ScriptURL.create(newURLString)

            val replacement = ScriptImpl.fromSource(newURL, script.contents, newId)
            _scriptByUrl += newURL -> replacement
            _scriptById += newId -> replacement
            replacement

          case None =>
            _scriptByUrl += script.url -> script
            _scriptById += script.id -> script
            script
        }
    }
  }

  def byId(id: ScriptIdentity): Option[Script] = {
    id match {
      case i: IdBasedScriptIdentity => _scriptById.get(i.id)
      case u: URLBasedScriptIdentity => _scriptByUrl.get(u.scriptURL)
      case other => scripts.find(other.matchesScript)
    }
  }
}
{'fraction_non_alphanumeric': 0.08214285714285714, 'fraction_numerical': 0.0004658385093167702, 'mean_word_length': 2.8615107913669067, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 25, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1464576', 'n_tokens_mistral': 1746, 'n_tokens_neox': 1685, 'n_words': 629}