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

Conversation

@liamappelbe
Copy link
Contributor

@liamappelbe liamappelbe commented Nov 30, 2023

This is a prototype of the PlatformIsolate API.

UPDATE (Jan 25): The PR is ready for review. PTAL.

The PlatformIsolate creation flow is:

  1. PlatformIsolate.spawn running on parent isolate (platform_isolate.dart)
    a. Create isolateReadyPort
    b. PlatformIsolateNativeApi::Spawn (platform_isolate.cc)
    c. DartIsolate::CreatePlatformIsolate (dart_isolate.cc)
    d. Isolate created. Entry point invocation task dispatched to platform thread
    e. PlatformIsolate.spawn returns a Future<Isolate>
  2. On the platform thread, _platformIsolateMain is invoked in the platform isolate
    a. Create entryPointPort
    b. Send Isolate.current metadata and entryPointPort back to the parent isolate via isolateReadyPort
  3. Back in the parent isolate, isolateReadyPort.handler is invoked
    a. Send the user's entryPoint and message to the platform isolate via entryPointPort
    b. Use received isolate metadata to create a new Isolate representing the platform isolate and complete the Future<Isolate>
  4. In the platform isolate, entryPointPort.handler is invoked
    a. Run the user's entryPoint(message)

The engine shutdown flow is handled by PlatformIsolateManager, which maintains a set of running platform isolates.

@rmacnak-google
Copy link
Contributor

Exiting the parent isolate is how Isolate.spawn used to work before https://codereview.chromium.org/1447353002.
See also https://codereview.chromium.org/1469063003.
It should be possible to avoid blocking the parent isolate, with something like this, which is roughly how Isolate.spawn works

void PlatformIsolateNativeApi::Spawn(Dart_Handle entry_point,
                                     Dart_Handle debug_name,
                                     Dart_Handle reply_port) {                                   
  const char* debug_name_cstr = (strdup) (Dart_StringToCStrig) debug_name;
  Dart_Isolate parent_isolate = Dart_CurrentIsolate();
  Dart_PersistentHandle entry_point_ph = (Dart_NewPersistentHandle) entry_point;
  Dart_Port reply_port = (Dart_SendPortGetId) reply_port;

  platform_task_runner->PostTask([debug_name_cstr, entry_point_ph, parent_isolate, reply_port]() {
     char* error = nullptr;
     new_isolate = Dart_CreateIsolateInGroup(parent_isolate, ... &error);
     if (error) {
       Dart_PostCObject(reply_port, error);
       return;
     }

     DartIsolate::InitializeIsolate(..., &error);
     if (error) {
       Dart_PostCObject(reply_port, error);
       Dart_ShutdownIsolate();
       return;
     }

     InvokeEntrypoint;
  };
  // Parent continues possibly even before Dart_CreateIsolateInGroup.
  // The open reply port prevents the parent for exiting while a spawn is in flight.
}

(Though maybe in the case we would rather block the UI isolate than run isolate initialization on the platform thread?)

bool init_ok =
DartIsolate::InitializePlatformIsolate(&child_isolate_data, &error);
// TODO: Do we need to set this isolate data on the Isolate somehow, or does
// InitializeIsolate do this? Might need a new API function for this.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be more like what is happening in CreateRootIsolate where the embedder data is created before the isolate. I.e.,


auto* isolate_group_data =
      static_cast<std::shared_ptr<DartIsolateGroupData>*>(
          Dart_CurrentIsolateGroupData());

UIDartState::Context context ...
auto child_isolate_data = std::make_unique<std::shared_ptr<DartIsolate>>(
      std::shared_ptr<DartIsolate>(new DartIsolate(*isolate_group_data)->GetSettings(),  // settings                                                                                                                             
                                                   true,      // is_root_isolate                                                                                                                      
                                                   context    // context                                                                                                                              
                                                   )));
Dart_CreateIsolateInGroup(parent_isolate, ...,
                          child_isolate_data.release(), // owernship passes to VM
                          &error);

because in this case the engine is creating the isolate. InitializePlatformIsolate is the reactionary case of the VM creating the isolate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. But when I set the child_isolate_data in Dart_CreateIsolateInGroup to a non-null value, the platform isolate never shuts down. @chinmaygarde Any idea why that would be?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you would need something in the message callback epilogue to notice the isolate is done. I think this doesn't happen for the UI isolates because the UI isolates have event sources other than receive ports.

MessageNotifyCallback() {
  pending_messages++;
  platform_task_runner.PostTask(OnMessage);
}

OnMessage() {
  ...
  pending_messages--;
  Dart_HandleMessage();
  if (pending_messages == 0 && !Dart_HasLivePorts()) {
    Dart_ShutdownIsolate();
  }
}

// TODO: Passing the persistent handle to the new isolate like this is a bit
// of a hack. Do we need to send it via a port?
Dart_PersistentHandle entry_point_handle =
Dart_NewPersistentHandle(entry_point);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is missing the copy-and-validate checking that happens in the regular Isolate.spawn case (the use of CopyMutableObjectGraph in Isolate_spawnFunction). There's not an API function for it, but sending the entry-point and initial message via a port would provide that.

void _platformIsolateMain(SendPort replyPort) {
  port = new RawReceivePort();
  port.handler = (entryPointMessage) {
     port.close();
     entryPointMessage[0].call(entryPointMessage[1]);
  };
  isolate = Isolate.current;
  replyPort.send([isolate.controlPort, isolate.pauseCapability, isolate.terminateCapability, port.sendPort]);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

// TODO: Need to figure out a better way of doing this.
static uint32_t GetCurrentThreadId();

// TODO: Delete this. Find a better way of getting the platform task runner.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UIDartState::Current()->GetTaskRunners()->GetPlatformTaskRunner() ?
(before exiting the parent isolate)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was using the global_platform_task_runner hack because the task runners I was getting from the UIDartState were often null. While doing the other refactors you suggested, I realized that's because the child isolates are being initialized using null_task_runners. So yeah, this is no longer necessary if I initialize the UIDartState::Context with real task runners.

static void Spawn(Dart_Handle entry_point, Dart_Handle debug_name);

// Using this function to verify we're on the platform thread for prototyping.
// TODO: Need to figure out a better way of doing this.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bool isRunningOnPlatformThread() {
 return GetTaskRunners().GetPlatformTaskRunner().RunsTasksOnCurrentThread();
}

?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's definitely better, but it's still exposing a very internal bit of engine machinery to the Dart PlatformIsolate API. Maybe when I write the test for real I should FFI back into C++ land and check the thread from there.

@liamappelbe
Copy link
Contributor Author

It should be possible to avoid blocking the parent isolate, with something like this, which is roughly how Isolate.spawn works

Whoops, missed this comment. No, this is what I was talking about in the Dart VM Hackers channel. If we dispatch the isolate creation to another thread, we have no way of knowing (or controlling) whether the parent isolate is entered. So we hit an assert in Dart_CreateIsolateInGroup that checks that the isolate isn't scheduled. I think this assert is too strict, since all we're doing is getting the group and origin ID from the member, but until that assert is weakened clients will have to create the isolate on the same thread as the parent.

Dart_ExitIsolate();
}

platform_isolate_manager_.ShutdownPlatformIsolates();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect shutdown of platform isolates to happen during shutdown of a Flutter engine instance (Engine/Shell/RuntimeController).

DartVM is a process-wide object that will remain alive after engine shutdown if at least one other engine instance is still active. Or in some configurations the DartVM may stay alive for the entire process lifetime (see Settings::leak_vm)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, but I'm not sure how to access the RuntimeController from DartIsolate::OnShutdownCallback() on the platform isolate. platform_configuration() is only non-null on the root isolate.

return false;
}

platform_isolate_manager->RegisterPlatformIsolate(new_isolate);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a race between this and the platform thread potentially calling ShutdownPlatformIsolates?

RegisterPlatformIsolate has an assertion that assumes that is_shutdown_ is false. But IIUC it's possible for shell shutdown to call ShutdownPlatformIsolates on the platform thread between the platform_isolate_manager->IsShutdown() call above and this point.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh right. I've tightened up this sequence. I think it should be fixed now.

RegisterPlatformIsolate now double checks is_shutdown_, and returns false if it is shutdown. In that case the caller stops setting up the isolate and does nothing else (ie doesn't explicitly shut it down). This is fine because this case means that the engine is being shutdown, so the isolate is about to be shutdown anyway.

RemovePlatformIsolate and ShutdownPlatformIsolates are only ever called on the platform thread, so we don't have the same race condition between them.

I've added comments about all this stuff too.

// RuntimeController* runtime = (RuntimeController*)(client);
// std::cout << "RuntimeController: " << (void*)runtime << std::endl;

// TODO: This doesn't work for platform isolates spawned from other child
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make things simpler if spawning of platform isolates was only allowed from within the root isolate?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this would be a lot simpler. I suppose this is a reasonable restriction, at least for now.

}

void DartIsolate::OnMessageEpilogue(Dart_Handle result) {
if (is_platform_isolate_ && Dart_CurrentIsolate() != nullptr) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is very dangerous logic, making strong assumptions.

Imagine the caller does this

Dart_EnterIsolate();
Dart_EnterScope();
... result = HandleMessage();
... OnMessageEpilogue(result);
Dart_ExitScope();
Dart_ExitIsolate();

If the OnMessageEpilogue() shuts the isolate down, the Dart_ExitScope() will segfault the program.

So you're assuming here:

  • The caller handles isolate death after the OnMessageEpilogue() call appropriately
  • The VM allows Dart_ShutdownIsolate() with actively entered handle scopes by embedder

It may be that this is guaranteed. If it is, it needs to have a comment explaining that here and on OnMessageEpilogue interface declarations all the way up to the caller that provides the guarantee.

}

void DartIsolate::OnMessageEpilogue(Dart_Handle result) {
if (is_platform_isolate_ && Dart_CurrentIsolate() != nullptr) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a much cleaner way would be if this OnMessageEpiloge() would return a bool that tells the caller that it should shutdown the isolate. That bool can be returned all the way to the message loop handler, which is the outermost place where Dart_EnterIsolate() was called, that's where Dart_ShutdownIsolate() should also belong.

FML_DCHECK(Dart_CurrentIsolate() == isolate());
FML_DCHECK(platform_isolate_pending_messages_ > 0);
--platform_isolate_pending_messages_;
if (platform_isolate_pending_messages_ == 0 && !Dart_HasLivePorts()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this responsibility belongs into the message handler loop: The message handler runs for the platform isolate, handling all messages. After all messages are handled, it checks whether there's no longer ports open, if so, it should shutdown the isolate.

I can see that some logic already exists for that in third_party/tonic/dart_message_handler.cc:

void DartMessageHandler::OnHandleMessage(DartState* dart_state) {
  DartIsolateScope scope(dart_state->isolate());
  DartApiScope dart_api_scope;
  ...
    // We are processing messages normally.
    result = Dart_HandleMessage();
    ...
    dart_state->MessageEpilogue(result);
    ...
  if (error) {
    UnhandledError(result);
  } else if (!Dart_HasLivePorts()) {
    // The isolate has no live ports and would like to exit.
      isolate_exited_ = true;
  }
  ...
}

void DartMessageHandler::UnhandledError(Dart_Handle error) {
  ...
  if (Dart_IsFatalError(error)) {
    ...
    Dart_ShutdownIsolate();
  }
}

I'm a little bit puzzled that the isolate_exited_ = true is being set but it doesn't call Dart_ShutdownIsolate() here.

Basically the logic you've written here should belong into the message handler: It should remember whether there's pending messages, and if there's no more pending messages and no open receive port, it should shut the isolate down.

FML_DCHECK(Dart_CurrentIsolate() == isolate());
FML_DCHECK(platform_isolate_pending_messages_ > 0);
--platform_isolate_pending_messages_;
if (platform_isolate_pending_messages_ == 0 && !Dart_HasLivePorts()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be that the UI isolate isn't using this path, because it will keep the UI isolate alive even if there's no open ports and no pending messages (as the flutter engine basically artificially injects drawFrame messages - which doesn't use the port mechanism).

I think this comment and the one above would make this much cleaner. It could be done as a separate PR before this lands.

/// cannot be registered after the manager is shutdown. Callable from any
/// thread. The result may be obsolete immediately after the call, except when
/// called on the platform thread.
bool IsShutdown();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may consider having 2 methods, so it's obvious on the call site if the caller gets a racy return value.

// Only callable on the platform thread.
bool HasShutdown() {
  ASSERT(current-thread-running-on-platform-task-runner);
  ...
}

// If returns true guaranteed to be shutdown, if false it may or may not have shutdown.
bool HasShutdownWithFalseNegativePossibility() {
   ...
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ping :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

platform_isolates_.clear();
}

bool PlatformIsolateManager::IsRegistered(Dart_Isolate isolate) {
Copy link
Member

@mkustermann mkustermann Feb 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then consider renaming to IsRegisteredForTestingOnly :)

Copy link
Member

@mkustermann mkustermann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think with remaining comments addressed we can land it.

// found in the LICENSE file.
part of dart.ui;

/// Runs [computation] in the platform thread and returns the result.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(mentioned before) Not a native speaker (so maybe get second opinion) but to me it seems we should use the "running on a thread" terminology instead of "running in a thread" (i.e. consistently rename in -> on in the PR).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also prefer using "on" in these comments and calling the API runOnPlatformThread

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


Future<SendPort> _spawnPlatformIsolate() {
final Completer<SendPort> sendPortCompleter = Completer<SendPort>();
final RawReceivePort receiver = RawReceivePort()..keepIsolateAlive = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we later allow calling this from a helper isolates, we may want to

  // When scheduling a computation
  if (_pending.isEmpty) {
    receiver.keepIsolateAlive = true;
  }
  _pending[nextId++] = ...

  // When getting result
  ... = _pending.removeAt(id);
  if (_pending.isEmpty) receiver.keepIsolateAlive = false;

Consider leaving a comment

(the keepIsolateAlive getter/setter can be optimized to not go to runtime)

throw IsolateSpawnException('Unable to spawn isolate: $message');
} else {
// This shouldn't happen.
throw IsolateSpawnException(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really related to spawning, is it? Maybe UnsupportedError

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed, per your other comment

return resultCompleter.future;
}

void _platformIsolateMain<T>(SendPort sendPort) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this method generic?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leftovers from the refactor.

// terminate all pending computations and reset the static variables.
} else if (message is _PlatformIsolateReadyMessage) {
_platformRunnerSendPort = message.computationPort;
Isolate.current.addOnExitListener(message.computationPort);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit hard to understand, consider instead letting the platform isolate itself attach the exit listener by giving it the Isolate as part of the closure.

  final currentIsolate = Isolate.current;
  final sendPort = receiver.sendPort;
  try { 
    _nativeSpawn((SendPort sendPort) => _platformIsolateMain(currentIsolate, sendPort), sendPort);
  } ... {}

_platformIsolate(Isolate uiIsolate, SendPort uiIsolatePort) {
  uiIsolate.addExitListener(...);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

final RawReceivePort receiver = RawReceivePort()..keepIsolateAlive = false;
receiver.handler = (Object? message) {
if (message == null) {
// This is the platform isolate's onExit handler.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we actually registering an exit handler (don't see where)?

Either we should throw an error if we think this cannot happen or we should just complete all pending RPCs with an error - but not silently ignore it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jason's comment mentions how to make a private test-only API, so I can now implement and test this logic.

Future<R>(computation);

/// Returns whether the current isolate is running in the platform thread.
bool isRunningInPlatformThread() => true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a getter? (also in -> on)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Dart_SetField(isolate_type, tonic::ToDart("_mayExit"), Dart_False());
FML_CHECK(!tonic::CheckAndHandleError(result));

tonic::DartInvoke(entry_point, {isolate_ready_port});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is passing the sendport to C++, capturing it in lambda, passing it back into Dart on the new isolate.

You can avoid this complexity by making the dart side capture this

  final uiIsolate = Isolate.current;
  final uiIsolateSendPort = ...;
  _nativeSpawn(() => _platformIsolateEntryPoint(uiIsolate, uiIsolateSendPort));

then you invoke entry_point here without any arguments.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

tonic::DartInvoke(entry_point, {isolate_ready_port});

if (Dart_CurrentIsolate() == nullptr) {
// The platform isolate entry point caused the isolate to shut down.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain how this can possibly happen? If it can't, it'd be better to assert it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer possible now that the PlatformIsolate.spawn method is removed.

// isolate shutdown. This can happen either during the ordinary platform
// isolate shutdown, or during ShutdownPlatformIsolates(). In either case
// we're on the platform thread.
// TODO(flutter/flutter#136314): Assert that we're on the platform thread.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add bullet points to the issue for any postponed TODOs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

// found in the LICENSE file.
part of dart.ui;

/// Runs [computation] in the platform thread and returns the result.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also prefer using "on" in these comments and calling the API runOnPlatformThread

/// Returns whether the current isolate is running in the platform thread.
@Native<Bool Function()>(
symbol: 'PlatformIsolateNativeApi::IsRunningInPlatformThread')
external bool isRunningInPlatformThread();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the use cases for making this a public API?

If this API is only used by tests to verify that the code is running on the right thread, then I'd prefer not to add it to the engine's public API.

Can this instead be a native function that is exported to tests by https://github.com/flutter/engine/blob/main/shell/testing/tester_main.cc and then invoked via FFI? This pattern is used by some other Flutter Dart tests.

false, // is_root_isolate
true, // is_platform_isolate
context))); // context
(*isolate_data)->platform_isolate_manager_ = platform_isolate_manager;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a new DartIsolate constructor that takes the PlatformIsolateManager as an argument and sets the is_platform_isolate_ flag.

This avoids the need to access the private platform_isolate_manager_ field here and allows removal of is_platform_isolate from the original DartIsolate constructor.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

const bool is_spawning_in_group_;
std::string domain_network_policy_;
uint32_t platform_isolate_pending_messages_ = 0;
PlatformIsolateManager* platform_isolate_manager_ = nullptr;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PlatformIsolateManager should be held in a std::shared_ptr within RuntimeController/DartIsolate. It should be captured as a std::weak_ptr in the tasks queued to the platform thread.

If engine shutdown happens before a platform thread task executes, then the RuntimeController will be destructed before the task runs. The tasks queued by DartIsolate::CreatePlatformIsolate can detect this safely with a weak_ptr.

@jason-simmons
Copy link
Member

Not sure if this is significant. But if I hot restart a Flutter app after the platform isolate has been created, then the service isolate reports an "isolate terminated by Kill service request" error.

Other than that, the hot restart behaves as expected.

@jason-simmons
Copy link
Member

Another issue I noticed is that some embedders (such as Linux desktop) use a custom task runner for the platform thread. With the custom task runner, the platform thread may not have an fml::MessageLoop or flush the fml::MessageLoopTaskQueues.

So in a debug mode build of Flutter on Linux, the Dart messages in the platform isolate will not execute because the DartIsolate::SetMessageHandlingTaskRunner dispatcher is sending the message task to the fml::MessageLoopTaskQueues. The messages will be processed if I patch DartIsolate::SetMessageHandlingTaskRunner to directly post the message task to the task runner for a platform isolate.

However, in release mode on Linux the platform thread never sets up an fml::MessageLoop. So creation of the platform isolate will fail an assert when the isolate's UIDartState tries to install the task observer on the platform thread.

@chinmaygarde Is there a way for embedders to support this with a custom task runner?

If not, then embedders may need a way to tell the engine to disable platform isolates.

@liamappelbe
Copy link
Contributor Author

The bot failures say ... contained forbidden string "[ERROR", caused by platform_isolate_shutdown_test.dart calling Isolate.exit.

[ERROR:flutter/shell/common/shell.cc(117)] Dart Error: isolate terminated by Isolate.exit

The actual error thrown here is an UnwindError, which is an internal error in the VM whose job is to shut down isolates. So this isn't really an error, it's just ordinary API usage. The C API function Dart_IsFatalError returns true iff the handle is an UnwindError. The function is probably misnamed, because the intention is for the embedder to just silently shut down the isolate (maybe with an info log, but certainly not an error log).

I'd like to change tonic::CheckAndHandleError to use an info log rather than an error log if it's an UnwindError, but at the moment the only log handler tonic has is for errors. So for now I'll just ignore the error. The actual isolate shutdown is handled later in the flow, by tonic::DartMessageHandler::UnhandledError.

Copy link
Member

@mkustermann mkustermann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm with comments, please also wait for jason

@@ -0,0 +1,169 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2024

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flutter uses 2013 throughout its codebase

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flutter uses 2013 throughout its codebase

That's not what go/copyright (external version https://opensource.google/documentation/reference/copyright) instructs us to do though. It should be the year when the file got first published.

Other coding styles (like Chromium1) seem to adhere to the go/copyright.

Footnotes

  1. https://chromium.googlesource.com/chromium/src/+/HEAD/styleguide/c++/c++.md#file-headers

/// reused for subsequent [runOnPlatformThread] calls. This means that global
/// state is maintained in that isolate between calls.
///
/// The [computation] and any state it captures will be sent to that isolate.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will be -> may be (e.g. calling this from the platform thread)

(for same reason I'd talk less about isolates in this comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

resultCompleter.complete(message.result);
}
} else {
// We encountered an error while starting the new isolate.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If that's true, shouldn't it be

  sendPortCompleter.completeError(...);

maybe make it

  if (!sendPortCompleter.isCompleted) {
    sendPortCompleter.completeError(...);
    return;
  }
  throw ...("unexpected message: $message");

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

final FutureOr<Object?> potentiallyAsyncResult = message.computation();
if (potentiallyAsyncResult is Future<Object?>) {
potentiallyAsyncResult.then((Object? result) {
sendPort.send(_ComputationResult(message.id, result, null, null));
Copy link
Member

@mkustermann mkustermann Feb 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe if this sendPort.send() throws, the error is uncaught in platform isolate and receiver never gets an answer. It needs a try/catch around I believe.

Consider adding a test that returns an object that cannot be sent across ports.

Consider also structuring the code differently (to e.g. not think the outermost try/catch will catch the error here):

late final FutureOr<Object?> potentiallyAsyncResult;
try {
  potentiallyAsyncResult = message.computation();
} catch (error, stack) {
  sendPort.send(_ComputationResult(message.id, null, error, stack));
  return;
}

// Sync result
if (potentiallyAsyncResult is! Future) {
  try {
    sendPort.send(_ComputationResult(message.id, null, error, stack));
  } ... { ... }
  return;
}

// Async result
potentiallyAsyncResult.then((result) {
 try { ... } catch { ... }
}, onError: (error, stack) {
 try { ... } catch { ... }
});

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, and caught a couple of other cases.

external void _nativeSpawn(Function entryPoint);

/// Whether the current isolate is running on the platform thread.
bool isRunningOnPlatformThread = _isRunningOnPlatformThread();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be final

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Dart_ThrowException(tonic::ToDart(
"PlatformIsolates can only be spawned on the root isolate."));
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jason mentioned the current PR may not work on desktop embedders (which may have their own platform thread event loop). If so, can we detect this here and throw? (ideally also test)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Flutter engine does not currently have a way to detect whether the embedder is using a custom platform thread task runner that does not support this usage.

After this lands I can follow up with another PR that adds an engine flag which will disable the platform isolate feature. If the embedder sets the flag, then this API will throw.

/// cannot be registered after the manager is shutdown. Callable from any
/// thread. The result may be obsolete immediately after the call, except when
/// called on the platform thread.
bool IsShutdown();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ping :)

// Need a method that works for ShutdownPlatformIsolates() too.
std::scoped_lock lock(lock_);
if (is_shutdown_) {
// Removal during ShutdownPlatformIsolates. Ignore, to avoid modifying
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could assert the set is empty if you made the ShutdownPlatformIsolates() use std::move() the set out of the field & make field empty, before iterating.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

bool IsRegisteredForTestingOnly(Dart_Isolate isolate);

private:
std::recursive_mutex lock_;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider mentioning why we need the recursion part.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

/// sendable between isolates. Objects that cannot be sent include open
/// files and sockets (see [SendPort.send] for details).
///
/// This method can only be invoked from the main isolate.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should mark this temporarily as

///
/// Experimental for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@liamappelbe liamappelbe changed the title Prototype platform isolates API Experimental platform isolates API Mar 4, 2024
@liamappelbe liamappelbe merged commit 9cbca80 into flutter:main Mar 4, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Mar 4, 2024
@liamappelbe liamappelbe deleted the platiso2 branch March 4, 2024 20:20
jason-simmons added a commit to jason-simmons/flutter_engine that referenced this pull request Mar 21, 2024
… for isolates running on the platform thread

Some embedders use a custom platform task runner that does not support fml::MessageLoopTaskQueues and may not have an fml::MessageLoop.

To support those embedders, DartIsolate::SetMessageHandlingTaskRunner will not use fml::MessageLoopTaskQueues for platform isolates.

See flutter#48551 (comment)
jason-simmons added a commit to jason-simmons/flutter_engine that referenced this pull request Mar 21, 2024
… for isolates running on the platform thread

Some embedders use a custom platform task runner that does not support fml::MessageLoopTaskQueues and may not have an fml::MessageLoop.

To support those embedders, DartIsolate::SetMessageHandlingTaskRunner will not use fml::MessageLoopTaskQueues for platform isolates.

See flutter#48551 (comment)
auto-submit bot pushed a commit that referenced this pull request Mar 22, 2024
… for isolates running on the platform thread (#51573)

Some embedders use a custom platform task runner that does not support fml::MessageLoopTaskQueues and may not have an fml::MessageLoop.

To support those embedders, DartIsolate::SetMessageHandlingTaskRunner will not use fml::MessageLoopTaskQueues for platform isolates.

See #48551 (comment)
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

platform-web Code specifically for the web engine

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants