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

GRDB Inflections takes too much memory (~600kb) #755

Closed
zavsby opened this issue Apr 10, 2020 · 12 comments
Closed

GRDB Inflections takes too much memory (~600kb) #755

zavsby opened this issue Apr 10, 2020 · 12 comments

Comments

@zavsby
Copy link

zavsby commented Apr 10, 2020

What did you do?

We are using GRDB for sqlite database in Notification service extension on iOS. It has very strict memory usage up to 24MB (including process itself). We are using database pool with maximum 1 reader and found that GRDB takes more than 1MB of memory persistently. Most of the memory is taken by Inflections structure (600kb or ~9% of all memory we are using):

197.27 KB 2.8% 555 Inflections.plural(:options::)
180.09 KB 2.5% 816 Inflections.irregularSuffix(::)
143.48 KB 2.0% 649 Inflections.singular(:options::)
85.89 KB 1.2% 327 Inflections.uncountableWords(_:)

Please see call stacks below:
https://imgur.com/pwcCtVj
https://imgur.com/BKuuB1o

As I understand we are not using any of these features (we have only tables without any relations).
Can we disable this feature so it won't consume memory if this feature is not required (we are not using any relations in tables in notification service extension) and how could we do this?
Thanks.

Environment

GRDB flavor(s): GRDB
GRDB version: 4.8.1
Installation method: manual
Xcode version: 11.4
Swift version: 5.2
Platform(s) running GRDB: iOS Notification Service extension
macOS version running Xcode: MacOS Catalina

@groue
Copy link
Owner

groue commented Apr 10, 2020

Hello @zavsby,

Thanks for reporting this.

As I understand we are not using any of these features (we have only tables without any relations).
Can we disable this feature so it won't consume memory if this feature is not required (we are not using any relations in tables in notification service extension) and how could we do this?

There is no such possibility today. (Edit: I was partially wrong - see comments below)

Since it would be good to lower the memory consumption of inflections, we could improve on the current straight and naive port from Ruby on Rails inflections. A simpler alternative would be to delay the loading of inflections in memory until they are actually used, so that only users of associations "pay" the price for inflections.

I would be quite interested in a contribution in this area. Do you feel like investigating the topic, for your own benefit, as well as the benefit of other users?

@groue
Copy link
Owner

groue commented Apr 10, 2020

https://imgur.com/pwcCtVj

This picture shows that your app does use associations (belongsTo, hasMany, etc.) It is inflections that help you declare associations without telling GRDB the raw low-level strings that make it work under the hood. They are a core component of the ergonomics of associations, and they won't be removed.

My suggestion above of "delay the loading of inflections in memory until they are actually used" will thus not help your app (and I'll now even assume, unless proven otherwise, that inflections are already lazily loaded).

@groue
Copy link
Owner

groue commented Apr 10, 2020

They are a core component of the ergonomics of associations, and they won't be removed.

However, you can use your own inflections in lieu of the default ones:

// Early in the lifetime of your app: remove default inflections
Inflections.default = Inflections()

This should remove from memory the default inflections. But some of your associations may break, and you may have to declare string keys explicitly in order to manually perform the job of inflections:

struct Team {
    // Explicit key in order to deal with missing inflections
    // ---------------------------------------v
    static let players = hasMany(Player.self, key: "players")
}

// Explicit key in order to deal with missing inflections
// ---------------------------------------------------v
let request = Team.annotated(with: Team.players.count.forKey("playerCount"))

See https://github.com/groue/GRDB.swift/blob/master/Documentation/AssociationsBasics.md#convention-for-database-table-names for more information

Please tell if this provides a quick fix for your app, thank you.

@groue groue added the support label Apr 10, 2020
@zavsby
Copy link
Author

zavsby commented Apr 10, 2020

Thanks for quick response. Actually we are not using any associations (we override databaseTableName and do not use relations for now). As I see from code Inflections are created here anyway in row let associations = query.relation.prefetchedAssociations:

public func makePreparedRequest(_ db: Database, forSingleResult singleResult: Bool) throws -> PreparedRequest {
        var query = self.query
        
        // Optimize query by setting a limit of 1 when appropriate
        if singleResult && !query.expectsSingleResult {
            query.limit = SQLLimit(limit: 1, offset: query.limit?.offset)
        }
        
        let (statement, adapter) = try SQLQueryGenerator(query).prepare(db)
        let associations = query.relation.prefetchedAssociations
        if associations.isEmpty {
            return PreparedRequest(statement: statement, adapter: adapter)
        } else {
            // Eager loading of prefetched associations
            return PreparedRequest(
                statement: statement,
                adapter: adapter,
                supplementaryFetch: { rows in
                    try prefetch(db, associations: associations, in: rows)
            })
        }
}

@groue Seems your suggestion to remove default inflections works well for us (for memory consumption). We will recheck if nothing is broken by this fix. Thanks.

@groue
Copy link
Owner

groue commented Apr 11, 2020

All right, @zavsby, thanks for the information 👍

I'll check if association objects can be created out of nowhere.

@groue
Copy link
Owner

groue commented Apr 11, 2020

I've run the full GRDB test suite, minus tests that involve associations, and inflections. The default inflections are never loaded in memory. This applies to the version 4.8.1 you are running, up to the the latest version.

Actually we are not using any associations

I'm somewhat embarrassed, because there are only two possibilities:

  • Something in your app is actually using associations, and you need to be very cautious.
  • GRDB creates associations out of thin air, and this would be a GRDB bug (but I could not reproduce it).

Would you please put a breakpoint in SQLAssociation.init(step:), right there: https://github.com/groue/GRDB.swift/blob/v4.8.1/GRDB/QueryInterface/SQL/SQLAssociation.swift#L72, and share the backtrace?

@zavsby
Copy link
Author

zavsby commented Apr 11, 2020

@groue I'm trying to reproduce it and seems to be very strange actually. It does not ever enter in SQLAssociation or Inflections code when I'm building GRDB in Debug so it does not take any memory.

But when I'm building it using release configuration it behaves unpredictable:
Screen Shot 2020-04-11 at 14 47 42

query.relation object has no any children and breakpoints inside flatMap of prefetchedAssociations are never called (I'm printing children.count and it is always 0). But Inflections code are called as you can see in call stack on the screenshot.

SQLAssociation.init(step:) is never called in release too.

Looks like swift optimizes code somehow in release and initializes Inflections before it is actually used but I cannot explain this...

@groue
Copy link
Owner

groue commented Apr 11, 2020

Wow, this is unexpected, isn't it? 😅

I'm able to reproduce this behavior as well, with a plain iOS app, using the manual installation method, compiled in Release configuration, running in the simulator. The following code is just enough:

try! DatabaseQueue().write { db in
    try db.create(table: "player") { t in
        t.autoIncrementedPrimaryKey("id")
    }
    struct Player: TableRecord, FetchableRecord {
        init(row: Row) { }
    }
    _ = try Player.fetchOne(db)
}

Now let's understand how to workaround this compiler "optimization".

@groue
Copy link
Owner

groue commented Apr 11, 2020

OK, I won't pretend I precisely understand what is happening, but we have a little more information from lldb.

If I set a breakpoint in Inflections.default, and issue the bt lldb command, I get the following stacktrace, where we "see" the aggressive optimization path chosen by the compiler:

stacktrace
* thread #1, queue = 'GRDB.DatabaseQueue', stop reason = breakpoint 1.1
  * frame #0: 0x0000000100839db8 GRDB`closure #1 in variable initialization expression of static Inflections.default at Inflections+English.swift:56:27 [opt]
    frame #1: 0x0000000100839d79 GRDB`globalinit_33_CA69184AD80881AC1CFE871315000FDE_func41 at Inflections+English.swift:47:48 [opt]
    frame #2: 0x0000000100d2de8e libdispatch.dylib`_dispatch_client_callout + 8
    frame #3: 0x0000000100d2f3db libdispatch.dylib`_dispatch_once_callout + 66
    frame #4: 0x00007fff51307e4a libswiftCore.dylib`swift_once + 26
    frame #5: 0x00000001009416ed GRDB`SQLRelation.prefetchedAssociations.getter [inlined] GRDB.Inflections.default.unsafeMutableAddressor : GRDB.Inflections at Inflections+English.swift:47:23 [opt]
    frame #6: 0x00000001009416da GRDB`SQLRelation.prefetchedAssociations.getter [inlined] (self=<unavailable>):Swift.String.singularized.getter : Swift.String at Inflections.swift:21 [opt]
    frame #7: 0x00000001009416da GRDB`SQLRelation.prefetchedAssociations.getter at SQLAssociation.swift:350 [opt]
    frame #8: 0x00000001009416da GRDB`SQLRelation.prefetchedAssociations.getter [inlined] GRDB.SQLAssociationKey.name(cardinality=<unavailable>, self=<unavailable>) -> Swift.String at SQLAssociation.swift:328 [opt]
    frame #9: 0x00000001009416da GRDB`SQLRelation.prefetchedAssociations.getter [inlined] GRDB.SQLAssociationStep.keyName.getter : Swift.String(self=<unavailable>) at SQLAssociation.swift:224 [opt]
    frame #10: 0x00000001009416da GRDB`SQLRelation.prefetchedAssociations.getter at SQLRelation.swift:173 [opt]
    frame #11: 0x00000001009416da GRDB`SQLRelation.prefetchedAssociations.getter [inlined] reabstraction thunk helper from @callee_guaranteed (@guaranteed GRDB.SQLAssociation) -> (@owned GRDB.SQLAssociation, @error @owned Swift.Error) to @escaping @callee_guaranteed (@in_guaranteed GRDB.SQLAssociation) -> (@out GRDB.SQLAssociation, @error @owned Swift.Error) at <compiler-generated>:0 [opt]
    frame #12: 0x00000001009416da GRDB`SQLRelation.prefetchedAssociations.getter [inlined] generic specialization <Swift.Array<GRDB.SQLAssociation>, GRDB.SQLAssociation> of (extension in Swift):Swift.Collection.map<A>((A.Element) throws -> A1) throws -> Swift.Array<A1> at <compiler-generated>:0 [opt]
    frame #13: 0x00000001009416da GRDB`SQLRelation.prefetchedAssociations.getter [inlined] closure #1 (key=<unavailable>, child=<unavailable>, key=<unavailable>, child=<unavailable>) -> Swift.Array<GRDB.SQLAssociation> in GRDB.SQLRelation.prefetchedAssociations.getter : Swift.Array<GRDB.SQLAssociation> at SQLRelation.swift:171 [opt]
    frame #14: 0x00000001009416da GRDB`SQLRelation.prefetchedAssociations.getter [inlined] reabstraction thunk helper from @callee_guaranteed (@guaranteed Swift.String, @guaranteed GRDB.SQLRelation.Child) -> (@owned Swift.Array<GRDB.SQLAssociation>, @error @owned Swift.Error) to @escaping @callee_guaranteed (@in_guaranteed (key: Swift.String, value: GRDB.SQLRelation.Child)) -> (@out Swift.Array<GRDB.SQLAssociation>, @error @owned Swift.Error) at <compiler-generated>:0 [opt]
    frame #15: 0x00000001009416da GRDB`SQLRelation.prefetchedAssociations.getter [inlined] generic specialization <GRDB.OrderedDictionary<Swift.String, GRDB.SQLRelation.Child>, Swift.Array<GRDB.SQLAssociation>> of (extension in Swift):Swift.Sequence.flatMap<A where A1: Swift.Sequence>((A.Element) throws -> A1) throws -> Swift.Array<A1.Element> at <compiler-generated>:0 [opt]
    frame #16: 0x00000001009416da GRDB`SQLRelation.prefetchedAssociations.getter(self=<unavailable>) at SQLRelation.swift:166 [opt]

Basically, everything is inlined. This exposes the eventual need for Inflections.default right into the code for SQLRelation.prefetchedAssociations. Now if we assume that the compiler preloads the Inflections.default invariant before the loop that may use it, we get the behavior we witness.

Let's put this interpretation to the test, and let's never inline uses of Inflections.default, so that it never appears unless actually needed.

How to do that? https://forums.swift.org/t/is-it-possible-to-prevent-inlining-and-tail-call-optimization/21680

This gives this patch:

 // GRDB/Utils/Inflections.swift
 extension String {
+    @inline(never)
     var pluralized: String {
         return Inflections.default.pluralize(self)
     }
     
+    @inline(never)
     var singularized: String {
         return Inflections.default.singularize(self)
     }
 }

Success! Inflections.default is no longer called in my simple app.

Will you tell if the same patch solves your issue, @zavsby, without setting Inflections.default to an empty instance?

@zavsby
Copy link
Author

zavsby commented Apr 13, 2020

@groue Hi, thanks for this fix. Looks like it works well and Inflections are not created now according to Allocations profiler.

@groue
Copy link
Owner

groue commented Apr 13, 2020

All right, @zavsby. Thank you very much, your help and patience have been very useful. This patch deserves to land on the master branch: #757

@groue
Copy link
Owner

groue commented Apr 13, 2020

Fix pushed in v4.12.2.

@groue groue closed this as completed Apr 13, 2020
groue added a commit that referenced this issue Dec 12, 2021
…ized release builds"

This reverts commit c62ccf1 (aimed at fixing #755).

We can do this because swiftlang/swift#30445 is present in the Swift 5.3 release https://github.com/apple/swift/releases/tag/swift-5.3-RELEASE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants