-
Notifications
You must be signed in to change notification settings - Fork 53
[SwiftDriver] Part III - Introduce synchronization between swift-frontend invocations #210
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
Changes from 4 commits
22484b4
148c99d
3853ce2
8dffbd4
56d9c20
f4eed9e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,6 +45,7 @@ _XCRemoteCache is a remote cache tool for Xcode projects. It reuses target artif | |
| - [Limitations](#limitations) | ||
| - [FAQ](#faq) | ||
| - [Development](#development) | ||
| - [Architectural Designs](#architectural-designs) | ||
| - [Release](#release) | ||
| * [Releasing CocoaPods plugin](#releasing-cocoapods-plugin) | ||
| * [Building release package](#building-release-package) | ||
|
|
@@ -291,7 +292,7 @@ where | |
|
|
||
| ```shell | ||
| ditto "${SCRIPT_INPUT_FILE_0}" "${SCRIPT_OUTPUT_FILE_0}" | ||
| [ -f "${SCRIPT_INPUT_FILE_1}" ] && ditto "${SCRIPT_INPUT_FILE_1}" "${SCRIPT_OUTPUT_FILE_1}" || rm "${SCRIPT_OUTPUT_FILE_1}" | ||
| [ -f "${SCRIPT_INPUT_FILE_1}" ] && ditto "${SCRIPT_INPUT_FILE_1}" "${SCRIPT_OUTPUT_FILE_1}" || rm -f "${SCRIPT_OUTPUT_FILE_1}" | ||
| ``` | ||
|
|
||
| where | ||
|
|
@@ -469,6 +470,10 @@ Follow the [FAQ](docs/FAQ.md) page. | |
|
|
||
| Follow the [Development](docs/Development.md) guide. It has all the information on how to get started. | ||
|
|
||
| ## Architectural designs | ||
|
|
||
| Follow the [Architectural designs](docs/design/ArchitecturalDesigns.md) document that describes and documents XCRemoteCache designs and implementation details. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 Minor - the graphs are hard to read for dark mode.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks for spotting that! Fixed it. |
||
|
|
||
| ## Release | ||
|
|
||
| To release a version, in [Releases](https://github.com/spotify/XCRemoteCache/releases) draft a new release with `v0.3.0{-rc0}` tag format. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| // Copyright (c) 2023 Spotify AB. | ||
| // | ||
| // Licensed to the Apache Software Foundation (ASF) under one | ||
| // or more contributor license agreements. See the NOTICE file | ||
| // distributed with this work for additional information | ||
| // regarding copyright ownership. The ASF licenses this file | ||
| // to you under the Apache License, Version 2.0 (the | ||
| // "License"); you may not use this file except in compliance | ||
| // with the License. You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, | ||
| // software distributed under the License is distributed on an | ||
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| // KIND, either express or implied. See the License for the | ||
| // specific language governing permissions and limitations | ||
| // under the License. | ||
|
|
||
| import Foundation | ||
|
|
||
| struct SwiftFrontendContext { | ||
| /// File lock used for synchronizing multiple invocations | ||
| let invocationLockFile: URL | ||
| } | ||
|
|
||
| extension SwiftFrontendContext { | ||
| init(_ swiftcContext: SwiftcContext, env: [String: String]) throws { | ||
| /// The LLBUILD_BUILD_ID ENV that describes the swiftc (parent) invocation | ||
| let llbuildId: String = try env.readEnv(key: "LLBUILD_BUILD_ID") | ||
| invocationLockFile = Self.self.buildLlbuildIdSharedLockUrl( | ||
| llbuildId: llbuildId, | ||
| tmpDir: swiftcContext.tempDir | ||
| ) | ||
| } | ||
|
|
||
| /// Generate the filename to be used to sycnhronize mutliple swift-frontend invocations | ||
| /// The same file is used in prebuild, xcswift-frontend and postbuild (to clean it up) | ||
| static func buildLlbuildIdSharedLockUrl(llbuildId: String, tmpDir: URL) -> URL { | ||
| return tmpDir.appendingPathComponent(llbuildId).appendingPathExtension("lock") | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -32,14 +32,105 @@ protocol SwiftFrontendOrchestrator { | |||||
| /// For the compilation action, tries to ackquire a lock and waits until the "emit-module" makes a decision | ||||||
| /// if the compilation should be skipped and a "mocking" should used instead | ||||||
| class CommonSwiftFrontendOrchestrator { | ||||||
| /// Content saved to the shared file | ||||||
| /// Safe to use forced unwrapping | ||||||
| private static let emitModuleContent = "done".data(using: .utf8)! | ||||||
|
|
||||||
| enum Action { | ||||||
| case emitModule | ||||||
| case compile | ||||||
| } | ||||||
| private let mode: SwiftcContext.SwiftcMode | ||||||
| private let action: Action | ||||||
| private let lockAccessor: ExclusiveFileAccessor | ||||||
| private let maxLockTimeout: TimeInterval | ||||||
|
|
||||||
| init(mode: SwiftcContext.SwiftcMode) { | ||||||
| init( | ||||||
| mode: SwiftcContext.SwiftcMode, | ||||||
| action: Action, | ||||||
| lockAccessor: ExclusiveFileAccessor, | ||||||
| maxLockTimeout: TimeInterval | ||||||
| ) { | ||||||
| self.mode = mode | ||||||
| self.action = action | ||||||
| self.lockAccessor = lockAccessor | ||||||
| self.maxLockTimeout = maxLockTimeout | ||||||
| } | ||||||
|
|
||||||
| func run(criticalSection: () throws -> Void) throws { | ||||||
| // TODO: implement synchronization in a separate PR | ||||||
| try criticalSection() | ||||||
| guard case .consumer(commit: .available) = mode else { | ||||||
| // no need to lock anything - just allow fallbacking to the `swiftc or swift-frontend` | ||||||
| // for a producer or a consumer where RC is disabled (we have already caught the | ||||||
| // cache miss) | ||||||
| try criticalSection() | ||||||
| return | ||||||
| } | ||||||
| try executeMockAttemp(criticalSection: criticalSection) | ||||||
| } | ||||||
|
|
||||||
| private func executeMockAttemp(criticalSection: () throws -> Void) throws { | ||||||
| switch action { | ||||||
| case .emitModule: | ||||||
| try validateEmitModuleStep(criticalSection: criticalSection) | ||||||
| case .compile: | ||||||
| try waitForEmitModuleLock(criticalSection: criticalSection) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
|
|
||||||
| /// Foor emit-module, wrap the critical section with the shared lock so other processes (compilation) | ||||||
| /// have to wait until emit-module finishes | ||||||
| /// Once the emit-module is done, the "magical" string is saved to the file and the lock is released | ||||||
| /// | ||||||
| /// Note: The design of wrapping the entire "emit-module" has a small performance downside if inside | ||||||
| /// the critical section, the code realizes that remote cache cannot be used | ||||||
| /// (in practice - a new file has been added) | ||||||
| /// None of compilation process (so with '-c' args) can continue until the entire emit-module logic finishes | ||||||
| /// Because it is expected to happen no that often and emit-module is usually quite fast, this makes the | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| /// implementation way simpler. If we ever want to optimize it, we should release the lock as early | ||||||
| /// as we know, the remote cache cannot be used. Then all other compilation process (-c) can run | ||||||
| /// in parallel with emit-module | ||||||
| private func validateEmitModuleStep(criticalSection: () throws -> Void) throws { | ||||||
| debugLog("starting the emit-module step: locking") | ||||||
| try lockAccessor.exclusiveAccess { handle in | ||||||
| debugLog("starting the emit-module step: locked") | ||||||
| // writing to the file content proactively - incase the critical section never returns | ||||||
| // (in case of a fallback to the local compilation), all awaiting swift-frontent processes | ||||||
| // will be immediatelly unblocked | ||||||
| handle.write(Self.self.emitModuleContent) | ||||||
| try criticalSection() | ||||||
| debugLog("lock file emit-module criticial end") | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /// Locks a shared file in a loop until its content is non-empty - meaning the "parent" emit-module | ||||||
| /// has already finished | ||||||
| private func waitForEmitModuleLock(criticalSection: () throws -> Void) throws { | ||||||
| // emit-module process should really quickly obtain a lock (it is always invoked | ||||||
| // by Xcode as a first process) | ||||||
| var executed = false | ||||||
| let startingDate = Date() | ||||||
| while !executed { | ||||||
| debugLog("lock file compilation trying to acquire a lock ....") | ||||||
| try lockAccessor.exclusiveAccess { handle in | ||||||
| if !handle.availableData.isEmpty { | ||||||
| // the file is not empty so the emit-module process is done with the "check" | ||||||
| debugLog("swift-frontend lock file is unlocked for compilation") | ||||||
| try criticalSection() | ||||||
| executed = true | ||||||
| } else { | ||||||
| debugLog("swift-frontend lock file is not ready for compilation") | ||||||
| } | ||||||
| } | ||||||
| // When a max locking time is achieved, execute anyway | ||||||
| if !executed && Date().timeIntervalSince(startingDate) > self.maxLockTimeout { | ||||||
| errorLog(""" | ||||||
| Executing command \(action) without lock synchronization. That may be cause by the\ | ||||||
| crashed or extremly long emit-module. Contact XCRemoteCache authors about this error. | ||||||
| """) | ||||||
| try criticalSection() | ||||||
| executed = true | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sidefix: found that if a first build has a cache miss, this snippet fails with an error that
$SCRIPT_OUTPUT_FILE_1doesn't exist. In such cases, we can safely no-op (so forcingrm)