vs_writing_to_file

Benchmark comparison of:

  • serializing & writing to file.
  • sending to channel.

While these operations are different this is a good comparison for difference of writing directly to a log file. VS sending the log message to a channel and writing to a file on a different thread.

GT-Sandbox-Snapshot

Code

package com.glassthought.sandbox

// Required dependencies:
// implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
// implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlin.system.measureNanoTime
import java.io.File

/**
 * Data class representing an object with 4 string fields.
 */
@Serializable
data class TestObject(
  val field1: String,
  val field2: String,
  val field3: String,
  val field4: String
)

/**
 * Benchmarks the serialization of [TestObject] and appends the serialized JSON to a file.
 *
 * @param iterations The number of times to perform the serialization and file write.
 * @return The total time in nanoseconds taken for all iterations.
 */
fun benchmarkSerializationAndFileWrite(iterations: Int): Long {
  // JSON serializer with default configuration.
  val json = Json { encodeDefaults = true }
  // Use a fixed file in /tmp. Ensure it exists.
  val filePath = "/tmp/serialization_benchmark.txt"
  File(filePath).apply {
    if (!exists()) createNewFile()
  }

  var totalTimeNs = 0L
  repeat(iterations) {
    // For consistency, the object is always the same.
    val testObj = TestObject("value1", "value2", "value3", "value4")
    // Measure the time taken for serializing and appending to file.
    val timeNs = measureNanoTime {
      // Serialize the object to JSON.
      val serialized = json.encodeToString(TestObject.serializer(), testObj)
      // Append the serialized string to the file (with newline).
      File(filePath).appendText("$serialized\n")
    }
    totalTimeNs += timeNs
  }
  return totalTimeNs
}

/**
 * Benchmarks sending [TestObject] instances to an infinitely buffered channel.
 *
 * @param iterations The number of times to send the object.
 * @param channel The [Channel] used for sending objects.
 * @return The total time in nanoseconds taken for all send operations.
 */
suspend fun benchmarkChannelSend(iterations: Int, channel: Channel<TestObject>): Long {
  var totalTimeNs = 0L
  repeat(iterations) {
    val testObj = TestObject("value1", "value2", "value3", "value4")
    // Measure the time taken to send the object into the channel.
    val timeNs = measureNanoTime {
      channel.send(testObj)
    }
    totalTimeNs += timeNs
  }
  return totalTimeNs
}

/**
 * Entry point of the benchmarking program.
 *
 * Runs both benchmarks sequentially and prints the total and average time for each.
 */
fun main() = runBlocking {
  // Total number of iterations for each benchmark.
  val iterations = 1000
  println("Starting benchmarks with $iterations iterations.\n")

  // Benchmark serialization and file write.
  val totalTimeSerialization = benchmarkSerializationAndFileWrite(iterations)
  println("Serialization & File Append Benchmark:")
  println("  Total time: $totalTimeSerialization ns / ${totalTimeSerialization / 1_000_000} ms")

  println("  Average per operation: ${totalTimeSerialization / iterations} ns\n")

  // Create an infinitely buffered channel.
  val channel = Channel<TestObject>(Channel.UNLIMITED)
  // Launch a simple consumer to drain the channel and avoid blocking.
  val consumerJob = launch {
    for (obj in channel) {
      // Minimal processing can be added here if needed.
    }
  }
  // Benchmark sending objects to the channel.
  val totalTimeChannelSend = benchmarkChannelSend(iterations, channel)
  println("Channel Send Benchmark:")
  println("  Total time: $totalTimeChannelSend ns / ${totalTimeChannelSend / 1_000_000} ms")
  println("  Average per operation: ${totalTimeChannelSend / iterations} ns\n")

  // Cleanup: close the channel and cancel the consumer.
  channel.close()
  consumerJob.cancelAndJoin()

  println("Benchmarks completed.")
}

Command to reproduce:

gt.sandbox.checkout.commit af6e6d6fdd9057871ad9 \
&& cd "${GT_SANDBOX_REPO}" \
&& cmd.run.announce "./gradlew run --quiet"

Recorded output of command:

Starting benchmarks with 1000 iterations.

Serialization & File Append Benchmark:
  Total time: 62412174 ns / 62 ms
  Average per operation: 62412 ns

Channel Send Benchmark:
  Total time: 4073618 ns / 4 ms
  Average per operation: 4073 ns

Benchmarks completed.