@@ -106,82 +106,117 @@ class DefaultBenchmarkSpanPrinter(
106106 val file = getFile(spanName)
107107
108108 FileWriter (file, true ).use { writer ->
109- // Write statistics for the spans (averages, percentiles, etc.)
110- val statisticalData = calculateStatistics(spans)
111-
112- if (statisticalData.isEmpty()) {
113- val timestamp = DATE_FORMAT .format(Date ())
114- writer.appendLine(" | $timestamp | N/A | No status entries recorded (batch size: ${spans.size} )" )
115- return @use
116- }
109+ // Separate spans into two groups: with exceptions and without exceptions
110+ val spansWithExceptions = spans.filter { it.getException() != null }
111+ val spansWithoutExceptions = spans.filter { it.getException() == null }
117112
118113 // Write session header
119114 val formattedTimestamp = DATE_FORMAT .format(Date (System .currentTimeMillis()))
115+ writer.appendLine(" " )
116+ writer.appendLine(" === Statistical Benchmark Session: $formattedTimestamp | Batch Size: ${spans.size} | With Exceptions: ${spansWithExceptions.size} | Without Exceptions: ${spansWithoutExceptions.size} ===" )
117+ writer.appendLine(" " )
120118
121- // Calculate total duration using dedicated end times (convert nanoseconds to milliseconds)
122- val totalDurationFormatted = spans.mapNotNull { span ->
123- val spanStartTime = span.getStartTimeInNanoSeconds()
124- val spanEndTime = span.getEndTimeInNanoSeconds()
125- if (spanEndTime > 0 ) { // Only include spans that have been ended
126- TimeUnit .NANOSECONDS .toMillis(spanEndTime - spanStartTime)
127- } else null
128- }.let { durations ->
129- if (durations.isNotEmpty()) {
130- val avgDuration = durations.average().toLong()
131- " ${avgDuration} ms"
132- } else {
133- " N/A"
134- }
119+ // Write statistics for spans WITHOUT exceptions
120+ if (spansWithoutExceptions.isNotEmpty()) {
121+ writeSpanGroupStatistics(writer, spansWithoutExceptions, " SUCCESS FLOWS (No Exceptions)" )
135122 }
136123
137- // Calculate average concurrent size
138- val avgConcurrentSize = spans.map { it.getConcurrentSilentRequestSize() }.average()
139- val avgConcurrentSizeFormatted = String .format(Locale .US , " %.2f" , avgConcurrentSize)
124+ // Write statistics for spans WITH exceptions, grouped by exception message
125+ if (spansWithExceptions.isNotEmpty()) {
126+ writeSpanGroupsByExceptionMessage(writer, spansWithExceptions)
127+ }
140128
141- writer.appendLine(" " )
142- writer.appendLine(" === Statistical Benchmark Session: $formattedTimestamp | Avg Total Duration: $totalDurationFormatted | Avg Concurrent Size: $avgConcurrentSizeFormatted | Batch Size: ${spans.size} ===" )
143- writer.appendLine(" " )
129+ writer.flush()
130+ }
131+ } catch (e: IOException ) {
132+ Logger .error(TAG , " IOException while writing averaged batch to file: $outputDirectoryAbsolutePath " , e)
133+ }
134+ }
144135
145- writer.appendLine(" | Status Entry | Metric | Time Since Previous | Time Since Start |" )
146- writer.appendLine(" |--------------------------------------------------|--------|---------------------|------------------|" )
136+ /* *
137+ * Write statistical data for a specific group of spans
138+ */
139+ private fun writeSpanGroupStatistics (writer : FileWriter , spans : List <IBenchmarkSpan >, groupTitle : String ) {
140+ val statisticalData = calculateStatistics(spans)
141+
142+ if (statisticalData.isEmpty()) {
143+ writer.appendLine(" === $groupTitle ===" )
144+ writer.appendLine(" No status entries recorded for this group (${spans.size} spans)" )
145+ writer.appendLine(" " )
146+ return
147+ }
147148
148- statisticalData.forEach { statsData ->
149- val paddedStatus = statsData.statusName.take(48 ).padEnd(48 )
149+ // Calculate group-specific metrics
150+ val totalDurationFormatted = spans.mapNotNull { span ->
151+ val spanStartTime = span.getStartTimeInNanoSeconds()
152+ val spanEndTime = span.getEndTimeInNanoSeconds()
153+ if (spanEndTime > 0 ) {
154+ TimeUnit .NANOSECONDS .toMillis(spanEndTime - spanStartTime)
155+ } else null
156+ }.let { durations ->
157+ if (durations.isNotEmpty()) {
158+ val avgDuration = durations.average().toLong()
159+ " ${avgDuration} ms"
160+ } else {
161+ " N/A"
162+ }
163+ }
150164
151- // Print only the configured metrics
152- metricsToDisplay.forEach { metricType ->
153- val metricLabel = metricType.displayName.padEnd(6 )
154- val sincePrevValue = getMetricValue(statsData.timeSincePreviousStats, metricType).padEnd(19 )
155- val sinceStartValue = getMetricValue(statsData.timeSinceStartStats, metricType).padEnd(16 )
165+ val avgConcurrentSize = spans.map { it.getConcurrentSilentRequestSize() }.average()
166+ val avgConcurrentSizeFormatted = String .format(Locale .US , " %.2f" , avgConcurrentSize)
156167
157- val statusColumn = if (metricType == metricsToDisplay.first()) {
158- paddedStatus
159- } else {
160- " " .padEnd(48 )
161- }
168+ writer.appendLine(" === $groupTitle ===" )
169+ writer.appendLine(" Avg Total Duration: $totalDurationFormatted | Avg Concurrent Size: $avgConcurrentSizeFormatted | Spans: ${spans.size} " )
170+ writer.appendLine(" " )
162171
163- writer.appendLine( " | $statusColumn | $metricLabel | $sincePrevValue | $sinceStartValue |" )
164- }
172+ writer.appendLine( " | Status Entry | Metric | Time Since Previous |" )
173+ writer.appendLine( " |--------------------------------------------------|--------|---------------------| " )
165174
166- // Separator line between status entries
167- writer.appendLine(" |--------------------------------------------------|--------|---------------------|------------------|" )
168- }
175+ statisticalData.forEach { statsData ->
176+ val paddedStatus = statsData.statusName.take(48 ).padEnd(48 )
169177
170- writer.appendLine(" " )
178+ // Print only the configured metrics
179+ metricsToDisplay.forEach { metricType ->
180+ val metricLabel = metricType.displayName.padEnd(6 )
181+ val sincePrevValue = getMetricValue(statsData.timeSincePreviousStats, metricType).padEnd(19 )
171182
172- // Check if any status contains "recordException" and print slowest exceptions
173- val hasExceptions = statisticalData.any { it.statusName == " recordException " }
174- if (hasExceptions) {
175- writeSlowestExceptions(writer, spans )
183+ val statusColumn = if (metricType == metricsToDisplay.first()) {
184+ paddedStatus
185+ } else {
186+ " " .padEnd( 48 )
176187 }
177188
178- writer.flush( )
189+ writer.appendLine( " | $statusColumn | $metricLabel | $sincePrevValue | " )
179190 }
180- } catch (e: IOException ) {
181- Logger .error(TAG , " IOException while writing averaged batch to file: $outputDirectoryAbsolutePath " , e)
191+
192+ // Separator line between status entries
193+ writer.appendLine(" |--------------------------------------------------|--------|---------------------|" )
182194 }
195+
196+ writer.appendLine(" " )
183197 }
184198
199+ /* *
200+ * Write separate statistical tables for each unique exception message
201+ */
202+ private fun writeSpanGroupsByExceptionMessage (writer : FileWriter , spansWithExceptions : List <IBenchmarkSpan >) {
203+ // Group spans by exception message
204+ val spansByExceptionMessage = spansWithExceptions.groupBy { span ->
205+ val exception = span.getException()
206+ val exceptionMessage = exception?.message ? : " Unknown Error"
207+ val exceptionType = exception?.javaClass?.simpleName ? : " Unknown"
208+ " $exceptionType : $exceptionMessage "
209+ }
210+
211+ // Write a table for each exception message group
212+ spansByExceptionMessage.entries.sortedBy { it.key }.forEachIndexed { index, (exceptionMessage, spans) ->
213+ val groupTitle = " ERROR FLOWS - ${exceptionMessage.take(60 )} "
214+ writeSpanGroupStatistics(writer, spans, groupTitle)
215+ }
216+ }
217+
218+
219+
185220 /* *
186221 * Get a file to write the benchmark result to.
187222 * Separate file for each span name.
@@ -206,51 +241,7 @@ class DefaultBenchmarkSpanPrinter(
206241 .takeIf { it.isNotEmpty() } ? : " span"
207242 }
208243
209- /* *
210- * Print the 5 slowest exceptions (if any) in the batch.
211- **/
212- private fun writeSlowestExceptions (writer : FileWriter , spans : List <IBenchmarkSpan >) {
213- // Collect all exception timings across all spans
214- data class ExceptionTiming (val spanIndex : Int , val timeSinceStartMs : Long , val exception : Throwable )
215-
216- val exceptionTimings = mutableListOf<ExceptionTiming >()
217244
218- spans.forEachIndexed { index, span ->
219- val exception = span.getException()
220- if (exception != null ) {
221- val statuses = span.getStatuses()
222- val startTime = span.getStartTimeInNanoSeconds()
223-
224- // Find the recordException status to get the timestamp
225- val exceptionStatus = statuses.find { it.first == " recordException" }
226- if (exceptionStatus != null ) {
227- val timeSinceStartMs = TimeUnit .NANOSECONDS .toMillis(exceptionStatus.second - startTime)
228- exceptionTimings.add(ExceptionTiming (index + 1 , timeSinceStartMs, exception))
229- }
230- }
231- }
232-
233- if (exceptionTimings.isNotEmpty()) {
234- // Sort by time since start (descending) and take top 5
235- val slowestExceptions = exceptionTimings.sortedByDescending { it.timeSinceStartMs }.take(5 )
236-
237- writer.appendLine(" === 5 Slowest Exceptions (Time Since Start) ===" )
238- writer.appendLine(" " )
239- writer.appendLine(" | Rank | Span # | Time Since Start | Exception Type | Message " )
240- writer.appendLine(" |------|--------|------------------|------------------------------------------|------------------------------------------" )
241-
242- slowestExceptions.forEachIndexed { rank, exceptionData ->
243- val rankStr = (rank + 1 ).toString().padEnd(4 )
244- val spanNumStr = exceptionData.spanIndex.toString().padEnd(6 )
245- val timeStr = " ${exceptionData.timeSinceStartMs} ms" .padEnd(16 )
246- val exceptionType = exceptionData.exception.javaClass.simpleName.take(40 ).padEnd(40 )
247- val exceptionMessage = (exceptionData.exception.message ? : " N/A" )
248- writer.appendLine(" | $rankStr | $spanNumStr | $timeStr | $exceptionType | $exceptionMessage " )
249- }
250-
251- writer.appendLine(" " )
252- }
253- }
254245
255246 /* *
256247 * Calculate statistical metrics (average, percentiles) for all status entries across the batch of spans.
@@ -263,73 +254,75 @@ class DefaultBenchmarkSpanPrinter(
263254 * Note: If the same status name appears multiple times, each occurrence is tracked separately
264255 * with an enumeration (e.g., "status [1]", "status [2]").
265256 *
257+ * The "Time Since Previous" column shows the statistical timing for how long each status
258+ * typically takes to execute relative to the previous status within individual spans.
259+ * When aggregated across spans, these represent independent timing measurements.
260+ *
266261 * @param spans List of spans to analyze (all spans should have the same span name)
267262 *
268263 * @return List of statistical data for each status occurrence, sorted by the first configured metric's time since start
269264 */
270265 private fun calculateStatistics (spans : List <IBenchmarkSpan >): List <StatisticalStatusData > {
271266 if (spans.isEmpty()) return emptyList()
272267
273- // Build a map of status position -> (display name, list of timing data)
274- // We need to track each occurrence separately across all spans
275- data class StatusOccurrence (val statusName : String , val occurrenceIndex : Int )
276- val statusOccurrencesMap = mutableMapOf<StatusOccurrence , MutableList <Pair <Long , Long >>>() // Pair<timeSincePrevious, timeSinceStart>
268+ // Use LinkedHashMap to maintain insertion order based on first span's status appearances
269+ val statusOccurrencesMap = linkedMapOf<String , MutableList <Long >>() // timeSincePrevious values
270+
271+ // Process first span to establish the order and which statuses to track
272+ val firstSpan = spans.first()
273+ val firstSpanStatuses = firstSpan.getStatuses()
274+ val firstSpanStatusCounts = mutableMapOf<String , Int >()
275+
276+ // Initialize the map with statuses from the first span to establish order
277+ firstSpanStatuses.forEach { (statusName, _) ->
278+ val spanOccurrenceIndex = firstSpanStatusCounts.getOrDefault(statusName, 0 ) + 1
279+ firstSpanStatusCounts[statusName] = spanOccurrenceIndex
280+ val occurrenceStatusName = getStatusName(spanOccurrenceIndex, statusName)
281+
282+ // Initialize empty list for this status (maintains insertion order)
283+ statusOccurrencesMap[occurrenceStatusName] = mutableListOf ()
284+ }
277285
286+ // Now process all spans (including the first one again) to collect timing data
278287 for (span in spans) {
279288 val statuses = span.getStatuses()
280289 val startTime = span.getStartTimeInNanoSeconds()
281-
282- // Track how many times we've seen each status name in this span
283- val statusCounts = mutableMapOf<String , Int >()
290+ val spanStatusCounts = mutableMapOf<String , Int >()
284291
285292 statuses.forEachIndexed { statusIndex, (statusName, timestamp) ->
286- // Determine which occurrence this is (1st, 2nd, 3rd, etc.)
287- val occurrenceIndex = statusCounts.getOrDefault(statusName, 0 ) + 1
288- statusCounts[statusName] = occurrenceIndex
289-
290- val timeSinceStartMs = TimeUnit .NANOSECONDS .toMillis(timestamp - startTime)
291-
292- val previousTime = if (statusIndex > 0 ) {
293- statuses[statusIndex - 1 ].second
294- } else {
295- startTime
293+ val spanOccurrenceIndex = spanStatusCounts.getOrDefault(statusName, 0 ) + 1
294+ spanStatusCounts[statusName] = spanOccurrenceIndex
295+ val occurrenceStatusName = getStatusName(spanOccurrenceIndex, statusName)
296+
297+ // Only process if this status was in the first span (exists in our map)
298+ statusOccurrencesMap[occurrenceStatusName]?.let {
299+ val previousTime = if (statusIndex > 0 ) {
300+ statuses[statusIndex - 1 ].second
301+ } else {
302+ startTime
303+ }
304+ val timeSincePreviousMs = TimeUnit .NANOSECONDS .toMillis(timestamp - previousTime)
305+ it.add(timeSincePreviousMs)
296306 }
297- val timeSincePreviousMs = TimeUnit .NANOSECONDS .toMillis(timestamp - previousTime)
298-
299- val occurrence = StatusOccurrence (statusName, occurrenceIndex)
300- statusOccurrencesMap.getOrPut(occurrence) { mutableListOf () }
301- .add(Pair (timeSincePreviousMs, timeSinceStartMs))
302307 }
303308 }
304309
305- val result = mutableListOf<StatisticalStatusData >()
306-
307- for ((occurrence, timingPairs) in statusOccurrencesMap) {
308- val timeSincePreviousValues = timingPairs.map { it.first }
309- val timeSinceStartValues = timingPairs.map { it.second }
310-
311- // Create display name with occurrence number if there are multiple occurrences
312- val displayName = if (occurrence.occurrenceIndex > 1 ) {
313- " ${occurrence.statusName} [${occurrence.occurrenceIndex} ]"
314- } else {
315- occurrence.statusName
316- }
317-
318- result.add(
319- StatisticalStatusData (
320- statusName = displayName,
321- timeSinceStartStats = calculateMetrics(timeSinceStartValues),
322- timeSincePreviousStats = calculateMetrics(timeSincePreviousValues)
323- )
310+ // LinkedHashMap maintains insertion order, so no sorting needed
311+ return statusOccurrencesMap.map { (statusName, timingSincePrevsValues) ->
312+ StatisticalStatusData (
313+ statusName = statusName,
314+ timeSincePreviousStats = calculateMetrics(timingSincePrevsValues)
324315 )
325316 }
317+ }
326318
327- // Sort by the first configured metric's Time Since Start value, or by status name if no metrics
328- return if (metricsToDisplay.isNotEmpty() ) {
329- result.sortedBy { it.timeSinceStartStats[metricsToDisplay.first()] ? : 0L }
319+ private fun getStatusName ( spanOccurrenceIndex : Int , statusName : String ): String {
320+ val occurrenceStatusName = if (spanOccurrenceIndex > 1 ) {
321+ " [ $spanOccurrenceIndex ] $statusName "
330322 } else {
331- result.sortedBy { it. statusName }
323+ statusName
332324 }
325+ return occurrenceStatusName
333326 }
334327
335328 private fun calculateMetrics (values : List <Long >): Map <MetricType , Long > {
@@ -383,16 +376,13 @@ class DefaultBenchmarkSpanPrinter(
383376 * Holds statistical data for a single status entry across multiple spans.
384377 *
385378 * This data class aggregates timing metrics for a specific status name,
386- * containing both "time since start" and "time since previous" statistics
387- * calculated across all spans in the batch.
379+ * containing "time since previous" statistics calculated across all spans in the batch.
388380 *
389- * @property statusName The name of the status entry (e.g., "acquireToken", "networkCall")
390- * @property timeSinceStartStats Map of metric type to value (in ms) for time elapsed from span start to this status
391- * @property timeSincePreviousStats Map of metric type to value (in ms) for time elapsed from previous status to this status
381+ * @property statusName The name of the status entry (e.g., "acquireToken", "networkCall")
382+ * @property timeSincePreviousStats Map of metric type to value (in ms) for time elapsed from previous status to this status
392383 */
393384 data class StatisticalStatusData (
394385 val statusName : String ,
395- val timeSinceStartStats : Map <MetricType , Long >,
396386 val timeSincePreviousStats : Map <MetricType , Long >
397387 )
398388
0 commit comments