Skip to content

Commit 13d60ca

Browse files
authored
[PackageLoading] Use a simpler manifest cache (#2840)
This switches the manifest cache approach used inside manifest loader to directly use sqlite database instead of going through llbuild. The main reason for this switch is to simplify caching behavior and take advantage of SQLite's WAL mode for concurrent access. A separate PR will make the manifest cache global for the user.
1 parent 9f06667 commit 13d60ca

File tree

3 files changed

+82
-304
lines changed

3 files changed

+82
-304
lines changed

Sources/PackageLoading/ManifestLoader.swift

Lines changed: 77 additions & 245 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import TSCBasic
1212
import PackageModel
1313
import TSCUtility
14-
import SPMLLBuild
1514
import Foundation
1615
public typealias FileSystem = TSCBasic.FileSystem
1716

@@ -137,6 +136,7 @@ public final class ManifestLoader: ManifestLoaderProtocol {
137136
}
138137
let cacheDir: AbsolutePath!
139138
let delegate: ManifestLoaderDelegate?
139+
let cache: PersistentCacheProtocol?
140140

141141
public init(
142142
manifestResources: ManifestResourceProvider,
@@ -155,6 +155,11 @@ public final class ManifestLoader: ManifestLoaderProtocol {
155155
try? localFileSystem.createDirectory(cacheDir, recursive: true)
156156
}
157157
self.cacheDir = cacheDir.map(resolveSymlinks)
158+
159+
self.cache = cacheDir.flatMap {
160+
// FIXME: It would be nice to emit a warning if we weren't able to create the cache.
161+
try? SQLiteBackedPersistentCache(cacheFilePath: $0.appending(component: "manifest.db"))
162+
}
158163
}
159164

160165
@available(*, deprecated)
@@ -465,16 +470,19 @@ public final class ManifestLoader: ManifestLoaderProtocol {
465470
pathOrContents = .path(inputPath)
466471
}
467472

468-
if !self.isManifestCachingEnabled {
469-
// Load directly if manifest caching is not enabled.
470-
result = parse(
473+
if let cache = self.cache {
474+
let key = ManifestCacheKey(
471475
packageIdentity: packageIdentity,
472-
pathOrContents: pathOrContents, toolsVersion: toolsVersion)
476+
pathOrContents: pathOrContents,
477+
toolsVersion: toolsVersion,
478+
env: ProcessEnv.vars,
479+
swiftpmVersion: Versioning.currentVersion.displayString
480+
)
481+
result = try loadManifestFromCache(key: key, cache: cache)
473482
} else {
474-
let key = ManifestLoadRule.RuleKey(
483+
result = parse(
475484
packageIdentity: packageIdentity,
476485
pathOrContents: pathOrContents, toolsVersion: toolsVersion)
477-
result = try getEngine().build(key: key)
478486
}
479487

480488
// Throw now if we weren't able to parse the manifest.
@@ -495,7 +503,68 @@ public final class ManifestLoader: ManifestLoaderProtocol {
495503
return parsedManifest
496504
}
497505

498-
fileprivate struct ManifestParseResult: LLBuildValue {
506+
fileprivate func loadManifestFromCache(
507+
key: ManifestCacheKey,
508+
cache: PersistentCacheProtocol
509+
) throws -> ManifestParseResult {
510+
let keyHash = try key.computeHash()
511+
let cacheHit = try keyHash.withData {
512+
try cache.get(key: $0)
513+
}.flatMap {
514+
try? JSONDecoder().decode(ManifestParseResult.self, from: $0)
515+
}
516+
if let result = cacheHit {
517+
return result
518+
}
519+
520+
let result = parse(
521+
packageIdentity: key.packageIdentity,
522+
pathOrContents: key.pathOrContents,
523+
toolsVersion: key.toolsVersion
524+
)
525+
526+
let encoder = JSONEncoder()
527+
if #available(macOS 10.15, *) {
528+
encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
529+
}
530+
531+
try keyHash.withData {
532+
try cache.put(key: $0, value: encoder.encode(result))
533+
}
534+
535+
return result
536+
}
537+
538+
fileprivate struct ManifestCacheKey {
539+
let packageIdentity: String
540+
let pathOrContents: ManifestPathOrContents
541+
let toolsVersion: ToolsVersion
542+
let env: [String: String]
543+
let swiftpmVersion: String
544+
545+
func computeHash() throws -> ByteString {
546+
let stream = BufferedOutputByteStream()
547+
stream <<< packageIdentity
548+
549+
switch pathOrContents {
550+
case .path(let path):
551+
stream <<< (try localFileSystem.readFileContents(path))
552+
case .contents(let contents):
553+
stream <<< contents
554+
}
555+
556+
stream <<< toolsVersion.description
557+
558+
for key in env.keys.sorted(by: >) {
559+
stream <<< key <<< env[key]!
560+
}
561+
stream <<< swiftpmVersion
562+
563+
return SHA256().hash(stream.bytes)
564+
}
565+
}
566+
567+
fileprivate struct ManifestParseResult: Codable {
499568
var hasErrors: Bool {
500569
return parsedManifest == nil
501570
}
@@ -739,25 +808,6 @@ public final class ManifestLoader: ManifestLoaderProtocol {
739808
// Bin dir will be set when developing swiftpm without building all of the runtimes.
740809
return resources.binDir ?? resources.libDir.appending(version.runtimeSubpath)
741810
}
742-
743-
/// Returns the build engine.
744-
private func getEngine() throws -> LLBuildEngine {
745-
if let engine = _engine {
746-
return engine
747-
}
748-
749-
let cacheDelegate = ManifestCacheDelegate()
750-
let engine = LLBuildEngine(delegate: cacheDelegate)
751-
cacheDelegate.loader = self
752-
753-
if isManifestCachingEnabled {
754-
try localFileSystem.createDirectory(cacheDir, recursive: true)
755-
try engine.attachDB(path: cacheDir.appending(component: "manifest.db").pathString)
756-
}
757-
_engine = engine
758-
return engine
759-
}
760-
private var _engine: LLBuildEngine?
761811
}
762812

763813
/// Returns the sandbox profile to be used when parsing manifest on macOS.
@@ -790,230 +840,12 @@ private func sandboxProfile(toolsVersion: ToolsVersion, cacheDirectories: [Absol
790840
return stream.bytes.description
791841
}
792842

793-
// MARK:- Caching support.
794-
795-
final class ManifestCacheDelegate: LLBuildEngineDelegate {
796-
797-
weak var loader: ManifestLoader!
798-
799-
func lookupRule(rule: String, key: Key) -> Rule {
800-
switch rule {
801-
case ManifestLoadRule.ruleName:
802-
return ManifestLoadRule(key, loader: loader)
803-
case FileInfoRule.ruleName:
804-
return FileInfoRule(key)
805-
case SwiftPMVersionRule.ruleName:
806-
return SwiftPMVersionRule()
807-
case ProcessEnvRule.ruleName:
808-
return ProcessEnvRule()
809-
default:
810-
fatalError("Unknown rule \(rule)")
811-
}
812-
}
813-
}
814-
815-
/// A rule to load a package manifest.
816-
///
817-
/// The rule can currently only load manifests which are physically present on
818-
/// the local file system. The rule will re-run if the manifest is modified.
819-
final class ManifestLoadRule: LLBuildRule {
820-
821-
fileprivate struct RuleKey: LLBuildKey {
822-
typealias BuildValue = ManifestLoader.ManifestParseResult
823-
typealias BuildRule = ManifestLoadRule
824-
825-
let packageIdentity: String
826-
let pathOrContents: ManifestPathOrContents
827-
let toolsVersion: ToolsVersion
828-
}
829-
830-
override class var ruleName: String { return "\(ManifestLoadRule.self)" }
831-
832-
private let key: RuleKey
833-
private weak var loader: ManifestLoader!
834-
835-
init(_ key: Key, loader: ManifestLoader) {
836-
self.key = RuleKey(key)
837-
self.loader = loader
838-
super.init()
839-
}
840-
841-
override func start(_ engine: LLTaskBuildEngine) {
842-
// FIXME: Ideally, we should expose an API in the manifest file to track individual
843-
// environment variables instead of blindly invalidating when *anything* changes.
844-
engine.taskNeedsInput(ProcessEnvRule.RuleKey(), inputID: 1)
845-
846-
engine.taskNeedsInput(SwiftPMVersionRule.RuleKey(), inputID: 2)
847-
if case .path(let path) = key.pathOrContents {
848-
engine.taskNeedsInput(FileInfoRule.RuleKey(path: path), inputID: 3)
849-
}
850-
}
851-
852-
override func isResultValid(_ priorValue: Value) -> Bool {
853-
// Always rebuild if we had a failure.
854-
do {
855-
let value = try RuleKey.BuildValue(priorValue)
856-
if value.hasErrors { return false }
857-
} catch {
858-
return false
859-
}
860-
861-
return super.isResultValid(priorValue)
862-
}
863-
864-
override func inputsAvailable(_ engine: LLTaskBuildEngine) {
865-
let value = loader.parse(
866-
packageIdentity: key.packageIdentity,
867-
pathOrContents: key.pathOrContents, toolsVersion: key.toolsVersion)
868-
engine.taskIsComplete(value)
869-
}
870-
}
871-
872-
// FIXME: Find a proper place for this rule.
873-
/// A rule to compute the current process environment.
874-
///
875-
/// This rule will always run.
876-
final class ProcessEnvRule: LLBuildRule {
877-
878-
struct RuleKey: LLBuildKey {
879-
typealias BuildValue = RuleValue
880-
typealias BuildRule = ProcessEnvRule
881-
}
882-
883-
struct RuleValue: LLBuildValue, Equatable {
884-
let env: [String: String]
885-
}
886-
887-
override class var ruleName: String { return "\(ProcessEnvRule.self)" }
888-
889-
override func isResultValid(_ priorValue: Value) -> Bool {
890-
// Always rebuild this rule.
891-
return false
892-
}
893-
894-
override func inputsAvailable(_ engine: LLTaskBuildEngine) {
895-
let env = ProcessInfo.processInfo.environment
896-
engine.taskIsComplete(RuleValue(env: env))
897-
}
898-
}
899-
900-
// FIXME: Find a proper place for this rule.
901-
/// A rule to get file info of a file on disk.
902-
final class FileInfoRule: LLBuildRule {
903-
904-
struct RuleKey: LLBuildKey {
905-
typealias BuildValue = RuleValue
906-
typealias BuildRule = FileInfoRule
907-
908-
let path: AbsolutePath
909-
}
910-
911-
typealias RuleValue = CodableResult<TSCBasic.FileInfo, StringError>
912-
913-
override class var ruleName: String { return "\(FileInfoRule.self)" }
914-
915-
private let key: RuleKey
916-
917-
init(_ key: Key) {
918-
self.key = RuleKey(key)
919-
super.init()
920-
}
921-
922-
override func isResultValid(_ priorValue: Value) -> Bool {
923-
let priorValue = try? RuleValue(priorValue)
924-
925-
// Always rebuild if we had a failure.
926-
if case .failure = priorValue?.result {
927-
return false
928-
}
929-
return getFileInfo(key.path).result == priorValue?.result
930-
}
931-
932-
override func inputsAvailable(_ engine: LLTaskBuildEngine) {
933-
engine.taskIsComplete(getFileInfo(key.path))
934-
}
935-
936-
private func getFileInfo(_ path: AbsolutePath) -> RuleValue {
937-
return RuleValue(body: {
938-
try localFileSystem.getFileInfo(key.path)
939-
})
940-
}
941-
}
942-
943-
// FIXME: Find a proper place for this rule.
944-
/// A rule to compute the current version of the pacakge manager.
945-
///
946-
/// This rule will always run.
947-
final class SwiftPMVersionRule: LLBuildRule {
948-
949-
struct RuleKey: LLBuildKey {
950-
typealias BuildValue = RuleValue
951-
typealias BuildRule = SwiftPMVersionRule
952-
}
953-
954-
struct RuleValue: LLBuildValue, Equatable {
955-
let version: String
956-
}
957-
958-
override class var ruleName: String { return "\(SwiftPMVersionRule.self)" }
959-
960-
override func isResultValid(_ priorValue: Value) -> Bool {
961-
// Always rebuild this rule.
962-
return false
963-
}
964-
965-
override func inputsAvailable(_ engine: LLTaskBuildEngine) {
966-
// FIXME: We need to include git hash in the version
967-
// string to make this rule more correct.
968-
let version = Versioning.currentVersion.displayString
969-
engine.taskIsComplete(RuleValue(version: version))
970-
}
971-
}
972-
973843
/// Enum to represent either the manifest path or its content.
974844
private enum ManifestPathOrContents {
975845
case path(AbsolutePath)
976846
case contents([UInt8])
977847
}
978848

979-
extension ManifestPathOrContents: Codable {
980-
private enum CodingKeys: String, CodingKey {
981-
case path
982-
case contents
983-
}
984-
985-
init(from decoder: Decoder) throws {
986-
let values = try decoder.container(keyedBy: CodingKeys.self)
987-
guard let key = values.allKeys.first(where: values.contains) else {
988-
throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Did not find a matching key"))
989-
}
990-
switch key {
991-
case .path:
992-
var unkeyedValues = try values.nestedUnkeyedContainer(forKey: key)
993-
let a1 = try unkeyedValues.decode(AbsolutePath.self)
994-
self = .path(a1)
995-
case .contents:
996-
var unkeyedValues = try values.nestedUnkeyedContainer(forKey: key)
997-
let a1 = try unkeyedValues.decode([UInt8].self)
998-
self = .contents(a1)
999-
}
1000-
}
1001-
1002-
func encode(to encoder: Encoder) throws {
1003-
var container = encoder.container(keyedBy: CodingKeys.self)
1004-
switch self {
1005-
case let .path(a1):
1006-
var unkeyedContainer = container.nestedUnkeyedContainer(forKey: .path)
1007-
try unkeyedContainer.encode(a1)
1008-
case let .contents(a1):
1009-
var unkeyedContainer = container.nestedUnkeyedContainer(forKey: .contents)
1010-
try unkeyedContainer.encode(a1)
1011-
}
1012-
}
1013-
}
1014-
1015-
extension CodableResult: LLBuildValue { }
1016-
1017849
extension TSCBasic.Diagnostic.Message {
1018850
static func duplicateTargetName(targetName: String) -> Self {
1019851
.error("duplicate target named '\(targetName)'")

0 commit comments

Comments
 (0)