Files
plezy/lib/utils/text_measure_cache.dart
T
edde746 c79ff68a7b perf(tv): shape spotlight text once per swap instead of twice
Every D-pad step on the TV home ends with a spotlight swap, and on a
low-end box that frame ran over budget: 12 % of UI-thread CPU during
navigation was paragraph layout, most of it shaping strings a second time
to measure them before the render tree shaped them for real.

- FittingTitleText no longer lays the title out to learn whether it fits:
  Text wraps and ellipsizes at maxLines, so the box can only overflow
  vertically, and one cached one-glyph line height settles that. Only a
  box shorter than maxLines lines still searches, now bracketed by the
  proportional shrink of the base layout and stopped at a 0.25 px
  tolerance instead of a fixed 12 bisections.
- FittedMetadataLine and inlineRatingBadgeWidth measure through a small
  bounded cache keyed by text, style, scaler and direction, so separators,
  type labels, certifications, runtimes and years measure once per
  session.
- MediaHub.isContinueWatchingHub/usesContinueWatchingAction memoize their
  regex tokenisation per key; every card build and step re-asked them.

Cold navigation on the Android 14 box (30 steps over three hubs): build
312-316 ms -> 284-285 ms, layout 530-540 ms -> 515 ms, UI frame p90
7.5-7.9 ms -> 6.8-7.2 ms.
2026-09-02 22:38:11 +02:00

42 lines
1.5 KiB
Dart

import 'dart:collection';
import 'package:flutter/painting.dart';
typedef _Key = (String, TextStyle, TextScaler, TextDirection);
final LinkedHashMap<_Key, Size> _sizes = LinkedHashMap<_Key, Size>();
const int _maxEntries = 512;
/// Size of [text] laid out on one unconstrained line, memoized per
/// (text, style, scaler, direction).
///
/// Metadata strips measure every field before building it so they can shed
/// parts that will not fit (#1893), and fitted titles need a line height to
/// know whether a box can overflow at all; each such measurement shapes a
/// string the render tree is about to shape again. Shaping dominates the TV
/// spotlight's info swap on low-end boxes, and the measured strings repeat
/// heavily — separators, "Movie", "TV-14", years, runtimes, "88%" — so a
/// small cache turns the measuring pass into hash lookups. Bounded and
/// insertion-ordered: the oldest entry goes first.
Size cachedSingleLineTextSize(
String text, {
required TextStyle style,
required TextScaler textScaler,
required TextDirection textDirection,
}) {
final key = (text, style, textScaler, textDirection);
final cached = _sizes[key];
if (cached != null) return cached;
final painter = TextPainter(
text: TextSpan(text: text, style: style),
textDirection: textDirection,
textScaler: textScaler,
maxLines: 1,
)..layout();
final size = painter.size;
painter.dispose();
if (_sizes.length >= _maxEntries) _sizes.remove(_sizes.keys.first);
_sizes[key] = size;
return size;
}