Flutter Engine
 
Loading...
Searching...
No Matches
dl_complexity_metal.cc
Go to the documentation of this file.
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
6
7// The numbers and weightings used in this file stem from taking the
8// data from the DisplayListBenchmarks suite run on an iPhone 12 and
9// applying very rough analysis on them to identify the approximate
10// trends.
11//
12// See the comments in display_list_complexity_helper.h for details on the
13// process and rationale behind coming up with these numbers.
14
15namespace flutter {
16
17DisplayListMetalComplexityCalculator*
18 DisplayListMetalComplexityCalculator::instance_ = nullptr;
19
20DisplayListMetalComplexityCalculator*
22 if (instance_ == nullptr) {
24 }
25 return instance_;
26}
27
28unsigned int
29DisplayListMetalComplexityCalculator::MetalHelper::BatchedComplexity() {
30 // Calculate the impact of saveLayer.
31 unsigned int save_layer_complexity;
32 if (save_layer_count_ == 0) {
33 save_layer_complexity = 0;
34 } else {
35 // saveLayer seems to have two trends; if the count is < 200,
36 // then the individual cost of a saveLayer is higher than if
37 // the count is > 200.
38 //
39 // However, the trend is strange and we should gather more data to
40 // get a better idea of how to represent the trend. That being said, it's
41 // very unlikely we'll ever hit a DisplayList with 200+ saveLayer calls
42 // in it, so we will calculate based on the more reasonably anticipated
43 // range of less than 200, with the trend line more weighted towards the
44 // lower end of that range (as the data itself doesn't present as a straight
45 // line). Further, we will easily hit our cache thresholds with such a
46 // large number of saveLayer calls.
47 //
48 // m = 1/2
49 // c = 1
50 save_layer_complexity = (save_layer_count_ + 2) * 100000;
51 }
52
53 unsigned int draw_text_blob_complexity;
54 if (draw_text_count_ == 0) {
55 draw_text_blob_complexity = 0;
56 } else {
57 // m = 1/240
58 // c = 0.75
59 draw_text_blob_complexity = (draw_text_count_ + 180) * 2500 / 3;
60 }
61
62 return save_layer_complexity + draw_text_blob_complexity;
63}
64
65void DisplayListMetalComplexityCalculator::MetalHelper::saveLayer(
66 const DlRect& bounds,
67 const SaveLayerOptions options,
68 const DlImageFilter* backdrop,
69 std::optional<int64_t> backdrop_id) {
70 if (IsComplex()) {
71 return;
72 }
73 if (backdrop) {
74 // Flutter does not offer this operation so this value can only ever be
75 // non-null for a frame-wide builder which is not currently evaluated for
76 // complexity.
77 AccumulateComplexity(Ceiling());
78 }
79 save_layer_count_++;
80}
81
82void DisplayListMetalComplexityCalculator::MetalHelper::drawLine(
83 const DlPoint& p0,
84 const DlPoint& p1) {
85 if (IsComplex()) {
86 return;
87 }
88 // The curve here may be log-linear, although it doesn't really match up that
89 // well. To avoid costly computations, try and do a best fit of the data onto
90 // a linear graph as a very rough first order approximation.
91
92 float non_hairline_penalty = 1.0f;
93 float aa_penalty = 1.0f;
94
95 if (!IsHairline()) {
96 non_hairline_penalty = 1.15f;
97 }
98 if (IsAntiAliased()) {
99 aa_penalty = 1.4f;
100 }
101
102 // Use an approximation for the distance to avoid floating point or
103 // sqrt() calls.
104 DlScalar distance = abs(p0.x - p1.x) + abs(p0.y - p1.y);
105
106 // The baseline complexity is for a hairline stroke with no AA.
107 // m = 1/45
108 // c = 5
109 unsigned int complexity =
110 ((distance + 225) * 4 / 9) * non_hairline_penalty * aa_penalty;
111
112 AccumulateComplexity(complexity);
113}
114
115void DisplayListMetalComplexityCalculator::MetalHelper::drawDashedLine(
116 const DlPoint& p0,
117 const DlPoint& p1,
118 DlScalar on_length,
119 DlScalar off_length) {
120 // Dashing is slightly more complex than a regular drawLine, but this
121 // op is so rare it is not worth measuring the difference.
122 drawLine(p0, p1);
123}
124
125void DisplayListMetalComplexityCalculator::MetalHelper::drawRect(
126 const DlRect& rect) {
127 if (IsComplex()) {
128 return;
129 }
130
131 unsigned int complexity;
132
133 // If stroked, cost scales linearly with the rectangle width/height.
134 // If filled, it scales with the area.
135 //
136 // Hairline stroke vs non hairline has no real penalty at smaller lengths,
137 // but it increases at larger lengths. There isn't enough data to get a good
138 // idea of the penalty at lengths > 1000px.
139 //
140 // There is also a kStrokeAndFill_Style that Skia exposes, but we do not
141 // currently use it anywhere in Flutter.
142 if (DrawStyle() == DlDrawStyle::kFill) {
143 // No real difference for AA with filled styles.
144 unsigned int area = rect.GetWidth() * rect.GetHeight();
145
146 // m = 1/9000
147 // c = 0
148 complexity = area / 225;
149 } else {
150 // Take the average of the width and height.
151 unsigned int length = (rect.GetWidth() + rect.GetHeight()) / 2;
152
153 // There is a penalty for AA being *disabled*.
154 if (IsAntiAliased()) {
155 // m = 1/65
156 // c = 0
157 complexity = length * 8 / 13;
158 } else {
159 // m = 1/35
160 // c = 0
161 complexity = length * 8 / 7;
162 }
163 }
164
165 AccumulateComplexity(complexity);
166}
167
168void DisplayListMetalComplexityCalculator::MetalHelper::drawOval(
169 const DlRect& bounds) {
170 if (IsComplex()) {
171 return;
172 }
173 // DrawOval scales very roughly linearly with the bounding box width/height
174 // (not area) for stroked styles without AA.
175 //
176 // Filled styles and stroked styles with AA scale linearly with the bounding
177 // box area.
178 unsigned int area = bounds.GetWidth() * bounds.GetHeight();
179
180 unsigned int complexity;
181
182 // There is also a kStrokeAndFill_Style that Skia exposes, but we do not
183 // currently use it anywhere in Flutter.
184 if (DrawStyle() == DlDrawStyle::kFill) {
185 // With filled styles, there is no significant AA penalty.
186 // m = 1/16000
187 // c = 0
188 complexity = area / 80;
189 } else {
190 if (IsAntiAliased()) {
191 // m = 1/7500
192 // c = 0
193 complexity = area * 2 / 75;
194 } else {
195 // Take the average of the width and height.
196 unsigned int length = (bounds.GetWidth() + bounds.GetHeight()) / 2;
197
198 // m = 1/80
199 // c = 0
200 complexity = length * 5 / 2;
201 }
202 }
203
204 AccumulateComplexity(complexity);
205}
206
207void DisplayListMetalComplexityCalculator::MetalHelper::drawCircle(
208 const DlPoint& center,
209 DlScalar radius) {
210 if (IsComplex()) {
211 return;
212 }
213
214 unsigned int complexity;
215
216 // There is also a kStrokeAndFill_Style that Skia exposes, but we do not
217 // currently use it anywhere in Flutter.
218 if (DrawStyle() == DlDrawStyle::kFill) {
219 // We can ignore pi here.
220 unsigned int area = radius * radius;
221 // m = 1/1300
222 // c = 5
223 complexity = (area + 6500) * 2 / 65;
224
225 // Penalty of around 5% when AA is disabled.
226 if (!IsAntiAliased()) {
227 complexity *= 1.05f;
228 }
229 } else {
230 // Hairline vs non-hairline has no significant performance difference.
231 if (IsAntiAliased()) {
232 // m = 1/7
233 // c = 7
234 complexity = (radius + 49) * 40 / 7;
235 } else {
236 // m = 1/16
237 // c = 8
238 complexity = (radius + 128) * 5 / 2;
239 }
240 }
241
242 AccumulateComplexity(complexity);
243}
244
245void DisplayListMetalComplexityCalculator::MetalHelper::drawRoundRect(
246 const DlRoundRect& rrect) {
247 if (IsComplex()) {
248 return;
249 }
250 // RRects scale linearly with the area of the bounding rect.
251 unsigned int area = rrect.GetBounds().Area();
252
253 // Drawing RRects is split into two performance tiers; an expensive
254 // one and a less expensive one. Both scale linearly with area.
255 //
256 // Expensive: All filled style, symmetric w/AA.
257 bool expensive = (DrawStyle() == DlDrawStyle::kFill) ||
258 (rrect.GetRadii().AreAllCornersSame() && IsAntiAliased());
259
260 unsigned int complexity;
261
262 // These values were worked out by creating a straight line graph (y=mx+c)
263 // approximately matching the measured data, normalising the data so that
264 // 0.0005ms resulted in a score of 100 then simplifying down the formula.
265 if (expensive) {
266 // m = 1/25000
267 // c = 2
268 // An area of 7000px^2 ~= baseline timing of 0.0005ms.
269 complexity = (area + 10500) / 175;
270 } else {
271 // m = 1/7000
272 // c = 1.5
273 // An area of 16000px^2 ~= baseline timing of 0.0005ms.
274 complexity = (area + 50000) / 625;
275 }
276
277 AccumulateComplexity(complexity);
278}
279
280void DisplayListMetalComplexityCalculator::MetalHelper::drawDiffRoundRect(
281 const DlRoundRect& outer,
282 const DlRoundRect& inner) {
283 if (IsComplex()) {
284 return;
285 }
286 // There are roughly four classes here:
287 // a) Filled style with AA enabled.
288 // b) Filled style with AA disabled.
289 // c) Complex RRect type with AA enabled and filled style.
290 // d) Everything else.
291 //
292 // a) and c) scale linearly with the area, b) and d) scale linearly with
293 // a single dimension (length). In all cases, the dimensions refer to
294 // the outer RRect.
295 unsigned int length =
296 (outer.GetBounds().GetWidth() + outer.GetBounds().GetHeight()) / 2;
297
298 unsigned int complexity;
299
300 // These values were worked out by creating a straight line graph (y=mx+c)
301 // approximately matching the measured data, normalising the data so that
302 // 0.0005ms resulted in a score of 100 then simplifying down the formula.
303 //
304 // There is also a kStrokeAndFill_Style that Skia exposes, but we do not
305 // currently use it anywhere in Flutter.
306 if (DrawStyle() == DlDrawStyle::kFill) {
307 unsigned int area = outer.GetBounds().Area();
308 if (!outer.GetRadii().AreAllCornersSame()) {
309 // m = 1/1000
310 // c = 1
311 complexity = (area + 1000) / 10;
312 } else {
313 if (IsAntiAliased()) {
314 // m = 1/3500
315 // c = 1.5
316 complexity = (area + 5250) / 35;
317 } else {
318 // m = 1/30
319 // c = 1
320 complexity = (300 + (10 * length)) / 3;
321 }
322 }
323 } else {
324 // m = 1/60
325 // c = 1.75
326 complexity = ((10 * length) + 1050) / 6;
327 }
328
329 AccumulateComplexity(complexity);
330}
331
332void DisplayListMetalComplexityCalculator::MetalHelper::drawRoundSuperellipse(
333 const DlRoundSuperellipse& rse) {
334 // Drawing RSEs on Skia falls back to RRect.
335 drawRoundRect(rse.ToApproximateRoundRect());
336}
337
338void DisplayListMetalComplexityCalculator::MetalHelper::drawPath(
339 const DlPath& path) {
340 if (IsComplex()) {
341 return;
342 }
343 // There is negligible effect on the performance for hairline vs. non-hairline
344 // stroke widths.
345 //
346 // The data for filled styles is currently suspicious, so for now we are going
347 // to assign scores based on stroked styles.
348
349 unsigned int line_verb_cost, quad_verb_cost, conic_verb_cost, cubic_verb_cost;
350
351 if (IsAntiAliased()) {
352 line_verb_cost = 75;
353 quad_verb_cost = 100;
354 conic_verb_cost = 160;
355 cubic_verb_cost = 210;
356 } else {
357 line_verb_cost = 67;
358 quad_verb_cost = 80;
359 conic_verb_cost = 140;
360 cubic_verb_cost = 210;
361 }
362
363 // There seems to be a fixed cost of around 1ms for calling drawPath.
364 unsigned int complexity =
365 200000 + CalculatePathComplexity(path, line_verb_cost, quad_verb_cost,
366 conic_verb_cost, cubic_verb_cost);
367
368 AccumulateComplexity(complexity);
369}
370
371void DisplayListMetalComplexityCalculator::MetalHelper::drawArc(
372 const DlRect& oval_bounds,
373 DlScalar start_degrees,
374 DlScalar sweep_degrees,
375 bool use_center) {
376 if (IsComplex()) {
377 return;
378 }
379 // Hairline vs non-hairline makes no difference to the performance.
380 // Stroked styles without AA scale linearly with the diameter.
381 // Stroked styles with AA scale linearly with the area except for small
382 // values. Filled styles scale linearly with the area.
383 unsigned int diameter =
384 (oval_bounds.GetWidth() + oval_bounds.GetHeight()) / 2;
385 unsigned int area = oval_bounds.GetWidth() * oval_bounds.GetHeight();
386
387 unsigned int complexity;
388
389 // These values were worked out by creating a straight line graph (y=mx+c)
390 // approximately matching the measured data, normalising the data so that
391 // 0.0005ms resulted in a score of 100 then simplifying down the formula.
392 //
393 // There is also a kStrokeAndFill_Style that Skia exposes, but we do not
394 // currently use it anywhere in Flutter.
395 if (DrawStyle() == DlDrawStyle::kStroke) {
396 if (IsAntiAliased()) {
397 // m = 1/8500
398 // c = 16
399 complexity = (area + 136000) * 2 / 765;
400 } else {
401 // m = 1/60
402 // c = 3
403 complexity = (diameter + 180) * 10 / 27;
404 }
405 } else {
406 if (IsAntiAliased()) {
407 // m = 1/20000
408 // c = 20
409 complexity = (area + 400000) / 900;
410 } else {
411 // m = 1/2100
412 // c = 8
413 complexity = (area + 16800) * 2 / 189;
414 }
415 }
416
417 AccumulateComplexity(complexity);
418}
419
420void DisplayListMetalComplexityCalculator::MetalHelper::drawPoints(
421 DlPointMode mode,
422 uint32_t count,
423 const DlPoint points[]) {
424 if (IsComplex()) {
425 return;
426 }
427 unsigned int complexity;
428
429 // If AA is off then they all behave similarly, and scale
430 // linearly with the point count.
431 if (!IsAntiAliased()) {
432 // m = 1/16000
433 // c = 0.75
434 complexity = (count + 12000) * 25 / 2;
435 } else {
436 if (mode == DlPointMode::kPolygon) {
437 // m = 1/1250
438 // c = 1
439 complexity = (count + 1250) * 160;
440 } else {
441 if (IsHairline() && mode == DlPointMode::kPoints) {
442 // This is a special case, it triggers an extremely fast path.
443 // m = 1/14500
444 // c = 0
445 complexity = count * 400 / 29;
446 } else {
447 // m = 1/2200
448 // c = 0.75
449 complexity = (count + 1650) * 1000 / 11;
450 }
451 }
452 }
453 AccumulateComplexity(complexity);
454}
455
456void DisplayListMetalComplexityCalculator::MetalHelper::drawVertices(
457 const std::shared_ptr<DlVertices>& vertices,
458 DlBlendMode mode) {
459 // There is currently no way for us to get the VertexMode from the SkVertices
460 // object, but for future reference:
461 //
462 // TriangleStrip is roughly 25% more expensive than TriangleFan.
463 // TriangleFan is roughly 5% more expensive than Triangles.
464
465 // For the baseline, it's hard to identify the trend. It might be O(n^1/2).
466 // For now, treat it as linear as an approximation.
467 //
468 // m = 1/4000
469 // c = 1
470 unsigned int complexity = (vertices->vertex_count() + 4000) * 50;
471
472 AccumulateComplexity(complexity);
473}
474
475void DisplayListMetalComplexityCalculator::MetalHelper::drawImage(
476 const sk_sp<DlImage> image,
477 const DlPoint& point,
478 DlImageSampling sampling,
479 bool render_with_attributes) {
480 if (IsComplex()) {
481 return;
482 }
483 // AA vs non-AA has a cost but it's dwarfed by the overall cost of the
484 // drawImage call.
485 //
486 // The main difference is if the image is backed by a texture already or not
487 // If we don't need to upload, then the cost scales linearly with the
488 // area of the image. If it needs uploading, the cost scales linearly
489 // with the square of the area (!!!).
490 DlISize dimensions = image->GetSize();
491 unsigned int area = dimensions.Area();
492
493 // m = 1/17000
494 // c = 3
495 unsigned int complexity = (area + 51000) * 4 / 170;
496
497 if (!image->isTextureBacked()) {
498 // We can't square the area here as we'll overflow, so let's approximate
499 // by taking the calculated complexity score and applying a multiplier to
500 // it.
501 //
502 // (complexity * area / 35000) + 1200 gives a reasonable approximation.
503 float multiplier = area / 35000.0f;
504 complexity = complexity * multiplier + 1200;
505 }
506
507 AccumulateComplexity(complexity);
508}
509
510void DisplayListMetalComplexityCalculator::MetalHelper::ImageRect(
511 const DlISize& size,
512 bool texture_backed,
513 bool render_with_attributes,
514 bool enforce_src_edges) {
515 if (IsComplex()) {
516 return;
517 }
518 // Two main groups here - texture-backed and non-texture-backed images.
519 //
520 // Within each group, they all perform within a few % of each other *except*
521 // when we have a strict constraint and anti-aliasing enabled.
522 unsigned int area = size.Area();
523
524 // These values were worked out by creating a straight line graph (y=mx+c)
525 // approximately matching the measured data, normalising the data so that
526 // 0.0005ms resulted in a score of 100 then simplifying down the formula.
527 unsigned int complexity;
528 if (texture_backed) {
529 // Baseline for texture-backed SkImages.
530 // m = 1/23000
531 // c = 2.3
532 complexity = (area + 52900) * 2 / 115;
533 if (render_with_attributes && enforce_src_edges && IsAntiAliased()) {
534 // There's about a 30% performance penalty from the baseline.
535 complexity *= 1.3f;
536 }
537 } else {
538 if (render_with_attributes && enforce_src_edges && IsAntiAliased()) {
539 // m = 1/12200
540 // c = 2.75
541 complexity = (area + 33550) * 2 / 61;
542 } else {
543 // m = 1/14500
544 // c = 2.5
545 complexity = (area + 36250) * 4 / 145;
546 }
547 }
548
549 AccumulateComplexity(complexity);
550}
551
552void DisplayListMetalComplexityCalculator::MetalHelper::drawImageNine(
553 const sk_sp<DlImage> image,
554 const DlIRect& center,
555 const DlRect& dst,
556 DlFilterMode filter,
557 bool render_with_attributes) {
558 if (IsComplex()) {
559 return;
560 }
561 // Whether uploading or not, the performance is comparable across all
562 // variations.
563 DlISize dimensions = image->GetSize();
564 unsigned int area = dimensions.Area();
565
566 // m = 1/8000
567 // c = 3
568 unsigned int complexity = (area + 24000) / 20;
569 AccumulateComplexity(complexity);
570}
571
572void DisplayListMetalComplexityCalculator::MetalHelper::drawDisplayList(
573 const sk_sp<DisplayList> display_list,
574 DlScalar opacity) {
575 if (IsComplex()) {
576 return;
577 }
578 MetalHelper helper(Ceiling() - CurrentComplexityScore());
579 if (opacity < SK_Scalar1 && !display_list->can_apply_group_opacity()) {
580 auto bounds = display_list->GetBounds();
581 helper.saveLayer(bounds, SaveLayerOptions::kWithAttributes, nullptr,
582 /*backdrop_id=*/-1);
583 }
584 display_list->Dispatch(helper);
585 AccumulateComplexity(helper.ComplexityScore());
586}
587
588void DisplayListMetalComplexityCalculator::MetalHelper::drawText(
589 const std::shared_ptr<DlText>& text,
590 DlScalar x,
591 DlScalar y) {
592 if (IsComplex()) {
593 return;
594 }
595
596 // DrawTextBlob has a high fixed cost, but if we call it multiple times
597 // per frame, that fixed cost is greatly reduced per subsequent call. This
598 // is likely because there is batching being done in SkCanvas.
599
600 // Increment draw_text_count_ and calculate the cost at the end.
601 draw_text_count_++;
602}
603
604void DisplayListMetalComplexityCalculator::MetalHelper::drawShadow(
605 const DlPath& path,
606 const DlColor color,
607 const DlScalar elevation,
608 bool transparent_occluder,
609 DlScalar dpr) {
610 if (IsComplex()) {
611 return;
612 }
613
614 // Elevation has no significant effect on the timings. Whether the shadow
615 // is cast by a transparent occluder or not has a small impact of around 5%.
616 //
617 // The path verbs do have an effect but only if the verb type is cubic; line,
618 // quad and conic all perform similarly.
619 float occluder_penalty = 1.0f;
620 if (transparent_occluder) {
621 occluder_penalty = 1.05f;
622 }
623
624 // The benchmark uses a test path of around 10 path elements. This is likely
625 // to be similar to what we see in real world usage, but we should benchmark
626 // different path lengths to see how much impact there is from varying the
627 // path length.
628 //
629 // For now, we will assume that there is no fixed overhead and that the time
630 // spent rendering the shadow for a path is split evenly amongst all the verbs
631 // enumerated.
632 unsigned int line_verb_cost = 20000; // 0.1ms
633 unsigned int quad_verb_cost = 20000; // 0.1ms
634 unsigned int conic_verb_cost = 20000; // 0.1ms
635 unsigned int cubic_verb_cost = 80000; // 0.4ms
636
637 unsigned int complexity =
638 0 + CalculatePathComplexity(path, line_verb_cost, quad_verb_cost,
639 conic_verb_cost, cubic_verb_cost);
640
641 AccumulateComplexity(complexity * occluder_penalty);
642}
643
644} // namespace flutter
static DisplayListMetalComplexityCalculator * GetInstance()
static const SaveLayerOptions kWithAttributes
int32_t x
FlutterVulkanImage * image
size_t length
std::u16string text
double y
impeller::Scalar DlScalar
impeller::RoundRect DlRoundRect
impeller::Rect DlRect
impeller::ISize32 DlISize
it will be possible to load the file into Perfetto s trace viewer use test Running tests that layout and measure text will not yield consistent results across various platforms Enabling this option will make font resolution default to the Ahem test font on all disable asset Prevents usage of any non test fonts unless they were explicitly Loaded via prefetched default font Indicates whether the embedding started a prefetch of the default font manager before creating the engine run In non interactive keep the shell running after the Dart script has completed enable serial On low power devices with low core running concurrent GC tasks on threads can cause them to contend with the UI thread which could potentially lead to jank This option turns off all concurrent GC activities domain network JSON encoded network policy per domain This overrides the DisallowInsecureConnections switch Embedder can specify whether to allow or disallow insecure connections at a domain level old gen heap size
impeller::RoundSuperellipse DlRoundSuperellipse
DlPointMode
Definition dl_types.h:15
@ kPolygon
draw each pair of overlapping points as a line segment
@ kPoints
draw each point separately
DEF_SWITCHES_START aot vmservice shared library Name of the *so containing AOT compiled Dart assets for launching the service isolate vm snapshot The VM snapshot data that will be memory mapped as read only SnapshotAssetPath must be present isolate snapshot The isolate snapshot data that will be memory mapped as read only SnapshotAssetPath must be present cache dir path
Definition switch_defs.h:52
@ kStroke
strokes boundary of shapes
@ kFill
fills interior of shapes
impeller::IRect32 DlIRect
it will be possible to load the file into Perfetto s trace viewer use test Running tests that layout and measure text will not yield consistent results across various platforms Enabling this option will make font resolution default to the Ahem test font on all disable asset Prevents usage of any non test fonts unless they were explicitly Loaded via prefetched default font Indicates whether the embedding started a prefetch of the default font manager before creating the engine run In non interactive mode
impeller::BlendMode DlBlendMode
impeller::Point DlPoint
std::vector< Point > points