mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
macOS: port ResizeSynchronizer to Swift (#168959)
This patch: * migrates FlutterResizeSynchronizer from Objective-C to Swift. * reorders the performCommit parameters to support trailing closure syntax in Swift. * adds an optional onTimeout parameter to ResizeSynchronizer.beginResize. * adds test coverage for the timeout case. * significantly improves the class documentation. * adds the first Swift Testing tests for the macOS embedder. * adds the new tests to run_test.py. Admittedly, the original purpose was just to add a class that we could write Swift tests for, but things got a bit out of hand. Issue: https://github.com/flutter/flutter/issues/168564 Issue: https://github.com/flutter/flutter/issues/144791 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
parent
a682cf27b2
commit
e27377ef4d
@ -269,7 +269,10 @@ group("unittests") {
|
||||
}
|
||||
|
||||
if (is_mac) {
|
||||
public_deps += [ "//flutter/shell/platform/darwin/macos:flutter_desktop_darwin_unittests" ]
|
||||
public_deps += [
|
||||
"//flutter/shell/platform/darwin/macos:flutter_desktop_darwin_swift_unittests",
|
||||
"//flutter/shell/platform/darwin/macos:flutter_desktop_darwin_unittests",
|
||||
]
|
||||
}
|
||||
|
||||
if (is_win) {
|
||||
|
||||
@ -52962,9 +52962,6 @@ ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterPla
|
||||
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformViewControllerTest.mm + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterRenderer.h + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterRenderer.mm + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.h + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.mm + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizerTest.mm + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterRunLoop.swift + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterSurface.h + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterSurface.mm + ../../../flutter/LICENSE
|
||||
@ -52999,6 +52996,8 @@ ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/KeyCodeMap
|
||||
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/KeyCodeMapTest.mm + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/KeyCodeMap_Internal.h + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/NSView+ClipsToBounds.h + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/ResizeSynchronizer.swift + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/ResizeSynchronizerTest.swift + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/TestFlutterPlatformView.h + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/TestFlutterPlatformView.mm + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/shell/platform/embedder/embedder.cc + ../../../flutter/LICENSE
|
||||
@ -55994,9 +55993,6 @@ FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatf
|
||||
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformViewControllerTest.mm
|
||||
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterRenderer.h
|
||||
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterRenderer.mm
|
||||
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.h
|
||||
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.mm
|
||||
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizerTest.mm
|
||||
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterRunLoop.swift
|
||||
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterSurface.h
|
||||
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterSurface.mm
|
||||
@ -56033,6 +56029,8 @@ FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/KeyCodeMap.g
|
||||
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/KeyCodeMapTest.mm
|
||||
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/KeyCodeMap_Internal.h
|
||||
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/NSView+ClipsToBounds.h
|
||||
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/ResizeSynchronizer.swift
|
||||
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/ResizeSynchronizerTest.swift
|
||||
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/TestFlutterPlatformView.h
|
||||
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/TestFlutterPlatformView.mm
|
||||
FILE: ../../../flutter/shell/platform/darwin/macos/framework/module.modulemap
|
||||
|
||||
@ -62,7 +62,10 @@ source_set("InternalFlutterSwift") {
|
||||
"//flutter/shell/platform/darwin/macos/framework", # For module.modulemap
|
||||
]
|
||||
bridge_header = "InternalFlutterSwift-Bridging-Header.h"
|
||||
sources = [ "framework/Source/FlutterRunLoop.swift" ]
|
||||
sources = [
|
||||
"framework/Source/FlutterRunLoop.swift",
|
||||
"framework/Source/ResizeSynchronizer.swift",
|
||||
]
|
||||
deps = [ "//flutter/shell/platform/darwin/common:framework_common" ]
|
||||
public = _flutter_framework_headers + framework_common_headers
|
||||
}
|
||||
@ -109,8 +112,6 @@ source_set("flutter_framework_source") {
|
||||
"framework/Source/FlutterPlatformViewController.mm",
|
||||
"framework/Source/FlutterRenderer.h",
|
||||
"framework/Source/FlutterRenderer.mm",
|
||||
"framework/Source/FlutterResizeSynchronizer.h",
|
||||
"framework/Source/FlutterResizeSynchronizer.mm",
|
||||
"framework/Source/FlutterSurface.h",
|
||||
"framework/Source/FlutterSurface.mm",
|
||||
"framework/Source/FlutterSurfaceManager.h",
|
||||
@ -188,6 +189,21 @@ test_fixtures("flutter_desktop_darwin_fixtures") {
|
||||
fixtures = [ "//flutter/third_party/icu/common/icudtl.dat" ]
|
||||
}
|
||||
|
||||
executable("flutter_desktop_darwin_swift_unittests") {
|
||||
testonly = true
|
||||
public_configs = [
|
||||
"//flutter/shell/platform/darwin/common:config",
|
||||
"//flutter/shell/platform/darwin/common:test_config",
|
||||
"//flutter/shell/platform/darwin/common:swift_testing_config",
|
||||
]
|
||||
sources = [ "framework/Source/ResizeSynchronizerTest.swift" ]
|
||||
deps = [
|
||||
":flutter_framework_source",
|
||||
"//flutter/shell/platform/darwin/common:swift_testing_main",
|
||||
"//flutter/shell/platform/darwin/common:test_utils_swift",
|
||||
]
|
||||
}
|
||||
|
||||
executable("flutter_desktop_darwin_unittests") {
|
||||
testonly = true
|
||||
cflags_objc = flutter_cflags_objc
|
||||
@ -210,7 +226,6 @@ executable("flutter_desktop_darwin_unittests") {
|
||||
"framework/Source/FlutterMutatorViewTest.mm",
|
||||
"framework/Source/FlutterPlatformNodeDelegateMacTest.mm",
|
||||
"framework/Source/FlutterPlatformViewControllerTest.mm",
|
||||
"framework/Source/FlutterResizeSynchronizerTest.mm",
|
||||
"framework/Source/FlutterSurfaceManagerTest.mm",
|
||||
"framework/Source/FlutterTextInputPluginTest.mm",
|
||||
"framework/Source/FlutterTextInputSemanticsObjectTest.mm",
|
||||
|
||||
@ -44,9 +44,9 @@ Builds are architecture-specific, and can be controlled by specifying
|
||||
## Testing
|
||||
|
||||
The macOS-specific embedder tests are built as the
|
||||
`flutter_desktop_darwin_unittests` binary. Like all gtest-based test binaries, a
|
||||
subset of tests can be run by applying a filter such as
|
||||
`--gtest_filter='FlutterViewControllerTest.*Key*'`.
|
||||
`flutter_desktop_darwin_unittests` and `flutter_desktop_darwin_swift_unittests`
|
||||
binaries. Like all gtest-based test binaries, a subset of tests can be run by
|
||||
applying a filter such as `--gtest_filter='FlutterViewControllerTest.*Key*'`.
|
||||
|
||||
More general details on testing can be found on the [Wiki][wiki_engine_testing].
|
||||
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
|
||||
#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h"
|
||||
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h"
|
||||
#include "shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.h"
|
||||
|
||||
#include <objc/objc.h>
|
||||
|
||||
@ -19,6 +18,7 @@
|
||||
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h"
|
||||
#import "flutter/shell/platform/darwin/common/framework/Source/FlutterBinaryMessengerRelay.h"
|
||||
#import "flutter/shell/platform/darwin/common/test_utils_swift/test_utils_swift.h"
|
||||
#import "flutter/shell/platform/darwin/macos/InternalFlutterSwift/InternalFlutterSwift.h"
|
||||
#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppDelegate.h"
|
||||
#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppLifecycleDelegate.h"
|
||||
#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterPluginMacOS.h"
|
||||
@ -909,9 +909,9 @@ TEST_F(FlutterEngineTest, ResizeSynchronizerNotBlockingRasterThreadAfterShutdown
|
||||
|
||||
std::thread rasterThread([&threadSynchronizer] {
|
||||
[threadSynchronizer performCommitForSize:CGSizeMake(100, 100)
|
||||
afterDelay:0
|
||||
notify:^{
|
||||
}
|
||||
delay:0];
|
||||
}];
|
||||
});
|
||||
|
||||
rasterThread.join();
|
||||
|
||||
@ -1,42 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERRESIZESYNCHRONIZER_H_
|
||||
#define FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERRESIZESYNCHRONIZER_H_
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h"
|
||||
|
||||
/**
|
||||
* Class responsible for coordinating window resize with content update.
|
||||
*/
|
||||
@interface FlutterResizeSynchronizer : NSObject
|
||||
|
||||
/**
|
||||
* Begins a resize operation for the given size. Block the thread until
|
||||
* performCommitForSize: with the same size is called.
|
||||
* While the thread is blocked Flutter messages are being pumped.
|
||||
* See [FlutterRunLoop pollFlutterMessagesOnce].
|
||||
*/
|
||||
- (void)beginResizeForSize:(CGSize)size notify:(nonnull dispatch_block_t)notify;
|
||||
|
||||
/**
|
||||
* Called from raster thread. Schedules the given block on platform thread
|
||||
* at given delay and unblocks the platform thread if waiting for the surface
|
||||
* during resize.
|
||||
*/
|
||||
- (void)performCommitForSize:(CGSize)size
|
||||
notify:(nonnull dispatch_block_t)notify
|
||||
delay:(NSTimeInterval)delay;
|
||||
|
||||
/**
|
||||
* Called when the view is shut down. Unblocks platform thread if blocked
|
||||
* during resize.
|
||||
*/
|
||||
- (void)shutDown;
|
||||
|
||||
@end
|
||||
|
||||
#endif // FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERRESIZESYNCHRONIZER_H_
|
||||
@ -1,62 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.h"
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#import "flutter/shell/platform/darwin/common/InternalFlutterSwiftCommon/InternalFlutterSwiftCommon.h"
|
||||
#import "flutter/shell/platform/darwin/macos/InternalFlutterSwift/InternalFlutterSwift.h"
|
||||
|
||||
@implementation FlutterResizeSynchronizer {
|
||||
std::atomic_bool _inResize;
|
||||
BOOL _shuttingDown;
|
||||
BOOL _didReceiveFrame;
|
||||
CGSize _contentSize;
|
||||
}
|
||||
|
||||
- (void)beginResizeForSize:(CGSize)size notify:(nonnull dispatch_block_t)notify {
|
||||
if (!_didReceiveFrame || _shuttingDown) {
|
||||
notify();
|
||||
return;
|
||||
}
|
||||
|
||||
_inResize = true;
|
||||
_contentSize = CGSizeMake(-1, -1);
|
||||
notify();
|
||||
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
|
||||
while (true) {
|
||||
if (CGSizeEqualToSize(_contentSize, size) || _shuttingDown) {
|
||||
break;
|
||||
}
|
||||
if (CFAbsoluteTimeGetCurrent() - start > 1.0) {
|
||||
[FlutterLogger logError:@"Resize timed out."];
|
||||
break;
|
||||
}
|
||||
[FlutterRunLoop.mainRunLoop pollFlutterMessagesOnce];
|
||||
}
|
||||
_inResize = false;
|
||||
}
|
||||
|
||||
- (void)performCommitForSize:(CGSize)size
|
||||
notify:(nonnull dispatch_block_t)notify
|
||||
delay:(NSTimeInterval)delay {
|
||||
if (_inResize) {
|
||||
delay = 0;
|
||||
}
|
||||
[FlutterRunLoop.mainRunLoop performAfterDelay:delay
|
||||
block:^{
|
||||
_didReceiveFrame = YES;
|
||||
_contentSize = size;
|
||||
notify();
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)shutDown {
|
||||
[FlutterRunLoop.mainRunLoop performBlock:^{
|
||||
_shuttingDown = YES;
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
@ -1,122 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "flutter/fml/synchronization/waitable_event.h"
|
||||
#import "flutter/shell/platform/darwin/macos/InternalFlutterSwift/InternalFlutterSwift.h"
|
||||
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.h"
|
||||
#import "flutter/testing/testing.h"
|
||||
|
||||
TEST(FlutterThreadSynchronizerTest, NotBlocked) {
|
||||
[FlutterRunLoop ensureMainLoopInitialized];
|
||||
|
||||
FlutterResizeSynchronizer* synchronizer = [[FlutterResizeSynchronizer alloc] init];
|
||||
__block BOOL performed = NO;
|
||||
|
||||
[NSThread detachNewThreadWithBlock:^{
|
||||
[synchronizer performCommitForSize:CGSizeMake(100, 100)
|
||||
notify:^{
|
||||
performed = YES;
|
||||
}
|
||||
delay:0];
|
||||
}];
|
||||
|
||||
CFTimeInterval start = CFAbsoluteTimeGetCurrent();
|
||||
|
||||
while (!performed && CFAbsoluteTimeGetCurrent() - start < 1.0) {
|
||||
[FlutterRunLoop.mainRunLoop pollFlutterMessagesOnce];
|
||||
}
|
||||
EXPECT_EQ(performed, YES);
|
||||
}
|
||||
|
||||
TEST(FlutterThreadSynchronizerTest, WaitForResize) {
|
||||
[FlutterRunLoop ensureMainLoopInitialized];
|
||||
|
||||
FlutterResizeSynchronizer* synchronizer = [[FlutterResizeSynchronizer alloc] init];
|
||||
|
||||
__block BOOL commit1 = NO;
|
||||
__block BOOL commit2 = NO;
|
||||
|
||||
// Capturing c++ objects in blocks requires copy constructor, that also applies
|
||||
// to __block variables where the copy is made on heap.
|
||||
fml::AutoResetWaitableEvent latch_;
|
||||
fml::AutoResetWaitableEvent& latch = latch_;
|
||||
|
||||
// Resize synchronizer must have at received one frame in order to block.
|
||||
__block BOOL didReceiveFrame = NO;
|
||||
[synchronizer performCommitForSize:CGSizeMake(10, 10)
|
||||
notify:^{
|
||||
didReceiveFrame = YES;
|
||||
}
|
||||
delay:0];
|
||||
|
||||
CFTimeInterval start = CFAbsoluteTimeGetCurrent();
|
||||
while (!didReceiveFrame && CFAbsoluteTimeGetCurrent() - start < 1.0) {
|
||||
[FlutterRunLoop.mainRunLoop pollFlutterMessagesOnce];
|
||||
}
|
||||
|
||||
// Now resize should block until it has received expected size.
|
||||
|
||||
[NSThread detachNewThreadWithBlock:^{
|
||||
latch.Wait();
|
||||
|
||||
[synchronizer performCommitForSize:CGSizeMake(50, 100)
|
||||
notify:^{
|
||||
commit1 = YES;
|
||||
}
|
||||
delay:0];
|
||||
|
||||
[synchronizer performCommitForSize:CGSizeMake(100, 100)
|
||||
notify:^{
|
||||
commit2 = YES;
|
||||
}
|
||||
delay:0];
|
||||
}];
|
||||
|
||||
[synchronizer beginResizeForSize:CGSizeMake(100, 100)
|
||||
notify:^{
|
||||
latch.Signal();
|
||||
}];
|
||||
|
||||
EXPECT_EQ(commit1, YES);
|
||||
EXPECT_EQ(commit2, YES);
|
||||
}
|
||||
|
||||
TEST(FlutterThreadSynchronizerTest, UnblocksOnShutDown) {
|
||||
[FlutterRunLoop ensureMainLoopInitialized];
|
||||
|
||||
FlutterResizeSynchronizer* synchronizer = [[FlutterResizeSynchronizer alloc] init];
|
||||
|
||||
// Resize synchronizer must have at received one frame in order to block.
|
||||
__block BOOL didReceiveFrame = NO;
|
||||
[synchronizer performCommitForSize:CGSizeMake(10, 10)
|
||||
notify:^{
|
||||
didReceiveFrame = YES;
|
||||
}
|
||||
delay:0];
|
||||
|
||||
CFTimeInterval start = CFAbsoluteTimeGetCurrent();
|
||||
while (!didReceiveFrame && CFAbsoluteTimeGetCurrent() - start < 1.0) {
|
||||
[FlutterRunLoop.mainRunLoop pollFlutterMessagesOnce];
|
||||
}
|
||||
|
||||
fml::AutoResetWaitableEvent latch_;
|
||||
fml::AutoResetWaitableEvent& latch = latch_;
|
||||
|
||||
[NSThread detachNewThreadWithBlock:^{
|
||||
latch.Wait();
|
||||
|
||||
[synchronizer shutDown];
|
||||
}];
|
||||
|
||||
[synchronizer beginResizeForSize:CGSizeMake(100, 100)
|
||||
notify:^{
|
||||
// Unblock resize
|
||||
latch.Signal();
|
||||
}];
|
||||
|
||||
// Subsequent calls should not block.
|
||||
[synchronizer beginResizeForSize:CGSizeMake(100, 100)
|
||||
notify:^{
|
||||
}];
|
||||
}
|
||||
@ -4,11 +4,12 @@
|
||||
|
||||
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h"
|
||||
|
||||
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.h"
|
||||
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h"
|
||||
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
|
||||
#import "flutter/shell/platform/darwin/common/InternalFlutterSwiftCommon/InternalFlutterSwiftCommon.h"
|
||||
#import "flutter/shell/platform/darwin/macos/InternalFlutterSwift/InternalFlutterSwift.h"
|
||||
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h"
|
||||
|
||||
@interface FlutterView () <FlutterSurfaceManagerDelegate> {
|
||||
FlutterViewIdentifier _viewIdentifier;
|
||||
__weak id<FlutterViewDelegate> _viewDelegate;
|
||||
@ -42,7 +43,7 @@
|
||||
}
|
||||
|
||||
- (void)onPresent:(CGSize)frameSize withBlock:(dispatch_block_t)block delay:(NSTimeInterval)delay {
|
||||
[_resizeSynchronizer performCommitForSize:frameSize notify:block delay:delay];
|
||||
[_resizeSynchronizer performCommitForSize:frameSize afterDelay:delay notify:block];
|
||||
}
|
||||
|
||||
- (FlutterSurfaceManager*)surfaceManager {
|
||||
@ -63,9 +64,12 @@
|
||||
[super setFrameSize:newSize];
|
||||
CGSize scaledSize = [self convertSizeToBacking:self.bounds.size];
|
||||
[_resizeSynchronizer beginResizeForSize:scaledSize
|
||||
notify:^{
|
||||
[_viewDelegate viewDidReshape:self];
|
||||
}];
|
||||
notify:^{
|
||||
[_viewDelegate viewDidReshape:self];
|
||||
}
|
||||
onTimeout:^{
|
||||
[FlutterLogger logError:@"Resize timed out"];
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -0,0 +1,151 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
|
||||
/// Coordinates Flutter view content updates with macOS window resizing.
|
||||
///
|
||||
/// When a native window containing a Flutter view is resized, the platform's window bounds can
|
||||
/// change before Flutter has been able to render a new frame matching those new dimensions. This
|
||||
/// asynchronicity can lead to undesirable visual effects such as:
|
||||
/// - Content temporarily appearing at the old size within the new, larger bounds.
|
||||
/// - Black "letterboxing" artifacts rendered at view edges.
|
||||
/// - A perception of lag in content resizing.
|
||||
///
|
||||
/// `ResizeSynchronizer` mitigates these issues by introducing a controlled blocking mechanism
|
||||
/// during the resize lifecycle:
|
||||
///
|
||||
/// 1. Initiation (Platform Thread): When a native window resize is detected (e.g., by a window
|
||||
/// delegate), the platform thread calls `beginResize(forSize:notify:onTimeout:)` with the new
|
||||
/// target dimensions. This method blocks the calling platform thread, pausing the completion of
|
||||
/// the native resize event. While blocked, it continuously polls for engine messages to process
|
||||
/// relevant updates. The `notify` closure is invoked just before blocking begins.
|
||||
///
|
||||
/// 2. Commit (Raster Thread): As the engine renders frames, the raster thread, upon presenting a
|
||||
/// new frame, calls `performCommit(forSize:afterDelay:notify:)`, providing the size of the
|
||||
/// frame just rendered.
|
||||
///
|
||||
/// 3. Unblocking: If the size of the committed frame matches the target size that `beginResize()`
|
||||
/// is waiting for, `beginResize()` unblocks. This allows the native resize operation to
|
||||
/// complete, now that Flutter's content is synchronized with the new window dimensions.
|
||||
///
|
||||
/// Safeguards:
|
||||
/// - Timeout: `beginResize()` includes a timeout mechanism to prevent indefinite blocking if a
|
||||
/// matching frame isn't committed in a timely manner. An optional `onTimeout` closure can be
|
||||
/// provided to handle this event.
|
||||
/// - Shutdown: The synchronization can be cleanly interrupted by calling `shutDown()`, which will
|
||||
/// also unblock any pending `beginResize()` call. After shutdown, `beginResize()` will no
|
||||
/// longer block.
|
||||
/// - Thread Safety: The class manages its internal state in a thread-safe manner to coordinate
|
||||
/// actions between the platform thread and the raster thread.
|
||||
@objc(FlutterResizeSynchronizer)
|
||||
public final class ResizeSynchronizer: NSObject {
|
||||
private static let invalidSize = CGSize(width: -1, height: -1)
|
||||
|
||||
// Synchronizes access to _isInResize_unsafe: isInResize is accessed from multiple threads and
|
||||
// thus requires synchronized access to the underlying storage.
|
||||
private let isInResizeLock = NSLock()
|
||||
private var _isInResize_unsafe: Bool = false
|
||||
private var isInResize: Bool {
|
||||
get {
|
||||
isInResizeLock.lock()
|
||||
defer { isInResizeLock.unlock() }
|
||||
return _isInResize_unsafe
|
||||
}
|
||||
set {
|
||||
isInResizeLock.lock()
|
||||
_isInResize_unsafe = newValue
|
||||
isInResizeLock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// True if the app is shutting down. Must be set on platform thread.
|
||||
private var isShuttingDown: Bool = false
|
||||
|
||||
// True if at least one frame has been presented. Must be set on platform thread.
|
||||
private var didReceiveFrame: Bool = false
|
||||
|
||||
// The updated view surface size. Must be set on platform thread.
|
||||
private var contentSize: CGSize = ResizeSynchronizer.invalidSize
|
||||
|
||||
/// Begins window resize operation to the specified size.
|
||||
///
|
||||
/// Blocks the thread until `performCommit(forSize:notify:delay:)` with the same size is called.
|
||||
/// While the thread is blocked, Flutter messages are being pumped.
|
||||
/// See `FlutterRunLoop.mainRunLoop.pollFlutterMessagesOnce()`.
|
||||
@objc public func beginResize(
|
||||
forSize size: CGSize,
|
||||
notify: () -> Void,
|
||||
onTimeout: (() -> Void)? = nil
|
||||
) {
|
||||
if !didReceiveFrame || isShuttingDown {
|
||||
// If we haven't yet received a frame, or we're shutting down, there's nothing to do.
|
||||
notify()
|
||||
return
|
||||
}
|
||||
|
||||
// Mark that we're in a resize and set the content size to a sentinel value.
|
||||
isInResize = true
|
||||
contentSize = ResizeSynchronizer.invalidSize
|
||||
|
||||
// Call the notify callback.
|
||||
notify()
|
||||
|
||||
// Spin, waiting for the commit (during frame present) for up to 1 second, then time out.
|
||||
let startTime = CFAbsoluteTimeGetCurrent()
|
||||
let timeoutDuration: TimeInterval = 1.0
|
||||
while true {
|
||||
// If no change to size, or we got a shutdown notice, bail out.
|
||||
if contentSize == size || isShuttingDown {
|
||||
break
|
||||
}
|
||||
|
||||
// If we've hit the timeout, notify the caller and bail out.
|
||||
if CFAbsoluteTimeGetCurrent() - startTime > timeoutDuration {
|
||||
onTimeout?()
|
||||
break
|
||||
}
|
||||
|
||||
// Process platform thread messages to pick up any updates to contentSize, didReceiveFrame,
|
||||
// isShuttingDown made in performCommit and shutdown methods.
|
||||
FlutterRunLoop.mainRunLoop.pollFlutterMessagesOnce()
|
||||
}
|
||||
isInResize = false
|
||||
}
|
||||
|
||||
/// Commit the updated frame size.
|
||||
///
|
||||
/// Schedules the given block on the platform thread with the given delay. Unblocks `beginResize`
|
||||
/// on the platform thread, if waiting for the surface during resize.
|
||||
///
|
||||
/// Called from the raster thread on frame present.
|
||||
@objc public func performCommit(
|
||||
forSize size: CGSize,
|
||||
afterDelay delay: TimeInterval,
|
||||
notify: @escaping () -> Void
|
||||
) {
|
||||
var effectiveDelay = delay
|
||||
|
||||
// If we're currently resizing, process immediately.
|
||||
if self.isInResize { // Accesses the computed property which uses the lock
|
||||
effectiveDelay = 0
|
||||
}
|
||||
|
||||
FlutterRunLoop.mainRunLoop.perform(afterDelay: effectiveDelay) {
|
||||
self.didReceiveFrame = true
|
||||
self.contentSize = size
|
||||
notify()
|
||||
}
|
||||
}
|
||||
|
||||
/// Notifies the synchronizer that the Flutter view is being shut down.
|
||||
///
|
||||
/// Unblocks the platform thread if blocked.
|
||||
@objc public func shutDown() {
|
||||
FlutterRunLoop.mainRunLoop.perform {
|
||||
self.isShuttingDown = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,212 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import InternalFlutterSwift
|
||||
import Testing
|
||||
|
||||
// Tests for `ResizeSynchronizer`.
|
||||
//
|
||||
// `FlutterRunLoop` asserts that it be called on the main thread, and thus the main thread is,
|
||||
// in effect, a source of implicit shared state/behaviour. Because `beginResize` is a blocking call
|
||||
// performed on the main thread, we serialise to avoid potential interactions between tests.
|
||||
@Suite("ResizeSynchronizer tests", .serialized)
|
||||
struct ResizeSynchronizerTest {
|
||||
|
||||
@MainActor
|
||||
@Test("performCommit callback executes when no resize is active")
|
||||
func testNotBlocked() async {
|
||||
FlutterRunLoop.ensureMainLoopInitialized()
|
||||
|
||||
let synchronizer = ResizeSynchronizer()
|
||||
var didReceiveFrame = false
|
||||
|
||||
// Call performCommit from raster thread during frame present.
|
||||
Thread.detachNewThread {
|
||||
synchronizer.performCommit(forSize: CGSize(width: 100, height: 100), afterDelay: 0) {
|
||||
didReceiveFrame = true
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the task is processed within the timeout.
|
||||
do {
|
||||
try await waitForCondition("didReceiveFrame to be true", timeout: 1.0) { didReceiveFrame }
|
||||
} catch {
|
||||
// Record the timeout and bail out of the test.
|
||||
Issue.record("\(error)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test("beginResize does not invoke onTimeout if performCommit called with matching frame size")
|
||||
func testBeginResizeDoesNotTimeOutWithMatchingPerformCommit() async {
|
||||
FlutterRunLoop.ensureMainLoopInitialized()
|
||||
|
||||
// Resize synchronizer must have presented a frame in order to block.
|
||||
let synchronizer = ResizeSynchronizer()
|
||||
var didReceiveFrame = false
|
||||
synchronizer.performCommit(forSize: CGSize(width: 10, height: 10), afterDelay: 0) {
|
||||
didReceiveFrame = true
|
||||
}
|
||||
do {
|
||||
try await waitForCondition("didReceiveFrame to be true", timeout: 1.0) { didReceiveFrame }
|
||||
} catch {
|
||||
// Record the timeout and bail out of the test.
|
||||
Issue.record("\(error)")
|
||||
return
|
||||
}
|
||||
|
||||
var commit1 = false
|
||||
var commit2 = false
|
||||
var didTimeout = false
|
||||
let latch = DispatchSemaphore(value: 0)
|
||||
|
||||
// Call performCommit from raster thread during frame present.
|
||||
Thread.detachNewThread {
|
||||
// Block until `beginResize` has been called.
|
||||
latch.wait()
|
||||
|
||||
// First commit size DOES NOT match that passed to beginResize.
|
||||
synchronizer.performCommit(forSize: CGSize(width: 50, height: 100), afterDelay: 0) {
|
||||
commit1 = true
|
||||
}
|
||||
|
||||
// Second commit size DOES match that passed to beginResize.
|
||||
synchronizer.performCommit(forSize: CGSize(width: 100, height: 100), afterDelay: 0) {
|
||||
commit2 = true
|
||||
}
|
||||
}
|
||||
|
||||
// This call blocks until performCommit is called with matching size, or times out.
|
||||
synchronizer.beginResize(forSize: CGSize(width: 100, height: 100)) {
|
||||
// Unblock the raster thread by signaling the latch.
|
||||
latch.signal()
|
||||
} onTimeout: {
|
||||
didTimeout = true
|
||||
}
|
||||
|
||||
// Verify beginResize did not timeout.
|
||||
#expect(
|
||||
didTimeout == false,
|
||||
"onTimeout was called even though performCommit was called with matching size")
|
||||
|
||||
// Verify performCommit callbacks were invoked.
|
||||
#expect(commit1 == true)
|
||||
#expect(commit2 == true)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test("beginResize invokes onTimeout if performCommit not called with matching frame size")
|
||||
func testBeginResizeDoesTimeOutWithoutMatchingPerformCommit() async {
|
||||
FlutterRunLoop.ensureMainLoopInitialized()
|
||||
|
||||
// Resize synchronizer must have presented a frame in order to block.
|
||||
let synchronizer = ResizeSynchronizer()
|
||||
var didReceiveFrame = false
|
||||
synchronizer.performCommit(forSize: CGSize(width: 10, height: 10), afterDelay: 0) {
|
||||
didReceiveFrame = true
|
||||
}
|
||||
do {
|
||||
try await waitForCondition("didReceiveFrame to be true", timeout: 1.0) { didReceiveFrame }
|
||||
} catch {
|
||||
// Record the timeout and bail out of the test.
|
||||
Issue.record("\(error)")
|
||||
return
|
||||
}
|
||||
|
||||
didReceiveFrame = false
|
||||
var didTimeout = false
|
||||
let latch = DispatchSemaphore(value: 0)
|
||||
|
||||
// Call performCommit from raster thread during frame present.
|
||||
Thread.detachNewThread {
|
||||
// Block until `beginResize` has been called.
|
||||
latch.wait()
|
||||
|
||||
// First commit size DOES NOT match that passed to beginResize.
|
||||
synchronizer.performCommit(forSize: CGSize(width: 50, height: 100), afterDelay: 0) {
|
||||
didReceiveFrame = true
|
||||
}
|
||||
}
|
||||
|
||||
// This call blocks until performCommit is called with matching size, or times out.
|
||||
synchronizer.beginResize(forSize: CGSize(width: 100, height: 100)) {
|
||||
// Unblock the raster thread by signaling the latch.
|
||||
latch.signal()
|
||||
} onTimeout: {
|
||||
didTimeout = true
|
||||
}
|
||||
|
||||
// Verify beginResize timed out.
|
||||
#expect(
|
||||
didTimeout == true,
|
||||
"onTimeout was not called even though performCommit was not called with matching size")
|
||||
|
||||
// Verify performCommit callback was invoked.
|
||||
#expect(didReceiveFrame == true)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test("shutDown unblocks an active beginResize and prevents future blocking")
|
||||
func testUnblocksOnShutdown() async {
|
||||
FlutterRunLoop.ensureMainLoopInitialized()
|
||||
let synchronizer = ResizeSynchronizer()
|
||||
|
||||
// Resize synchronizer must have received one frame in order to block.
|
||||
var didReceiveFrame = false
|
||||
synchronizer.performCommit(forSize: CGSize(width: 10, height: 10), afterDelay: 0) {
|
||||
didReceiveFrame = true
|
||||
}
|
||||
do {
|
||||
try await waitForCondition("didReceiveFrame to be true", timeout: 1.0) { didReceiveFrame }
|
||||
} catch {
|
||||
// Record the timeout and bail out of the test.
|
||||
Issue.record("\(error)")
|
||||
return
|
||||
}
|
||||
|
||||
// Block until we receive a performCommit with a matching frameSize.
|
||||
let latch = DispatchSemaphore(value: 0)
|
||||
Thread.detachNewThread {
|
||||
// Block until `beginResize` has been called, and signals the latch.
|
||||
latch.wait()
|
||||
|
||||
synchronizer.shutDown()
|
||||
}
|
||||
|
||||
synchronizer.beginResize(forSize: CGSize(width: 100, height: 100)) {
|
||||
// Unblock resize.
|
||||
latch.signal()
|
||||
}
|
||||
|
||||
// Subsequent calls should not block.
|
||||
synchronizer.beginResize(forSize: CGSize(width: 100, height: 100)) {}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Returns when `condition` is true, or throws if it's not true at the expiration of `timeout`.
|
||||
// Polls `FlutterRunLoop.mainRunLoop` to execute any posted tasks every `pollingInterval` seconds.
|
||||
@MainActor
|
||||
private func waitForCondition(
|
||||
_ description: String,
|
||||
timeout: TimeInterval,
|
||||
pollingInterval: TimeInterval = 0.01,
|
||||
condition: @escaping @MainActor () -> Bool
|
||||
) async throws {
|
||||
let startTime = CFAbsoluteTimeGetCurrent()
|
||||
while !condition() {
|
||||
if CFAbsoluteTimeGetCurrent() - startTime > timeout {
|
||||
struct TimeoutError: Error, CustomStringConvertible {
|
||||
let message: String
|
||||
var description: String { message }
|
||||
}
|
||||
throw TimeoutError(message: "Timeout waiting for \(description) after \(timeout) seconds")
|
||||
}
|
||||
// Pump messages to ensure performCommit blocks execute.
|
||||
FlutterRunLoop.mainRunLoop.pollFlutterMessagesOnce()
|
||||
// Allow other tasks to run, avoid pegging the CPU.
|
||||
try await Task.sleep(nanoseconds: UInt64(pollingInterval * 1_000_000_000))
|
||||
}
|
||||
}
|
||||
@ -506,6 +506,15 @@ def run_cc_tests(build_dir, executable_filter, coverage, capture_core_dump):
|
||||
xvfb.stop_virtual_x(build_name)
|
||||
|
||||
if is_mac():
|
||||
# macOS Desktop unit tests written in Swift.
|
||||
run_engine_executable(
|
||||
build_dir,
|
||||
'flutter_desktop_darwin_swift_unittests',
|
||||
executable_filter,
|
||||
shuffle_flags,
|
||||
coverage=coverage
|
||||
)
|
||||
|
||||
# flutter_desktop_darwin_unittests uses global state that isn't handled
|
||||
# correctly by gtest-parallel.
|
||||
# https://github.com/flutter/flutter/issues/104789
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user