Skip to content

Commit

Permalink
Added a small Cache abstraction and replaced TrieMap-based cache …
Browse files Browse the repository at this point in the history
…implementation with `ConcurrentHashMap`. This change introduces potential minor performance improvements and compatibility with [GraalVM](https://www.graalvm.org/) `native-image`.
  • Loading branch information
OlegIlyenko committed May 11, 2018
1 parent f432c24 commit 6179740
Show file tree
Hide file tree
Showing 15 changed files with 254 additions and 64 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Upcoming

* Added a small `Cache` abstraction and replaced `TrieMap`-based cache implementation with `ConcurrentHashMap`. This change introduces
potential minor performance improvements and compatibility with [GraalVM](https://www.graalvm.org/) `native-image`.
* Fixed new line tracking when parsing block-strings
* Ast schema builder now considers `additionalTypes` when it validates the type extensions
* `KnownDirectives` validation now correctly handles type and schema extensions
Expand Down
9 changes: 4 additions & 5 deletions src/main/scala/sangria/execution/FieldCollector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ import sangria.ast.OperationType
import sangria.parser.SourceMapper
import sangria.schema._
import sangria.ast
import sangria.util.Cache

import scala.collection.concurrent.TrieMap
import scala.collection.mutable.{Set MutableSet, Map MutableMap, ArrayBuffer}

import scala.util.{Try, Failure, Success}
import scala.collection.mutable.{ArrayBuffer, Map => MutableMap, Set => MutableSet}
import scala.util.{Failure, Success, Try}

class FieldCollector[Ctx, Val](
schema: Schema[Ctx, Val],
Expand All @@ -18,7 +17,7 @@ class FieldCollector[Ctx, Val](
valueCollector: ValueCollector[Ctx, _],
exceptionHandler: ExceptionHandler) {

private val resultCache = TrieMap[(ExecutionPath.PathCacheKey, String), Try[CollectedFields]]()
private val resultCache = Cache.empty[(ExecutionPath.PathCacheKey, String), Try[CollectedFields]]

def collectFields(path: ExecutionPath, tpe: ObjectType[Ctx, _], selections: Vector[ast.SelectionContainer]): Try[CollectedFields] =
resultCache.getOrElseUpdate(path.cacheKey tpe.name, {
Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/sangria/execution/ValueCoercionHelper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import sangria.marshalling.{InputUnmarshaller, RawResultMarshaller, ResultMarsha
import sangria.parser.SourceMapper
import sangria.renderer.{QueryRenderer, SchemaRenderer}
import sangria.schema._
import sangria.util.Cache
import sangria.validation._

import scala.collection.concurrent.TrieMap
import scala.collection.immutable.VectorBuilder

class ValueCoercionHelper[Ctx](sourceMapper: Option[SourceMapper] = None, deprecationTracker: DeprecationTracker = DeprecationTracker.empty, userContext: Option[Ctx] = None) {
Expand Down Expand Up @@ -38,7 +38,7 @@ class ValueCoercionHelper[Ctx](sourceMapper: Option[SourceMapper] = None, deprec
fromScalarMiddleware: Option[(Any, InputType[_]) Option[Either[Violation, Any]]],
allowErrorsOnDefault: Boolean = false,
valueMap: Nothing Any = defaultValueMapFn,
defaultValueInfo: Option[TrieMap[String, Any]] = None,
defaultValueInfo: Option[Cache[String, Any]] = None,
undefinedValues: Option[VectorBuilder[String]] = None
)(
acc: marshaller.MapBuilder,
Expand Down
9 changes: 5 additions & 4 deletions src/main/scala/sangria/execution/ValueCollector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ import sangria.marshalling._
import sangria.parser.SourceMapper
import sangria.renderer.QueryRenderer
import sangria.schema._
import sangria.util.Cache
import sangria.validation._
import scala.collection.concurrent.TrieMap

import scala.collection.immutable.VectorBuilder
import scala.util.{Failure, Success, Try}

class ValueCollector[Ctx, Input](schema: Schema[_, _], inputVars: Input, sourceMapper: Option[SourceMapper], deprecationTracker: DeprecationTracker, userContext: Ctx, exceptionHandler: ExceptionHandler, fromScalarMiddleware: Option[(Any, InputType[_]) Option[Either[Violation, Any]]], ignoreErrors: Boolean)(implicit um: InputUnmarshaller[Input]) {
val coercionHelper = new ValueCoercionHelper[Ctx](sourceMapper, deprecationTracker, Some(userContext))

private val argumentCache = TrieMap[(ExecutionPath.PathCacheKey, Vector[ast.Argument]), Try[Args]]()
private val argumentCache = Cache.empty[(ExecutionPath.PathCacheKey, Vector[ast.Argument]), Try[Args]]

def getVariableValues(definitions: Vector[ast.VariableDefinition], fromScalarMiddleware: Option[(Any, InputType[_]) Option[Either[Violation, Any]]]): Try[Map[String, VariableValue]] =
if (!um.isMapNode(inputVars))
Expand Down Expand Up @@ -72,7 +73,7 @@ object ValueCollector {
val astArgMap = argumentAsts groupBy (_.name) mapValues (_.head)
val marshaller = CoercedScalaResultMarshaller.default
val errors = new VectorBuilder[Violation]
val defaultInfo = Some(TrieMap.empty[String, Any])
val defaultInfo = Some(Cache.empty[String, Any])
val undefinedArgs = Some(new VectorBuilder[String])

val res = argumentDefs.foldLeft(marshaller.emptyMapNode(argumentDefs.map(_.name)): marshaller.MapBuilder) {
Expand Down Expand Up @@ -107,7 +108,7 @@ object ValueCollector {
}

case class VariableValue(fn: (ResultMarshaller, ResultMarshaller, InputType[_]) Either[Vector[Violation], Trinary[ResultMarshaller#Node]]) {
private val cache = TrieMap[(Int, Int), Either[Vector[Violation], Trinary[ResultMarshaller#Node]]]()
private val cache = Cache.empty[(Int, Int), Either[Vector[Violation], Trinary[ResultMarshaller#Node]]]

def resolve(marshaller: ResultMarshaller, firstKindMarshaller: ResultMarshaller, actualType: InputType[_]): Either[Vector[Violation], Trinary[firstKindMarshaller.Node]] =
cache.getOrElseUpdate(System.identityHashCode(firstKindMarshaller) System.identityHashCode(actualType.namedType),
Expand Down
14 changes: 7 additions & 7 deletions src/main/scala/sangria/execution/deferred/FetcherCache.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package sangria.execution.deferred

import scala.collection.concurrent.TrieMap
import sangria.util.Cache

trait FetcherCache {
def cacheKey(id: Any): Any
Expand All @@ -26,8 +26,8 @@ object FetcherCache {
}

class SimpleFetcherCache extends FetcherCache {
private val cache = TrieMap[Any, Any]()
private val relCache = TrieMap[Any, Seq[Any]]()
private val cache = Cache.empty[Any, Any]
private val relCache = Cache.empty[Any, Seq[Any]]

def cacheKey(id: Any) = id
def cacheKeyRel(rel: Any, relId: Any) = rel relId
Expand All @@ -43,7 +43,7 @@ class SimpleFetcherCache extends FetcherCache {
cache.update(cacheKey(id), value)
}

def updateRel[T](rel: Any, relId: Any, idFn: (T) Any, values: Seq[T]) = {
def updateRel[T](rel: Any, relId: Any, idFn: T Any, values: Seq[T]) = {
if (cacheableRel(rel, relId)) {
values.foreach { v
update(idFn(v), v)
Expand All @@ -62,9 +62,9 @@ class SimpleFetcherCache extends FetcherCache {
cache.remove(id)

override def clearRel(rel: Any) =
relCache.keys.toVector.foreach {
case key @ (_, _) relCache.remove(key)
case _ // do nothing
relCache.removeKeys {
case key @ (r, _) if r == rel true
case _ false
}

override def clearRelId(rel: Any, relId: Any) =
Expand Down
15 changes: 8 additions & 7 deletions src/main/scala/sangria/schema/AstSchemaMaterializer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@ import sangria.ast.{AstLocation, OperationType, TypeDefinition}
import sangria.execution.MaterializedSchemaValidationError
import sangria.parser.SourceMapper
import sangria.renderer.QueryRenderer
import sangria.util.Cache
import sangria.validation._

import scala.collection.concurrent.TrieMap
import scala.reflect.ClassTag
import scala.collection.Set

class AstSchemaMaterializer[Ctx] private (val document: ast.Document, builder: AstSchemaBuilder[Ctx]) {
import AstSchemaMaterializer.extractSchemaInfo

private val sdlOrigin = SDLOrigin(document)

private val typeDefCache = TrieMap[(MatOrigin, String), Type with Named]()
private val scalarAliasCache = TrieMap[ScalarAlias[_, _], ScalarAlias[_, _]]()
private val typeDefCache = Cache.empty[(MatOrigin, String), Type with Named]
private val scalarAliasCache = Cache.empty[ScalarAlias[_, _], ScalarAlias[_, _]]

private lazy val typeDefs: Vector[ast.TypeDefinition] = document.definitions.collect {
case d: ast.TypeDefinition d
Expand Down Expand Up @@ -236,7 +237,7 @@ class AstSchemaMaterializer[Ctx] private (val document: ast.Document, builder: A
def findUnusedTypes(): (Set[String], Vector[Type with Named]) = {
resolveAllLazyFields()

val referenced = typeDefCache.map(_._2.name).toSet
val referenced = typeDefCache.mapToSet((_, v) v.name)
val notReferenced = typeDefs.filterNot(tpe Schema.isBuiltInType(tpe.name) || referenced.contains(tpe.name))
val notReferencedAdd = builder.additionalTypes.filterNot(tpe Schema.isBuiltInType(tpe.name) || referenced.contains(tpe.name))

Expand All @@ -263,7 +264,7 @@ class AstSchemaMaterializer[Ctx] private (val document: ast.Document, builder: A
prevCount = typeDefCache.size
iteration += 1

typeDefCache.values.foreach {
typeDefCache.forEachValue {
case o: ObjectLikeType[_, _] o.fields
case o: InputObjectType[_] o.fields
case _ // do nothing
Expand Down Expand Up @@ -362,9 +363,9 @@ class AstSchemaMaterializer[Ctx] private (val document: ast.Document, builder: A
val resolved = builder.resolveNameConflict(
origin,
allCandidates ++
typeDefCache.find(_._2.name == typeName).map{case ((o, _), v) BuiltMaterializedTypeInst(o, v)}.toVector)
typeDefCache.find((_, v) v.name == typeName).map{case ((o, _), v) BuiltMaterializedTypeInst(o, v)}.toVector)

if (!resolved.isInstanceOf[BuiltMaterializedTypeInst] && typeDefCache.keySet.exists(_._2 == resolved.name))
if (!resolved.isInstanceOf[BuiltMaterializedTypeInst] && typeDefCache.keyExists(_._2 == resolved.name))
throw SchemaMaterializationException("Name conflict resolution produced already existing type name")
else
getNamedType(origin, resolved)
Expand Down
6 changes: 3 additions & 3 deletions src/main/scala/sangria/schema/Context.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import sangria.parser.SourceMapper
import sangria.{ast, introspection}
import sangria.execution.deferred.Deferred
import sangria.streaming.SubscriptionStream
import sangria.util.Cache

import scala.collection.concurrent.TrieMap
import scala.concurrent.{ExecutionContext, Future}
import scala.reflect.ClassTag
import scala.util.{Failure, Try}
Expand Down Expand Up @@ -296,7 +296,7 @@ case class Context[Ctx, Val](
}
}

case class Args(raw: Map[String, Any], argsWithDefault: Set[String], optionalArgs: Set[String], undefinedArgs: Set[String], defaultInfo: TrieMap[String, Any]) {
case class Args(raw: Map[String, Any], argsWithDefault: Set[String], optionalArgs: Set[String], undefinedArgs: Set[String], defaultInfo: Cache[String, Any]) {
private def getAsOptional[T](name: String): Option[T] =
raw.get(name).asInstanceOf[Option[Option[T]]].flatten

Expand Down Expand Up @@ -348,7 +348,7 @@ case class Args(raw: Map[String, Any], argsWithDefault: Set[String], optionalArg
}

object Args {
val empty = new Args(Map.empty, Set.empty, Set.empty, Set.empty, TrieMap.empty)
val empty = new Args(Map.empty, Set.empty, Set.empty, Set.empty, Cache.empty)

def apply(definitions: List[Argument[_]], values: (String, Any)*): Args =
apply(definitions, values.toMap)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import sangria.introspection._
import sangria.marshalling._
import sangria.parser.DeliveryScheme.Throw
import sangria.renderer.SchemaRenderer
import sangria.util.Cache

import scala.collection.concurrent.TrieMap
import scala.util.{Failure, Success}

class IntrospectionSchemaMaterializer[Ctx, T : InputUnmarshaller](introspectionResult: T, builder: IntrospectionSchemaBuilder[Ctx]) {
private val typeDefCache = TrieMap[String, Type with Named]()
private val typeDefCache = Cache.empty[String, Type with Named]

private lazy val schemaDef = IntrospectionParser.parse(introspectionResult)

Expand All @@ -27,13 +27,13 @@ class IntrospectionSchemaMaterializer[Ctx, T : InputUnmarshaller](introspectionR

def findUnusedTypes(allTypes: Seq[IntrospectionType]): List[Type with Named] = {
// first init all lazy fields. TODO: think about better solution
typeDefCache.values.foreach {
typeDefCache.forEachValue {
case o: ObjectLikeType[_, _] o.fields
case o: InputObjectType[_] o.fields
case _ // do nothing
}

val referenced = typeDefCache.keySet
val referenced = typeDefCache
val notReferenced = allTypes.filterNot(tpe Schema.isBuiltInType(tpe.name) || referenced.contains(tpe.name))

notReferenced.toList map (tpe getNamedType(tpe.name))
Expand Down
38 changes: 38 additions & 0 deletions src/main/scala/sangria/util/Cache.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package sangria.util

import scala.collection.{Map, Set}

trait Cache[Key, Value] {
def size: Int

def contains(key: Key): Boolean
def apply(key: Key): Value
def get(key: Key): Option[Value]
def getOrElse(key: Key, default: Value): Value
def update(key: Key, value: Value): Unit
def remove(key: Key): Unit
def clear(): Unit

// NOTE: that `getOrElseUpdate` allows a race condition between value retrieval and cache update.
// It is an explicit decision to avoid any kind of synchronization (it is preferred to recompute value multiple times than to synchronize)
def getOrElseUpdate(key: Key, fn: Value): Value
def find(fn: (Key, Value) Boolean): Option[(Key, Value)]
def mapToSet[R](fn: (Key, Value) R): Set[R]
def mapValues[R](fn: Value R): Map[Key, R]
def keyExists(fn: Key Boolean): Boolean
def forEachValue(fn: Value Unit): Unit
def removeKeys(fn: Key Boolean): Unit
}

object Cache {
def empty[Key, Value]: Cache[Key, Value] = emptyConcurrentHashMap[Key, Value]

def emptyTrieMap[Key, Value] = new TrieMapCache[Key, Value]
def emptyConcurrentHashMap[Key, Value] = new ConcurrentHashMapCache[Key, Value]

def apply[Key, Value](elems: (Key, Value)*) = {
val c = empty[Key, Value]
elems.foreach {case (key, value) c(key) = value}
c
}
}
117 changes: 117 additions & 0 deletions src/main/scala/sangria/util/ConcurrentHashMapCache.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package sangria.util

import java.util.concurrent.ConcurrentHashMap

class ConcurrentHashMapCache[Key, Value] extends Cache[Key, Value] {
private val cache = new ConcurrentHashMap[Key, Value]()

def size = cache.size

def contains(key: Key) = cache.containsKey(key)
def apply(key: Key) =
cache.get(key) match {
case null throw new NoSuchElementException
case v v
}

def get(key: Key) = Option(cache.get(key))
def getOrElse(key: Key, default: Value) = cache.get(key) match {
case null default
case v v
}

def update(key: Key, value: Value) = cache.put(key, value)
def remove(key: Key) = cache.remove(key)
def clear() = cache.clear()

def getOrElseUpdate(key: Key, fn: Value) = cache.get(key) match {
case null
val res = fn
cache.put(key, res)
res

case v v
}

def find(fn: (Key, Value) Boolean) = {
val it = cache.entrySet().iterator()
var res: Option[(Key, Value)] = None

while (it.hasNext && res.isEmpty) {
val elem = it.next()

if (fn(elem.getKey, elem.getValue))
res = Some(elem.getKey elem.getValue)
}

res
}

def mapToSet[R](fn: (Key, Value) R) = {
val it = cache.entrySet().iterator()
val res = scala.collection.mutable.Set[R]()

while (it.hasNext) {
val elem = it.next()

res += fn(elem.getKey, elem.getValue)
}

res
}

def mapValues[R](fn: Value R) = {
val it = cache.entrySet().iterator()
val res = scala.collection.mutable.Map[Key, R]()

while (it.hasNext) {
val elem = it.next()

res(elem.getKey) = fn(elem.getValue)
}

res
}

def keyExists(fn: Key Boolean): Boolean = {
val it = cache.entrySet().iterator()

while (it.hasNext) {
val elem = it.next()

if (fn(elem.getKey)) return true
}

false
}

def forEachValue(fn: Value Unit) = {
val it = cache.values().iterator()

while (it.hasNext) {
val elem = it.next()

fn(elem)
}
}

def removeKeys(fn: Key Boolean) = {
val it = cache.keySet().iterator()

while (it.hasNext) {
val elem = it.next()

if (fn(elem)) it.remove()
}
}

def canEqual(other: Any): Boolean = other.isInstanceOf[ConcurrentHashMapCache[_, _]]

override def equals(other: Any): Boolean = other match {
case that: ConcurrentHashMapCache[_, _] (that canEqual this) && cache == that.cache
case _ false
}

override def hashCode(): Int =
31 * cache.hashCode()
}
Loading

0 comments on commit 6179740

Please sign in to comment.