Esta página ainda não foi traduzida, por isso é exibida em inglês. Ajude a traduzir

Most frequent log level

FxDart wins

Requirement

Given a slice of application logs, count how many entries each level (INFO / WARN / ERROR) has and print the most frequent one with its count. The data is in the code below; both versions must print the line shown under Expected output.

Expected output
Most frequent level: WARN (4 of 9)

Side by side

Native Dart

FxDart

Why they differ

Native Dart has no countBy: the closest is package:collection's groupListsBy, which builds a list of every entry per level just so you can take the lengths — or a hand-written Map.update loop. Picking the winner then needs a reduce with an explicit comparison. FxDart names both steps: countBy goes straight to the counts (it's terminal — it returns a plain Map), and fx(counts.entries).maxBy(...) re-enters the chain to pick the largest entry. Two named ideas instead of two hand-built ones.

Where the time actually goes

Counting is almost pure hash-map work, so this case is really a measurement of how many times each element touches the map. Broken down by cost per element at N=1,000,000:

what the loop doesns per element
walk the list0.3
+ load the .level field0.7
+ hash it1.7
+ count with a switch into four locals12.5
+ one map probe20.9
+ two map probes29.3

Traversal and the key extractor are free — under 1 ns between them. The map is everything. And the obvious hand-written line, counts[k] = (counts[k] ?? 0) + 1, probes the map twice: once to read, once to write back. That second probe is about 30% of the runtime, and it is the reason a named operator can beat the loop you would have written. Since 0.8.4 countBy counts into a mutable cell parked in the map, so the read hands back the cell and the increment goes through that reference — the map is written once per distinct level instead of once per entry.

Why the benchmark crosses over

Here is the same case swept across four scales, with the third implementation the section above mentions but does not chart — a hand-written counting loop, which is what you would write if you were not reaching for package:collection at all.

NgroupListsByhand loop FxDartvs groupListsByvs hand loop
10,000351 µs291 µs199 µs 1.76× faster1.46× faster
100,0003.6 ms2.9 ms2.0 ms 1.83× faster1.47× faster
400,00018.2 ms11.6 ms7.8 ms 2.32× faster1.48× faster
1,000,00044.5 ms28.8 ms19.4 ms 2.30× faster1.48× faster

Read the last column first, because it is the one that does not move: against a hand-written loop FxDart is ~1.47× faster at every scale, from ten thousand entries to a million. That constant is the single map probe from the section above — the operator can afford a trick that is too fiddly to be worth hand-writing, and it pays the same dividend at every N.

This page used to say the opposite. Before 0.8.4, countBy did the same two probes as the loop plus the chain's overhead, and the honest number here was ~1.4× slower at every scale. Only the FxDart column moved: re-measured on the same machine, groupListsBy and the hand loop land within 2% of their old figures.

The groupListsBy column widens on top of that, and the memory column is where that shows:

NgroupListsByhand loopFxDart
10,00018.8 MB14.1 MB14.2 MB
100,00033.8 MB16.1 MB16.2 MB
400,00059.8 MB24.7 MB24.7 MB
1,000,00088.8 MB44.8 MB44.8 MB

countBy and the hand loop hold the same memory — within 0.1 MB at every scale — because both keep four counters and nothing else. groupListsBy materialises every one of the million entries into per-level Lists just to take their lengths, and by N=1,000,000 that is 44 MB of garbage it has to allocate and the collector has to walk.

That tax is also what makes it erratic. Across 25 samples at N=1,000,000, groupListsBy ranged 38.8–50.1 ms — an 11 ms spread — while FxDart ranged 19.1–20.3 ms and the hand loop 27.9–30.4 ms. Its slow samples are collections the other two never trigger. So its gap is part pipeline and part garbage; the other two columns are just pipeline.

The bar chart above still reads tie at N=10,000 even though FxDart is comfortably ahead there, because 365 µs against 191 µs is 174 µs — real, but under the harness's 0.6 ms floor. Nobody perceives 174 µs, so the badge refuses to claim a win.

The fair summary, then: countBy gives you a hand loop's memory profile and beats a hand loop's time by ~1.47×, with a named operator's readability. It is a rare case where the library version is simply the better choice on every axis — and the reason is not clever compilation, it is that the operator only has to be written carefully once.

Method: measured on the machine named in the Benchmark section — 5 interleaved rounds × 5 measured iterations = 25 samples per implementation per scale, AOT-compiled, a fresh process per sample, medians reported. All three implementations return an identical checksum at every scale.

Benchmark

Apple M1 Max, 32 GB RAM · Dart 3.12.2 (AOT-compiled) · 2026-08-24

N = 100

Time Tie

Native Dart 3.9 µs
FxDart 2.1 µs

Peak memory Tie

Native Dart 16.5 MB
FxDart 16.5 MB

N = 10,000

Time Tie

Native Dart 353 µs
FxDart 193 µs

Peak memory FxDart wins

Native Dart 20.0 MB
FxDart 14.2 MB

N = 1,000,000

Time FxDart wins

Native Dart 44.8 ms
FxDart 19.7 ms

Peak memory FxDart wins

Native Dart 85.7 MB
FxDart 44.9 MB

Bars are medians of repeated timed iterations in fresh processes per side (small N is batched for timer resolution). Sides within 5% of each other — or within 0.6 ms, a difference no person can perceive — count as a tie; close relative races are re-measured up to 5 runs. In an app, anything under a few milliseconds is invisible to the user regardless of which bar is shorter. Memory is peak process RSS. The Dart VM and the dataset are identical on both sides, so the difference between the two bars is what the pipeline itself holds onto.