@@ -90,6 +90,9 @@ package final class BuildDescriptionManager: Sendable {
9090 /// The in-memory cache of build descriptions.
9191 private let inMemoryCachedBuildDescriptions : HeavyCache < BuildDescriptionSignature , BuildDescription >
9292
93+ /// Build descriptions explicitly retained by clients.
94+ private let retainedBuildDescriptions : Registry < BuildDescriptionSignature , ( BuildDescription , UInt ) > = . init( )
95+
9396 /// The last build plan request. Used to generate a diff of the current plan for debugging purposes.
9497 private let lastBuildPlanRequest : SWBMutex < BuildPlanRequest ? > = . init( nil )
9598
@@ -254,35 +257,44 @@ package final class BuildDescriptionManager: Sendable {
254257 /// During normal operation (outside of tests), this should always be called on `queue`.
255258 package enum BuildDescriptionRequest {
256259 /// Retrieve or create a build description based on a build plan.
257- case newOrCached( BuildPlanRequest , bypassActualTasks: Bool , useSynchronousBuildDescriptionSerialization: Bool )
260+ case newOrCached( BuildPlanRequest , bypassActualTasks: Bool , useSynchronousBuildDescriptionSerialization: Bool , retain : Bool )
258261 /// Retrieve an existing build description, build planning has been avoided. If the build description is not available then `getNewOrCachedBuildDescription` will fail.
259- case cachedOnly( BuildDescriptionID , request: BuildRequest , buildRequestContext: BuildRequestContext , workspaceContext: WorkspaceContext )
262+ case cachedOnly( BuildDescriptionID , request: BuildRequest , buildRequestContext: BuildRequestContext , workspaceContext: WorkspaceContext , retain : Bool )
260263
261264 var buildRequest : BuildRequest {
262265 switch self {
263- case . newOrCached( let planRequest, _, _) : return planRequest. buildRequest
264- case . cachedOnly( _, let request, _, _) : return request
266+ case . newOrCached( let planRequest, _, _, _ ) : return planRequest. buildRequest
267+ case . cachedOnly( _, let request, _, _, _ ) : return request
265268 }
266269 }
267270
268271 var buildRequestContext : BuildRequestContext {
269272 switch self {
270- case . newOrCached( let planRequest, _, _) : return planRequest. buildRequestContext
271- case . cachedOnly( _, _, let buildRequestContext, _) : return buildRequestContext
273+ case . newOrCached( let planRequest, _, _, _ ) : return planRequest. buildRequestContext
274+ case . cachedOnly( _, _, let buildRequestContext, _, _ ) : return buildRequestContext
272275 }
273276 }
274277
275278 var planRequest : BuildPlanRequest ? {
276279 switch self {
277- case . newOrCached( let planRequest, _, _) : return planRequest
280+ case . newOrCached( let planRequest, _, _, _ ) : return planRequest
278281 case . cachedOnly: return nil
279282 }
280283 }
281284
282285 var workspaceContext : WorkspaceContext {
283286 switch self {
284- case . newOrCached( let planRequest, _, _) : return planRequest. workspaceContext
285- case . cachedOnly( _, _, _, let workspaceContext) : return workspaceContext
287+ case . newOrCached( let planRequest, _, _, _) : return planRequest. workspaceContext
288+ case . cachedOnly( _, _, _, let workspaceContext, _) : return workspaceContext
289+ }
290+ }
291+
292+ var retain : Bool {
293+ switch self {
294+ case . newOrCached( _, _, _, let retain) :
295+ retain
296+ case . cachedOnly( _, _, _, _, let retain) :
297+ retain
286298 }
287299 }
288300
@@ -305,8 +317,8 @@ package final class BuildDescriptionManager: Sendable {
305317
306318 func signature( cacheDir: Path ) throws -> BuildDescriptionSignature {
307319 switch self {
308- case . newOrCached( let planRequest, _, _) : return try BuildDescriptionSignature . buildDescriptionSignature ( planRequest, cacheDir: cacheDir)
309- case . cachedOnly( let buildDescriptionID, _, _, _) : return BuildDescriptionSignature . buildDescriptionSignature ( buildDescriptionID)
320+ case . newOrCached( let planRequest, _, _, _ ) : return try BuildDescriptionSignature . buildDescriptionSignature ( planRequest, cacheDir: cacheDir)
321+ case . cachedOnly( let buildDescriptionID, _, _, _, _ ) : return BuildDescriptionSignature . buildDescriptionSignature ( buildDescriptionID)
310322 }
311323 }
312324 }
@@ -317,6 +329,8 @@ package final class BuildDescriptionManager: Sendable {
317329 description = lastDescription
318330 } else if let inMemoryDescription = inMemoryCachedBuildDescriptions [ signature] {
319331 description = inMemoryDescription
332+ } else if let retainedDescription = retainedBuildDescriptions [ signature] {
333+ description = retainedDescription. 0
320334 } else {
321335 description = nil
322336 }
@@ -397,18 +411,35 @@ package final class BuildDescriptionManager: Sendable {
397411 }
398412 }
399413
414+ if request. retain {
415+ retainedBuildDescriptions. update ( signature, update: { ( $0. 0 , $0. 1 + 1 ) } , default: { ( buildDescription, 0 ) } )
416+ }
417+
400418 return BuildDescriptionRetrievalInfo ( buildDescription: buildDescription, source: source, inMemoryCacheSize: inMemoryCachedBuildDescriptions. count, onDiskCachePath: buildDescriptionPath)
401419 }
402420
403421 /// Returns a build description for a particular workspace and request.
404422 ///
405423 /// - Returns: A build description, or nil if cancelled.
406- package func getBuildDescription( _ request: BuildPlanRequest , bypassActualTasks: Bool = false , useSynchronousBuildDescriptionSerialization: Bool = false , clientDelegate: any TaskPlanningClientDelegate , constructionDelegate: any BuildDescriptionConstructionDelegate ) async throws -> BuildDescription ? {
407- let descRequest = BuildDescriptionRequest . newOrCached ( request, bypassActualTasks: bypassActualTasks, useSynchronousBuildDescriptionSerialization: useSynchronousBuildDescriptionSerialization)
424+ package func getBuildDescription( _ request: BuildPlanRequest , bypassActualTasks: Bool = false , useSynchronousBuildDescriptionSerialization: Bool = false , retained : Bool , clientDelegate: any TaskPlanningClientDelegate , constructionDelegate: any BuildDescriptionConstructionDelegate ) async throws -> BuildDescription ? {
425+ let descRequest = BuildDescriptionRequest . newOrCached ( request, bypassActualTasks: bypassActualTasks, useSynchronousBuildDescriptionSerialization: useSynchronousBuildDescriptionSerialization, retain : retained )
408426 let retrievalInfo = try await getNewOrCachedBuildDescription ( descRequest, clientDelegate: clientDelegate, constructionDelegate: constructionDelegate)
409427 return retrievalInfo? . buildDescription
410428 }
411429
430+ package func releaseBuildDescription( id: BuildDescriptionID ) {
431+ self . retainedBuildDescriptions. update ( BuildDescriptionSignature . buildDescriptionSignature ( id) , update: {
432+ let newCount = $0. 1 - 1
433+ if newCount == 0 {
434+ return nil
435+ } else {
436+ return ( $0. 0 , newCount)
437+ }
438+ } , default: {
439+ nil
440+ } )
441+ }
442+
412443 /// Returns the path in which the`XCBuildData` directory will live. That location is uses to cache build descriptions for a particular workspace and request, the manifest, and the `build.db` database for llbuild.
413444 package static func cacheDirectory( _ request: BuildPlanRequest ) throws -> Path {
414445 return try cacheDirectory ( request. buildRequest, buildRequestContext: request. buildRequestContext, workspaceContext: request. workspaceContext)
@@ -514,7 +545,7 @@ package final class BuildDescriptionManager: Sendable {
514545 }
515546
516547 // Unable to load from disk, create a new description
517- guard case let . newOrCached( request, bypassActualTasks, useSynchronousBuildDescriptionSerialization) = request else {
548+ guard case let . newOrCached( request, bypassActualTasks, useSynchronousBuildDescriptionSerialization, _ ) = request else {
518549 preconditionFailure ( " entered build construction path but request was for existing cached description " )
519550 }
520551
0 commit comments