|
5 | 5 | package io.flutter.embedding.engine.renderer; |
6 | 6 |
|
7 | 7 | import android.annotation.TargetApi; |
| 8 | +import android.content.ComponentCallbacks2; |
8 | 9 | import android.graphics.Bitmap; |
9 | 10 | import android.graphics.ImageFormat; |
10 | 11 | import android.graphics.Rect; |
@@ -361,17 +362,47 @@ public void run() { |
361 | 362 | final class ImageReaderSurfaceProducer |
362 | 363 | implements TextureRegistry.SurfaceProducer, TextureRegistry.ImageConsumer { |
363 | 364 | private static final String TAG = "ImageReaderSurfaceProducer"; |
364 | | - private static final int MAX_IMAGES = 4; |
| 365 | + private static final int MAX_IMAGES = 5; |
| 366 | + |
| 367 | + // Flip when debugging to see verbose logs. |
| 368 | + private static final boolean VERBOSE_LOGS = false; |
| 369 | + |
| 370 | + // We must always cleanup on memory pressure on Android 14 due to a bug in Android. |
| 371 | + // It is safe to do on all versions so we unconditionally have this set to true. |
| 372 | + private static final boolean CLEANUP_ON_MEMORY_PRESSURE = true; |
365 | 373 |
|
366 | 374 | private final long id; |
367 | 375 |
|
368 | 376 | private boolean released; |
369 | 377 | private boolean ignoringFence = false; |
370 | 378 |
|
371 | | - private int requestedWidth = 0; |
372 | | - private int requestedHeight = 0; |
373 | | - |
374 | 379 | /** Internal class: state held per image produced by image readers. */ |
| 380 | + private boolean trimOnMemoryPressure = CLEANUP_ON_MEMORY_PRESSURE; |
| 381 | + |
| 382 | + // The requested width and height are updated by setSize. |
| 383 | + private int requestedWidth = 1; |
| 384 | + private int requestedHeight = 1; |
| 385 | + // Whenever the requested width and height change we set this to be true so we |
| 386 | + // create a new ImageReader (inside getSurface) with the correct width and height. |
| 387 | + // We use this flag so that we lazily create the ImageReader only when a frame |
| 388 | + // will be produced at that size. |
| 389 | + private boolean createNewReader = true; |
| 390 | + |
| 391 | + // State held to track latency of various stages. |
| 392 | + private long lastDequeueTime = 0; |
| 393 | + private long lastQueueTime = 0; |
| 394 | + private long lastScheduleTime = 0; |
| 395 | + private int numTrims = 0; |
| 396 | + |
| 397 | + private Object lock = new Object(); |
| 398 | + // REQUIRED: The following fields must only be accessed when lock is held. |
| 399 | + private final ArrayDeque<PerImageReader> imageReaderQueue = new ArrayDeque<PerImageReader>(); |
| 400 | + private final HashMap<ImageReader, PerImageReader> perImageReaders = |
| 401 | + new HashMap<ImageReader, PerImageReader>(); |
| 402 | + private PerImage lastDequeuedImage = null; |
| 403 | + private PerImageReader lastReaderDequeuedFrom = null; |
| 404 | + |
| 405 | + /** Internal class: state held per Image produced by ImageReaders. */ |
375 | 406 | private class PerImage { |
376 | 407 | public final ImageReader reader; |
377 | 408 | public final Image image; |
@@ -415,7 +446,226 @@ public void onImageAvailable(ImageReader reader) { |
415 | 446 | } |
416 | 447 | onImage(new PerImage(reader, image)); |
417 | 448 | } |
418 | | - }; |
| 449 | + r.image.close(); |
| 450 | + } |
| 451 | + return perImage; |
| 452 | + } |
| 453 | + |
| 454 | + PerImage dequeueImage() { |
| 455 | + if (imageQueue.size() == 0) { |
| 456 | + return null; |
| 457 | + } |
| 458 | + PerImage r = imageQueue.removeFirst(); |
| 459 | + return r; |
| 460 | + } |
| 461 | + |
| 462 | + /** returns true if we can prune this reader */ |
| 463 | + boolean canPrune() { |
| 464 | + return imageQueue.size() == 0 && lastReaderDequeuedFrom != this; |
| 465 | + } |
| 466 | + |
| 467 | + void close() { |
| 468 | + closed = true; |
| 469 | + if (VERBOSE_LOGS) { |
| 470 | + Log.i(TAG, "Closing reader=" + reader.hashCode()); |
| 471 | + } |
| 472 | + reader.close(); |
| 473 | + imageQueue.clear(); |
| 474 | + } |
| 475 | + } |
| 476 | + |
| 477 | + double deltaMillis(long deltaNanos) { |
| 478 | + double ms = (double) deltaNanos / (double) 1000000.0; |
| 479 | + return ms; |
| 480 | + } |
| 481 | + |
| 482 | + PerImageReader getOrCreatePerImageReader(ImageReader reader) { |
| 483 | + PerImageReader r = perImageReaders.get(reader); |
| 484 | + if (r == null) { |
| 485 | + r = new PerImageReader(reader); |
| 486 | + perImageReaders.put(reader, r); |
| 487 | + imageReaderQueue.add(r); |
| 488 | + if (VERBOSE_LOGS) { |
| 489 | + Log.i(TAG, "imageReaderQueue#=" + imageReaderQueue.size()); |
| 490 | + } |
| 491 | + } |
| 492 | + return r; |
| 493 | + } |
| 494 | + |
| 495 | + void pruneImageReaderQueue() { |
| 496 | + boolean change = false; |
| 497 | + // Prune nodes from the head of the ImageReader queue. |
| 498 | + while (imageReaderQueue.size() > 1) { |
| 499 | + PerImageReader r = imageReaderQueue.peekFirst(); |
| 500 | + if (!r.canPrune()) { |
| 501 | + // No more ImageReaders can be pruned this round. |
| 502 | + break; |
| 503 | + } |
| 504 | + imageReaderQueue.removeFirst(); |
| 505 | + perImageReaders.remove(r.reader); |
| 506 | + r.close(); |
| 507 | + change = true; |
| 508 | + } |
| 509 | + if (change && VERBOSE_LOGS) { |
| 510 | + Log.i(TAG, "Pruned image reader queue length=" + imageReaderQueue.size()); |
| 511 | + } |
| 512 | + } |
| 513 | + |
| 514 | + void onImage(ImageReader reader, Image image) { |
| 515 | + PerImage queuedImage = null; |
| 516 | + synchronized (lock) { |
| 517 | + PerImageReader perReader = getOrCreatePerImageReader(reader); |
| 518 | + queuedImage = perReader.queueImage(image); |
| 519 | + } |
| 520 | + if (queuedImage == null) { |
| 521 | + // We got a late image. |
| 522 | + return; |
| 523 | + } |
| 524 | + if (VERBOSE_LOGS) { |
| 525 | + if (lastQueueTime != 0) { |
| 526 | + long now = System.nanoTime(); |
| 527 | + long queueDelta = now - lastQueueTime; |
| 528 | + Log.i( |
| 529 | + TAG, |
| 530 | + "" |
| 531 | + + reader.hashCode() |
| 532 | + + " enqueued image=" |
| 533 | + + queuedImage.image.hashCode() |
| 534 | + + " queueDelta=" |
| 535 | + + deltaMillis(queueDelta)); |
| 536 | + lastQueueTime = now; |
| 537 | + } else { |
| 538 | + lastQueueTime = System.nanoTime(); |
| 539 | + } |
| 540 | + } |
| 541 | + scheduleEngineFrame(); |
| 542 | + } |
| 543 | + |
| 544 | + PerImage dequeueImage() { |
| 545 | + PerImage r = null; |
| 546 | + synchronized (lock) { |
| 547 | + for (PerImageReader reader : imageReaderQueue) { |
| 548 | + r = reader.dequeueImage(); |
| 549 | + if (r == null) { |
| 550 | + // This reader is probably about to get pruned. |
| 551 | + continue; |
| 552 | + } |
| 553 | + if (VERBOSE_LOGS) { |
| 554 | + if (lastDequeueTime != 0) { |
| 555 | + long now = System.nanoTime(); |
| 556 | + long dequeueDelta = now - lastDequeueTime; |
| 557 | + long queuedFor = now - r.queuedTime; |
| 558 | + long scheduleDelay = now - lastScheduleTime; |
| 559 | + Log.i( |
| 560 | + TAG, |
| 561 | + "" |
| 562 | + + reader.reader.hashCode() |
| 563 | + + " dequeued image=" |
| 564 | + + r.image.hashCode() |
| 565 | + + " queuedFor= " |
| 566 | + + deltaMillis(queuedFor) |
| 567 | + + " dequeueDelta=" |
| 568 | + + deltaMillis(dequeueDelta) |
| 569 | + + " scheduleDelay=" |
| 570 | + + deltaMillis(scheduleDelay)); |
| 571 | + lastDequeueTime = now; |
| 572 | + } else { |
| 573 | + lastDequeueTime = System.nanoTime(); |
| 574 | + } |
| 575 | + } |
| 576 | + if (lastDequeuedImage != null) { |
| 577 | + if (VERBOSE_LOGS) { |
| 578 | + Log.i( |
| 579 | + TAG, |
| 580 | + "" |
| 581 | + + lastReaderDequeuedFrom.reader.hashCode() |
| 582 | + + " closing image=" |
| 583 | + + lastDequeuedImage.image.hashCode()); |
| 584 | + } |
| 585 | + // We must keep the last image dequeued open until we are done presenting |
| 586 | + // it. We have just dequeued a new image (r). Close the previously dequeued |
| 587 | + // image. |
| 588 | + lastDequeuedImage.image.close(); |
| 589 | + lastDequeuedImage = null; |
| 590 | + } |
| 591 | + // Remember the last image and reader dequeued from. We do this because we must |
| 592 | + // keep both of these alive until we are done presenting the image. |
| 593 | + lastDequeuedImage = r; |
| 594 | + lastReaderDequeuedFrom = reader; |
| 595 | + break; |
| 596 | + } |
| 597 | + pruneImageReaderQueue(); |
| 598 | + } |
| 599 | + return r; |
| 600 | + } |
| 601 | + |
| 602 | + @Override |
| 603 | + public void onTrimMemory(int level) { |
| 604 | + if (!trimOnMemoryPressure) { |
| 605 | + return; |
| 606 | + } |
| 607 | + if (level < ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { |
| 608 | + return; |
| 609 | + } |
| 610 | + synchronized (lock) { |
| 611 | + numTrims++; |
| 612 | + } |
| 613 | + cleanup(); |
| 614 | + createNewReader = true; |
| 615 | + } |
| 616 | + |
| 617 | + private void releaseInternal() { |
| 618 | + cleanup(); |
| 619 | + released = true; |
| 620 | + } |
| 621 | + |
| 622 | + private void cleanup() { |
| 623 | + synchronized (lock) { |
| 624 | + for (PerImageReader pir : perImageReaders.values()) { |
| 625 | + if (lastReaderDequeuedFrom == pir) { |
| 626 | + lastReaderDequeuedFrom = null; |
| 627 | + } |
| 628 | + pir.close(); |
| 629 | + } |
| 630 | + perImageReaders.clear(); |
| 631 | + if (lastDequeuedImage != null) { |
| 632 | + lastDequeuedImage.image.close(); |
| 633 | + lastDequeuedImage = null; |
| 634 | + } |
| 635 | + if (lastReaderDequeuedFrom != null) { |
| 636 | + lastReaderDequeuedFrom.close(); |
| 637 | + lastReaderDequeuedFrom = null; |
| 638 | + } |
| 639 | + imageReaderQueue.clear(); |
| 640 | + } |
| 641 | + } |
| 642 | + |
| 643 | + @TargetApi(API_LEVELS.API_33) |
| 644 | + private void waitOnFence(Image image) { |
| 645 | + try { |
| 646 | + SyncFence fence = image.getFence(); |
| 647 | + fence.awaitForever(); |
| 648 | + } catch (IOException e) { |
| 649 | + // Drop. |
| 650 | + } |
| 651 | + } |
| 652 | + |
| 653 | + private void maybeWaitOnFence(Image image) { |
| 654 | + if (image == null) { |
| 655 | + return; |
| 656 | + } |
| 657 | + if (ignoringFence) { |
| 658 | + return; |
| 659 | + } |
| 660 | + if (Build.VERSION.SDK_INT >= API_LEVELS.API_33) { |
| 661 | + // The fence API is only available on Android >= 33. |
| 662 | + waitOnFence(image); |
| 663 | + return; |
| 664 | + } |
| 665 | + // Log once per ImageTextureEntry. |
| 666 | + ignoringFence = true; |
| 667 | + Log.w(TAG, "ImageTextureEntry can't wait on the fence on Android < 33"); |
| 668 | + } |
419 | 669 |
|
420 | 670 | ImageReaderSurfaceProducer(long id) { |
421 | 671 | this.id = id; |
@@ -662,8 +912,28 @@ public void disableFenceForTest() { |
662 | 912 | } |
663 | 913 |
|
664 | 914 | @VisibleForTesting |
665 | | - public int readersToCloseSize() { |
666 | | - return readersToClose.size(); |
| 915 | + public int numImageReaders() { |
| 916 | + synchronized (lock) { |
| 917 | + return imageReaderQueue.size(); |
| 918 | + } |
| 919 | + } |
| 920 | + |
| 921 | + @VisibleForTesting |
| 922 | + public int numTrims() { |
| 923 | + synchronized (lock) { |
| 924 | + return numTrims; |
| 925 | + } |
| 926 | + } |
| 927 | + |
| 928 | + @VisibleForTesting |
| 929 | + public int numImages() { |
| 930 | + int r = 0; |
| 931 | + synchronized (lock) { |
| 932 | + for (PerImageReader reader : imageReaderQueue) { |
| 933 | + r += reader.imageQueue.size(); |
| 934 | + } |
| 935 | + } |
| 936 | + return r; |
667 | 937 | } |
668 | 938 | } |
669 | 939 |
|
|
0 commit comments