Skip to content
This repository was archived by the owner on Jan 31, 2025. It is now read-only.

Commit 87775a6

Browse files
authored
Optimize rendering of text for timers. (#4724)
Previously the fitting of the text into the boxes for the timers took too long. Root cause for this is a fairly complicated computation for determining the width of a rendered text. This seems to be used inside the method to elide text as well. This PR replaces the QFontMetrics::elidedText and QFontMetrics::horizontalAdvance by a simplified, less accurate method. We compute a lookup table storing the rendered width of all ascii characters. The width is computed as the sum of the width of all characters in a string (plus a heuristic to obtain an upper bound of the actual width). The method to elide text is is implemented on basis of this simplified width calculation. Compare #4626.
1 parent ac88ecb commit 87775a6

File tree

5 files changed

+150
-54
lines changed

5 files changed

+150
-54
lines changed

src/OrbitGl/QtTextRenderer.cpp

Lines changed: 120 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <GteVector.h>
88
#include <absl/meta/type_traits.h>
99

10+
#include <QChar>
1011
#include <QColor>
1112
#include <QFont>
1213
#include <QFontDatabase>
@@ -18,6 +19,7 @@
1819
#include <algorithm>
1920
#include <cfloat>
2021
#include <cmath>
22+
#include <cstdint>
2123
#include <string>
2224

2325
#include "Introspection/Introspection.h"
@@ -29,7 +31,19 @@ namespace orbit_gl {
2931

3032
namespace {
3133

32-
float GetYOffsetFromAlignment(QtTextRenderer::VAlign alignment, float height) {
34+
// Qt offers a QFontMetrics::horizontalAdvance to determine the width of a rendered string. This
35+
// method is fairly slow. For rendering the text in the timers we therefore use a different method:
36+
// We compute a lookup table storing the rendered width of all the characters and sum over all the
37+
// characters in a string (compare GetCharacterWidthLookup, GetStringWidthFast below). The result is
38+
// consistently a bit shorter than the correct result provided by QFontMetrics::horizontalAdvance.
39+
// Applying the heuristic below to the result from the lookup reliably yields a fairly tight upper
40+
// bound for the true width of the rendered string. Don't try to make sense of the formula - it is
41+
// just a line fitted to example data.
42+
[[nodiscard]] int MaximumHeuristic(int width, int length, uint32_t font_size) {
43+
return 2 + (length * static_cast<int>(font_size)) / (12 * 14) + width;
44+
}
45+
46+
[[nodiscard]] float GetYOffsetFromAlignment(QtTextRenderer::VAlign alignment, float height) {
3347
switch (alignment) {
3448
case QtTextRenderer::VAlign::Top:
3549
return 0.f;
@@ -47,7 +61,7 @@ float GetYOffsetFromAlignment(QtTextRenderer::VAlign alignment, float height) {
4761
}
4862
}
4963

50-
float GetXOffsetFromAlignment(QtTextRenderer::HAlign alignment, float width) {
64+
[[nodiscard]] float GetXOffsetFromAlignment(QtTextRenderer::HAlign alignment, float width) {
5165
// kOffset is a hack to compensate for subtle differences in the placement of the rendered text
5266
// under Linux and Windows. Setting kOffset == 0 under Windows results in texts starting left of
5367
// the interval border for unknown reasons (also see https://github.com/google/orbit/issues/4627).
@@ -126,7 +140,7 @@ void QtTextRenderer::AddText(const char* text, float x, float y, float z, TextFo
126140
font.setPixelSize(formatting.font_size);
127141
QFontMetrics metrics(font);
128142
float y_offset = GetYOffsetFromAlignment(formatting.valign, height_entire_text);
129-
const float single_line_height = GetStringHeight(".", formatting.font_size);
143+
const float single_line_height = GetSingleLineStringHeight(formatting.font_size);
130144
QStringList lines = text_as_qstring.split("\n");
131145
float max_line_width = 0.f;
132146
for (const auto& line : lines) {
@@ -156,9 +170,13 @@ void QtTextRenderer::AddText(const char* text, float x, float y, float z, TextFo
156170
float QtTextRenderer::AddTextTrailingCharsPrioritized(const char* text, float x, float y, float z,
157171
TextFormatting formatting,
158172
size_t trailing_chars_length) {
159-
ORBIT_SCOPE_FUNCTION;
160-
QString text_as_qstring(text);
161-
const size_t text_length = text_as_qstring.length();
173+
// Early-out: If we can't fit a single char, there's no use to do all the expensive
174+
// calculations below - this is a major bottleneck in some cases
175+
if (formatting.max_size >= 0 && GetMinimumTextWidth(formatting.font_size) > formatting.max_size) {
176+
return 0.f;
177+
}
178+
179+
const size_t text_length = std::strlen(text);
162180
if (text_length == 0) {
163181
return 0.f;
164182
}
@@ -169,40 +187,44 @@ float QtTextRenderer::AddTextTrailingCharsPrioritized(const char* text, float x,
169187
text, trailing_chars_length);
170188
trailing_chars_length = text_length;
171189
}
172-
// Early-out: If we can't fit a single char, there's no use to do all the expensive
173-
// calculations below - this is a major bottleneck in some cases
174-
if (formatting.max_size >= 0 && GetMinimumTextWidth(formatting.font_size) > formatting.max_size) {
175-
return 0.f;
176-
}
177190

178191
const float max_width =
179192
formatting.max_size == -1.f ? FLT_MAX : viewport_->WorldToScreen({formatting.max_size, 0})[0];
193+
const QString text_as_qstring(text);
194+
const CharacterWidthLookup& lookup = GetCharacterWidthLookup(formatting.font_size);
180195
const QString trailing_text = text_as_qstring.right(trailing_chars_length);
181-
const QString leading_text = text_as_qstring.left(text_length - trailing_chars_length);
182-
QFont font = QFontDatabase::systemFont(QFontDatabase::GeneralFont);
183-
font.setPixelSize(formatting.font_size);
184-
QFontMetrics metrics(font);
185-
const float trailing_text_width = GetStringWidth(trailing_text, formatting.font_size);
186-
QString elided_text;
196+
const float trailing_text_width = GetStringWidthFast(trailing_text, lookup, formatting.font_size);
197+
// If the trailing text fits we (potentially) elide the leading text.
187198
if (trailing_text_width < max_width) {
188-
elided_text = metrics.elidedText(leading_text, Qt::ElideRight,
189-
static_cast<int>(max_width - trailing_text_width));
199+
const QString leading_text = text_as_qstring.left(text_length - trailing_chars_length);
200+
const QString elided_text =
201+
ElideText(leading_text, static_cast<int>(max_width - trailing_text_width), lookup,
202+
formatting.font_size);
203+
return AddFittingSingleLineText(elided_text + trailing_text, x, y, z, formatting, lookup);
190204
}
205+
// If the trailing text doesn't fit we simply elide the entire text (the trailing text is not
206+
// preserved in this case).
207+
const QString elided_text =
208+
ElideText(text_as_qstring, static_cast<int>(max_width), lookup, formatting.font_size);
191209
if (elided_text.isEmpty()) {
192-
// We can't fit any elided string with the trailing characters preserved so we elide the entire
193-
// string and accept that the trailing characters are truncated.
194-
elided_text = metrics.elidedText(text_as_qstring, Qt::ElideRight, static_cast<int>(max_width));
195-
if (elided_text.isEmpty()) {
196-
return 0.f;
197-
}
198-
return AddFittingSingleLineText(elided_text, x, y, z, formatting);
210+
return 0.f;
199211
}
200-
return AddFittingSingleLineText(elided_text + trailing_text, x, y, z, formatting);
212+
return AddFittingSingleLineText(elided_text, x, y, z, formatting, lookup);
201213
}
202214

203215
float QtTextRenderer::GetStringWidth(const char* text, uint32_t font_size) {
204216
QString text_as_qstring(text);
205-
QStringList lines = text_as_qstring.split("\n");
217+
return GetStringWidth(text_as_qstring, font_size);
218+
}
219+
220+
float QtTextRenderer::GetStringHeight(const char* text, uint32_t font_size) {
221+
QString text_as_qstring(text);
222+
int number_of_lines = text_as_qstring.count('\n') + 1;
223+
return static_cast<float>(number_of_lines) * GetSingleLineStringHeight(font_size);
224+
}
225+
226+
float QtTextRenderer::GetStringWidth(const QString& text, uint32_t font_size) {
227+
QStringList lines = text.split("\n");
206228
float max_width = 0.f;
207229
QFont font = QFontDatabase::systemFont(QFontDatabase::GeneralFont);
208230
font.setPixelSize(static_cast<int>(font_size));
@@ -214,38 +236,87 @@ float QtTextRenderer::GetStringWidth(const char* text, uint32_t font_size) {
214236
return max_width;
215237
}
216238

217-
float QtTextRenderer::GetStringHeight(const char* text, uint32_t font_size) {
218-
QString text_as_qstring(text);
219-
QStringList lines = text_as_qstring.split("\n");
220-
const float number_of_lines = lines.size();
221-
QFont font = QFontDatabase::systemFont(QFontDatabase::GeneralFont);
222-
font.setPixelSize(static_cast<int>(font_size));
223-
QFontMetrics metrics(font);
224-
return number_of_lines * viewport_->ScreenToWorld(Vec2i(0, metrics.height()))[1];
225-
}
226-
227-
float QtTextRenderer::GetStringWidth(const QString& text, uint32_t font_size) {
228-
std::string text_as_string = text.toStdString();
229-
return GetStringWidth(text_as_string.c_str(), font_size);
230-
}
231-
232239
float QtTextRenderer::GetMinimumTextWidth(uint32_t font_size) {
233240
auto minimum_string_width_it = minimum_string_width_cache_.find(font_size);
234241
if (minimum_string_width_it != minimum_string_width_cache_.end()) {
235242
return minimum_string_width_it->second;
236243
}
237-
// Only if we can fit one wide (hence the "W") character plus the ellipsis dots we start rendering
238-
// text. Otherwise we leave the space empty.
239-
constexpr char const* kMinimumString = "W...";
244+
// Only if we can fit one wide (hence the "W") character we start rendering text. Otherwise we
245+
// leave the space empty.
246+
constexpr char const* kMinimumString = "W";
240247
const float width = GetStringWidth(kMinimumString, font_size);
241248
minimum_string_width_cache_[font_size] = width;
242249
return width;
243250
}
244251

252+
float QtTextRenderer::GetSingleLineStringHeight(uint32_t font_size) {
253+
int metrics_height = 0;
254+
auto it = single_line_height_cache_.find(font_size);
255+
if (it == single_line_height_cache_.end()) {
256+
QFont font = QFontDatabase::systemFont(QFontDatabase::GeneralFont);
257+
font.setPixelSize(static_cast<int>(font_size));
258+
QFontMetrics metrics(font);
259+
int height = metrics.height();
260+
metrics_height = single_line_height_cache_[font_size] = height;
261+
} else {
262+
metrics_height = it->second;
263+
}
264+
return viewport_->ScreenToWorld(Vec2i(0, metrics_height))[1];
265+
}
266+
267+
const QtTextRenderer::CharacterWidthLookup& QtTextRenderer::GetCharacterWidthLookup(
268+
uint32_t font_size) {
269+
auto it = character_width_lookup_cache_.find(font_size);
270+
if (it == character_width_lookup_cache_.end()) {
271+
QFont font = QFontDatabase::systemFont(QFontDatabase::GeneralFont);
272+
font.setPixelSize(static_cast<int>(font_size));
273+
QFontMetrics metrics(font);
274+
CharacterWidthLookup lut;
275+
for (int i = 0; i < 256; i++) {
276+
lut[i] = metrics.horizontalAdvance(QChar(i));
277+
}
278+
it = character_width_lookup_cache_.emplace(font_size, lut).first;
279+
}
280+
return it->second;
281+
}
282+
283+
float QtTextRenderer::GetStringWidthFast(const QString& text, const CharacterWidthLookup& lookup,
284+
uint32_t font_size) {
285+
int width = 0;
286+
for (const QChar& c : text) {
287+
width += lookup[c.toLatin1()];
288+
}
289+
const int horizontal_advance = MaximumHeuristic(width, text.length(), font_size);
290+
return viewport_->ScreenToWorld(Vec2i(horizontal_advance, 0))[0];
291+
}
292+
293+
QString QtTextRenderer::ElideText(const QString& text, int max_width,
294+
const CharacterWidthLookup& lookup, uint32_t font_size) {
295+
int width_lookup = 0;
296+
int characters = 0;
297+
while (characters < text.length()) {
298+
const int next_char_width = lookup[text[characters].toLatin1()];
299+
if (MaximumHeuristic(width_lookup + next_char_width, characters, font_size) > max_width) {
300+
break;
301+
}
302+
width_lookup += next_char_width;
303+
characters++;
304+
}
305+
if (characters == text.length()) {
306+
return text;
307+
}
308+
QString result = text.left(characters);
309+
if (characters > 0) {
310+
result[characters - 1] = ' ';
311+
}
312+
return result;
313+
}
314+
245315
float QtTextRenderer::AddFittingSingleLineText(const QString& text, float x, float y, float z,
246-
TextFormatting formatting) {
247-
const float width = GetStringWidth(text, formatting.font_size);
248-
const float single_line_height = GetStringHeight(".", formatting.font_size);
316+
const TextFormatting& formatting,
317+
const CharacterWidthLookup& lookup) {
318+
const float width = GetStringWidthFast(text, lookup, formatting.font_size);
319+
const float single_line_height = GetSingleLineStringHeight(formatting.font_size);
249320
Vec2i pen_pos = viewport_->WorldToScreen(Vec2(x, y));
250321
LayeredVec2 transformed = translations_.TranslateXYZAndFloorXY(
251322
{{static_cast<float>(pen_pos[0]), static_cast<float>(pen_pos[1])}, z});

src/OrbitGl/include/OrbitGl/MockTextRenderer.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
#include <QPainter>
1313
#include <algorithm>
14+
#include <cstdint>
1415
#include <set>
1516
#include <vector>
1617

@@ -42,6 +43,8 @@ class MockTextRenderer : public TextRenderer {
4243
[[nodiscard]] float GetStringWidth(const char* text, uint32_t font_size) override;
4344
[[nodiscard]] float GetStringHeight(const char* text, uint32_t font_size) override;
4445

46+
[[nodiscard]] float GetMinimumTextWidth(uint32_t /*font_size*/) override { return 0.f; };
47+
4548
[[nodiscard]] bool HasAddTextsSameLength() const {
4649
return num_characters_in_add_text_.size() <= 1;
4750
}

src/OrbitGl/include/OrbitGl/QtTextRenderer.h

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,21 @@ class QtTextRenderer : public TextRenderer {
3939

4040
[[nodiscard]] float GetStringWidth(const char* text, uint32_t font_size) override;
4141
[[nodiscard]] float GetStringHeight(const char* text, uint32_t font_size) override;
42+
[[nodiscard]] float GetMinimumTextWidth(uint32_t font_size) override;
4243

4344
private:
45+
using CharacterWidthLookup = std::array<int, 256>;
46+
4447
[[nodiscard]] float GetStringWidth(const QString& text, uint32_t font_size);
45-
[[nodiscard]] float GetMinimumTextWidth(uint32_t font_size);
48+
[[nodiscard]] float GetSingleLineStringHeight(uint32_t font_size);
49+
[[nodiscard]] const CharacterWidthLookup& GetCharacterWidthLookup(uint32_t font_size);
50+
[[nodiscard]] float GetStringWidthFast(const QString& text, const CharacterWidthLookup& lookup,
51+
uint32_t font_size);
52+
[[nodiscard]] static QString ElideText(const QString& text, int max_width,
53+
const CharacterWidthLookup& lookup, uint32_t font_size);
4654
[[nodiscard]] float AddFittingSingleLineText(const QString& text, float x, float y, float z,
47-
TextFormatting formatting);
48-
55+
const TextFormatting& formatting,
56+
const CharacterWidthLookup& lookup);
4957
struct StoredText {
5058
StoredText() = default;
5159
StoredText(const QString& text, int x, int y, int w, int h, TextFormatting formatting)
@@ -59,6 +67,8 @@ class QtTextRenderer : public TextRenderer {
5967
};
6068
absl::flat_hash_map<float, std::vector<StoredText>> stored_text_;
6169
absl::flat_hash_map<uint32_t, float> minimum_string_width_cache_;
70+
absl::flat_hash_map<uint32_t, CharacterWidthLookup> character_width_lookup_cache_;
71+
absl::flat_hash_map<uint32_t, int> single_line_height_cache_;
6272
};
6373

6474
} // namespace orbit_gl

src/OrbitGl/include/OrbitGl/TextRendererInterface.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class TextRendererInterface {
3838

3939
// Add a - potentially multiline - text at the given position and z-layer and with the specifier
4040
// formatting. If formatting.max_size is set all lines are elided to fit into this width.
41+
// It is allowed for `text` to contain unicode characters.
4142
virtual void AddText(const char* text, float x, float y, float z, TextFormatting formatting) = 0;
4243
virtual void AddText(const char* text, float x, float y, float z, TextFormatting formatting,
4344
Vec2* out_text_pos, Vec2* out_text_size) = 0;
@@ -46,13 +47,20 @@ class TextRendererInterface {
4647
// The renderer will shorten the text if the width exceeds formatting.max_size. The shortening
4748
// will happen in a way that tries to preserve the given numnber of trailing characters.
4849
// This is mainly used to preserve the duration in the text of time intervals. E.g. something like
49-
// "MyVeryLongButNotSoImportantMethodName 2.35 ms" will render as "MyVery...2.35 ms".
50+
// "MyVeryLongButNotSoImportantMethodName 2.35 ms" will render as "MyVery 2.35 ms".
51+
// `text` must not contain unicode characters - this method is ascii only. The reason for that is
52+
// that this allows for a much quicker heuristic for shortening strings as described above.
5053
virtual float AddTextTrailingCharsPrioritized(const char* text, float x, float y, float z,
5154
TextFormatting formatting,
5255
size_t trailing_chars_length) = 0;
5356

57+
// Return the width and height of `text` in world coordinates. The text might contain line breaks
58+
// and unicode characters.
5459
[[nodiscard]] virtual float GetStringWidth(const char* text, uint32_t font_size) = 0;
5560
[[nodiscard]] virtual float GetStringHeight(const char* text, uint32_t font_size) = 0;
61+
62+
// Returns the width of a minimum single character of given font size.
63+
[[nodiscard]] virtual float GetMinimumTextWidth(uint32_t font_size) = 0;
5664
};
5765

5866
} // namespace orbit_gl

src/OrbitGl/include/OrbitGl/TimerTrack.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <stdint.h>
1010

1111
#include <atomic>
12+
#include <cstdint>
1213
#include <map>
1314
#include <memory>
1415
#include <optional>
@@ -166,7 +167,9 @@ class TimerTrack : public Track {
166167

167168
[[nodiscard]] inline bool BoxHasRoomForText(orbit_gl::TextRenderer& text_renderer,
168169
const float width) {
169-
return text_renderer.GetStringWidth("w", layout_->GetFontSize()) < width;
170+
const uint32_t font_size = layout_->GetFontSize();
171+
const float width_of_single_char = text_renderer.GetMinimumTextWidth(font_size);
172+
return width_of_single_char < width;
170173
}
171174

172175
[[nodiscard]] bool ShouldHaveBorder(
@@ -180,6 +183,7 @@ class TimerTrack : public Track {
180183
OrbitApp* app_ = nullptr;
181184

182185
orbit_client_data::TimerData* timer_data_;
186+
absl::flat_hash_map<uint32_t, float> width_of_single_char_cache_;
183187
};
184188

185189
#endif // ORBIT_GL_TIMER_TRACK_H_

0 commit comments

Comments
 (0)