Skip to content

Commit b696fdf

Browse files
szegedijenkins
authored and
jenkins
committed
util-core: Add Memoize.classValue
Problem We'd like to use `java.lang.ClassValue`, but its Java API looks cumbersome in Scala as it requires subclassing and overriding a protected method for every class value. Solution Add a `Memoize.classValue` method for `Class[_] => T` functions that resembles ordinary memoization in its API but uses `ClassValue` internally. Differential Revision: https://phabricator.twitter.biz/D825673
1 parent bbde116 commit b696fdf

File tree

3 files changed

+64
-1
lines changed

3 files changed

+64
-1
lines changed

CHANGELOG.rst

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Unreleased
1010
New Features
1111
~~~~~~~~~~~~
1212

13+
* util-core: Added `Memoize.classValue` as a Scala-friendly API for `java.lang.ClassValue`. ``PHAB_ID=D825673``
14+
1315
* util-jvm: Register JVM expression including memory pool usages (including code cache, compressed class space,
1416
eden space, sheap, metaspace, survivor space, and old gen) and open file descriptors count in StatsReceiver.
1517
``PHAB_ID=D820472``

util-core/src/main/scala/com/twitter/util/Memoize.scala

+17
Original file line numberDiff line numberDiff line change
@@ -164,4 +164,21 @@ object Memoize {
164164
case _ => missing(a)
165165
}
166166
}
167+
168+
/**
169+
* Thread-safe memoization for a function taking Class objects,
170+
* using [[java.lang.ClassValue]].
171+
*
172+
* @param f the function to memoize using a `ClassValue`
173+
* @tparam T the return type of the function
174+
* @return a memoized function over classes that stores memoized values
175+
* in a `ClassValue`
176+
*/
177+
def classValue[T](f: Class[_] => T): Class[_] => T = {
178+
val cv = new ClassValue[T] {
179+
def computeValue(c: Class[_]): T = f(c)
180+
}
181+
182+
cv.get
183+
}
167184
}

util-core/src/test/scala/com/twitter/util/MemoizeTest.scala

+45-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package com.twitter.util
22

33
import com.twitter.conversions.DurationOps._
44
import java.util.concurrent.atomic.AtomicInteger
5-
import java.util.concurrent.{TimeUnit, CountDownLatch => JavaCountDownLatch}
5+
import java.util.concurrent.atomic.AtomicReference
6+
import java.util.concurrent.{CountDownLatch => JavaCountDownLatch}
7+
import java.util.concurrent.TimeUnit
68
import org.mockito.Mockito._
79
import org.scalatest.funsuite.AnyFunSuite
810

@@ -185,4 +187,46 @@ class MemoizeTest extends AnyFunSuite {
185187
assert(memoizer.size == memoizer.snap.size)
186188
assert(memoizer.snap.size == 1)
187189
}
190+
191+
test("Memoize.classValue: returns the same value for the same input") {
192+
val cv = Memoize.classValue { _ =>
193+
new Object()
194+
}
195+
// just some anonymous classes
196+
val clazz1 = new Object() {}.getClass
197+
val clazz2 = new Object() {}.getClass
198+
assert(clazz1 ne clazz2)
199+
200+
val o11 = cv(clazz1)
201+
val o12 = cv(clazz1)
202+
val o21 = cv(clazz2)
203+
val o22 = cv(clazz2)
204+
assert(o11 eq o12)
205+
assert(o21 eq o22)
206+
assert(o11 ne o21)
207+
}
208+
209+
test("Memoize.classValue: racing threads observe the same value") {
210+
val Concurrency = 16
211+
val ready = new JavaCountDownLatch(Concurrency)
212+
val cv = Memoize.classValue { _ =>
213+
ready.await(1, TimeUnit.SECONDS)
214+
new Object()
215+
}
216+
217+
val clazz = new Object() {}.getClass
218+
219+
val values = new Array[AtomicReference[Object]](Concurrency)
220+
val threads = (0 until Concurrency).map { i =>
221+
new Thread(() => {
222+
ready.countDown()
223+
values(i) = new AtomicReference(cv(clazz))
224+
})
225+
}
226+
threads.foreach { _.start() }
227+
threads.foreach { _.join(1000L) }
228+
229+
val v0 = values(0).get()
230+
assert(values.forall(_.get eq v0))
231+
}
188232
}

0 commit comments

Comments
 (0)