Skip to content

Commit

Permalink
Add ops
Browse files Browse the repository at this point in the history
  • Loading branch information
htmldoug committed Oct 13, 2021
1 parent 2d53bb0 commit 6e90bd9
Show file tree
Hide file tree
Showing 13 changed files with 1,026 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package rallyhealth.weepickle.v1

import com.rallyhealth.weepickle.v1.core._

/**
* Returns the number of elements in an array or object.
*
* Useful for benchmarks to prevent the compiler from cheating.
*/
class CardinalityVisitor extends SimpleVisitor[Any, Int] {

override def expectedMsg: String = "expected obj/arr"

override def visitObject(
length: Int
): ObjVisitor[Any, Int] = new ObjVisitor[Any, Int] {
private var i = 0

override def visitKey(): Visitor[_, _] = NoOpVisitor

override def visitKeyValue(
v: Any
): Unit = ()

override def subVisitor: Visitor[_, _] = NoOpVisitor

override def visitValue(
v: Any
): Unit = i += 1

override def visitEnd(): Int = i
}

override def visitArray(
length: Int
): ArrVisitor[Any, Int] = new ArrVisitor[Any, Int] {
private var i = 0

override def subVisitor: Visitor[_, _] = NoOpVisitor

override def visitValue(
v: Any
): Unit = i += 1

override def visitEnd(): Int = i
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package rallyhealth.weepickle.v1.core.ops

import com.rallyhealth.weepickle.v1.core.ops.ConcatFromInputs
import com.rallyhealth.weepickle.v1.core.{FromInput, Visitor}
import org.openjdk.jmh.annotations._
import rallyhealth.weepickle.v1.CardinalityVisitor

import java.util.concurrent.TimeUnit

/**
* ==Quick Run==
* bench / Jmh / run -f1 -wi 2 -i 3 .*ConcatFromInputsBench
*
* ==Profile with Flight Recorder==
* bench / Jmh / run -prof jfr -f1 .*ConcatFromInputsBench
*
* ==Jmh Visualizer Report==
* bench / Jmh / run -prof gc -rf json -rff ConcatFromInputsBench-results.json .*ConcatFromInputsBench
*
* ==Sample Results==
* bench / Jmh / run -f3 -wi 3 -i 3 .*ConcatFromInputsBench
* {{{
* Benchmark Mode Cnt Score Error Units
* ConcatFromInputsBench.arrs thrpt 9 96.627 ± 6.636 ops/s
* ConcatFromInputsBench.objs thrpt 9 72.950 ± 13.792 ops/s
* }}}
*
* @see https://github.com/ktoso/sbt-jmh
*/
@Warmup(iterations = 3, time = 5, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 3, time = 5, timeUnit = TimeUnit.SECONDS)
@State(Scope.Benchmark)
@BenchmarkMode(Array(Mode.Throughput))
@OutputTimeUnit(TimeUnit.SECONDS)
@Fork(jvmArgsAppend = Array("-Xmx350m", "-XX:+HeapDumpOnOutOfMemoryError", "-XX:-BackgroundCompilation", "-XX:-TieredCompilation"), value = 1)
class ConcatFromInputsBench {

@Benchmark
def objs: Int = ConcatFromInputs.from(manyObjs).get.transform(sink)

@Benchmark
def arrs: Int = ConcatFromInputs.from(manyArrs).get.transform(sink)

private val manyObjs = {
// not using FromScala(Obj(...)) to avoid profiler noise from singleObj.size(), etc.
val singleObj = new FromInput {
override def transform[T](
to: Visitor[_, T]
): T = {
val obj = to.visitObject(1).narrow
obj.visitKeyValue(obj.visitKey().visitString("key"))
obj.visitValue(obj.subVisitor.visitTrue())
obj.visitEnd()
}
}
Seq.fill(1000 * 1000)(singleObj)
}

private val manyArrs = {
// not using FromScala(Arr(...)) to avoid profiler noise from singleArr.size(), etc.
val singleArr = new FromInput {
override def transform[T](
to: Visitor[_, T]
): T = {
val arr = to.visitArray(1).narrow
arr.visitValue(arr.subVisitor.visitTrue())
arr.visitEnd()
}
}
Seq.fill(1000 * 1000)(singleArr)
}

private def sink = new CardinalityVisitor
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.rallyhealth.weepickle.v1.core.ops

import rallyhealth.weepickle.v1.core.ops.ConcatFromInputsBench
import utest._

object ConcatFromInputsBenchTests extends TestSuite {

override val tests = Tests {
test("objs")(new ConcatFromInputsBench().objs ==> 1000000)
test("arrs")(new ConcatFromInputsBench().arrs ==> 1000000)
}
}
17 changes: 17 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ lazy val weepickle = project
.dependsOn(
`weepickle-implicits`,
weejson,
`weepickle-laws` % Test,
)

/**
Expand Down Expand Up @@ -114,6 +115,9 @@ lazy val `weepickle-tests` = project
*/
lazy val weejson = project
.dependsOn(`weejson-jackson`)
.settings(
Test / scalacOptions := (Test / scalacOptions).value.filterNot(_ == "-deprecation"),
)

lazy val weepack = project
.dependsOn(
Expand Down Expand Up @@ -237,3 +241,16 @@ lazy val `weepickle-macro-lint-tests` = project
.settings(scalacOptions := (scalacOptions.value ++ Seq("-Xlint", "-Xfatal-warnings")).distinct)
.settings(crossScalaVersions := supportedScala2Versions) // TODO: Scala 3?
// .settings(scalacOptions += "-Ymacro-debug-lite")

lazy val `weepickle-laws` = project
.dependsOn(
`weepickle-core`,
weejson % "compile;test->test",
)
.settings(
mimaPreviousArtifacts := (if (version.value < "1.7") Set.empty else mimaPreviousArtifacts.value),
libraryDependencies ++= Seq(
"com.lihaoyi" %% "sourcecode" % "0.2.7",
"org.scalacheck" %% "scalacheck" % "1.14.3" % Test,
),
)
2 changes: 1 addition & 1 deletion project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.9.2")
addSbtPlugin("com.dwijnand" % "sbt-dynver" % "4.1.1")
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.2")
addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.7")
addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2")
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.rallyhealth.weejson.v1

import com.rallyhealth.weejson.v1._
import org.scalacheck.{Arbitrary, Gen}
import org.scalacheck.{Arbitrary, Gen, Shrink}

import scala.collection.mutable.ArrayBuffer

Expand Down Expand Up @@ -54,4 +54,12 @@ trait GenValue {
implicit val arbNum: Arbitrary[Num] = Arbitrary {
Arbitrary.arbitrary[Double].map(Num(_))
}

implicit val shrinkValue: Shrink[Value] = Shrink[Value] {
case Obj(map) => Shrink.shrink(map).map(Obj(_))
case Arr(buf) => Shrink.shrink(buf).map(Arr(_))
case Num(bd) => Shrink.shrink(bd).map(Num(_))
case Str(s) => Shrink.shrink(s).map(Str(_))
case _ => Stream.empty
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package com.rallyhealth.weepickle.v1.core.ops

import com.rallyhealth.weepickle.v1.core.Visitor.{ArrDelegate, ObjDelegate}
import com.rallyhealth.weepickle.v1.core._
import scala.collection.compat._

import scala.annotation.tailrec

object ConcatFromInputs {

/**
* Concatenates elements of one or more objects or arrays.
*
* ==Examples==
* - {{{ [1] ++ [2] ==> [1, 2] }}}
* - {{{ {"a": 0} ++ {"b": 1} ==> {"a": 0, "b": 1} }}}
* - {{{ {"a": 0} ++ {"a": 1} ==> {"a": 0, "a": 1} }}}
* - {{{ {"a": 0} ++ [false] ==> }}} [[com.rallyhealth.weepickle.v1.core.Abort]]
*
* ==Caveats==
* - Does not report object or array length. Will not work for msgpack.
*/
def apply(
head: FromInput,
tail: FromInput*
): FromInput = from(head +: tail).get

/**
* Concatenates elements of one or more objects or arrays.
*
* ==Examples==
* - {{{ [1] ++ [2] ==> [1, 2] }}}
* - {{{ {"a": 0} ++ {"b": 1} ==> {"a": 0, "b": 1} }}}
* - {{{ {"a": 0} ++ {"a": 1} ==> {"a": 0, "a": 1} }}}
* - {{{ {"a": 0} ++ [false] ==> }}} [[com.rallyhealth.weepickle.v1.core.Abort]]
*
* ==Caveats==
* - Does not report object or array length. Will not work for msgpack.
*/
def from(
inputs: Iterable[FromInput]
): Option[FromInput] = {
if (inputs.isEmpty) None
else if (inputs.sizeCompare(1) == 0) Some(inputs.head)
else {
// flatten for stack safety
val flattened =
if (inputs.exists(_.isInstanceOf[ConcatFromInputs])) {
inputs.flatMap {
case c: ConcatFromInputs => c.inputs
case other => other :: Nil
}
} else inputs
Some(new ConcatFromInputs(flattened))
}
}

private[this] class ConcatFromInputs(
val inputs: Iterable[FromInput]
) extends FromInput {

assert(inputs.sizeCompare(1) > 0)

override def transform[T](
to: Visitor[_, T]
): T = {
val it = inputs.iterator
it.next().transform {
new SimpleVisitor[Any, T] {
override def expectedMsg: String = "expected arr or obj"

override def visitObject(
length: Int
): ObjVisitor[Any, T] = {
val obj = to.visitObject(-1).narrow // final length unknown
val nonLastObj = new ObjDelegate[Any, T](obj) {
override def visitEnd() = null.asInstanceOf[T]
}

new ObjDelegate[Any, T](obj) {
override def visitEnd(): T = {
@tailrec def transformNext(): T = {
val result = it.next().transform {
new SimpleVisitor[Any, T] {
override def expectedMsg: String = "expected another obj"

override def visitObject(
length: Int
): ObjVisitor[Any, T] = {
// suppress visitEnd if more input is coming
if (it.hasNext) nonLastObj
else obj
}
}
}

if (it.hasNext) transformNext()
else result
}

transformNext()
}
}
}

override def visitArray(
length: Int
): ArrVisitor[Any, T] = {
val arr = to.visitArray(-1).narrow // final length unknown
val nonLastArr = new ArrDelegate[Any, T](arr) {
override def visitEnd() = null.asInstanceOf[T]
}

new ArrDelegate[Any, T](arr) {
override def visitEnd(): T = {
@tailrec def transformNext(): T = {
val result = it.next().transform {
new SimpleVisitor[Any, T] {
override def expectedMsg: String = "expected another arr"

override def visitArray(
length: Int
): ArrVisitor[Any, T] = {
// suppress visitEnd if more input is coming
if (it.hasNext) nonLastArr
else arr
}
}
}

if (it.hasNext) transformNext()
else result
}

transformNext()
}
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.rallyhealth.weepickle.v1.core.ops

import com.rallyhealth.weepickle.v1.core.FromInput

/**
* Mixes extensions into bundles, e.g. WeePickle.
*/
trait FromInputOpsImplicits {

implicit def asFromInputOps(a: FromInput): FromInputOps = new FromInputOps(a)
}

/**
* Extension operations on [[FromInput]] that are part of the core API.
*
* More methods may be added here without breaking binary compatibility (which
* is not possible with traits).
*/
class FromInputOps(
val a: FromInput
) extends AnyVal {

/**
* Concatenates elements of one or more objects or arrays.
*
* ==Examples==
* - {{{ [1] ++ [2] ==> [1, 2] }}}
* - {{{ {"a": 0} ++ {"b": 1} ==> {"a": 0, "b": 1} }}}
* - {{{ {"a": 0} ++ {"a": 1} ==> {"a": 0, "a": 1} }}}
* - {{{ {"a": 0} ++ [false] ==> }}} [[com.rallyhealth.weepickle.v1.core.Abort]]
*
* ==Caveats==
* - Does not report object or array length. Will not work for msgpack.
*/
def ++(
b: FromInput
): FromInput = ConcatFromInputs(a, b)
}
Loading

0 comments on commit 6e90bd9

Please sign in to comment.