Skip to content

Commit

Permalink
Implementing basic caching
Browse files Browse the repository at this point in the history
  • Loading branch information
nerdsupremacist committed May 13, 2020
1 parent 627367d commit cbd85d0
Show file tree
Hide file tree
Showing 23 changed files with 393 additions and 38 deletions.
27 changes: 27 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@
"version": "0.17.0"
}
},
{
"package": "CRuntime",
"repositoryURL": "https://github.com/nerdsupremacist/CRuntime.git",
"state": {
"branch": null,
"revision": "95f911318d8c885f6fc05e971471f94adfd39405",
"version": "2.1.2"
}
},
{
"package": "CwlCatchException",
"repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git",
Expand All @@ -37,6 +46,15 @@
"version": "1.2.0"
}
},
{
"package": "CwlDemangle",
"repositoryURL": "https://github.com/mattgallagher/CwlDemangle.git",
"state": {
"branch": null,
"revision": "b268c08207d2990664605c49b2c71cbd0df3445b",
"version": "0.1.0"
}
},
{
"package": "CwlPreconditionTesting",
"repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git",
Expand Down Expand Up @@ -82,6 +100,15 @@
"version": "2.2.0"
}
},
{
"package": "Runtime",
"repositoryURL": "https://github.com/nerdsupremacist/Runtime.git",
"state": {
"branch": "master",
"revision": "3c87a4e966baff36c8d0bc35620f833a55c6f201",
"version": null
}
},
{
"package": "SourceKitten",
"repositoryURL": "https://github.com/jpsim/SourceKitten.git",
Expand Down
2 changes: 2 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ let package = Package(
.package(url: "https://github.com/nicklockwood/SwiftFormat.git", .upToNextMajor(from: "0.42.0")),
.package(url: "https://github.com/apple/swift-syntax.git", .exact("0.50200.0")),
.package(url: "https://github.com/kylef/PathKit.git", .upToNextMajor(from: "1.0.0")),
.package(url: "https://github.com/nerdsupremacist/Runtime.git", .branch("master")),
],
targets: [
.target(name: "Graphaello", dependencies: [
Expand All @@ -27,6 +28,7 @@ let package = Package(
"PathKit",
"SwiftFormat",
"SwiftSyntax",
"Runtime",
]),
]
)
173 changes: 173 additions & 0 deletions Sources/Graphaello/Cache/FileCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@

import Foundation
import Runtime
import PathKit

class FileCache<Key: Hashable> {
private let folder: Path
private let index: Path
private let capacity: Int
private var hashStore: OrderedSet<Int>

init(folder: Path, capacity: Int) throws {
self.folder = folder
self.index = folder + ".index"
self.capacity = capacity

if !folder.exists {
try folder.mkpath()
}

if index.exists {
let bytes = Array(try index.read())
hashStore = OrderedSet(bytes.rebound(to: Int.self))
} else {
hashStore = []
}
}

deinit {
try! store()
}

func load(key: Key) throws -> Data? {
let hash = try computeHash(of: key)
let file = self.file(for: hash)
if file.exists {
assert(hashStore.contains(hash))
use(hash: hash)
return try file.read()
} else {
return nil
}
}

func store(data: Data, for key: Key) throws {
let hash = try computeHash(of: key)

if hashStore.contains(hash) && hashStore.count >= capacity {
try evictLeastRecentlyUsed()
}

use(hash: hash)
try file(for: hash).write(data)
}
}

extension FileCache {

struct StringCannotBeStoredInCacheError : Error {
let string: String
let encoding: String.Encoding
}

func load(key: Key, encoding: String.Encoding = .utf8) throws -> String? {
return try load(key: key).flatMap { String(data: $0, encoding: encoding) }
}

func store(string: String, for key: Key, encoding: String.Encoding = .utf8) throws {
guard let data = string.data(using: encoding) else { throw StringCannotBeStoredInCacheError(string: string, encoding: encoding) }
try store(data: data, for: key)
}

}

extension FileCache {

func tryCache(key: Key, encoding: String.Encoding = .utf8, builder: () throws -> String) throws -> String {
if let cached = try load(key: key, encoding: encoding) {
return cached
}

let computed = try builder()
try store(string: computed, for: key, encoding: encoding)
return computed
}

}

extension Optional {
func tryCache<Key : Hashable>(key: Key,
encoding: String.Encoding = .utf8,
builder: () throws -> String) throws -> String where Wrapped == FileCache<Key> {
guard let wrapped = self else { return try builder() }
return try wrapped.tryCache(key: key, encoding: encoding, builder: builder)
}
}

extension FileCache {

private func file(for hash: Int) -> Path {
return folder + String(hash)
}

private func computeHash(of key: Key) throws -> Int {
var hasher = try reliableHasher()
key.hash(into: &hasher)
return hasher.finalize()
}

private func use(hash: Int) {
hashStore.remove(hash)
hashStore.append(hash)
}

private func store() throws {
let data = Data(buffer: hashStore.rebound(to: UInt8.self))
try index.write(data)
}

private func evictLeastRecentlyUsed() throws {
let leastRecentlyUsed = hashStore.removeFirst()
let file = self.file(for: leastRecentlyUsed)
try file.delete()
}

}

extension Collection {

fileprivate func rebound<T>(to type: T.Type) -> UnsafeBufferPointer<T> {
let pointer = UnsafeMutableBufferPointer<Element>.allocate(capacity: count)
_ = pointer.initialize(from: self)
let rawPointer = UnsafeRawPointer(pointer.baseAddress!)
let size = count * MemoryLayout<Element>.size / MemoryLayout<T>.size
return UnsafeBufferPointer(start: rawPointer.assumingMemoryBound(to: type), count: size)
}

fileprivate func rebound<T>(to type: T.Type) -> [T] {
let pointer = self.rebound(to: type) as UnsafeBufferPointer<T>
let rebound = Array(pointer)
pointer.deallocate()
return rebound
}

}

// Stolen from https://github.com/apple/swift/blob/master/stdlib/public/core/SipHash.swift
// in order to replicate the exact format in bytes
private struct _State {
private var v0: UInt64 = 0x736f6d6570736575
private var v1: UInt64 = 0x646f72616e646f6d
private var v2: UInt64 = 0x6c7967656e657261
private var v3: UInt64 = 0x7465646279746573
private var v4: UInt64 = 0
private var v5: UInt64 = 0
private var v6: UInt64 = 0
private var v7: UInt64 = 0
}

private func reliableHasher() throws -> Hasher {
return try createInstance { coreProperty in
return try createInstance(of: coreProperty.type) { property in
switch property.name {
case "_buffer":
return try createInstance(of: property.type)
case "_state":
return withUnsafeBytes(of: _State()) { $0.baseAddress!.unsafeLoad(as: property.type) }
default:
fatalError()
}
}
}
}
17 changes: 11 additions & 6 deletions Sources/Graphaello/Commands/Codegen/CodegenCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ class CodegenCommand : Command {
@CommandFlag(description: "Should not format the generated Swift Code.")
var skipFormatting: Bool

@CommandFlag(description: "Should not cache generated code.")
var skipCache: Bool

@CommandOption(default: 100, description: "Maximum number of items that should be cached.")
var cacheSize: Int

var description: String {
return "Generates a file with all the boilerplate code for your GraphQL Code"
}
Expand All @@ -34,8 +40,11 @@ class CodegenCommand : Command {
let project = try self.project.open()
Console.print(result: "Using \(inverse: project.fileName)")

let cacheFolder = PathKit.Path(project.path.deletingLastComponent.string) + ".build" + "graphaello"
let cache = skipCache ? nil : try FileCache<AnyHashable>(folder: cacheFolder, capacity: cacheSize)

Console.print(title: "☕️ Extracting APIs + Structs:")
let extracted = try pipeline.extract(from: project)
let extracted = try pipeline.extract(from: project).with(cache: cache)
Console.print(result: "Found \(extracted.apis.count) APIs")
extracted.apis.forEach { api in
Console.print(result: "\(inverse: api.name)", indentation: 2)
Expand Down Expand Up @@ -79,11 +88,7 @@ class CodegenCommand : Command {

Console.print(title: "🎁 Bundling it all together", indentation: 1)

var autoGeneratedFile = try pipeline.generate(prepared: prepared)
if !skipFormatting {
Console.print(title: "💄 Formatting Code", indentation: 2)
autoGeneratedFile = try format(autoGeneratedFile)
}
let autoGeneratedFile = try pipeline.generate(prepared: prepared, useFormatting: !skipFormatting)

Console.print(result: "Generated \(autoGeneratedFile.components(separatedBy: "\n").count) lines of code")
Console.print(result: "You're welcome 🙃", indentation: 2)
Expand Down
29 changes: 29 additions & 0 deletions Sources/Graphaello/Extensions/OrderedHashableDictionary.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@

import Foundation

@propertyWrapper
class OrderedHashableDictionary<Key : Hashable & Comparable, Value: Hashable>: Hashable {
var wrappedValue: [Key : Value]

init(wrappedValue: [Key : Value]) {
self.wrappedValue = wrappedValue
}

static func == (lhs: OrderedHashableDictionary<Key, Value>, rhs: OrderedHashableDictionary<Key, Value>) -> Bool {
return lhs.wrappedValue == rhs.wrappedValue
}

func hash(into hasher: inout Hasher) {
let sorted = wrappedValue.sorted { $0.key < $1.key }

// Based from: https://github.com/apple/swift/blob/master/stdlib/public/core/Dictionary.swift
var commutativeHash = 0
for (k, v) in sorted {
var elementHasher = hasher
elementHasher.combine(k)
elementHasher.combine(v)
commutativeHash ^= elementHasher.finalize()
}
hasher.combine(commutativeHash)
}
}
9 changes: 9 additions & 0 deletions Sources/Graphaello/Extensions/OrderedSet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@ public struct OrderedSet<E: Hashable>: Equatable, Collection {
}
}

@discardableResult
public mutating func remove(_ element: Element) -> Element? {
guard let removed = set.remove(element) else { return nil }
if let index = array.firstIndex(of: element) {
array.remove(at: index)
}
return removed
}

/// Remove and return the element at the beginning of the ordered set.
public mutating func removeFirst() -> Element {
let firstElement = array.removeFirst()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import Foundation

struct ApolloCodeGenRequest {
struct ApolloCodeGenRequest: Hashable {
let api: API
let code: String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

import Foundation
import Stencil

extension CodeTransformable where Self: Hashable {

func cached(using cache: FileCache<AnyHashable>?) -> CachedCodeTransformable<Self> {
return CachedCodeTransformable(code: self, cache: cache)
}

}

struct CachedCodeTransformable<Code : CodeTransformable & Hashable> : CodeTransformable {
let code: Code
let cache: FileCache<AnyHashable>?

func code(using context: Stencil.Context, arguments: [Any?]) throws -> String {
return try cache.tryCache(key: code) {
return try code.code(using: context, arguments: arguments)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@

import Foundation
import SwiftFormat
import Stencil

extension SwiftCodeTransformable {

func withFormatting(format formatCode: Bool) -> FormattedSwiftCodeTransformable<Self> {
return FormattedSwiftCodeTransformable(code: self, formatCode: formatCode)
}

}

struct FormattedSwiftCodeTransformable<Code : SwiftCodeTransformable>: SwiftCodeTransformable {
let code: Code
let formatCode: Bool

func code(using context: Stencil.Context, arguments: [Any?]) throws -> String {
let code = try self.code.code(using: context, arguments: arguments)
if formatCode {
return try format(code)
}
return code
}
}


extension FormattedSwiftCodeTransformable: Equatable where Code: Equatable { }

extension FormattedSwiftCodeTransformable: Hashable where Code: Hashable { }
Loading

0 comments on commit cbd85d0

Please sign in to comment.