You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Original article and executable demonstration by JR, creator of Telemetry. Prepared with AI assistance. Fictional data; no account, network request, credential, SDK or hosted integration.
Tested locally on Java 15.0.1. The compiler was run with a 384 MiB heap and two active processors; the executable used a 96 MiB heap. Sixteen checks passed. No claim this is production telemetry code or a performance benchmark.
The reducer assumes non-overlapping batches with consistent units and population definitions. It does not provide deduplication, windowing, percentiles or thread safety. Article submission and publication are separate actions recorded outside this package.
Permission: You may copy and adapt the original article and code with attribution to JR and retention of the Telemetry relationship and AI-assistance disclosure. Linked external documentation remains governed by its own terms.
Kotlin grouped summaries: two ways to get a plausible wrong average
By JR, creator of Telemetry, a tool for querying event data with SQL. Prepared with AI assistance; the accompanying Kotlin example was compiled and tested locally. All measurements below are fictional. No service or account is needed.
An average can look reasonable while answering the wrong question. Consider three summaries produced by workers:
Service
Measurements
Total elapsed milliseconds
api
1
1,000
ingest
2
60
api
99
990
For api, there are 100 measurements totaling 1,990 milliseconds. Its mean is 19.9 milliseconds. The ingest mean is 30 milliseconds. A small Kotlin grouping expression can lose both answers before a dashboard ever sees the data.
Mistake one: one mutable seed, several keys
This looks like a compact way to aggregate the rows:
There is only one MutableSummary() expression. Both keys end up referring to that same object. With the fixture above, each entry reports 102 measurements and 2,050 milliseconds. Checking only that result contains two keys would miss the problem.
That assertion is supposed to pass for this deliberately broken implementation. It is evidence of the problem, not a correctness check for a production report.
Kotlin documents that the fixed-initial-value overload supplies the same initial value to each group. It also offers an overload that calculates an initial value for each key. Official Grouping API reference.
Sharing the initial immutable zero value is harmless here. Every update returns a fresh summary; it does not change the seed or another group's current result. This version is easy to inspect, although it allocates an object for each update. The example makes no performance claim about that tradeoff.
Math.addExact is deliberate. A wrapped negative total would still be a valid Long, but it would be an invalid measurement summary. Here, overflow fails explicitly. A real reporting pipeline must decide how to surface that failure rather than silently continuing with corrupted numbers.
The factory starts each group at zero. The operation then adds its first batch, just as it adds subsequent batches. Initializing the seed with the first batch's numbers and then adding that batch again would double-count it. The single-batch test protects that boundary.
These examples process a list sequentially. A separate mutable accumulator per key does not make a map safe for concurrent mutation, and the code is not a streaming-window implementation.
Mistake two: averaging the batch averages
Even after fixing the grouping, this calculation is wrong for a per-measurement mean:
(1000 / 1 + 990 / 99) / 2 = 505 ms
It gives the one-measurement batch as much influence as the 99-measurement batch. Retaining both counts and totals lets us calculate the requested statistic:
(1000 + 990) / (1 + 99) = 19.9 ms
A mean of means can be intentional when the question gives every worker or batch equal weight. Label it that way. It is not interchangeable with the mean duration of all measurements.
Check the data contract too
The runnable example rejects blank service names, negative counts and negative totals. A batch with zero measurements must have a zero total. Its mean is absent, represented by null. Two measured durations totaling zero have a mean of 0.0; they are different data.
The fixture assumes that batches contain disjoint observations, use the same unit and describe the same reporting population. Repeated delivery of a batch violates that contract. This reducer does not deduplicate, align time windows or distinguish failed requests from successful ones. Those decisions belong upstream and should be explicit before totals are combined.
Totals stay in integer milliseconds until the final division. The displayed Double mean is approximate, especially at very large magnitudes. The code does not calculate percentiles: a count and total alone cannot recover a latency distribution.
Run the checks
The companion Summaries.kt uses only Kotlin's standard library and JVM APIs:
The local run used Kotlin/JVM 2.4.20 and Java 15.0.1. Its 16 checks cover the shared-reference failure, both fixes, a one-batch group, unequal batch sizes, empty input, zero observations, zero duration, invalid data and arithmetic overflow. They also verify that reordering this fixture and splitting its 99-measurement batch preserve the correct result.
Those are useful properties to test before sending summaries to any reporting tool. A dashboard can display the fields it receives perfectly and still display the wrong answer.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters