Skip to content

tp: add cold sorts by key for sorts which are not hot - #7710

Merged
LalitMaganti merged 1 commit into
mainfrom
dev/lalitm/tp-cold-sort
Oct 12, 2026
Merged

LalitMaganti merged 1 commit into
mainfrom
dev/lalitm/tp-cold-sort

Conversation

@LalitMaganti

@LalitMaganti LalitMaganti commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds ColdSortByKey and ColdStableSortByKey, replacements for std::sort and std::stable_sort which sort by an integer key through one sort compiled once rather than once per call site, and moves the sorts which are not hot over to them. This saves 110KB of trace_processor_shell's code on Linux and 147KB of the Wasm module.

Why

std::sort and std::stable_sort are templates: every element type and comparator gets its own copy, at 3-4KB each. Most of TP's sorts run once per query or once per trace on a handful of elements, where that code buys nothing.

Changes

  • util/cold_sort.h: each caller extracts an integer key per element into {key, index} pairs, which one std::sort compiled in cold_sort.cc orders, then moves the elements into place by following the permutation's cycles. Ties break on the index, so the sort is stable; ColdSortByKey and ColdStableSortByKey share it, and the names record whether a caller relies on that. ...Descending variants sort the other way. Up to 64 elements the pairs live on the stack, so small sorts never allocate.
  • Moved over: query planning's filter order, the PerfettoSQL parser, SQL rewrites, trace type detection, system probes' CPU info, perf feature sections, CPU per UID, the ninja log, the symbolizer's report, metatrace args, SurfaceFlinger layer order, interval intersect's operand order, JSON export's async end events, Android logs and the memory graph's owners.
    • Perf feature sections (offset, then size, both descending) and memory graph owners (priority descending, then size) are two passes, the second stable.
    • SurfaceFlinger's (z, id) is packed into one 64-bit key.
  • Hot sorts stay on std::sort: the executor and interpreter, the dataframe sort, the trace sorter, interval intersect's interval sort, the flamegraph, slice layout and heap graphs.

Example

// Before: a copy of std::sort for this lambda.
std::sort(items.begin(), items.end(), [](const Item& a, const Item& b) {
  return a.offset < b.offset;
});
// After: a key extraction calling the shared sort.
ColdSortByKey(items.begin(), items.end(),
              [](const Item& item) { return item.offset; });

Benchmarks

Size, release builds:

Before After
trace_processor_shell .text, Linux x86-64 9,434,514 bytes 9,324,850 bytes (-109,664, -1.16%)
trace_processor.wasm 14,235,488 bytes 14,088,363 bytes (-147,125, -1.03%)

Per call site, one {int64, uint32} sort by a lambda at -O2 on x86-64:

Code
std::sort 4.4 KB
std::stable_sort 2.8 KB
ColdSortByKey 0.45 KB, plus 4 KB shared once

Speed on Linux x86-64, sorting by an int64 field:

Elements 12-byte elements: std::sort / by key 64-byte elements: std::sort / by key
3 4.8 / 10 ns 6.5 / 12 ns
16 39 / 76 ns 70 / 82 ns
1,000 5.2 / 10.4 us 10.0 / 12.4 us
100,000 5.2 / 7.4 ms 5.5 / 7.2 ms

Mocks of each moved call site, with its element type, key and comparator, show the sorts whose comparators did lookups or copies getting faster: JSON export 6-9x, CPU infos 7-20x, SurfaceFlinger 2.6-4x, trace type detection and the symbolizer 2.3x. The largest slowdown is Android logs, 1.7ms per 100k lines.

Testing

New ColdSortTests check against std::stable_sort across sizes, including both sides of the stack storage threshold, and descending order, signed key extremes and move-only elements. Unit tests of the moved call sites and the full diff test suite pass.

@LalitMaganti
LalitMaganti requested a review from a team as a code owner September 30, 2026 22:24
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

@LalitMaganti
LalitMaganti force-pushed the dev/lalitm/tp-cold-sort branch from 33b2d57 to 5e2f876 Compare September 30, 2026 22:52
@LalitMaganti
LalitMaganti requested review from Vibhu-Agarwal and sashwinbalaji and removed request for sashwinbalaji September 30, 2026 22:55

@Vibhu-Agarwal Vibhu-Agarwal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The index-sort + captureless-thunk trick is neat. I went through all the call sites asking "how often, and with how many elements"; all look cold except the two interval-intersect ones (question inline).

Two things on the description:

  • It lists the symbolizer among the moved sites, but there's no symbolizer change in the diff.
  • Since the motivation is size, the Wasm delta might be a good number to quote, as that's where TP's code size is felt most.

Comment thread src/trace_processor/core/exec/interval_intersect.cc Outdated
Comment thread src/trace_processor/util/cold_sort_unittest.cc
Comment thread src/trace_processor/util/cold_sort.h Outdated
@LalitMaganti
LalitMaganti force-pushed the dev/lalitm/tp-cold-sort branch from 5e2f876 to fe2294e Compare October 9, 2026 19:44
@LalitMaganti

Copy link
Copy Markdown
Member Author

Thanks! Rather than reverting the interval intersect sites, I made ColdSort allocation-free for small inputs, so a 3-element sort is now 13ns vs 6ns for std::sort. Addressed the rest too.

@LalitMaganti
LalitMaganti force-pushed the dev/lalitm/tp-cold-sort branch from fe2294e to 8b5cb9a Compare October 9, 2026 19:50
std::sort and std::stable_sort are compiled for every element type and
comparator, at 3-4KB each, even where the sort runs once per query or
per trace on a handful of elements.

ColdSortByKey and ColdStableSortByKey sort by an integer key through one
std::sort of {key, index} pairs, compiled once. Each caller adds a few
hundred bytes. Comparing integers inline keeps them within 2x of
std::sort, and faster where the comparator did lookups or copies.

Moves the sorts in query planning, the PerfettoSQL parser, SQL rewrites,
trace type detection, system probes, perf feature sections, CPU per UID,
the ninja log, the symbolizer, metatrace args, SurfaceFlinger layers,
interval intersect's operand order, JSON export, Android logs and the
memory graph over. This saves 110KB of trace_processor_shell's code on
Linux and 147KB of the Wasm module.
@LalitMaganti
LalitMaganti force-pushed the dev/lalitm/tp-cold-sort branch from 8b5cb9a to a6e65a7 Compare October 9, 2026 20:57
@LalitMaganti LalitMaganti changed the title tp: add ColdSort for sorts which are not hot tp: add cold sorts by key for sorts which are not hot Oct 9, 2026
@LalitMaganti

LalitMaganti commented Oct 9, 2026 •

Copy link
Copy Markdown
Member Author

FYI I reworked thi a bit since your last look: instead of calling the comparator through a function pointer, callers now pass an integer key and there's a single shared std::sort on {key, index} pairs. It ends up both smaller (110KB on Linux, 147KB on Wasm) and a lot faster than the previous version.

I tested every callsite against its original sort: most are the same or faster, and the worst case is ~1.7ms extra per 100k Android log lines, which I think is fine.

@Vibhu-Agarwal Vibhu-Agarwal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The by-key version is both simpler to read and better on every number in the description; nice

Comment thread src/trace_processor/util/cold_sort.h
@LalitMaganti
LalitMaganti merged commit 5f89265 into main Oct 12, 2026
29 of 30 checks passed
@LalitMaganti
LalitMaganti deleted the dev/lalitm/tp-cold-sort branch October 12, 2026 14:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants