diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-08-03 00:42:36 +0900 |
|---|---|---|
| committer | Sho Sakuma <me@m1sk9.dev> | 2026-08-05 00:42:23 +0900 |
| commit | 8ef90fc0a3fad0e71af6c786c4d5739ac81fea71 (patch) | |
| tree | a9381abad208987c1e05566ef551bc4dfe667588 /platform-paper | |
| parent | 2d8b613a3ad48acafd683f5286e29c3f1c167275 (diff) | |
| download | LunaticChat-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')
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") + } } |
