Skip to content
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

Split cache into read cache and write cache #1551

Merged
merged 1 commit into from
May 24, 2017
Merged

Conversation

marcelofabri
Copy link
Collaborator

Alternative fix for #1550 as suggested by @norio-nomura

This makes things slightly more complex, but it reduced by 50-70% the execution time when there's a cache available.

@jpsim
Copy link
Collaborator

jpsim commented May 23, 2017

I like the general idea, but flushWriteCache() mutates readCache but reads to readCache aren't locked, so the mutation could happen in the middle of reading.

@SwiftLintBot
Copy link

SwiftLintBot commented May 23, 2017

12 Messages
📖 Linting Aerial with this PR took 0.52s vs 0.36s on master (44% slower)
📖 Linting Alamofire with this PR took 2.74s vs 2.46s on master (11% slower)
📖 Linting Firefox with this PR took 12.47s vs 10.38s on master (20% slower)
📖 Linting Kickstarter with this PR took 14.7s vs 14.11s on master (4% slower)
📖 Linting Moya with this PR took 0.34s vs 0.34s on master (0% slower)
📖 Linting Nimble with this PR took 1.33s vs 1.36s on master (2% faster)
📖 Linting Quick with this PR took 0.43s vs 0.47s on master (8% faster)
📖 Linting Realm with this PR took 2.04s vs 2.16s on master (5% faster)
📖 Linting SourceKitten with this PR took 0.89s vs 0.91s on master (2% faster)
📖 Linting Sourcery with this PR took 2.14s vs 2.24s on master (4% faster)
📖 Linting Swift with this PR took 10.08s vs 9.87s on master (2% slower)
📖 Linting WordPress with this PR took 10.43s vs 10.51s on master (0% faster)

Generated by 🚫 Danger

@marcelofabri
Copy link
Collaborator Author

Yes, but this flushWriteCache should only be called after the violations were processed. Everything was originally on save, but I had to expose it for unit tests 😅

lock.unlock()
try json.write(to: url, atomically: true, encoding: .utf8)
}

internal func flushWriteCache() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

flushWriteCacheToReadCache() might be a clearer (but more verbose) name?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Otherwise it isn't clear that this function is mutating readCache.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sounds good to me - this method shouldn't be used often, so I'm cool with it being more verbose

@jpsim
Copy link
Collaborator

jpsim commented May 23, 2017

Yes, but this flushWriteCache should only be called after the violations were processed. Everything was originally on save, but I had to expose it for unit tests 😅

I guess this brings us to whether or not we want to properly support arbitrary uses of SwiftLintFramework outside of just our swiftlint CLI. We may not call save() while other linters are running, but the public API certainly allows you to do so.

@marcelofabri
Copy link
Collaborator Author

We may not call save() while other linters are running, but the public API certainly allows you to do so.

Agreed, but I think we should be fine if we document this limitation. I think the performance win for swiftlint is worth the less safety for other (potential) consumers of the public API.

@jpsim
Copy link
Collaborator

jpsim commented May 23, 2017

I agree, but it should be clearly documented.

@marcelofabri
Copy link
Collaborator Author

I agree, but it should be clearly documented.

Sure, will add that later and a changelog entry. Just wanted to get feedback on the general approach first 😊

guard !writeCache.isEmpty else {
return
}
flushWriteCacheToReadCache()
lock.lock()
Copy link
Collaborator

Choose a reason for hiding this comment

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

This lock() (and subsequent unlock()) can be removed if we maintain that:

  1. readCache can only be mutated in flushWriteCacheToReadCache()
  2. flushWriteCacheToReadCache() can only be called from save()
  3. save() cannot be called concurrently

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Actually, we can make save() thread-safe if we think about a way to support the unit tests. Maybe something like this

func flushWriteCacheToReadCache() {
    readCache = merge()
}

func merge() -> [String: Any] {
  // ...
}

func save() {
   // ...
    let cache = merge()
    let json = toJSON(cache)
   // ...
}

(please ignore the bad names and lack of details)

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think that's doable. Give it a try and see! 😄

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done! 💯

@@ -89,6 +87,8 @@ class LinterCacheTests: XCTestCase {
private func cacheAndValidate(violations: [StyleViolation], forFile: String, configuration: Configuration,
file: StaticString = #file, line: UInt = #line) {
cache.cache(violations: violations, forFile: forFile, configuration: configuration)
cache.flushWriteCacheToReadCache()
Copy link
Collaborator

Choose a reason for hiding this comment

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

If this test is the only reason that readCache is mutable, how about making a method that returns a new LinterCache instance from mergeCaches() and using it here, and changing readCache to immutable?

@@ -15,30 +15,35 @@ internal enum LinterCacheError: Error {
}

public final class LinterCache {
private var cache = [String: Any]()
private var readCache = [String: Any]()
private var writeCache = [String: Any]()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Changing readCache and writeCache from [String: Any] to [String: [String: Any]] boosted performance on my test.

@codecov-io
Copy link

codecov-io commented May 24, 2017

Codecov Report

Merging #1551 into master will increase coverage by 0.3%.
The diff coverage is 72.09%.

Impacted file tree graph

@@            Coverage Diff            @@
##           master    #1551     +/-   ##
=========================================
+ Coverage   83.75%   84.06%   +0.3%     
=========================================
  Files         188      189      +1     
  Lines        9597     9726    +129     
=========================================
+ Hits         8038     8176    +138     
+ Misses       1559     1550      -9
Impacted Files Coverage Δ
Source/SwiftLintFramework/Models/LinterCache.swift 77.31% <71.79%> (-1.35%) ⬇️
...sts/SwiftLintFrameworkTests/LinterCacheTests.swift 86.19% <75%> (+0.77%) ⬆️
...tLintFramework/Rules/OverriddenSuperCallRule.swift 100% <0%> (ø) ⬆️
...SwiftLintFramework/Rules/ProhibitedSuperRule.swift 100% <0%> (ø) ⬆️
...wiftLintFramework/Models/Configuration+Cache.swift 51.28% <0%> (ø)
...intFramework/Extensions/Dictionary+SwiftLint.swift 85.5% <0%> (+0.21%) ⬆️
...urce/SwiftLintFramework/Models/Configuration.swift 94.28% <0%> (+13.44%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 8ed4424...251f8b8. Read the comment docs.

Copy link
Collaborator

@norio-nomura norio-nomura left a comment

Choose a reason for hiding this comment

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

Looks good to me other than one comment.
On linting Carthage's repository and hitting all the cache,
rc1:

swiftlint lint --enable-all-rules --quiet 13.27s user 1.22s system 128% cpu 11.245 total

0ab8bfa:

swiftlint lint --enable-all-rules --quiet 14.30s user 1.27s system 197% cpu 7.900 total

98832ab:

swiftlint lint --enable-all-rules --quiet 5.72s user 1.06s system 143% cpu 4.737 total

👍

for (key, value) in writeCache {
var filesCache = (cache[key] as? [String: Any]) ?? [:]
for (file, fileCache) in (value as? [String: Any]) ?? [:] {
var filesCache = cache[key] ?? [:]
Copy link
Collaborator

Choose a reason for hiding this comment

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

If key does not exist, value can be inserted to cache[key] directly.
Sorry, I was aware of it before but I forgot to write a comment.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, but is it worth the extra complexity?

for (key, value) in writeCache {
    if var filesCache = cache[key] {
        for (file, fileCache) in value {
            filesCache[file] = fileCache
        }
        cache[key] = filesCache
    } else {
        cache[key] = value
    }
}

Copy link
Collaborator

@jpsim jpsim left a comment

Choose a reason for hiding this comment

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

Looks great!

let cache = LinterCache()
cache.fileManager = TestFileManager()
return cache
return LinterCache(fileManager: TestFileManager())
Copy link
Collaborator

Choose a reason for hiding this comment

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

This doesn't need to be in a closure anymore.

@@ -49,12 +66,12 @@ public final class LinterCache {
let configurationDescription = configuration.cacheDescription

lock.lock()
var filesCache = (cache[configurationDescription] as? [String: Any]) ?? [:]
var filesCache = (writeCache[configurationDescription]) ?? [:]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Parens are surperfluous now.

CHANGELOG.md Outdated
@@ -93,6 +93,10 @@
[Marcelo Fabri](https://github.com/marcelofabri)
[#1504](https://github.com/realm/SwiftLint/issues/1504)

* Improve cache performance by splitting it into read cache and write cache.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just add your name & issue to the item introducing caching? We don't usually include fixes or improvements to unreleased things in the changelog.

@marcelofabri marcelofabri merged commit bdcfe1b into master May 24, 2017
@marcelofabri marcelofabri deleted the write-cache branch May 24, 2017 15:38
@marcelofabri
Copy link
Collaborator Author

🚢

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants