Skip to content

Commit

Permalink
feat: add Linux support (#40)
Browse files Browse the repository at this point in the history
* linux: remove Combine subjects

* linux: Combine is not available

* linux: use CDispatch

* linux: add pthread based UnfairLock

* linux: remove Combine

* linux: remove XCTMetric usage

* chore: update Package.resolved

* linux: add test support

* chore: Add Dockerfile lint pre-commit

* chore: add async algorithms

* chore: add actionlint

* chore: test on linux too

* linux: conditionally include os.log

* linux

* linux: Remove cache

---------

Co-authored-by: danthorpe <[email protected]>
  • Loading branch information
danthorpe and danthorpe authored May 11, 2024
1 parent 89151e2 commit 6980753
Show file tree
Hide file tree
Showing 16 changed files with 217 additions and 122 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:
done
- name: Fix permissions
run: 'sudo chown -R $USER docs-out'
run: 'sudo chown -R "$USER" docs-out'

- name: Publish documentation to GitHub Pages
uses: JamesIves/github-pages-deploy-action@v4
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ jobs:
"os": "macos-14",
"swift": "5.10",
"xcode": "15.3"
},
{
"os": "ubuntu-latest",
"swift": "5.10"
}
]
}
Expand Down
8 changes: 8 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ repos:
- id: check-added-large-files
- id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable
- repo: https://github.com/pryorda/dockerfilelint-precommit-hooks
rev: v0.1.0
hooks:
- id: dockerfilelint
- repo: https://github.com/rhysd/actionlint
rev: v1.7.0
hooks:
- id: actionlint
- repo: https://github.com/realm/SwiftLint
rev: 0.54.0
hooks:
Expand Down
13 changes: 13 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#FROM swiftlang/swift:nightly-master-
#FROM swift:amazonlinux2
#FROM swift:ubuntu-latest
FROM swift:5.9

WORKDIR /tmp

ADD .build/checkouts ./.build/checkouts
ADD Sources ./Sources
ADD Tests ./Tests
ADD Package.swift ./

CMD swift test
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ PLATFORM_MAC_CATALYST = macOS,variant=Mac Catalyst
PLATFORM_TVOS = tvOS Simulator,id=$(call udid_for,tvOS 17.2,TV)
PLATFORM_VISIONOS = visionOS Simulator,id=$(call udid_for,visionOS 1.0,Vision)
PLATFORM_WATCHOS = watchOS Simulator,id=$(call udid_for,watchOS 10.2,Watch)
PWD=$(shell pwd)

default:
test-all
Expand All @@ -17,6 +18,10 @@ docs-all:
$(MAKE) docs output=$(output) tag=$(tag) basepath=$(basepath) target=Protected
$(MAKE) docs output=$(output) tag=$(tag) basepath=$(basepath) target=ShortID

test-linux:
docker build -f Dockerfile -t linuxtest .
docker run linuxtest

docs:
mkdir -p $(output)/$(tag)/$(target)
swift package \
Expand Down
13 changes: 11 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@
"version" : "1.2.2"
}
},
{
"identity" : "swift-async-algorithms",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-async-algorithms",
"state" : {
"revision" : "da4e36f86544cdf733a40d59b3a2267e3a7bbf36",
"version" : "1.0.0"
}
},
{
"identity" : "swift-clocks",
"kind" : "remoteSourceControl",
Expand All @@ -32,8 +41,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
"revision" : "f504716c27d2e5d4144fa4794b12129301d17729",
"version" : "1.0.3"
"revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb",
"version" : "1.1.0"
}
},
{
Expand Down
8 changes: 8 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,21 @@ AssertionExtras
.xcTestDynamicOverlay,
]
}
#if !os(Linux)
Cache
<+ 📦 {
$0.createProduct = .library
$0.dependsOn = [
Extensions
]
$0.with = [
.asyncAlgorithms,
.deque,
.dependencies,
.orderedCollections,
]
}
#endif
Extensions
<+ 📦 {
$0.createProduct = .library
Expand All @@ -85,6 +88,7 @@ Reachability
$0.createProduct = .library
$0.createUnitTests = false
$0.with = [
.asyncAlgorithms,
.dependencies,
.xcTestDynamicOverlay,
]
Expand All @@ -107,6 +111,7 @@ ShortID
// MARK: - 👜 3rd Party Dependencies

package.dependencies = [
.package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.2"),
.package(url: "https://github.com/apple/swift-collections", from: "1.0.2"),
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0"),
Expand All @@ -116,6 +121,9 @@ package.dependencies = [
]

extension Target.Dependency {
static let asyncAlgorithms: Self = .product(
name: "AsyncAlgorithms", package: "swift-async-algorithms"
)
static let customDump: Self = .product(
name: "CustomDump", package: "swift-custom-dump"
)
Expand Down
123 changes: 68 additions & 55 deletions Sources/Cache/Cache.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import Combine
import AsyncAlgorithms
import Dependencies
import DequeModule
import Extensions
import Foundation
import OrderedCollections

#if canImport(os.log)
import os.log
#endif

#if canImport(UIKit)
import UIKit
Expand Down Expand Up @@ -39,18 +42,16 @@ public actor Cache<Key: Hashable, Value> {

public var limit: UInt

#if canImport(os.log)
var logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "works.dan.swift-utilities", category: "Cache")
#endif
var absoluteUpperLimit: UInt {
limit + max(10, UInt(Double(limit) * 0.1))
}
var data: Storage
var access: Access
var eventDelegate = PassthroughSubject<Event, Never>()
var evictionsDelegate = PassthroughSubject<EvictionEvent, Never>()

var evictions: some AsyncSequence {
evictionsDelegate.values
}
var _evictions = AsyncStream<EvictionEvent>.makeStream()
var _events = AsyncStream<Event>.makeStream()

@Dependency(\.date) private var date

Expand All @@ -70,7 +71,11 @@ public actor Cache<Key: Hashable, Value> {
}

public init(limit: UInt, data: Storage) {
self.init(limit: limit, data: data, didReciveSystemEvents: SystemEvent.publisher().values)
self.init(
limit: limit,
data: data,
didReciveSystemEvents: SystemEvent.stream()
)
}

public init(limit: UInt, items: [Key: Value], duration: TimeInterval) {
Expand All @@ -79,14 +84,18 @@ public actor Cache<Key: Hashable, Value> {
data: items.reduce(into: Storage()) { storage, element in
storage[element.key] = CachedValue.with(value: element.value, duration: duration)
},
didReciveSystemEvents: SystemEvent.publisher().values
didReciveSystemEvents: SystemEvent.stream()
)
}

@available(macOS 12.0, *)
@available(iOS 15.0, *)
public init(limit: UInt) {
self.init(limit: limit, data: .init(), didReciveSystemEvents: SystemEvent.publisher().values)
self.init(
limit: limit,
data: .init(),
didReciveSystemEvents: SystemEvent.stream()
)
}

func startReceivingSystemEvents<SystemEvents: AsyncSequence>(
Expand All @@ -97,29 +106,25 @@ public actor Cache<Key: Hashable, Value> {
for try await event in stream {
switch event {
case .applicationWillSuspend:
eventDelegate.send(.shouldPersistCachedValues(data))
_events.continuation.yield(.shouldPersistCachedValues(data))
case .applicationDidReceiveMemoryPressure(.warning):
evictionsDelegate.send(.memoryPressure)
_evictions.continuation.yield(.memoryPressure)
case .applicationDidReceiveMemoryPressure(.normal):
break
}
}
} catch {
#if canImport(os.log)
logger.error("🗂 ⚠️ Caught error receiving system events: \(error)")
#endif
}
}

func handleEvictionEvents() async {
do {
for try await eviction in evictions {
if let eviction = eviction as? EvictionEvent {
let countToRemove = calculateEvictionCount(from: eviction)
let rangeToRemove = countToRemove ..< access.endIndex
evictCachedValues(forKeys: access[rangeToRemove], reason: eviction)
}
}
} catch {
logger.error("🗂 ⚠️ Caught error handling eviction event: \(error)")
for await eviction in _evictions.stream {
let countToRemove = calculateEvictionCount(from: eviction)
let rangeToRemove = countToRemove ..< access.endIndex
evictCachedValues(forKeys: access[rangeToRemove], reason: eviction)
}
}

Expand Down Expand Up @@ -163,7 +168,7 @@ extension Cache.CachedValue: Codable where Value: Codable {}
extension Cache {

public var events: some AsyncSequence {
eventDelegate.values
_events.stream
}

public var count: Int {
Expand Down Expand Up @@ -218,16 +223,18 @@ extension Cache {

fileprivate func evictCachedValues(forKeys keys: some Collection<Key>, reason event: EvictionEvent) {
let slice = data.slice(keys)
#if canImport(os.log)
logger.info("🗂 Will evict \(keys.map(String.init(describing:))) due to: \(event.description)")
eventDelegate.send(.willEvictCachedValues(slice, reason: event))
#endif
_events.continuation.yield(.willEvictCachedValues(slice, reason: event))
slice.keys.forEach(removeCachedValue(forKey:))
}

fileprivate func updateAccess(for key: Key) {
removeAccess(for: key)
access.insert(key, at: 0)
if access.count >= absoluteUpperLimit {
evictionsDelegate.send(.countLimit)
_evictions.continuation.yield(.countLimit)
}
}

Expand All @@ -243,40 +250,46 @@ extension Cache {
@available(iOS 15.0, *)
extension Cache.SystemEvent {

static func publisher(notificationCenter center: NotificationCenter = .default) -> AnyPublisher<Self, Never> {
let subject = PassthroughSubject<Cache.SystemEvent, Never>()
let queue = DispatchQueue(label: "dan.works.swift-utilities.cache.memory-pressure")
let source = DispatchSource.makeMemoryPressureSource(eventMask: .all, queue: queue)
source.setEventHandler {
var event = source.data
event.formIntersection([.critical, .warning, .normal])
if event.contains([.warning, .critical]) {
subject.send(.applicationDidReceiveMemoryPressure(.warning))
} else {
subject.send(.applicationDidReceiveMemoryPressure(.normal))
static func stream(notificationCenter center: NotificationCenter = .default) -> AsyncStream<Self> {
let memoryPressure = AsyncStream { continuation in
let queue = DispatchQueue(label: "dan.works.swift-utilities.cache.memory-pressure")
let source = DispatchSource.makeMemoryPressureSource(eventMask: .all, queue: queue)
source.setEventHandler {
var event = source.data
event.formIntersection([.critical, .warning, .normal])
if event.contains([.warning, .critical]) {
continuation.yield(Cache.SystemEvent.applicationDidReceiveMemoryPressure(.warning))
} else {
continuation.yield(.applicationDidReceiveMemoryPressure(.normal))
}
}
}

return
Publishers
.Merge(
center.publisher(for: .willResignActiveNotification),
center.publisher(for: .willTerminateNotification)
)
let willResign =
center
.notifications(named: .willResignActiveNotification)
.map { _ in Cache.SystemEvent.applicationWillSuspend }
#if os(iOS)
.merge(
with:
center.publisher(
for: UIApplication.didReceiveMemoryWarningNotification
)
.map { _ in
Cache.SystemEvent.applicationDidReceiveMemoryPressure(.warning)
}
)
#endif
.merge(with: subject.handleEvents(receiveSubscription: { _ in }))
.eraseToAnyPublisher()
.eraseToStream()

let willTerminate =
center
.notifications(named: .willTerminateNotification)
.map { _ in Cache.SystemEvent.applicationWillSuspend }
.eraseToStream()

let willSuspend = merge(willResign, willTerminate)

#if os(iOS)
let additionalMemoryWarning =
center
.notifications(named: UIApplication.didReceiveMemoryWarningNotification)
.map { _ in Cache.SystemEvent.applicationDidReceiveMemoryPressure(.warning) }
.eraseToStream()

return merge(memoryPressure, willSuspend, additionalMemoryWarning).eraseToStream()
#else
return merge(memoryPressure, willSuspend).eraseToStream()
#endif
}
}

Expand Down
6 changes: 6 additions & 0 deletions Sources/Extensions/Calendar+.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import Foundation

#if os(Linux)
import let CDispatch.NSEC_PER_MSEC
#else
import Dispatch
#endif

extension Calendar {

/// Approximately the end of the day.
Expand Down
2 changes: 2 additions & 0 deletions Sources/Extensions/Publisher+.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#if os(iOS) || os(watchOS) || os(tvOS) || os(macOS)
import Combine
import Foundation

Expand Down Expand Up @@ -32,3 +33,4 @@ extension Publisher {
}
}
}
#endif
6 changes: 6 additions & 0 deletions Sources/Extensions/Task+.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import Foundation

#if os(Linux)
import let CDispatch.NSEC_PER_SEC
#else
import Dispatch
#endif

extension Task where Success == Never, Failure == Never {
public static func sleep(seconds timeInterval: TimeInterval) async throws {
guard !timeInterval.isNaN && timeInterval.isFinite else { return }
Expand Down
Loading

0 comments on commit 6980753

Please sign in to comment.