Skip to content

Commit

Permalink
add some benchmarks for odbv1 memory consumption (#11)
Browse files Browse the repository at this point in the history
This is more odbv1 code, and a template for future odbv2 benchmarks.

Co-authored-by: Michael Pollmeier <michael@michaelpollmeier.com>
  • Loading branch information
bbrehm and mpollmeier committed Feb 20, 2023
1 parent 6374f04 commit eefa3fd
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 3 deletions.
4 changes: 4 additions & 0 deletions .gitignore
@@ -0,0 +1,4 @@
/target/
/project/target/
/project/project/
/benchJoern/target/
8 changes: 8 additions & 0 deletions README.md
Expand Up @@ -112,3 +112,11 @@ Via implicits, an external property can be made to look like a "real" property o
on the same node races against itself) with only O(1) total synchronization events, as opposed to hashmaps that require
synchronization of some kind on every access (typically via atomics).

# Odbv1 / Joern benchmarks.
After `sbt clean compile stage` you can run an analogue of `./benchJoern/target/universal/stage/bin/bench-joern -J-Xmx20G -Djdk.attach.allowAttachSelf ./cpg.bin`
to generate some benchmarks (timing and memory consumption). This relies on `jcmd`, so make sure you have the full jdk installed, not
just the jre. For this you need an example graph that you can conveniently generate with joern and take from the workspace
(don't forget to save). Since this uses joern domain classes, it is incompatible with ocular/codescience graphs. It is also
incompatible with the legacy proto format (just load in joern and save).

Feel free to add more benchmarks or prettify the output!
9 changes: 9 additions & 0 deletions benchJoern/build.sbt
@@ -0,0 +1,9 @@
name := "bench-joern"
enablePlugins(JavaAppPackaging)

libraryDependencies ++= Seq(
"io.joern" %% "semanticcpg" % "1.1.1455",
"com.jerolba" % "jmnemohistosyne" % "0.2.3",
"org.openjdk.jol" % "jol-core" % "0.16",
"org.slf4j" % "slf4j-simple" % "2.0.6" % Optional,
)
122 changes: 122 additions & 0 deletions benchJoern/src/main/scala/io/joern/joernBench/bench.scala
@@ -0,0 +1,122 @@
package io.joern.joernBench
import better.files.Dsl.cp
import com.jerolba.jmnemohistosyne.{HistogramEntry, Histogramer, MemoryHistogram}
import io.shiftleft.codepropertygraph.cpgloading.{CpgLoader, CpgLoaderConfig}
import io.shiftleft.codepropertygraph.generated.Cpg
import overflowdb.Config

import scala.jdk.CollectionConverters.IteratorHasAsScala
import scala.util.{Success, Try}

object Bench {

class MeasurementBox {
var timeNanos = -1L
// the @volatile is really just to disincentivize the jvm from clearing up writes to the result
@volatile var result: Any = null
@volatile var histo: MemoryHistogram = null

def clear(): Unit = {
result = null
}
}

def measure(code: => Any): MeasurementBox = {
val box = new MeasurementBox
box.histo = Histogramer.getDiff(() => {
val tic = System.nanoTime()
box.result = code
val toc = System.nanoTime()
box.timeNanos = toc - tic
})
box
}

def loadCopyFile(filename: String): Cpg = {
val newLoc = better.files.File(filename + ".tmp")
cp(better.files.File(filename), newLoc)
val odbConfig = Config.withDefaults.withStorageLocation(newLoc.toString())
val config = CpgLoaderConfig.withDefaults.doNotCreateIndexesOnLoad.withOverflowConfig(odbConfig)
CpgLoader.loadFromOverflowDb(config)
}

def makeIndices(cpg: Cpg): Unit = {
CpgLoader.createIndexes(cpg)
}

def touchGraph(cpg: Cpg): Int = {
cpg.graph.edgeCount()
}

def modHisto(histo: MemoryHistogram, top: Int, nodecount: Int): (Long, String) = {
val lst = histo
.iterator()
.asScala
.toBuffer
.sortBy { e: HistogramEntry => -scala.math.abs(e.getSize) }
.take(if (top > 0) top else java.lang.Integer.MAX_VALUE)
.filter { _.getInstances != 0 }
val total = lst.iterator.map { e => e.getSize }.sum
val str = lst.iterator
.map { e =>
s"${e.getClassName}, ${e.getInstances}, ${e.getSize}, ${e.getInstances * 1.0 / nodecount}, ${e.getSize * 1.0 / e.getInstances}, ${e.getSize * 1.0 / nodecount}"
}
.mkString("Class, #instances, total size, instances/node, bytes/instance, bytes/node\n", "\n", "")
(total, str)
}

def printHisto(sect: String, box: MeasurementBox, nodecount: Int): Unit = {
println(
s"\n<=========\nRunning ${sect} in ${box.timeNanos} ns == ${box.timeNanos * 1e-6} ms == ${box.timeNanos * 1e-3 / nodecount} us/node and costing ${box.histo.getTotalMemory} bytes == ${box.histo.getTotalMemory * 1.0 / (1 << 20)} MB == ${box.histo.getTotalMemory * 1.0 / nodecount} bytes/node."
)
val top20 = modHisto(box.histo, 20, nodecount)
println(s"Top 20 account for ${top20._1 * 100.0 / box.histo.getTotalMemory}%:")
println(top20._2)
println("=========>\n\n")
}

def main(args: Array[String]): Unit = {
println(
s"VM is version ${System.getProperty("java.runtime.version")} with max heap ${java.lang.Runtime.getRuntime.maxMemory >> 20} mb.\n\n"
)
val box = new MeasurementBox
box.histo = new Histogramer().createHistogram()
val cpgBox = measure { loadCopyFile(args(0)) }
val nodecount = cpgBox.result.asInstanceOf[Cpg].graph.nodeCount()
val callcount = cpgBox.result.asInstanceOf[Cpg].graph.nodeCount("CALL")
val indexify = measure { makeIndices(cpgBox.result.asInstanceOf[Cpg]) }
val touch1 = measure { touchGraph(cpgBox.result.asInstanceOf[Cpg]) }
val touch2 = measure { touchGraph(cpgBox.result.asInstanceOf[Cpg]) }
val close = measure { cpgBox.result.asInstanceOf[Cpg].close() }
println(s"Graph with ${nodecount} nodes (${callcount} calls) and ${touch1.result} edges at ${args(0)}")
val histoAfter = new Histogramer().createHistogram()
box.histo = histoAfter.diff(box.histo)
val free = measure { cpgBox.result = null }
box.timeNanos = cpgBox.timeNanos + indexify.timeNanos + touch1.timeNanos + touch2.timeNanos + free.timeNanos
printHisto("complete benchmark", box, nodecount)
printHisto("copy and load cpg file", cpgBox, nodecount)
printHisto("load/create indexes", indexify, nodecount)
printHisto("count edges (force complete loading)", touch1, nodecount)
printHisto("count edges again", touch2, nodecount)
printHisto("close graph", close, nodecount)
printHisto("free memory", free, nodecount)

println(s"VM details according to JOL: ${org.openjdk.jol.vm.VM.current().details()}. Layout of top consumers: \n\n")
println(
box.histo
.iterator()
.asScala
.toBuffer
.sortBy { e: HistogramEntry => -scala.math.abs(e.getSize) }
.take(20)
.flatMap { e => Try(Class.forName(e.getClassName)) match { case Success(v) => Some(v); case _ => None } }
.iterator
.map { c =>
org.openjdk.jol.info.ClassLayout.parseClass(c).toPrintable
}
.mkString("\n\n")
)

}

}
3 changes: 2 additions & 1 deletion build.sbt
Expand Up @@ -5,9 +5,10 @@ ThisBuild / crossScalaVersions := Seq("2.13.8", "3.1.2")
publish / skip := true

lazy val core = project.in(file("core"))
lazy val benchJoern = project.in(file("benchJoern"))

ThisBuild / libraryDependencies ++= Seq(
"org.slf4j" % "slf4j-simple" % "1.7.36" % Test,
"org.slf4j" % "slf4j-simple" % "2.0.6" % Test,
"org.scalatest" %% "scalatest" % "3.2.12" % Test,
)

Expand Down
4 changes: 2 additions & 2 deletions project/plugins.sbt
@@ -1,2 +1,2 @@
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6")

addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6")
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.8.1")

0 comments on commit eefa3fd

Please sign in to comment.