ui: do not OOM when leaving log window open for a while

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
Jason A. Donenfeld 2023-04-03 15:13:43 +02:00
parent 18a06b0a51
commit 59da677c23

View File

@ -27,6 +27,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.collection.CircularArray
import androidx.core.app.ShareCompat import androidx.core.app.ShareCompat
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -62,8 +63,8 @@ import java.util.regex.Pattern
class LogViewerActivity : AppCompatActivity() { class LogViewerActivity : AppCompatActivity() {
private lateinit var binding: LogViewerActivityBinding private lateinit var binding: LogViewerActivityBinding
private lateinit var logAdapter: LogEntryAdapter private lateinit var logAdapter: LogEntryAdapter
private var logLines = arrayListOf<LogLine>() private var logLines = CircularArray<LogLine>()
private var rawLogLines = StringBuffer() private var rawLogLines = CircularArray<String>()
private var recyclerView: RecyclerView? = null private var recyclerView: RecyclerView? = null
private var saveButton: MenuItem? = null private var saveButton: MenuItem? = null
private val year by lazy { private val year by lazy {
@ -113,7 +114,7 @@ class LogViewerActivity : AppCompatActivity() {
binding.shareFab.setOnClickListener { binding.shareFab.setOnClickListener {
revokeLastUri() revokeLastUri()
val key = KeyPair().privateKey.toHex() val key = KeyPair().privateKey.toHex()
LOGS[key] = rawLogLines.toString().toByteArray(Charsets.UTF_8) LOGS[key] = rawLogBytes()
lastUri = Uri.parse("content://${BuildConfig.APPLICATION_ID}.exported-log/$key") lastUri = Uri.parse("content://${BuildConfig.APPLICATION_ID}.exported-log/$key")
val shareIntent = ShareCompat.IntentBuilder(this) val shareIntent = ShareCompat.IntentBuilder(this)
.setType("text/plain") .setType("text/plain")
@ -150,13 +151,24 @@ class LogViewerActivity : AppCompatActivity() {
private val downloadsFileSaver = DownloadsFileSaver(this) private val downloadsFileSaver = DownloadsFileSaver(this)
private fun rawLogBytes() : ByteArray {
val builder = StringBuilder()
for (i in 0 until rawLogLines.size()) {
builder.append(rawLogLines[i])
builder.append('\n')
}
val ret = builder.toString().toByteArray(Charsets.UTF_8)
builder.clear()
return ret
}
private suspend fun saveLog() { private suspend fun saveLog() {
var exception: Throwable? = null var exception: Throwable? = null
var outputFile: DownloadsFileSaver.DownloadsFile? = null var outputFile: DownloadsFileSaver.DownloadsFile? = null
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
outputFile = downloadsFileSaver.save("wireguard-log.txt", "text/plain", true) outputFile = downloadsFileSaver.save("wireguard-log.txt", "text/plain", true)
outputFile?.outputStream?.write(rawLogLines.toString().toByteArray(Charsets.UTF_8)) outputFile?.outputStream?.write(rawLogBytes())
} catch (e: Throwable) { } catch (e: Throwable) {
outputFile?.delete() outputFile?.delete()
exception = e exception = e
@ -191,24 +203,27 @@ class LogViewerActivity : AppCompatActivity() {
var priorModified = false var priorModified = false
val bufferedLogLines = arrayListOf<LogLine>() val bufferedLogLines = arrayListOf<LogLine>()
var timeout = 1000000000L / 2 // The timeout is initially small so that the view gets populated immediately. var timeout = 1000000000L / 2 // The timeout is initially small so that the view gets populated immediately.
val MAX_LINES = (1 shl 17) - 1
val MAX_BUFFERED_LINES = (1 shl 14) - 1
while (true) { while (true) {
val line = stdout.readLine() ?: break val line = stdout.readLine() ?: break
rawLogLines.append(line) if (rawLogLines.size() >= MAX_LINES)
rawLogLines.append('\n') rawLogLines.popFirst()
rawLogLines.addLast(line)
val logLine = parseLine(line) val logLine = parseLine(line)
if (logLine != null) { if (logLine != null) {
bufferedLogLines.add(logLine) bufferedLogLines.add(logLine)
} else { } else {
if (bufferedLogLines.isNotEmpty()) { if (bufferedLogLines.isNotEmpty()) {
bufferedLogLines.last().msg += "\n$line" bufferedLogLines.last().msg += "\n$line"
} else if (logLines.isNotEmpty()) { } else if (!logLines.isEmpty) {
logLines.last().msg += "\n$line" logLines[logLines.size() - 1].msg += "\n$line"
priorModified = true priorModified = true
} }
} }
val timeNow = System.nanoTime() val timeNow = System.nanoTime()
if ((timeNow - timeLastNotify) < timeout && stdout.ready()) if (bufferedLogLines.size < MAX_BUFFERED_LINES && (timeNow - timeLastNotify) < timeout && stdout.ready())
continue continue
timeout = 1000000000L * 5 / 2 // Increase the timeout after the initial view has something in it. timeout = 1000000000L * 5 / 2 // Increase the timeout after the initial view has something in it.
timeLastNotify = timeNow timeLastNotify = timeNow
@ -219,13 +234,23 @@ class LogViewerActivity : AppCompatActivity() {
logAdapter.notifyItemChanged(posStart - 1) logAdapter.notifyItemChanged(posStart - 1)
priorModified = false priorModified = false
} }
logLines.addAll(bufferedLogLines) val fullLen = logLines.size() + bufferedLogLines.size
if (fullLen >= MAX_LINES) {
val numToRemove = fullLen - MAX_LINES + 1
logLines.removeFromStart(numToRemove)
logAdapter.notifyItemRangeRemoved(0, numToRemove)
posStart -= numToRemove
}
for (bufferedLine in bufferedLogLines) {
logLines.addLast(bufferedLine)
}
bufferedLogLines.clear() bufferedLogLines.clear()
logAdapter.notifyItemRangeInserted(posStart, logLines.size - posStart) logAdapter.notifyItemRangeInserted(posStart, logLines.size() - posStart)
posStart = logLines.size posStart = logLines.size()
if (isScrolledToBottomAlready) { if (isScrolledToBottomAlready) {
recyclerView?.scrollToPosition(logLines.size - 1) recyclerView?.scrollToPosition(logLines.size() - 1)
} }
} }
} }
@ -279,7 +304,7 @@ class LogViewerActivity : AppCompatActivity() {
} }
} }
override fun getItemCount() = logLines.size override fun getItemCount() = logLines.size()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context) val view = LayoutInflater.from(parent.context)