JetBrains / lincheck

Framework for testing concurrent data structures
Mozilla Public License 2.0
576 stars 33 forks source link

Simplify bytecode generation in `TestThreadExecutionGenerator` #196

Open eupp opened 1 year ago

eupp commented 1 year ago

Currently, subclasses of TestThreadExecution class, which are used to run scenario threads, are generated from bytecode (see TestThreadExecutionGenerator class). It makes it hard to maintain and modify the execution scenario running logic (I recently faced this issue in #146). However, it looks like most part of the logic can actually be implemented as a plain Kotlin code. Some examples:

eupp commented 1 year ago

@ndkoval @alefedor do you remember what parts exactly require bytecode transformation? Do you have any ideas how to isolate them?

alefedor commented 1 year ago

Hi @eupp !

Currently, there is no bytecode generation for init and post parts. They are handled in ParallelThreadsRunner using Java Reflections. https://github.com/JetBrains/lincheck/blob/2dcaa9acb11790a4b96d36b810862f6aa78cc837/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/ParallelThreadsRunner.kt#L233

So the reason why lincheck fails with the thrown exception is that executeActor does not try to wrap any thrown exception on method.invoke(...).

As for TestThreadExecutionGenerator, it is possibly to re-write most byte-code generation to Kotlin code. I did it the another project. The idea is to write most of the method in Kotlin code and then replace some "stub" invocations with transformed bytecode. It looked like this

override fun call() {
        INITIALIZATION_STUB()
        ...
        val timeSpent = measureNanoTime {
            for (operationId in operationIds) {
                RUN_OPERATION_STUB(operationId)
            }
        }
        ...
        FINALIZATION_STUB()
        return timeSpent
}

Though you still have to do in the transformed byte-code the following:

One part that may be tricky to implement this way is expection handling, since you can directly catch expected exceptions only in the transformed code but we want this logic in Kotlin code. This issue can be bypassed if we catch all exceptions in Kotlin code and then check whether they are instances of expected exception classes

eupp commented 1 year ago

Hi @alefedor, thank you for clarification!

I like the approach with stub functions. It allows to implement the class "template" in plain Kotlin code, making it easy to read and maintain.

With respect to that, my question is what parts exactly we need to stub and generate? The part that executes actor? We need to generate custom bytecode for it to avoid boxing arguments and result of primitive types?

As for handling exceptions, wouldn't the #181 solve the issues with them by unifying expected/unexpected exceptions handling logic? (cc @avpotapov00)

alefedor commented 1 year ago

@eupp

what parts exactly we need to stub and generate? The part that executes actor?

Yes, it seems that only the part that generates actors.

Then, the transformer replaces i-th call of a stub method with i-th actor in the scenario. Boxing is not an issue for arguments. Primitive types are pushed directly in the bytecode, non-primitive ones are stored in an array during transformation and then loaded in the transformed code. For simplicity, the result should always be wrapped in byte-code to be Lincheck Result.

wouldn't the https://github.com/JetBrains/lincheck/pull/181 solve the issues with them by unifying expected/unexpected exceptions handling logic

Yes, this pull request addresses the issue by handling Java Reflection invocations.

eupp commented 1 year ago

Boxing is not an issue for arguments.

I meant that boxing is the problem we are trying to solve with bytecode generation, isn't it? Otherwise we could just invoke actors directly in the Kotlin code. What are other reasons why we generate custom bytecode here?

alefedor commented 1 year ago

I meant that boxing is the problem we are trying to solve with bytecode generation, isn't it?

That's actually a very good question. Indeed, byte-code generation helps to prevent boxing and overhead of Java Reflections. Since these costs are present in every invocation, we need to get rid of them if they are the bottleneck