From 3ca5064a72179290521d72d695a67b32e9bf3439 Mon Sep 17 00:00:00 2001 From: Diego Date: Tue, 11 Sep 2018 21:22:42 -0300 Subject: Improvements in ThreadLocalStorage --- build.sbt | 13 +++- .../kamon/bench/ThreadLocalStorageBenchmark.scala | 81 ++++++++++++++++++++++ .../kamon/context/ThreadLocalStorageSpec.scala | 1 - .../src/main/scala/kamon/context/Storage.scala | 33 +++++++-- project/plugins.sbt | 1 + 5 files changed, 120 insertions(+), 9 deletions(-) create mode 100644 kamon-core-bench/src/main/scala/kamon/bench/ThreadLocalStorageBenchmark.scala diff --git a/build.sbt b/build.sbt index 3d77510f..d553d3ed 100644 --- a/build.sbt +++ b/build.sbt @@ -17,7 +17,7 @@ lazy val kamon = (project in file(".")) .settings(moduleName := "kamon") .settings(noPublishing: _*) - .aggregate(core, testkit, coreTests) + .aggregate(core, testkit, coreTests, coreBench) val commonSettings = Seq( scalaVersion := "2.12.4", @@ -74,3 +74,14 @@ lazy val coreTests = (project in file("kamon-core-tests")) "ch.qos.logback" % "logback-classic" % "1.2.2" % "test" ) ).dependsOn(testkit) + + +lazy val coreBench = (project in file("kamon-core-bench")) + .enablePlugins(JmhPlugin) + .settings( + moduleName := "kamon-core-bench", + resolvers += Resolver.mavenLocal, + fork in Test := true) + .settings(noPublishing: _*) + .settings(commonSettings: _*) + .dependsOn(core) \ No newline at end of file diff --git a/kamon-core-bench/src/main/scala/kamon/bench/ThreadLocalStorageBenchmark.scala b/kamon-core-bench/src/main/scala/kamon/bench/ThreadLocalStorageBenchmark.scala new file mode 100644 index 00000000..96588798 --- /dev/null +++ b/kamon-core-bench/src/main/scala/kamon/bench/ThreadLocalStorageBenchmark.scala @@ -0,0 +1,81 @@ +/* ========================================================================================= + * Copyright © 2013-2018 the kamon project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + * ========================================================================================= + */ + + +package kamon.bench + +import java.util.concurrent.TimeUnit + +import kamon.context.Storage.Scope +import kamon.context.{Context, Key, Storage} +import org.openjdk.jmh.annotations._ + +@State(Scope.Benchmark ) +class ThreadLocalStorageBenchmark { + + val TestKey: Key[Int] = Key.local("test-key", 0) + val ScopeWithKey: Context = Context.create().withKey(TestKey, 43) + + val TLS: Storage = new OldThreadLocal + val FTLS: Storage = new Storage.ThreadLocal + + + @Benchmark + @BenchmarkMode(Array(Mode.AverageTime)) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Fork + def currentThreadLocal: Context = { + val scope = TLS.store(ScopeWithKey) + TLS.current() + scope.close() + TLS.current() + } + + @Benchmark + @BenchmarkMode(Array(Mode.AverageTime)) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Fork + def fastThreadLocal: Context = { + val scope = FTLS.store(ScopeWithKey) + FTLS.current() + scope.close() + FTLS.current() + } +} + + +class OldThreadLocal extends Storage { + private val tls = new java.lang.ThreadLocal[Context]() { + override def initialValue(): Context = Context.Empty + } + + override def current(): Context = + tls.get() + + override def store(context: Context): Scope = { + val newContext = context + val previousContext = tls.get() + tls.set(newContext) + + new Scope { + override def context: Context = newContext + override def close(): Unit = tls.set(previousContext) + } + } +} + +object OldThreadLocal { + def apply(): OldThreadLocal = new OldThreadLocal() +} \ No newline at end of file diff --git a/kamon-core-tests/src/test/scala/kamon/context/ThreadLocalStorageSpec.scala b/kamon-core-tests/src/test/scala/kamon/context/ThreadLocalStorageSpec.scala index 12e0ca58..d2c4e57e 100644 --- a/kamon-core-tests/src/test/scala/kamon/context/ThreadLocalStorageSpec.scala +++ b/kamon-core-tests/src/test/scala/kamon/context/ThreadLocalStorageSpec.scala @@ -45,7 +45,6 @@ class ThreadLocalStorageSpec extends WordSpec with Matchers { TLS.current() shouldBe Context.Empty } - } val TLS: Storage = new Storage.ThreadLocal diff --git a/kamon-core/src/main/scala/kamon/context/Storage.scala b/kamon-core/src/main/scala/kamon/context/Storage.scala index 4f4e6cbb..f116142d 100644 --- a/kamon-core/src/main/scala/kamon/context/Storage.scala +++ b/kamon-core/src/main/scala/kamon/context/Storage.scala @@ -15,6 +15,8 @@ package kamon.context +import kamon.context.Storage.ThreadLocal.ContextHolder + trait Storage { def current(): Context def store(context: Context): Storage.Scope @@ -27,28 +29,45 @@ object Storage { def close(): Unit } - + /** + * Wrapper that implements optimized {@link ThreadLocal} access pattern ideal for heavily used + * ThreadLocals. + * + * It is faster to use a mutable holder object and always perform ThreadLocal.get() and never use + * ThreadLocal.set(), because the value is more likely to be found in the ThreadLocalMap direct hash + * slot and avoid the slow path ThreadLocalMap.getEntryAfterMiss(). + * + * Important: this thread local will live in ThreadLocalMap forever, so use with care. + */ class ThreadLocal extends Storage { - private val tls = new java.lang.ThreadLocal[Context]() { - override def initialValue(): Context = Context.Empty + private val tls = new java.lang.ThreadLocal[ContextHolder]() { + override def initialValue() = new ContextHolder(Context.Empty) } override def current(): Context = - tls.get() + get() override def store(context: Context): Scope = { val newContext = context - val previousContext = tls.get() - tls.set(newContext) + val previousContext = get() + set(newContext) new Scope { override def context: Context = newContext - override def close(): Unit = tls.set(previousContext) + override def close(): Unit = set(previousContext) } } + + private def get():Context = + tls.get().value + + private def set(value:Context) : Unit = + tls.get().value = value } object ThreadLocal { def apply(): ThreadLocal = new ThreadLocal() + + final class ContextHolder(var value:Context) } } \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt index 323e4f2f..3192be2f 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,5 @@ resolvers += Resolver.bintrayIvyRepo("kamon-io", "sbt-plugins") addSbtPlugin("io.kamon" % "kamon-sbt-umbrella" % "0.0.15") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.0") +addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.3") -- cgit v1.2.3