Skip to content

Commit 4df0495

Browse files
committed
performance: stream preview tabs from storage
1 parent 22fb671 commit 4df0495

6 files changed

Lines changed: 61 additions & 3 deletions

File tree

app/src/main/java/com/kyhsgeekcode/disassembler/project/ProjectDataStorage.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ object ProjectDataStorage {
2727
return resolveToRead(relPath)?.extension ?: ""
2828
}
2929

30+
fun getFileContentPreview(relPath: String, maxBytes: Int): ByteArray {
31+
val file = resolveToRead(relPath)!!
32+
return readPreviewBytes(file, maxBytes)
33+
}
34+
3035
// @UnstableDefault
3136
// private fun getOriginalOrGen(relPath: String): File {
3237
// val orig = ProjectManager.getOriginal(relPath)
@@ -149,6 +154,18 @@ internal fun shouldCacheFileContent(sizeBytes: Long): Boolean {
149154
return sizeBytes <= MAX_CACHED_FILE_CONTENT_BYTES
150155
}
151156

157+
internal fun readPreviewBytes(file: File, maxBytes: Int): ByteArray {
158+
require(maxBytes >= 0) { "maxBytes must be non-negative" }
159+
if (maxBytes == 0) {
160+
return byteArrayOf()
161+
}
162+
file.inputStream().use { inputStream ->
163+
val buffer = ByteArray(maxBytes)
164+
val bytesRead = inputStream.read(buffer, 0, maxBytes).coerceAtLeast(0)
165+
return buffer.copyOf(bytesRead)
166+
}
167+
}
168+
152169
enum class DataType {
153170
FileContent;
154171
}

app/src/main/java/com/kyhsgeekcode/disassembler/ui/tabs/AnalysisTab.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
2323
import kotlinx.coroutines.flow.StateFlow
2424
import timber.log.Timber
2525

26+
private const val MAX_ANALYZED_FILE_BYTES = 512 * 1024
2627

2728
sealed class AnalysisState {
2829
object Ready : AnalysisState()
@@ -37,11 +38,19 @@ class AnalysisTabData(val data: TabKind.AnalysisResult) : PreparedTabData() {
3738
val state = _state as StateFlow<AnalysisState>
3839
private val _image = MutableStateFlow<Drawable?>(null)
3940
val image = _image as StateFlow<Drawable?>
41+
private val _notice = MutableStateFlow<String?>(null)
42+
val notice = _notice as StateFlow<String?>
4043

4144
lateinit var analyzer: Analyzer
4245
override suspend fun prepare() {
43-
val bytes = ProjectDataStorage.getFileContent(data.relPath)
46+
val bytes = ProjectDataStorage.getFileContentPreview(data.relPath, MAX_ANALYZED_FILE_BYTES)
4447
Timber.d("Given relPath: ${data.relPath}")
48+
val fileSize = ProjectDataStorage.resolveToRead(data.relPath)?.length() ?: bytes.size.toLong()
49+
_notice.value = if (fileSize > bytes.size) {
50+
"Analyzing first ${bytes.size} bytes of $fileSize bytes"
51+
} else {
52+
null
53+
}
4554
analyzer = Analyzer(bytes)
4655
analyzer.analyze { c, t, stage ->
4756
_state.value = AnalysisState.Running(c, t, stage)
@@ -60,13 +69,15 @@ fun AnalysisTab(data: TabData, viewModel: MainViewModel) {
6069
val result = preparedTabData.result.collectAsState()
6170
val state = preparedTabData.state.collectAsState()
6271
val image = preparedTabData.image.collectAsState()
72+
val notice = preparedTabData.notice.collectAsState()
6373

6474
Column(
6575
Modifier
6676
.fillMaxSize()
6777
.verticalScroll(rememberScrollState()),
6878
verticalArrangement = Arrangement.spacedBy(10.dp)
6979
) {
80+
notice.value?.let { Text(it) }
7081
when (val s = state.value) {
7182
is AnalysisState.Ready -> {
7283

app/src/main/java/com/kyhsgeekcode/disassembler/ui/tabs/HexTab.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ class HexTabData(val data: TabKind.Hex) : PreparedTabData() {
2020
val preview = _preview as StateFlow<com.kyhsgeekcode.disassembler.ui.components.HexPreview>
2121

2222
override suspend fun prepare() {
23-
_preview.value = buildHexPreview(ProjectDataStorage.getFileContent(data.relPath))
23+
_preview.value = buildHexPreview(
24+
ProjectDataStorage.getFileContentPreview(
25+
data.relPath,
26+
com.kyhsgeekcode.disassembler.ui.components.MAX_RENDERED_HEX_BYTES
27+
)
28+
)
2429
}
2530
}
2631

app/src/main/java/com/kyhsgeekcode/disassembler/ui/tabs/TextTab.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ class TextTabData(val data: TabKind.Text) : PreparedTabData() {
4848
val notice = _notice as StateFlow<String?>
4949

5050
override suspend fun prepare() {
51-
val preview = buildTextContentPreview(ProjectDataStorage.getFileContent(relPath))
51+
val preview = buildTextContentPreview(
52+
ProjectDataStorage.getFileContentPreview(relPath, MAX_RENDERED_TEXT_BYTES)
53+
)
5254
_notice.value = if (preview.isTruncated) {
5355
"Showing first ${preview.bytes.size} bytes of ${preview.originalSize} bytes"
5456
} else {

app/src/test/java/com/kyhsgeekcode/disassembler/project/ProjectDataStorageCachePolicyTest.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package com.kyhsgeekcode.disassembler.project
22

3+
import java.nio.file.Files
4+
import kotlin.io.path.createTempDirectory
35
import kotlin.test.Test
6+
import kotlin.test.assertContentEquals
47
import kotlin.test.assertFalse
58
import kotlin.test.assertTrue
69

@@ -16,4 +19,22 @@ class ProjectDataStorageCachePolicyTest {
1619
assertFalse(shouldCacheFileContent(MAX_CACHED_FILE_CONTENT_BYTES + 1L))
1720
assertFalse(shouldCacheFileContent(150L * 1024 * 1024))
1821
}
22+
23+
@Test
24+
fun `readPreviewBytes returns full content when under limit`() {
25+
val file = createTempDirectory("project-data-storage").resolve("small.bin").toFile().apply {
26+
writeBytes(byteArrayOf(1, 2, 3, 4))
27+
}
28+
29+
assertContentEquals(byteArrayOf(1, 2, 3, 4), readPreviewBytes(file, maxBytes = 8))
30+
}
31+
32+
@Test
33+
fun `readPreviewBytes truncates content at limit`() {
34+
val file = createTempDirectory("project-data-storage").resolve("large.bin").toFile().apply {
35+
writeBytes(byteArrayOf(1, 2, 3, 4, 5, 6))
36+
}
37+
38+
assertContentEquals(byteArrayOf(1, 2, 3, 4), readPreviewBytes(file, maxBytes = 4))
39+
}
1940
}

docs/maintenance/implementation-log.ko.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
| 이슈 `#219`, `#523` 대용량 파일 확장 cache key OOM 완화 | archive/dex/app expansion cache 경로가 더 이상 파일 전체를 `readBytes()`로 읽지 않고, streaming SHA-256으로 cache key를 계산하도록 바꿨다 | 큰 파일을 확장하기 전에 해시 계산만으로 메모리를 크게 잡아먹던 경계를 제거해서 대용량 import의 선행 OOM 위험을 줄였다 | `app/src/main/java/com/kyhsgeekcode/filechooser/model/ExpandedFileCache.kt`, `app/src/test/java/com/kyhsgeekcode/filechooser/model/ExpandedFileCacheTest.kt` | 완료 |
3939
| 이슈 `#523` 대용량 Hex 탭 렌더링 완화 | Hex 탭이 매우 큰 파일을 열 때 전체 바이트 배열을 그대로 Compose에 전달하지 않고, 상한이 있는 preview만 렌더링하며 원본 크기 안내 문구를 보여주도록 바꿨다 | 큰 파일을 Hex 화면에 띄우는 순간 UI 레이어가 과도한 메모리와 레이아웃 비용을 쓰는 경로를 줄여서 large-file cluster를 더 좁혔다 | `app/src/main/java/com/kyhsgeekcode/disassembler/ui/components/HexView.kt`, `app/src/main/java/com/kyhsgeekcode/disassembler/ui/tabs/HexTab.kt`, `app/src/test/java/com/kyhsgeekcode/disassembler/ui/components/HexViewLayoutTest.kt` | 완료 |
4040
| 이슈 `#523` 대용량 Text 탭 렌더링 완화 | Text 탭이 매우 큰 파일을 열 때 전체 바이트를 그대로 highlight하지 않고, 상한이 있는 preview만 decode/highlight하며 원본 크기 안내 문구를 보여주도록 바꿨다 | 큰 텍스트/스몰리/XML 파일을 Text 화면에 띄우는 순간 AnnotatedString과 syntax highlighter가 과도한 메모리를 잡는 경로를 줄였다 | `app/src/main/java/com/kyhsgeekcode/disassembler/ui/tabs/TextTab.kt`, `app/src/test/java/com/kyhsgeekcode/disassembler/ui/tabs/TextPreviewTest.kt` | 완료 |
41+
| 이슈 `#523` preview 탭 전체 파일 읽기 제거 | Hex/Text/Analysis preview가 더 이상 전체 파일을 `getFileContent()`로 읽지 않고, `ProjectDataStorage`의 bounded preview read helper로 필요한 바이트만 읽도록 바꿨다 | large-file 미리보기 화면을 여는 것만으로 전체 파일을 메모리에 올리던 경계를 제거해, preview 탭에서의 선행 메모리 사용량을 크게 줄였다 | `app/src/main/java/com/kyhsgeekcode/disassembler/project/ProjectDataStorage.kt`, `app/src/main/java/com/kyhsgeekcode/disassembler/ui/tabs/HexTab.kt`, `app/src/main/java/com/kyhsgeekcode/disassembler/ui/tabs/TextTab.kt`, `app/src/main/java/com/kyhsgeekcode/disassembler/ui/tabs/AnalysisTab.kt`, `app/src/test/java/com/kyhsgeekcode/disassembler/project/ProjectDataStorageCachePolicyTest.kt` | 완료 |
4142
| 이슈 `#95` content URI import 파일 충돌 완화 | content URI를 app-private `imports/` 아래로 복사할 때 같은 display name이 이미 있으면 `_1`, `_2` suffix를 붙인 새 파일로 저장하도록 바꿨다 | 같은 이름의 문서를 여러 번 가져오거나 provider가 동일한 표시 이름을 줄 때 기존 import를 조용히 덮어쓰는 문제를 막아 SAF 기반 import 경계를 더 안전하게 만들었다 | `app/src/main/java/com/kyhsgeekcode/disassembler/viewmodel/MainViewModel.kt`, `app/src/test/java/com/kyhsgeekcode/disassembler/viewmodel/ImportedFileNameTest.kt` | 완료 |
4243
| 이슈 `#95` imported project reopen 경계 정리 | exported project ZIP을 import한 뒤 `sourceFilePath`, `generatedFolder`, `project_info.json` 경로를 새 프로젝트 디렉터리 기준으로 재작성하고, temp extract 경로 매핑을 제거하도록 바꿨다 | SAF나 로컬 파일에서 가져온 project archive를 다시 열었을 때 삭제된 temp extract 경로나 예전 경로를 계속 참조하는 문제를 막아 reopen/open 경계를 실제 프로젝트 위치로 닫았다 | `app/src/main/java/com/kyhsgeekcode/disassembler/project/ProjectManager.kt`, `app/src/test/java/com/kyhsgeekcode/disassembler/ProjectManagerTest.kt` | 완료 |
4344
| 이슈 `#95` project archive / 기존 project reopen 연결 | chooser의 `Open as project` 흐름이 기존 project 디렉터리와 exported project ZIP을 실제로 열도록 연결하고, project zip은 `project_info.json` 존재 여부로 판별하게 바꿨다 | 이전에는 `openProject` 플래그가 state에만 저장되고 실질 동작이 없어서 reopen 경계가 끊겨 있었는데, 이제 기존 project 디렉터리는 바로 열고 exported project ZIP은 바로 import할 수 있다 | `app/src/main/java/com/kyhsgeekcode/disassembler/project/ProjectManager.kt`, `app/src/main/java/com/kyhsgeekcode/disassembler/viewmodel/MainViewModel.kt`, `app/src/main/java/com/kyhsgeekcode/filechooser/model/FileItem.kt`, `app/src/test/java/com/kyhsgeekcode/disassembler/viewmodel/ProjectOpenActionTest.kt` | 완료 |
@@ -71,6 +72,7 @@
7172
| import destination 파일명 테스트 | 통과 | 같은 display name으로 여러 번 import해도 app-private 파일이 덮어써지지 않는 규칙을 고정 |
7273
| imported project relocation 테스트 | 통과 | project archive import 후 `sourceFilePath`, `generatedFolder`, `project_info.json` 경로를 새 프로젝트 위치로 다시 맞추는 규칙을 고정 |
7374
| project open action 테스트 | 통과 | `Open as project`가 기존 project 디렉터리와 exported project ZIP에 대해 올바른 reopen/import 동작을 고르는 규칙을 고정 |
75+
| preview read helper 테스트 | 통과 | preview 탭용 제한 읽기가 파일 전체를 읽지 않고 상한까지만 반환하는 규칙을 고정 |
7476
| architecture mapping 테스트 | 통과 | `x86_64`, `PPC64`가 64-bit mode로 매핑되는 규칙을 고정 |
7577
| binary manual setup reload 테스트 | 통과 | override autosetup 변경 시 disassembly 재로드 필요 여부를 고정 |
7678
| project export archive 테스트 | 통과 | ZIP 엔트리가 절대경로를 포함하지 않고 `sourceFilePath`, `baseFolder/...`로 묶이는지 확인 |

0 commit comments

Comments
 (0)