Using a rails application www.codetriage.com it uses mime/types and when it boots uses:
Without loading mime/types it uses:
Thats 52.168 - 38.9 # => 13.268 mb of savings or 25% of all RAM usage. This is a non-trivial amount of memory to use.
I've got some causes and some ideas, but I want some more eyes and some feedback before moving forwards.
Memory Causes
As far as I can tell there are two main culprits that are causing memory use.
1) Loading large JSON blob. Loading a 547 KB file into JSON and converting to a hash takes a bunch of memory. This is done in the loader. The entire json blob and resultant hash cannot fit into memory so Ruby must malloc more. Unfortunately Ruby never free-s memory after it's been allocated. If you are using a large Rails app, this isn't a concern since those empty ruby object slots will eventually be used, however if you're running a really small service, this is a non-trivial operation. While we could optimize this, it likely won't have an impact for most applications.
2) Lots of large objects retained. The mime/types gem proactively generates and retains 1800+ objects that each have quite a bit of data in them. I'm pretty sure this is where the bulk of the memory problems come from. Since we never release a reference to unused mime types, we never get this memory back. The list of types is only going to get longer, however the effective number of types used on a system is dramatically lower than the default set.
While it's currently possible to export and use a custom cache this process isn't easy and most developers don't know that this capability exists.
Potential Solution to #2
Note: Anything we do here will make loading mime/types slower, we're effectively trading off RAM for speed so we need to benchmark any solutions thoroughly.
We don't necessarily have to make any tradeoffs default, instead we could offer them as a flag, however it would be ideal if we could find a middle-ground that was fast enough with less than 5~10% (random number I came up with) total RAM impact.
Any options to lazily create or evaluate MIME::Types could be enhanced by encouraging other libraries to explicitly declare common types they expect to use.
Option Lazy load from JSON Hash) Don't coerce default values into MIME::Type objects. These objects expand the data stored in the default cache quite a bit and are very heavy. Instead we could store the resultant JSON hash in memory and scan it to lazily generate MIME::Type objects so we only create what we need. The first time a mime type is needed it is coerced and retained so that we never have to seek for it again.
Viability: Speed impact: minimal, decreased RAM impact: medium. Depending on common access patterns, and how we store and search the data this could be fast. We are still storing a bunch of data we will never use in memory but it's cheaper than what we're currently doing. We would end up with duplicate info retained in two places, but we could either delete from the source data or it may be inconsequential to keep both around.
Option Distributed File Store) We could get really fancy and try to create a ton of small files each named with how it is accessed so we could simply see if that file exists and read it's contents when it is accessed. If there are multiple common access patterns, we could have different directories with different file names that would redirect or refer to another file containing the full info.
Viability: Speed impact: depends, decreased RAM impact: large (good). We would literally only store the objects in memory we need, so RAM use would be as close to minimal as possible. Reading from disk is really really slow so speed would probably be negatively impacted for most cases except those that only need one or two mime types. In this case data scans would be prohibitively expensive and we might need to keep a stash of JSON data around on disk lest we access and read from 1800+ files.
Option Lazy JSON File ) We lazilly create each and every MIME::Type by loading in the json file and searching for the entry we want when it isn't in memory already.
Viability: Speed impact: large (bad), decreased RAM impact: large (good). Might not be so bad for some cases, for others, this would be a world of hurt. It would help RAM more than the first option of storing the JSON in memory but it would provide us with no random access capabilities, any lookups would require a scan.
Option X) Hopefully there's some other options I've not yet considered. Maybe we could mark the MIME::Types that are being used and provide some kind of a MIME::Type.clean to undefine or remove references to types not being used? We would have to do it in conjunction with a lazy loading lest we be forced to load the whole into memory again if a new mime type gets referenced. Maybe we can use some binary data blob or store. It would be sweet to have a sqlite3 table we could query against, but that would add undue complexity and dependencies to the project. There's no clear winner yet, go crazy and recommend something.
Next Steps
Im interested in concerns that you as library maintainer have with any or all of these plans. You know how people commonly use this gem and maybe you could help provide a top 5 (or whatever number) of use cases. I'm also interested in alternative solutions. If we can figure out one that makes sense to try, i'll be happy to work on an spike implementation so we can benchmark speed an memory use. Hopefully you're interested. Let me know what you think 😄
Using a rails application www.codetriage.com it uses mime/types and when it boots uses:
Without loading mime/types it uses:
Thats
52.168 - 38.9 # => 13.268mb of savings or 25% of all RAM usage. This is a non-trivial amount of memory to use.I've got some causes and some ideas, but I want some more eyes and some feedback before moving forwards.
Memory Causes
As far as I can tell there are two main culprits that are causing memory use.
1) Loading large JSON blob. Loading a 547 KB file into JSON and converting to a hash takes a bunch of memory. This is done in the loader. The entire json blob and resultant hash cannot fit into memory so Ruby must malloc more. Unfortunately Ruby never free-s memory after it's been allocated. If you are using a large Rails app, this isn't a concern since those empty ruby object slots will eventually be used, however if you're running a really small service, this is a non-trivial operation. While we could optimize this, it likely won't have an impact for most applications.
2) Lots of large objects retained. The mime/types gem proactively generates and retains 1800+ objects that each have quite a bit of data in them. I'm pretty sure this is where the bulk of the memory problems come from. Since we never release a reference to unused mime types, we never get this memory back. The list of types is only going to get longer, however the effective number of types used on a system is dramatically lower than the default set.
While it's currently possible to export and use a custom cache this process isn't easy and most developers don't know that this capability exists.
Potential Solution to #2
We don't necessarily have to make any tradeoffs default, instead we could offer them as a flag, however it would be ideal if we could find a middle-ground that was fast enough with less than 5~10% (random number I came up with) total RAM impact.
Any options to lazily create or evaluate
MIME::Typescould be enhanced by encouraging other libraries to explicitly declare common types they expect to use.Option Lazy load from JSON Hash) Don't coerce default values into
MIME::Typeobjects. These objects expand the data stored in the default cache quite a bit and are very heavy. Instead we could store the resultant JSON hash in memory and scan it to lazily generateMIME::Typeobjects so we only create what we need. The first time a mime type is needed it is coerced and retained so that we never have to seek for it again.Viability: Speed impact: minimal, decreased RAM impact: medium. Depending on common access patterns, and how we store and search the data this could be fast. We are still storing a bunch of data we will never use in memory but it's cheaper than what we're currently doing. We would end up with duplicate info retained in two places, but we could either delete from the source data or it may be inconsequential to keep both around.
Option Distributed File Store) We could get really fancy and try to create a ton of small files each named with how it is accessed so we could simply see if that file exists and read it's contents when it is accessed. If there are multiple common access patterns, we could have different directories with different file names that would redirect or refer to another file containing the full info.
Viability: Speed impact: depends, decreased RAM impact: large (good). We would literally only store the objects in memory we need, so RAM use would be as close to minimal as possible. Reading from disk is really really slow so speed would probably be negatively impacted for most cases except those that only need one or two mime types. In this case data scans would be prohibitively expensive and we might need to keep a stash of JSON data around on disk lest we access and read from 1800+ files.
Option Lazy JSON File ) We lazilly create each and every
MIME::Typeby loading in the json file and searching for the entry we want when it isn't in memory already.Viability: Speed impact: large (bad), decreased RAM impact: large (good). Might not be so bad for some cases, for others, this would be a world of hurt. It would help RAM more than the first option of storing the JSON in memory but it would provide us with no random access capabilities, any lookups would require a scan.
Option X) Hopefully there's some other options I've not yet considered. Maybe we could mark the MIME::Types that are being used and provide some kind of a
MIME::Type.cleanto undefine or remove references to types not being used? We would have to do it in conjunction with a lazy loading lest we be forced to load the whole into memory again if a new mime type gets referenced. Maybe we can use some binary data blob or store. It would be sweet to have a sqlite3 table we could query against, but that would add undue complexity and dependencies to the project. There's no clear winner yet, go crazy and recommend something.Next Steps
Im interested in concerns that you as library maintainer have with any or all of these plans. You know how people commonly use this gem and maybe you could help provide a top 5 (or whatever number) of use cases. I'm also interested in alternative solutions. If we can figure out one that makes sense to try, i'll be happy to work on an spike implementation so we can benchmark speed an memory use. Hopefully you're interested. Let me know what you think 😄