1 /*
<lambda>null2 * Copyright 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.compose.runtime.tooling
18
19 import androidx.compose.runtime.snapshots.fastForEach
20
21 /**
22 * A diagnostic exception with a composition stack trace. This exception is usually appended to the
23 * suppressed exceptions when [androidx.compose.runtime.Composer.setDiagnosticStackTraceEnabled]
24 * flag is set to true.
25 */
26 internal expect class DiagnosticComposeException(
27 trace: List<ComposeStackTraceFrame>,
28 ) : RuntimeException
29
30 internal data class ComposeStackTraceFrame(
31 val sourceInfo: ParsedSourceInformation,
32 val groupOffset: Int?
33 )
34
35 internal class ParsedSourceInformation(
36 val isCall: Boolean,
37 val functionName: String?,
38 val fileName: String?,
39 val packageHash: String?,
40 val lineNumbers: IntArray,
41 val dataString: String
42 )
43
44 internal fun Throwable.tryAttachComposeStackTrace(
45 trace: () -> List<ComposeStackTraceFrame>
46 ): Boolean {
47 var result = false
48 if (suppressedExceptions.none { it is DiagnosticComposeException }) {
49 val traceException =
50 try {
51 val frames = trace()
52 result = frames.isNotEmpty()
53 if (result) DiagnosticComposeException(frames) else null
54 } catch (e: Throwable) {
55 // Attach the exception thrown while collecting trace.
56 // Usually this means that the slot table is malformed.
57 e
58 }
59 if (traceException != null) {
60 addSuppressed(traceException)
61 }
62 }
63 return result
64 }
65
attachComposeStackTracenull66 internal fun Throwable.attachComposeStackTrace(
67 trace: () -> List<ComposeStackTraceFrame>
68 ): Throwable = apply { tryAttachComposeStackTrace(trace) }
69
appendStackTracenull70 internal fun StringBuilder.appendStackTrace(trace: List<ComposeStackTraceFrame>) {
71 var currentFunction: String? = null
72 var currentFile: String? = null
73 val lines = buildList {
74 trace.asReversed().fastForEach { frame ->
75 val sourceInfo = frame.sourceInfo
76 val functionName = sourceInfo.functionName ?: currentFunction ?: "<unknown function>"
77 val fileName = sourceInfo.fileName ?: currentFile ?: "<unknown file>"
78 val lineNumbers = sourceInfo.lineNumbers
79 val resolvedLine =
80 if (frame.groupOffset != null && frame.groupOffset < lineNumbers.size) {
81 lineNumbers[frame.groupOffset].toString()
82 } else {
83 if (IncludeDebugInfo) {
84 "<no offset ${frame.groupOffset} in $lineNumbers>"
85 } else {
86 "<unknown line>"
87 }
88 }
89
90 val traceLine = buildString {
91 append(functionName)
92 append('(')
93 append(fileName)
94 append(':')
95 append(resolvedLine)
96 append(')')
97
98 if (IncludeDebugInfo) {
99 append(", parsed from ")
100 append(sourceInfo.dataString)
101 append(", group offset: ")
102 append(frame.groupOffset)
103 }
104 }
105
106 if (!sourceInfo.isCall) {
107 // replace previous line for source info, since this line will provide more
108 // precise info for line numbers from previous entry
109 val line = removeLastOrNull()
110 if (IncludeDebugInfo) {
111 add("$line (collapsed)")
112 }
113 }
114
115 // Filter first subcomposition frames that point to rememberCompositionContext.
116 if (
117 sourceInfo.functionName == "rememberCompositionContext" &&
118 sourceInfo.packageHash == RuntimePackageHash
119 ) {
120 if (IncludeDebugInfo) {
121 add("$traceLine (ignored)")
122 }
123 } else {
124 add(traceLine)
125 }
126
127 currentFunction = functionName
128 currentFile = fileName
129 }
130 }
131 lines.asReversed().fastForEach { appendLine("\tat $it") }
132 }
133
134 private const val RuntimePackageHash = "9igjgp"
135
136 private const val IncludeDebugInfo = false
137