summaryrefslogtreecommitdiff
path: root/platform-paper
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-08-03 00:42:36 +0900
committerSho Sakuma <me@m1sk9.dev>2026-08-05 00:42:23 +0900
commit8ef90fc0a3fad0e71af6c786c4d5739ac81fea71 (patch)
treea9381abad208987c1e05566ef551bc4dfe667588 /platform-paper
parent2d8b613a3ad48acafd683f5286e29c3f1c167275 (diff)
downloadLunaticChat-8ef90fc0a3fad0e71af6c786c4d5739ac81fea71.tar.gz
LunaticChat-8ef90fc0a3fad0e71af6c786c4d5739ac81fea71.tar.bz2
LunaticChat-8ef90fc0a3fad0e71af6c786c4d5739ac81fea71.zip
perf: convert the words of a message concurrently
RomanjiConverter awaited the Google IME call for one word before starting the next, though the words are independent. Callers convert under a single one-second timeout covering the whole message, so an N-word message needed N round trips inside a budget sized for roughly one: past the first word or two the remaining calls were cancelled, and because cache.put is only reached after a call returns, the discarded work was not even remembered. The next identical message repeated it. The words now run under one coroutineScope, so the message costs one round trip rather than N. The new test pins the concurrency itself rather than the timing: the fake API client records how many calls are in flight at once, which fails against the sequential version. Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'platform-paper')
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/RomanjiConverter.kt75
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/converter/RomanjiConverterTest.kt21
2 files changed, 59 insertions, 37 deletions
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/RomanjiConverter.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/RomanjiConverter.kt
index c961af8..a50a4aa 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/RomanjiConverter.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/RomanjiConverter.kt
@@ -2,6 +2,9 @@ package dev.m1sk9.lunaticChat.paper.converter
import dev.m1sk9.lunaticChat.engine.converter.GoogleIMEClient
import dev.m1sk9.lunaticChat.engine.converter.KanaConverter
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
import java.util.logging.Logger
class RomanjiConverter(
@@ -30,52 +33,50 @@ class RomanjiConverter(
return null
}
- val words = input.split(" ")
- val results = mutableListOf<String>()
+ val words = input.split(" ").filter { it.isNotEmpty() }
- for (word in words) {
- if (word.isEmpty()) {
- continue
+ // The words are independent, and callers convert under a timeout covering the whole
+ // message. Awaiting them one at a time makes an N-word message cost N round trips, so a
+ // long message runs out of budget after the first word or two.
+ val results =
+ coroutineScope {
+ words.map { word -> async { convertWord(word) } }.awaitAll()
}
- // Check cache first
- val cached = cache.get(word)
- if (cached != null) {
- if (debugMode) {
- logger.info("Cache hit for word: $word -> $cached")
- }
- results.add(cached)
- continue
- }
+ return results.joinToString(" ")
+ }
- // Pre-validate: Check if the word is valid romaji before attempting conversion
- // This prevents partial conversion of English words (e.g., "This" -> "てぃs")
- if (!KanaConverter.isValidRomaji(word)) {
- if (debugMode) {
- logger.info("Word is not valid romaji, keeping original: $word")
- }
- results.add(word)
- continue
+ private suspend fun convertWord(word: String): String {
+ cache.get(word)?.let { cached ->
+ if (debugMode) {
+ logger.info("Cache hit for word: $word -> $cached")
}
+ return cached
+ }
- // Step 1: Romanji -> Hiragana
- val hiragana = KanaConverter.toHiragana(word)
+ // Pre-validate: Check if the word is valid romaji before attempting conversion
+ // This prevents partial conversion of English words (e.g., "This" -> "てぃs")
+ if (!KanaConverter.isValidRomaji(word)) {
+ if (debugMode) {
+ logger.info("Word is not valid romaji, keeping original: $word")
+ }
+ return word
+ }
- // Step 2: Hiragana -> Kanji/Kana
- val converted =
- try {
- apiClient.convert(hiragana)
- } catch (e: Exception) {
- logger.warning("Failed to convert $hiragana: ${e.message}")
- hiragana // Use hiragana if API fails
- }
+ // Step 1: Romanji -> Hiragana
+ val hiragana = KanaConverter.toHiragana(word)
- // Cache the word-level conversion
- cache.put(word, converted)
- results.add(converted)
- }
+ // Step 2: Hiragana -> Kanji/Kana
+ val converted =
+ try {
+ apiClient.convert(hiragana)
+ } catch (e: Exception) {
+ logger.warning("Failed to convert $hiragana: ${e.message}")
+ hiragana // Use hiragana if API fails
+ }
- return results.joinToString(" ")
+ cache.put(word, converted)
+ return converted
}
private fun isRomajiOnly(input: String): Boolean =
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/converter/RomanjiConverterTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/converter/RomanjiConverterTest.kt
index 1e6d3c0..a7e0a52 100644
--- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/converter/RomanjiConverterTest.kt
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/converter/RomanjiConverterTest.kt
@@ -7,7 +7,9 @@ import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
+import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
+import java.util.concurrent.atomic.AtomicInteger
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
@@ -298,4 +300,23 @@ class RomanjiConverterTest {
assertEquals("おはよう", result)
verify(exactly = 1) { cache.put("ohayou", "おはよう") }
}
+
+ @Test
+ fun `words in one message are converted concurrently`() =
+ runBlocking {
+ val (converter, _, apiClient) = createConverter()
+ val inFlight = AtomicInteger(0)
+ val peakInFlight = AtomicInteger(0)
+
+ coEvery { apiClient.convert(any()) } coAnswers {
+ peakInFlight.updateAndGet { maxOf(it, inFlight.incrementAndGet()) }
+ delay(50)
+ inFlight.decrementAndGet()
+ "変換"
+ }
+
+ converter.convert("konnichiwa ohayou arigatou")
+
+ assertEquals(3, peakInFlight.get(), "each word should be in flight at the same time")
+ }
}