Skip to content

Commit a855073

Browse files
SokolovaMariaelizarov
authored andcommitted
Tracing atomic operations
* Avoid trace allocations for default non-traced atomics. Trace format changes. * Supports naming traces with trace.name("foo"). * System property 'kotlinx.atomicfu.trace.thread' to include thread name into the default format on JVM. * Improved Trace.toString implementation. * A separate type for TraceFormat: - Avoids boxing of integer index. - Makes it a bit less convenient to use (but Ok, since it is not a common feature). - Its full removal from bytecode is not supported now (might be Ok, TBD). Fixes #20
1 parent 10f7065 commit a855073

File tree

31 files changed

+830
-90
lines changed

31 files changed

+830
-90
lines changed

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ The idiomatic way to use atomic operations in Kotlin.
2323
* [User-defined extensions on atomics](#user-defined-extensions-on-atomics)
2424
* [Locks](#locks)
2525
* [Testing of lock-free data structures](#testing-lock-free-data-structures-on-jvm).
26+
* [Tracing operations](#tracing-operations)
2627

2728
## Example
2829

@@ -338,3 +339,35 @@ execution of tests with the original (non-transformed) classes for Maven:
338339
```
339340

340341
For Gradle there is nothing else to add. Tests are always run using original (non-transformed) classes.
342+
343+
### Tracing operations
344+
345+
You can debug your tests tracing atomic operations with a special trace object:
346+
347+
```kotlin
348+
private val trace = Trace()
349+
private val current = atomic(0, trace)
350+
351+
fun update(x: Int): Int {
352+
// custom trace message
353+
trace { "calling update($x)" }
354+
// automatic tracing of modification operations
355+
return current.getAndAdd(x)
356+
}
357+
```
358+
359+
All trace messages are stored in a cyclic array inside `trace`.
360+
361+
You can optionally set the size of trace's message array and format function. For example,
362+
you can add a current thread name to the traced messages:
363+
364+
```kotlin
365+
private val trace = Trace(size = 64) {
366+
index, // index of a trace message
367+
text // text passed when invoking trace { text }
368+
-> "$index: [${Thread.currentThread().name}] $text"
369+
}
370+
```
371+
372+
`trace` is only seen before transformation and completely erased after on Kotlin/JVM and Kotlin/JS.
373+

atomicfu-gradle-plugin/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ apply plugin: 'java-gradle-plugin'
77

88
apply from: rootProject.file('gradle/compile-options.gradle')
99

10-
ext.configureKotlin(org.jetbrains.kotlin.gradle.tasks.KotlinCompile)
10+
ext.configureKotlin()
1111

1212
dependencies {
1313
compile gradleApi()

atomicfu-maven-plugin/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ apply plugin: 'maven'
77

88
apply from: rootProject.file('gradle/compile-options.gradle')
99

10-
ext.configureKotlin(org.jetbrains.kotlin.gradle.tasks.KotlinCompile)
10+
ext.configureKotlin()
1111

1212
dependencies {
1313
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"

atomicfu-transformer/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ apply plugin: 'kotlin'
66

77
apply from: rootProject.file('gradle/compile-options.gradle')
88

9-
ext.configureKotlin(org.jetbrains.kotlin.gradle.tasks.KotlinCompile)
9+
ext.configureKotlin()
1010

1111
dependencies {
1212
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"

atomicfu-transformer/src/main/kotlin/kotlinx/atomicfu/transformer/AtomicFUTransformer.kt

Lines changed: 127 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

55
@file:Suppress("EXPERIMENTAL_FEATURE_WARNING")
@@ -24,6 +24,10 @@ private const val JUCA_PKG = "java/util/concurrent/atomic"
2424
private const val JLI_PKG = "java/lang/invoke"
2525
private const val ATOMIC = "atomic"
2626

27+
private const val TRACE = "Trace"
28+
private const val TRACE_BASE = "TraceBase"
29+
private const val TRACE_FORMAT = "TraceFormat"
30+
2731
private val INT_ARRAY_TYPE = getType("[I")
2832
private val LONG_ARRAY_TYPE = getType("[J")
2933
private val BOOLEAN_ARRAY_TYPE = getType("[Z")
@@ -75,6 +79,17 @@ private const val GET_VALUE = "getValue"
7579
private const val SET_VALUE = "setValue"
7680

7781
private const val AFU_CLS = "$AFU_PKG/AtomicFU"
82+
private const val TRACE_KT = "$AFU_PKG/TraceKt"
83+
private const val TRACE_BASE_CLS = "$AFU_PKG/$TRACE_BASE"
84+
85+
private val TRACE_BASE_TYPE = getObjectType(TRACE_BASE_CLS)
86+
87+
private val TRACE_APPEND = MethodId(TRACE_BASE_CLS, "append", getMethodDescriptor(VOID_TYPE, STRING_TYPE), INVOKEVIRTUAL)
88+
private val TRACE_DEFAULT_ARGS = "I${OBJECT_TYPE.descriptor}"
89+
private const val DEFAULT = "\$default"
90+
91+
private val TRACE_FACTORY = MethodId(TRACE_KT, TRACE, "(IL$AFU_PKG/$TRACE_FORMAT;)L$AFU_PKG/$TRACE_BASE;", INVOKESTATIC)
92+
private val TRACE_PARTIAL_ARGS_FACTORY = MethodId(TRACE_KT, "$TRACE$DEFAULT", "(IL$AFU_PKG/$TRACE_FORMAT;$TRACE_DEFAULT_ARGS)L$AFU_PKG/$TRACE_BASE;", INVOKESTATIC)
7893

7994
private val FACTORIES: Set<MethodId> = setOf(
8095
MethodId(AFU_CLS, ATOMIC, "(Ljava/lang/Object;)L$AFU_PKG/AtomicRef;", INVOKESTATIC),
@@ -86,7 +101,17 @@ private val FACTORIES: Set<MethodId> = setOf(
86101
MethodId("$AFU_PKG/AtomicLongArray", "<init>", "(I)V", INVOKESPECIAL),
87102
MethodId("$AFU_PKG/AtomicBooleanArray", "<init>", "(I)V", INVOKESPECIAL),
88103
MethodId("$AFU_PKG/AtomicArray", "<init>", "(I)V", INVOKESPECIAL),
89-
MethodId("$AFU_PKG/AtomicFU_commonKt", "atomicArrayOfNulls", "(I)L$AFU_PKG/AtomicArray;", INVOKESTATIC)
104+
MethodId("$AFU_PKG/AtomicFU_commonKt", "atomicArrayOfNulls", "(I)L$AFU_PKG/AtomicArray;", INVOKESTATIC),
105+
106+
MethodId(AFU_CLS, ATOMIC, "(Ljava/lang/Object;L$TRACE_BASE_CLS;)L$AFU_PKG/AtomicRef;", INVOKESTATIC),
107+
MethodId(AFU_CLS, ATOMIC, "(IL$TRACE_BASE_CLS;)L$AFU_PKG/AtomicInt;", INVOKESTATIC),
108+
MethodId(AFU_CLS, ATOMIC, "(JL$TRACE_BASE_CLS;)L$AFU_PKG/AtomicLong;", INVOKESTATIC),
109+
MethodId(AFU_CLS, ATOMIC, "(ZL$TRACE_BASE_CLS;)L$AFU_PKG/AtomicBoolean;", INVOKESTATIC),
110+
111+
MethodId(AFU_CLS, ATOMIC + DEFAULT, "(Ljava/lang/Object;L$TRACE_BASE_CLS;$TRACE_DEFAULT_ARGS)L$AFU_PKG/AtomicRef;", INVOKESTATIC),
112+
MethodId(AFU_CLS, ATOMIC + DEFAULT, "(IL$TRACE_BASE_CLS;$TRACE_DEFAULT_ARGS)L$AFU_PKG/AtomicInt;", INVOKESTATIC),
113+
MethodId(AFU_CLS, ATOMIC + DEFAULT, "(JL$TRACE_BASE_CLS;$TRACE_DEFAULT_ARGS)L$AFU_PKG/AtomicLong;", INVOKESTATIC),
114+
MethodId(AFU_CLS, ATOMIC + DEFAULT, "(ZL$TRACE_BASE_CLS;$TRACE_DEFAULT_ARGS)L$AFU_PKG/AtomicBoolean;", INVOKESTATIC)
90115
)
91116

92117
private operator fun Int.contains(bit: Int) = this and bit != 0
@@ -155,6 +180,8 @@ class AtomicFUTransformer(
155180

156181
private val fields = mutableMapOf<FieldId, FieldInfo>()
157182
private val accessors = mutableMapOf<MethodId, FieldInfo>()
183+
private val traceFields = mutableSetOf<FieldId>()
184+
private val traceAccessors = mutableSetOf<MethodId>()
158185
private val removeMethods = mutableSetOf<MethodId>()
159186

160187
override fun transform() {
@@ -329,16 +356,20 @@ class AtomicFUTransformer(
329356
val fieldType = getType(fi.desc)
330357
val accessorMethod = MethodId(className, name, desc, accessToInvokeOpcode(access))
331358
info("$field accessor $name found")
332-
val fieldInfo = registerField(field, fieldType, isStatic)
333-
fieldInfo.accessors += accessorMethod
334-
accessors[accessorMethod] = fieldInfo
359+
if (fieldType == TRACE_BASE_TYPE) {
360+
traceAccessors.add(accessorMethod)
361+
} else {
362+
val fieldInfo = registerField(field, fieldType, isStatic)
363+
fieldInfo.accessors += accessorMethod
364+
accessors[accessorMethod] = fieldInfo
365+
}
335366
}
336367
}
337368
}
338369

339370
// returns a type on which this is a potential accessor
340371
private fun getPotentialAccessorType(access: Int, className: String, methodType: Type): Type? {
341-
if (methodType.returnType !in AFU_TYPES) return null
372+
if (methodType.returnType !in AFU_TYPES && methodType.returnType != TRACE_BASE_TYPE) return null
342373
return if (access and ACC_STATIC != 0) {
343374
if (access and ACC_FINAL != 0 && methodType.argumentTypes.isEmpty()) {
344375
// accessor for top-level atomic
@@ -423,6 +454,12 @@ class AtomicFUTransformer(
423454
transformed = true
424455
return fv
425456
}
457+
// skip trace field
458+
if (fieldType == TRACE_BASE_TYPE) {
459+
traceFields += FieldId(className, name, desc)
460+
transformed = true
461+
return null
462+
}
426463
return super.visitField(access, name, desc, signature, value)
427464
}
428465

@@ -493,11 +530,11 @@ class AtomicFUTransformer(
493530
exceptions: Array<out String>?
494531
): MethodVisitor? {
495532
val methodId = MethodId(className, name, desc, accessToInvokeOpcode(access))
496-
if (methodId in accessors || methodId in removeMethods) {
533+
if (methodId in accessors || methodId in traceAccessors || methodId in removeMethods) {
497534
// drop and skip the methods that were found in Phase 1
498535
// todo: should remove those methods from kotlin metadata, too
499536
transformed = true
500-
return null
537+
return null // drop accessor
501538
}
502539
val sourceInfo = SourceInfo(methodId, source)
503540
val superMV = if (name == "<clinit>" && desc == "()V") {
@@ -532,8 +569,8 @@ class AtomicFUTransformer(
532569
// remove unused methods from metadata
533570
metadata?.let {
534571
val mt = MetadataTransformer(
535-
removeFields = fields.keys,
536-
removeMethods = accessors.keys + removeMethods
572+
removeFields = fields.keys + traceFields,
573+
removeMethods = accessors.keys + traceAccessors + removeMethods
537574
)
538575
if (mt.transformMetadata(it)) transformed = true
539576
if (cv != null) it.accept(cv.visitAnnotation(KOTLIN_METADATA_DESC, true))
@@ -946,29 +983,29 @@ class AtomicFUTransformer(
946983

947984
private fun putPrimitiveTypeWrapper(
948985
factoryInsn: MethodInsnNode,
986+
initStart: AbstractInsnNode,
949987
f: FieldInfo,
950988
next: FieldInsnNode
951989
): AbstractInsnNode? {
952990
// generate wrapper class for static fields of primitive type
953991
val factoryArg = getMethodType(factoryInsn.desc).argumentTypes[0]
954992
generateRefVolatileClass(f, factoryArg)
955-
val firstInitInsn = FlowAnalyzer(next).getInitStart()
956993
// remove calling atomic factory for static field and following putstatic
957994
val afterPutStatic = next.next
958995
instructions.remove(factoryInsn)
959996
instructions.remove(next)
960-
initRefVolatile(f, factoryArg, firstInitInsn, afterPutStatic)
997+
initRefVolatile(f, factoryArg, initStart, afterPutStatic)
961998
return afterPutStatic
962999
}
9631000

9641001
private fun putJucaAtomicArray(
9651002
arrayfactoryInsn: MethodInsnNode,
1003+
initStart: AbstractInsnNode,
9661004
f: FieldInfo,
9671005
next: FieldInsnNode
9681006
): AbstractInsnNode? {
9691007
// replace with invoking j.u.c.a.Atomic*Array constructor
9701008
val jucaAtomicArrayDesc = f.typeInfo.fuType.descriptor
971-
val initStart = FlowAnalyzer(next).getInitStart().next
9721009
if (initStart.opcode == NEW) {
9731010
// change descriptor of NEW instruction
9741011
(initStart as TypeInsnNode).desc = descToName(jucaAtomicArrayDesc)
@@ -992,10 +1029,10 @@ class AtomicFUTransformer(
9921029

9931030
private fun putPureVhArray(
9941031
arrayFactoryInsn: MethodInsnNode,
1032+
initStart: AbstractInsnNode,
9951033
f: FieldInfo,
9961034
next: FieldInsnNode
9971035
): AbstractInsnNode? {
998-
val initStart = FlowAnalyzer(next).getInitStart().next
9991036
if (initStart.opcode == NEW) {
10001037
// remove dup
10011038
instructions.remove(initStart.next)
@@ -1015,6 +1052,55 @@ class AtomicFUTransformer(
10151052
return next.next
10161053
}
10171054

1055+
// erases pushing atomic factory trace arguments
1056+
// returns the first value argument push
1057+
private fun eraseTraceInit(atomicFactory: MethodInsnNode, isArrayFactory: Boolean): AbstractInsnNode {
1058+
val initStart = FlowAnalyzer(atomicFactory).getInitStart(1)
1059+
if (isArrayFactory) return initStart
1060+
var lastArg = atomicFactory.previous
1061+
val valueArgInitLast = FlowAnalyzer(atomicFactory).getValueArgInitLast()
1062+
while (lastArg != valueArgInitLast) {
1063+
val prev = lastArg.previous
1064+
instructions.remove(lastArg)
1065+
lastArg = prev
1066+
}
1067+
return initStart
1068+
}
1069+
1070+
private fun eraseTraceInfo(append: AbstractInsnNode): AbstractInsnNode {
1071+
// remove append trace instructions: from append invocation up to getfield Trace or accessor to Trace field
1072+
val afterAppend = append.next
1073+
var start = append
1074+
val isGetFieldTrace = { insn: AbstractInsnNode ->
1075+
insn.opcode == GETFIELD && (start as FieldInsnNode).desc == getObjectType(TRACE_BASE_CLS).descriptor }
1076+
val isTraceAccessor = { insn: AbstractInsnNode ->
1077+
if (insn is MethodInsnNode) {
1078+
val methodId = MethodId(insn.owner, insn.name, insn.desc, insn.opcode)
1079+
methodId in traceAccessors
1080+
} else false
1081+
}
1082+
while (!(isGetFieldTrace(start) || isTraceAccessor(start))) {
1083+
start = start.previous
1084+
}
1085+
// now start contains Trace getfield insn or Trace accessor
1086+
if (isTraceAccessor(start)) {
1087+
instructions.remove(start.previous.previous)
1088+
instructions.remove(start.previous)
1089+
} else {
1090+
instructions.remove(start.previous)
1091+
}
1092+
if (start.next is VarInsnNode) {
1093+
val v = (start.next as VarInsnNode).`var`
1094+
localVariables.removeIf { it.index == v }
1095+
}
1096+
while (start != afterAppend) {
1097+
val next = start.next
1098+
instructions.remove(start)
1099+
start = next
1100+
}
1101+
return afterAppend
1102+
}
1103+
10181104
private fun transform(i: AbstractInsnNode): AbstractInsnNode? {
10191105
when (i) {
10201106
is MethodInsnNode -> {
@@ -1026,15 +1112,18 @@ class AtomicFUTransformer(
10261112
val fieldId = (next as? FieldInsnNode)?.checkPutFieldOrPutStatic()
10271113
?: abort("factory $methodId invocation must be followed by putfield")
10281114
val f = fields[fieldId]!!
1115+
val isArray = AFU_CLASSES[i.owner]?.let { it.originalType.sort == ARRAY } ?: false
1116+
// erase pushing arguments for trace initialisation
1117+
val newInitStart = eraseTraceInit(i, isArray)
10291118
// in FU mode wrap values of top-level primitive atomics into corresponding *RefVolatile class
10301119
if (!vh && f.isStatic && !f.isArray) {
1031-
return putPrimitiveTypeWrapper(i, f, next)
1120+
return putPrimitiveTypeWrapper(i, newInitStart, f, next)
10321121
}
10331122
if (f.isArray) {
10341123
return if (vh) {
1035-
putPureVhArray(i, f, next)
1124+
putPureVhArray(i, newInitStart, f, next)
10361125
} else {
1037-
putJucaAtomicArray(i, f, next)
1126+
putJucaAtomicArray(i, newInitStart, f, next)
10381127
}
10391128
}
10401129
instructions.remove(i)
@@ -1063,10 +1152,30 @@ class AtomicFUTransformer(
10631152
transformed = true
10641153
return fixupLoadedAtomicVar(f, j)
10651154
}
1155+
methodId == TRACE_FACTORY || methodId == TRACE_PARTIAL_ARGS_FACTORY -> {
1156+
// remove trace factory and following putfield
1157+
val argsSize = getMethodType(methodId.desc).argumentTypes.size
1158+
val putfield = i.next
1159+
val next = putfield.next
1160+
val depth = if (i.opcode == INVOKESPECIAL) 2 else argsSize
1161+
val initStart = FlowAnalyzer(i.previous).getInitStart(depth).previous
1162+
var lastArg = i
1163+
while (lastArg != initStart) {
1164+
val prev = lastArg.previous
1165+
instructions.remove(lastArg)
1166+
lastArg = prev
1167+
}
1168+
instructions.remove(initStart) // aload of the parent class
1169+
instructions.remove(putfield)
1170+
return next
1171+
}
1172+
methodId == TRACE_APPEND -> {
1173+
return eraseTraceInfo(i)
1174+
}
10661175
methodId in removeMethods -> {
10671176
abort(
10681177
"invocation of method $methodId on atomic types. " +
1069-
"Make the later method 'inline' to use it", i
1178+
"Make the latter method 'inline' to use it", i
10701179
)
10711180
}
10721181
i.opcode == INVOKEVIRTUAL && i.owner in AFU_CLASSES -> {

0 commit comments

Comments
 (0)