Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Isolated synchronous deinit #60057

Open
wants to merge 42 commits into
base: main
Choose a base branch
from

Conversation

nickolas-pohilets
Copy link
Contributor

@nickolas-pohilets nickolas-pohilets commented Jul 14, 2022

Implementation of the Isolated asynchronous deinit

Still pending:

  • I suspect that logic for adding attributes for inferred isolation is not correct, would like some input from the Core team about this.
  • Still need to integrate with distributed actors, waiting for [Distributed] prevent remote distributed actor's from running deinit bodies #60050 to be merged.
  • Didn't copy changes to BackDeployConcurrency yet. I expect that I'll have to make changes based on review comments, and want to apply all changes to BackDeployConcurrency in one go. I would appreciate some advice for maintaining the two in sync.
  • Investigate fast path for self-isolated default actor's deinit

Copy link
Member

@DougGregor DougGregor left a comment

Choose a reason for hiding this comment

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

This is fantastic! A couple of comments and questions, both in design and implementation, but it's very much going in the right direction.

include/swift/AST/Decl.h Outdated Show resolved Hide resolved
@@ -606,7 +606,8 @@ struct PrintOptions {
result.SkipSwiftPrivateClangDecls = true;
result.SkipPrivateStdlibDecls = true;
result.SkipUnderscoredStdlibProtocols = true;
result.SkipDeinit = true;
result.SkipDeinit = false; // Deinit may have isolation attributes, which
Copy link
Member

Choose a reason for hiding this comment

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

This will make interfaces a little more noisy in the common case, but that's okay. The alternative would be to print only when there is something interesting (i.e., a nonisolated or custom attribute).

lib/ClangImporter/ImportDecl.cpp Show resolved Hide resolved
lib/SILGen/SILGenDestructor.cpp Outdated Show resolved Hide resolved
lib/Sema/TypeCheckConcurrency.cpp Outdated Show resolved Hide resolved
stdlib/public/Concurrency/MainActor.swift Outdated Show resolved Hide resolved
AdHocJob(JobPriority priority, void *context, AdHocWorkFunction *work)
: Job({JobKind::AdHoc, priority}, &process), Context(context),
Work(work) {
TaskLocal::copyTo(&Local, nullptr);
Copy link
Member

Choose a reason for hiding this comment

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

There's an interesting design question in here. Do we want the task locals from the task that did the last "release" to be available to the deinitializer? It does match today's semantics, but it could be expensive to copy all of the task locals, which are not likely to be used by many/most deinitializers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For GAIT, I expect that vast majority of releases to happen within isolating actor, so I'm not very concerned about performance costs of the slow path. But for actors, that's probably not true. Maybe what you mentioned below can help to reduce switching for actors.

Copy link
Contributor

@ktoso ktoso Jul 20, 2022

Choose a reason for hiding this comment

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

I would not depend on having task locals available in a deinit, and especially since we don't guarantee WHICH exact locals those will be, so basing any tracing infra on "object got deallocated and must have right locals" is going to be wrong at some point so not usable "for real" I think...

I'd say losing the locals is expected. For anything that needs things at "end of scope" (like tracing) the usual pattern is to wrap in a closure, not really rely on deinit.

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 think some task-locals can still be useful in the deinit even if you are not sure which task/thread performs the last release. For example, if you have a logger applied to entire app or a large subsystem, you can expect that it will be available in any task/thread launched within that scope. So it does not matter which one makes the last release, as long you are confident that last release happens within this coarse-grained scope, you can still meaningfully use logger's task local in the deinit.

test/Concurrency/deinit_isolation.swift Outdated Show resolved Hide resolved
lib/SILGen/SILGenDestructor.cpp Show resolved Hide resolved
@DougGregor
Copy link
Member

  • Didn't copy changes to BackDeployConcurrency yet. I expect that I'll have to make changes based on review comments, and want to apply all changes to BackDeployConcurrency in one go. I would appreciate some advice for maintaining the two in sync.

BackDeployConcurrency is actually supposed to be frozen in time, and not updated with new features. For back-deployment of this feature, we'll have to bring up another statically-linked back-deployment library that would be linked into all problems that build with a deployment target that predates this work. @etcwilde is starting to bring up such a library to back-deploy other concurrency-related fixes. Once that's in place, this PR can build on that work.

@nickolas-pohilets
Copy link
Contributor Author

BackDeployConcurrency is actually supposed to be frozen in time, and not updated with new features. For back-deployment of this feature, we'll have to bring up another statically-linked back-deployment library that would be linked into all problems that build with a deployment target that predates this work. @etcwilde is starting to bring up such a library to back-deploy other concurrency-related fixes. Once that's in place, this PR can build on that work.

I would really appreciate a document about how to work with BackDeployConcurrency. Could not find anything in the repo.

@ktoso ktoso self-requested a review July 19, 2022 11:18
@ktoso
Copy link
Contributor

ktoso commented Jul 20, 2022

AFAICS the distributed adjustment looks good 👍
We only resignID as before in the local branch, and the remote does only destroy fields properly, looks good to me.

}
let r = g.wait(timeout: .now() + .milliseconds(500))
expectEqual(r, .timedOut)
expectCrashLater(withMessage: "Assertion failed: (!oldState.getFirstJob() && \"actor has queued jobs at destruction\"), function destroy")
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'm not sure if that is even valid to test for assertions in stdlib as an expected behaviour. If stdlib is build without assertions, test will fail

@DougGregor
Copy link
Member

@swift-ci please test

1 similar comment
@DougGregor
Copy link
Member

@swift-ci please test

@nickolas-pohilets nickolas-pohilets force-pushed the mpokhylets/isolated-deinit branch 6 times, most recently from 5877715 to 47c4b24 Compare August 1, 2022 13:00
@xwu
Copy link
Collaborator

xwu commented Aug 1, 2022

@swift-ci please test

@xwu
Copy link
Collaborator

xwu commented Aug 2, 2022

@swift-ci please test

@stephencelis
Copy link
Contributor

I asked on the review thread, but maybe it'll get more visibility here. Any chance we can get a toolchain to test?

@theblixguy
Copy link
Collaborator

@swift-ci please build toolchain

1 similar comment
@Jumhyn
Copy link
Member

Jumhyn commented Aug 26, 2022

@swift-ci please build toolchain

@nickolas-pohilets nickolas-pohilets force-pushed the mpokhylets/isolated-deinit branch 2 times, most recently from 324c20d to dedd1e4 Compare August 26, 2022 19:16
@Jumhyn
Copy link
Member

Jumhyn commented Aug 26, 2022

@swift-ci please build toolchain macOS

@Jumhyn
Copy link
Member

Jumhyn commented Aug 26, 2022

@swift-ci please build toolchain

@DougGregor
Copy link
Member

@swift-ci please build toolchain macOS

…m overriden declaration.

But if isolation was inferred from class isolation and matches with isolation of the overriden declaration, attributes are added.
It cannot be used for executing general-purpose work, because such function would need to have a different signature to pass isolated actor instance.

And being explicit about using this method only for deinit allows to use object pointer for comparison with executor identity.
* Recursive deallocation with multiple locks held
* (UB) Escaping self triggers assertion in stdlib
Failing: Distributed/Runtime/distributed_actor_deinit.swift
…-deinit

# Conflicts:
#	include/swift/Basic/Features.def
#	lib/SILGen/SILGenDestructor.cpp
#	test/Concurrency/flow_isolation.swift
#	test/abi/macOS/arm64/concurrency.swift
#	test/abi/macOS/x86_64/concurrency.swift
@nickolas-pohilets
Copy link
Contributor Author

nickolas-pohilets commented Jul 11, 2024

Rebased and updated implementation:

  • Removed all code changes related to TL, but updated and preserved unit-tests.
  • Preserved flags parameter, as an extension point for the future. No strong opinion about this, I don't mind removing it.
  • Removed changes for async deinit to separate branch (can be recovered later).
  • Removed changes to python tests (out of scope, see also Fix failures in update_checkout tests because of missing user identity #61510).
  • Allowed @preconcurrency attribute on deist
  • Fixed a bug in slow path of the isolated deinit, by removing pair of swift_retain()/swift_release() around defaultActorDrain().
  • Always use Legacy_NoCheckIsolated_NonCrashing when checking for current executor, even in Swift 6.

Branch is ready to run CI and resume code review process.

@hborla
Copy link
Member

hborla commented Jul 11, 2024

@swift-ci please build toolchain

@hborla
Copy link
Member

hborla commented Jul 11, 2024

@swift-ci please test

@hborla
Copy link
Member

hborla commented Jul 11, 2024

@swift-ci please build toolchain

@hborla
Copy link
Member

hborla commented Jul 11, 2024

@swift-ci please test

@hborla
Copy link
Member

hborla commented Jul 19, 2024

@swift-ci please build toolchain

Task_EnqueueJob = 12,
Task_AddPendingGroupTaskUnconditionally = 13,
Task_IsDiscardingTask = 14,
Task_FunctionConsumesContext = 15,
Copy link
Contributor

Choose a reason for hiding this comment

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

formatting nitpick

@@ -326,7 +326,7 @@ SIMPLE_DECL_ATTR(_noAllocation, NoAllocation,
OnAbstractFunction | OnSubscript | UserInaccessible | ABIStableToAdd | ABIStableToRemove | APIStableToAdd | APIStableToRemove,
124)
SIMPLE_DECL_ATTR(preconcurrency, Preconcurrency,
OnFunc | OnConstructor | OnProtocol | OnGenericType | OnVar | OnSubscript | OnEnumElement | OnImport | ABIStableToAdd | ABIBreakingToRemove | APIBreakingToAdd | APIBreakingToRemove,
OnFunc | OnConstructor | OnDestructor | OnProtocol | OnGenericType | OnVar | OnSubscript | OnEnumElement | OnImport | ABIStableToAdd | ABIBreakingToRemove | APIBreakingToAdd | APIBreakingToRemove,
Copy link
Contributor

Choose a reason for hiding this comment

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

do we need preconcurrency on deinit? I think we talked about how it shouldn't really matter there.



ERROR(isolated_deinit_no_isolation,none,
"deinit is marked isolated, but containing class %0 is not isolated to an actor",
Copy link
Contributor

Choose a reason for hiding this comment

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

this needs to handle struct as well because noncopyable struct can have deinits.

ERROR(isolated_deinit_no_isolation,none,
      "deinit is marked isolated, but containing %0 %1 is not isolated to an actor",
      (DescriptiveDeclKind, DeclName))

here's a patch: 03196c5

Copy link
Contributor

Choose a reason for hiding this comment

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

So that's the good news; the bad news is that making

@SecondActor
struct SecondActorIsolatedNoncopyableStruct: ~Copyable {
  isolated deinit {
#if !SILGEN
    isolatedFunc() // expected-error {{call to global actor 'FirstActor'-isolated global function 'isolatedFunc()' in a synchronous global actor 'SecondActor'-isolated context}}
#endif
  }
}

not blow up seems will take more work; First the emitDistributedRemoteActorDeinit cannot assume we're a class, this is still pretty simple:

// auto cd = cast<ClassDecl>(dd->getDeclContext()->getSelfNominalTypeDecl());
  // ->
  auto Nominal = dd->getDeclContext()->getSelfNominalTypeDecl();

and adjust accordingly; there's only one place which needs to check for the class there.

Sadly other code will blow up then, this can be looked into afterwards after we land the initial work perhaps as I don't think this is common, but worth taking note.

case 'Z':
Args = None;
Kind = Node::Kind::IsolatedDeallocator;
break;
Copy link
Contributor

Choose a reason for hiding this comment

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

style: nitpick to follow the same style as rest of the switch

SILValue selfValue, DestructorDecl *dd, bool isIsolated,
llvm::function_ref<void()> emitLocalDeinit) {
RegularLocation loc(dd);
loc.markAutoGenerated();

auto cd = cast<ClassDecl>(dd->getDeclContext()->getSelfNominalTypeDecl());
Copy link
Contributor

Choose a reason for hiding this comment

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

This may fail because ~Copyable sturct's deinit

@@ -402,20 +401,10 @@ extern "C" SWIFT_CC(swift) void _swift_task_enqueueOnExecutor(
const SerialExecutorWitnessTable *wtable);

SWIFT_CC(swift)
static bool swift_task_isCurrentExecutorImpl(SerialExecutorRef expectedExecutor) {
static bool isCurrentExecutor(SerialExecutorRef expectedExecutor,
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh I see, because we need to check for the optimization... this refactoring overlaps (does the same) as some other ongoing work so we'll conflict a bit but that's fine -- yeah you're doing the right thing to try to use the legacy mode during checking here... I think this will be good enough as it's "just" an optimization

//
// We always use "legacy" checking mode here, because that's the desired
// behaviour for this use case. This does not change with SDK version or
// language mode.
Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, sounds good. Indeed as a best effort the legacy mode will give us sometimes a good answer...

I'd love to get the bool returning APIs "always" but it's been a struggle to get those exposed as there's fears about such API being abused.

}

func isCurrent(_ a: AnyActor) -> Bool {
return isCurrentExecutor(getExecutor(a))
Copy link
Contributor

Choose a reason for hiding this comment

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

can't rely on the func returning bool because its runtime dependent, you have to either use an env var to make the test forced into legacy mode -- annoying, or make the test expressed in terms of "check" and just check that we don't crash in situations.

I worry about this just coming back to bite us very soon and us having to refactor all those tests that assume the bool returning here.

Copy link
Contributor

Choose a reason for hiding this comment

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

The test can do the same thing really, but let's not assume we're able to return the bool.

In practice returning a false here would become an assertion error so that's still doing what the test should be doing... but sadly these modes make it more confusing than it should be heh.

// I wish we didnt have this behavior but we do so let's try to not double down more on the -> bool "lie"

}
let r = g.wait(timeout: .now() + .milliseconds(500))
expectEqual(r, .timedOut)
expectCrashLater(withMessage: "Assertion failed: (!oldState.getFirstUnprioritisedJob() && \"actor has queued jobs at destruction\"), function destroy")
Copy link
Contributor

Choose a reason for hiding this comment

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

This expect... probably should move to the tests.test() {} block

// Ideally these tests should be compile-time errors
let tests = TestSuite("EscapingSelf")
tests.test("escape while locked") {
// TODO: Investigate failure on Linux and re-enable this test
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 we can handle this via

// REQUIRES: swift_stdlib_asserts

because we do have some builds which have assertions enabled, so then this test would work, also on linux.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

10 participants