Skip to content

[SwiftLexicalLookup] Unqualified lookup caching #3068

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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ if(NOT DEFINED SWIFTSYNTAX_PACKAGE_NAME)
set(SWIFTSYNTAX_PACKAGE_NAME "${SWIFT_MODULE_ABI_NAME_PREFIX}${PROJECT_NAME}")
endif()

add_compile_options(
"$<$<COMPILE_LANGUAGE:Swift>:SHELL:-Xfrontend -package-name -Xfrontend ${SWIFTSYNTAX_PACKAGE_NAME}>"
)

# Determine the module triple.
if("${SWIFT_HOST_MODULE_TRIPLE}" STREQUAL "")
set(module_triple_command "${CMAKE_Swift_COMPILER}" -print-target-info)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
add_swift_syntax_library(SwiftCompilerPluginMessageHandling
CompilerPluginMessageHandler.swift
Diagnostics.swift
LRUCache.swift
Macros.swift
PluginMacroExpansionContext.swift
PluginMessageCompatibility.swift
Expand Down
1 change: 1 addition & 0 deletions Sources/SwiftLexicalLookup/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

add_swift_syntax_library(SwiftLexicalLookup
IdentifiableSyntax.swift
LookupCache.swift
LookupName.swift
LookupResult.swift
SimpleLookupQueries.swift
Expand Down
105 changes: 105 additions & 0 deletions Sources/SwiftLexicalLookup/LookupCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import SwiftSyntax

/// Unqualified lookup cache. Should be used when performing
/// large sequences of adjacent lookups to maximise performance.
public class LookupCache {
/// Cached results of `ScopeSyntax.lookupParent` calls.
/// Identified by `SyntaxIdentifier`.
private let ancestorResultsCache: LRUCache<SyntaxIdentifier, [LookupResult]>
/// Cached results of `SequentialScopeSyntax.sequentialLookup` calls.
/// Identified by `SyntaxIdentifier`.
private let sequentialResultsCache: LRUCache<SyntaxIdentifier, [LookupResult]>
/// Looked-up scope identifiers during cache accesses.
private var hits: Set<SyntaxIdentifier> = []

private let dropMod: Int
private var evictionCount = 0

/// Creates a new unqualified lookup cache.
/// `capacity` describes the maximum amount of entries in the cache.
/// The cache size is maintained according to the LRU (Least Recently Used) policy.
/// `drop` parameter specifies how many eviction calls will be
/// ignored before evicting not-hit members from subsequent lookups.
///
/// Example cache eviction sequences (s - skip, e - evict):
/// - `drop = 0` - `e -> e -> e -> e -> e -> ...`
/// - `drop = 1` - `s -> e -> s -> s -> e -> ...`
/// - `drop = 3` - `s -> s -> s -> e -> s -> ...`
///
/// - Note: `drop = 0` effectively maintains exactly one path of cached results to
/// the root in the cache (assuming we evict cache members after each lookup in a sequence of lookups).
/// Higher the `drop` value, more such paths can potentially be stored in the cache at any given moment.
/// Because of that, a higher `drop` value also translates to a higher number of cache-hits,
/// but it might not directly translate to better performance. Because of a larger memory footprint,
/// memory accesses could take longer, slowing down the eviction process. That's why the `drop` value
/// could be fine-tuned to maximize the performance given file size,
/// number of lookups, and amount of available memory.
public init(capacity: Int, drop: Int = 0) {
Copy link
Member

Choose a reason for hiding this comment

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

Just an idea: Would it make sense to move the drop parameter to evictEntriesWithoutHit. That way clients don’t have to think about the dropping cache eviction policy unless they start calling evictEntriesWithoutHit. It would also open up the option to vary the size of the cache dynamically depending on the shape of the code that we’re in (not sure if that’s useful or not). It would also remove the need for bypassDropCounter in that function because you could pass drop: 0 there, I think.

self.ancestorResultsCache = LRUCache(capacity: (capacity + 1) / 2)
self.sequentialResultsCache = LRUCache(capacity: capacity / 2)
self.dropMod = drop + 1
}

/// Get cached ancestor results for the given `id`.
/// `nil` if there's no cache entry for the given `id`.
/// Adds `id` and ids of all ancestors to the cache `hits`.
func getCachedAncestorResults(id: SyntaxIdentifier) -> [LookupResult]? {
guard let results = ancestorResultsCache[id] else { return nil }
Copy link
Member

Choose a reason for hiding this comment

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

If the user doesn’t call evictEntriesWithoutHit, hits will keep on growing indefinitely. Should we clear up hits periodically for elements that are no longer in the cache? Eg. as a kind of garbage collection if hits.count > capacity * 2. Or should we only keep track of hits if the user opts into it inside the initializer?

hits.formUnion(results.map(\.scope.id))
hits.insert(id)
return results
}

/// Set cached ancestor results for the given `id`.
/// Adds `id` to the cache `hits`.
func setCachedAncestorResults(id: SyntaxIdentifier, results: [LookupResult]) {
hits.insert(id)
ancestorResultsCache[id] = results
}

/// Get cached sequential lookup results for the given `id`.
/// `nil` if there's no cache entry for the given `id`.
/// Adds `id` to the cache `hits`.
func getCachedSequentialResults(id: SyntaxIdentifier) -> [LookupResult]? {
guard let results = sequentialResultsCache[id] else { return nil }
hits.insert(id)
return results
}

/// Set cached sequential lookup results for the given `id`.
/// Adds `id` to the cache `hits`.
func setCachedSequentialResults(id: SyntaxIdentifier, results: [LookupResult]) {
hits.insert(id)
sequentialResultsCache[id] = results
}

/// Removes all cached entries without a hit, unless it's prohibited
/// by the internal drop counter (as specified by `drop` in the initializer).
/// The dropping behavior can be disabled for this call with the `bypassDropCounter`
/// parameter, resulting in immediate eviction of entries without a hit.
public func evictEntriesWithoutHit(bypassDropCounter: Bool = false) {
if !bypassDropCounter {
evictionCount = (evictionCount + 1) % dropMod
guard evictionCount != 0 else { return }
}

for key in ancestorResultsCache.keysInCache.union(sequentialResultsCache.keysInCache).subtracting(hits) {
ancestorResultsCache[key] = nil
sequentialResultsCache[key] = nil
}
Comment on lines +98 to +101
Copy link
Member

Choose a reason for hiding this comment

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

Not sure how performance sensitive this is or how expensive the Set operations are but I would imagine it would be more performant if we just did the following because it requires no set modifications.

    for key in ancestorResultsCache where !hits.contains(key) {
      ancestorResultsCache[key] = nil
    }
    for key in sequentialResultsCache where !hits.contains(key) {
      sequentialResultsCache[key] = nil
    }


hits = []
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ protocol CanInterleaveResultsLaterScopeSyntax: ScopeSyntax {
_ identifier: Identifier?,
at lookUpPosition: AbsolutePosition,
with config: LookupConfig,
cache: LookupCache?,
resultsToInterleave: [LookupResult]
) -> [LookupResult]
}
7 changes: 5 additions & 2 deletions Sources/SwiftLexicalLookup/Scopes/FunctionScopeSyntax.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ extension FunctionScopeSyntax {
@_spi(Experimental) public func lookup(
_ identifier: Identifier?,
at lookUpPosition: AbsolutePosition,
with config: LookupConfig
with config: LookupConfig,
cache: LookupCache?
) -> [LookupResult] {
var thisScopeResults: [LookupResult] = []

Expand All @@ -39,6 +40,7 @@ extension FunctionScopeSyntax {
identifier,
at: position,
with: config,
cache: cache,
propagateToParent: false
)
}
Expand All @@ -47,7 +49,8 @@ extension FunctionScopeSyntax {
+ lookupThroughGenericParameterScope(
identifier,
at: lookUpPosition,
with: config
with: config,
cache: cache
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,21 @@ protocol GenericParameterScopeSyntax: ScopeSyntax {}
@_spi(Experimental) public func lookup(
_ identifier: Identifier?,
at lookUpPosition: AbsolutePosition,
with config: LookupConfig
with config: LookupConfig,
cache: LookupCache?
) -> [LookupResult] {
return defaultLookupImplementation(
identifier,
at: lookUpPosition,
with: config,
cache: cache,
propagateToParent: false
)
+ lookupBypassingParentResults(
identifier,
at: lookUpPosition,
with: config
with: config,
cache: cache
)
}

Expand All @@ -76,16 +79,22 @@ protocol GenericParameterScopeSyntax: ScopeSyntax {}
private func lookupBypassingParentResults(
_ identifier: Identifier?,
at lookUpPosition: AbsolutePosition,
with config: LookupConfig
with config: LookupConfig,
cache: LookupCache?
) -> [LookupResult] {
guard let parentScope else { return [] }

if let parentScope = Syntax(parentScope).asProtocol(SyntaxProtocol.self)
as? WithGenericParametersScopeSyntax
{
return parentScope.returningLookupFromGenericParameterScope(identifier, at: lookUpPosition, with: config)
return parentScope.returningLookupFromGenericParameterScope(
identifier,
at: lookUpPosition,
with: config,
cache: cache
)
} else {
return lookupInParent(identifier, at: lookUpPosition, with: config)
return lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ protocol IntroducingToSequentialParentScopeSyntax: ScopeSyntax {
func lookupFromSequentialParent(
_ identifier: Identifier?,
at lookUpPosition: AbsolutePosition,
with config: LookupConfig
with config: LookupConfig,
cache: LookupCache?
) -> [LookupResult]
}
12 changes: 7 additions & 5 deletions Sources/SwiftLexicalLookup/Scopes/NominalTypeDeclSyntax.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,18 @@ extension NominalTypeDeclSyntax {
@_spi(Experimental) public func returningLookupFromGenericParameterScope(
_ identifier: Identifier?,
at lookUpPosition: AbsolutePosition,
with config: LookupConfig
with config: LookupConfig,
cache: LookupCache?
) -> [LookupResult] {
if let inheritanceClause, inheritanceClause.range.contains(lookUpPosition) {
return lookupInParent(identifier, at: lookUpPosition, with: config)
return lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache)
} else if let genericParameterClause, genericParameterClause.range.contains(lookUpPosition) {
return lookupInParent(identifier, at: lookUpPosition, with: config)
return lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache)
} else if name.range.contains(lookUpPosition) || genericWhereClause?.range.contains(lookUpPosition) ?? false {
return lookupInParent(identifier, at: lookUpPosition, with: config)
return lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache)
} else {
return [.lookForMembers(in: Syntax(self))] + lookupInParent(identifier, at: lookUpPosition, with: config)
return [.lookForMembers(in: Syntax(self))]
+ lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache)
}
}
}
Loading
Loading