@@ -106,80 +106,97 @@ 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
125+ if (spansWithExceptions.isNotEmpty()) {
126+ writeSpanGroupStatistics(writer, spansWithExceptions, " ERROR FLOWS (With Exceptions) " )
140127
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( " " )
128+ // Also write the slowest exceptions for this group
129+ writeSlowestExceptions(writer, spansWithExceptions )
130+ }
144131
145- writer.appendLine(" | Status Entry | Metric | Time Since Previous | Time Since Start |" )
146- writer.appendLine(" |--------------------------------------------------|--------|---------------------|------------------|" )
132+ writer.flush()
133+ }
134+ } catch (e: IOException ) {
135+ Logger .error(TAG , " IOException while writing averaged batch to file: $outputDirectoryAbsolutePath " , e)
136+ }
137+ }
147138
148- statisticalData.forEach { statsData ->
149- val paddedStatus = statsData.statusName.take(48 ).padEnd(48 )
139+ /* *
140+ * Write statistical data for a specific group of spans
141+ */
142+ private fun writeSpanGroupStatistics (writer : FileWriter , spans : List <IBenchmarkSpan >, groupTitle : String ) {
143+ val statisticalData = calculateStatistics(spans)
150144
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 )
145+ if (statisticalData.isEmpty()) {
146+ writer.appendLine(" === $groupTitle ===" )
147+ writer.appendLine(" No status entries recorded for this group (${spans.size} spans)" )
148+ writer.appendLine(" " )
149+ return
150+ }
156151
157- val statusColumn = if (metricType == metricsToDisplay.first()) {
158- paddedStatus
159- } else {
160- " " .padEnd(48 )
161- }
152+ // Calculate group-specific metrics
153+ val totalDurationFormatted = spans.mapNotNull { span ->
154+ val spanStartTime = span.getStartTimeInNanoSeconds()
155+ val spanEndTime = span.getEndTimeInNanoSeconds()
156+ if (spanEndTime > 0 ) {
157+ TimeUnit .NANOSECONDS .toMillis(spanEndTime - spanStartTime)
158+ } else null
159+ }.let { durations ->
160+ if (durations.isNotEmpty()) {
161+ val avgDuration = durations.average().toLong()
162+ " ${avgDuration} ms"
163+ } else {
164+ " N/A"
165+ }
166+ }
162167
163- writer.appendLine( " | $statusColumn | $metricLabel | $sincePrevValue | $sinceStartValue | " )
164- }
168+ val avgConcurrentSize = spans.map { it.getConcurrentSilentRequestSize() }.average( )
169+ val avgConcurrentSizeFormatted = String .format( Locale . US , " %.2f " , avgConcurrentSize)
165170
166- // Separator line between status entries
167- writer.appendLine( " |--------------------------------------------------|--------|---------------------|------------------| " )
168- }
171+ writer.appendLine( " === $groupTitle === " )
172+ writer.appendLine( " Avg Total Duration: $totalDurationFormatted | Avg Concurrent Size: $avgConcurrentSizeFormatted | Spans: ${spans.size} " )
173+ writer.appendLine( " " )
169174
170- writer.appendLine(" " )
175+ writer.appendLine(" | Status Entry | Metric | Time Since Previous |" )
176+ writer.appendLine(" |--------------------------------------------------|--------|---------------------|" )
177+
178+ statisticalData.forEach { statsData ->
179+ val paddedStatus = statsData.statusName.take(48 ).padEnd(48 )
171180
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)
181+ // Print only the configured metrics
182+ metricsToDisplay.forEach { metricType ->
183+ val metricLabel = metricType.displayName.padEnd(6 )
184+ val sincePrevValue = getMetricValue(statsData.timeSincePreviousStats, metricType).padEnd(19 )
185+
186+ val statusColumn = if (metricType == metricsToDisplay.first()) {
187+ paddedStatus
188+ } else {
189+ " " .padEnd(48 )
176190 }
177191
178- writer.flush( )
192+ writer.appendLine( " | $statusColumn | $metricLabel | $sincePrevValue | " )
179193 }
180- } catch (e: IOException ) {
181- Logger .error(TAG , " IOException while writing averaged batch to file: $outputDirectoryAbsolutePath " , e)
194+
195+ // Separator line between status entries
196+ writer.appendLine(" |--------------------------------------------------|--------|---------------------|" )
182197 }
198+
199+ writer.appendLine(" " )
183200 }
184201
185202 /* *
@@ -263,31 +280,42 @@ class DefaultBenchmarkSpanPrinter(
263280 * Note: If the same status name appears multiple times, each occurrence is tracked separately
264281 * with an enumeration (e.g., "status [1]", "status [2]").
265282 *
283+ * The "Time Since Previous" column shows the statistical timing for how long each status
284+ * typically takes to execute relative to the previous status within individual spans.
285+ * When aggregated across spans, these represent independent timing measurements.
286+ *
266287 * @param spans List of spans to analyze (all spans should have the same span name)
267288 *
268289 * @return List of statistical data for each status occurrence, sorted by the first configured metric's time since start
269290 */
270291 private fun calculateStatistics (spans : List <IBenchmarkSpan >): List <StatisticalStatusData > {
271292 if (spans.isEmpty()) return emptyList()
272293
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>
294+ // Build a map of status name -> list of timing data
295+ // Status names are already occurrence-specific (e.g., "[2] SetStatus:ERROR" for second occurrence)
296+ val statusOccurrencesMap = mutableMapOf<String , MutableList <Long >>() // timeSincePrevious values
297+
298+ // Track the chronological positions of each status occurrence across spans
299+ val statusPositions = mutableMapOf<String , MutableList <Int >>()
277300
278301 for (span in spans) {
279302 val statuses = span.getStatuses()
280303 val startTime = span.getStartTimeInNanoSeconds()
281304
282- // Track how many times we've seen each status name in this span
283- val statusCounts = mutableMapOf<String , Int >()
305+ // Track how many times we've seen each status name in this specific span
306+ val spanStatusCounts = mutableMapOf<String , Int >()
284307
285308 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
309+ // Determine which occurrence this is within this span (1st, 2nd, 3rd, etc.)
310+ val spanOccurrenceIndex = spanStatusCounts .getOrDefault(statusName, 0 ) + 1
311+ spanStatusCounts [statusName] = spanOccurrenceIndex
289312
290- val timeSinceStartMs = TimeUnit .NANOSECONDS .toMillis(timestamp - startTime)
313+ // Create the occurrence-specific status name if this is not the first occurrence
314+ val occurrenceStatusName = if (spanOccurrenceIndex > 1 ) {
315+ " [$spanOccurrenceIndex ] $statusName "
316+ } else {
317+ statusName
318+ }
291319
292320 val previousTime = if (statusIndex > 0 ) {
293321 statuses[statusIndex - 1 ].second
@@ -296,40 +324,37 @@ class DefaultBenchmarkSpanPrinter(
296324 }
297325 val timeSincePreviousMs = TimeUnit .NANOSECONDS .toMillis(timestamp - previousTime)
298326
299- val occurrence = StatusOccurrence (statusName, occurrenceIndex)
300- statusOccurrencesMap.getOrPut(occurrence) { mutableListOf () }
301- .add(Pair (timeSincePreviousMs, timeSinceStartMs))
327+ // Use the occurrence-specific status name as the key (no need for StatusOccurrence data class)
328+ statusOccurrencesMap.getOrPut(occurrenceStatusName) { mutableListOf () }
329+ .add(timeSincePreviousMs)
330+
331+ // Track the position of this status occurrence within the span
332+ statusPositions.getOrPut(occurrenceStatusName) { mutableListOf () }.add(statusIndex)
302333 }
303334 }
304335
305336 val result = mutableListOf<StatisticalStatusData >()
306337
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} ]"
338+ for ((statusName, timingSincePrevsValues) in statusOccurrencesMap) {
339+ // Calculate median chronological position for sorting
340+ val positions = statusPositions[statusName] ? : emptyList()
341+ val medianPosition = if (positions.isNotEmpty()) {
342+ positions.sorted()[positions.size / 2 ]
314343 } else {
315- occurrence.statusName
344+ Int . MAX_VALUE
316345 }
317346
318347 result.add(
319348 StatisticalStatusData (
320- statusName = displayName ,
321- timeSinceStartStats = calculateMetrics(timeSinceStartValues ),
322- timeSincePreviousStats = calculateMetrics(timeSincePreviousValues)
349+ statusName = statusName ,
350+ timeSincePreviousStats = calculateMetrics(timingSincePrevsValues ),
351+ medianChronologicalPosition = medianPosition
323352 )
324353 )
325354 }
326355
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 }
330- } else {
331- result.sortedBy { it.statusName }
332- }
356+ // Sort by chronological order (median position within spans), then by status name for ties
357+ return result.sortedWith(compareBy({ it.medianChronologicalPosition }, { it.statusName }))
333358 }
334359
335360 private fun calculateMetrics (values : List <Long >): Map <MetricType , Long > {
@@ -383,17 +408,16 @@ class DefaultBenchmarkSpanPrinter(
383408 * Holds statistical data for a single status entry across multiple spans.
384409 *
385410 * 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.
411+ * containing "time since previous" statistics calculated across all spans in the batch.
388412 *
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
413+ * @property statusName The name of the status entry (e.g., "acquireToken", "networkCall")
414+ * @property timeSincePreviousStats Map of metric type to value (in ms) for time elapsed from previous status to this status
415+ * @property medianChronologicalPosition The median position of this status within spans, used for chronological sorting
392416 */
393417 data class StatisticalStatusData (
394418 val statusName : String ,
395- val timeSinceStartStats : Map <MetricType , Long >,
396- val timeSincePreviousStats : Map < MetricType , Long >
419+ val timeSincePreviousStats : Map <MetricType , Long >,
420+ val medianChronologicalPosition : Int = Int . MAX_VALUE
397421 )
398422
399423 /* *
0 commit comments