aaronpowell / vscode-profile-switcher

A VS Code extension for switching settings easily
https://marketplace.visualstudio.com/items?itemName=aaronpowell.vscode-profile-switcher&wt.mc_id=profileswitcher-github-aapowell
MIT License
177 stars 19 forks source link

SOLUTION: Fixing the bloat and performance hits, and optimizing compatability #40

Closed NerdyDeedsLLC closed 3 years ago

NerdyDeedsLLC commented 3 years ago

@aaronpowell

I love the idea of this. LOVE it. I've been a full-time dev for DECADES, and have RADICALLY different workflows for work vs personal, different languages, different servers/clouds, and different extensions for them all. I also write my own, a half dozen of which I have never published to the store, as they are too hyper-specific to me or my environments. Indeed, I only FOUND this extension because, fed up that starting a new project takes me 20 minutes of config, I was going to just code the damn thing myself.

But, see, here's the thing: it's safe to assume that most of the people using something like this have a great deal of configurations to handle. That's WHY they care enough to deal with it. In my case, as a for-instance, I run a BUNCH of extensions. Thing is, this tool just carbon copies the entirety of my extended profile configuration. I installed, and, not thinking twice, spun up configs for HOME - VanillaJS (Default), HOME - REACT, HOME - VUE, HOME - VSC Extensions, HOME - Chrome/FF Extensions, WORK - VanillaJS (Default), WORK - REACT, WORK - VUE, and BASH - Advanced Tooling.

Then swung over to the extensions settings, thinking to fine-tailor/refine/add the always-ons, etc. "There are none? Huh. Okay. Didn't GUI 'em. S'cool." So I manually open the settings file.

...which is now over 9400 lines long.

Now, this is bad, in and of itself. Worse was when I realized SettingsSync had, with a crisp salute and a "Daaaw, you got it boss!" already overwrote my previous config with this behemoth. But the thing that really irked me? THEY'RE ALL THE SAME CONFIG. I hadn't CHANGED anything yet. I just did what I thought was the equivalent of "Save As, Save As, Save As, Save As"... except you're NOT saving the file. You're APPENDING onto the existing one.

Now, I get that: SettingsSync won't do you much good for loose, external files. That's fair. But, as another for-instance, here, yanno what I almost NEVER change? My editor font. Or my preferred quote style. Or whether or not I want the IDE to auto-close (ANYTHING). Or the SETTINGS for most of the 180 extensions I run SOMETIMES, situation-specific, some of which are 200+ lines of configuration. Each.

So what if... and I'm just spitballing here... You have a DEFAULT CONFIG, containing ALL of the IDE settings, ALL of the extension data (and with all of them set to DISABLED, so the IDE loads FAST and the ExtensionHostManager ONLY needs to worry about the couple exceptions: "initialization AlwaysOn's" and ProfileSwitcher).

Then, when I create a profile, it takes a similar snapshot, and recursively DIFFS the two JSON objects. The DIFFERENCES get stored, NOT the whole config (e.g. if I have an object with all the alphabet as keys, and a letter-appropriate animal as values - {"a":"alligator", "b":"babboon", "c":"cheetah" ...} - I only worry about storing the data when you change {"s":"sloth"} to {"s":"sabretooth tiger"}, and even then, I ONLY store the ONE key/value pair).

OnLoad, VSC ingests the default config. Your extension finishes initialization, and begins to load the desired profile. This is accomplished by

  1. Reading the active, default profile. We'll call it nativeProf
  2. Reading the profile that Profile Switcher had last loaded or has been instructed to. We'll call it switcherProf
  3. Ingesting the nativeProf JSON object, and deep-copying it, replicating all of the information contained within EXCEPT extension data into a new object, call it constructedProfile (let constructedProfile = Object.assign({}, nativeProf); constructedProfile.extensions = [];).

    The logic here will make sense in a sec

  4. Now append in the differences, along with the extensions the user has enabled for this profile. constructedProfile = Object.assign(constructedProfile, switcherProf);

    Capital! Now we have all the basic settings from the profile, we've nondestructively overwritten them with the saved profile's preferences (those that differed, anyway), and we now have a collection containing ONLY the extensions the user was running in that profile (Object.assign(constructedProfile, **alwaysOnExtensions**, switcherProf); if you like, to handle that use case too).

  5. Iterate that extension collection, and replace the settings for each with the ones in the default config, then overwrite THOSE in turn with the diffs from the profile (same process as above: constructedProfile.extensions = Object.entries(extensions).map(ext=>Object.assign({}, nativeProf.extensions[ext[0]], alwaysOnExtensions.extensions[ext[0]], ext[1]).

    It may make more sense to do a find() in there; I've not looked at the code yet, but you get the idea.

  6. Apply constructedProfile as the active profile. Bam. Done.

The net result here is you wind with with the following structure:

{
    {vanillaConfig},
    {activeProfileSwitcherConfig},
    profileSwitcherProfiles: [
        {
            {...work},
            extensions:[
                {ext1.id},
                {ext2.id},
                {
                    {ext3.id},
                    {ext3_CHANGED_settings}
                },
                ...
            ]
        },
        {home},
        ...
    ]
}

... which, while it does result in a substantive increase in the size of vanillaConfig, it'll very likely be less than double, and I GUARANTEE less than the octuple I'm looking at now. Additionally, it'll leverage all the activation and usage code you have now. Finally, SettingsSync will store both all the saved profiles, the default profile, and the most recently active profile, too.

_Note that in the above example, home, being a carbon copy of the default config, is functionally an empty object, existing only so we can reference it's key as one of the available profiles defined in Profile Switcher. Likewise, ext1 and ext2 (who ARE in the profile, but whose configurations are the same as the system settings), exist only as their ID's. ext3 (let's say maybe a linter) DOES have different settings (because maybe because your work makes you flip ignore_dumbass_rules to false)._

LIkewise, this set of changes both obviates the issue of "how do I make changes to SEVERAL profiles if I decide I really need to use this new set of snippets EVERYWHERE I use JS code?" and opens you up for the ability to present what would be a fabulously-useful dialog announcing...

! The following preferences changed since loading this profile. Save these changes? !
[ Yes ] [ No ]

...and followed by a...

! Save to all profiles? Or just the current one? !
[ Save To All Stored Profiles ] [ Currently-Active Profile Only ]

If you're open to contributions, I'll fork the code, implement, and open a PR myself. I'm not just wandering in here to tell you how to do your job; I'll happily double down and help out. I'm not kidding when I say I both love the idea and had been gearing up to just make it myself. I have a similar tool for Chrome (where I have some 450 extensions installed, though generally only USE 4-8 at a time). Big shock, though: the tools that exist for such management are either woefully-underpowered, excruciatingly-slow, or hinky as all get out - written in Chinese and asking for a credit card number as a CAPTCHA solution ("Prove you're a human! Buy me something nice!").

aaronpowell commented 3 years ago

I am aware that the plugin is simplistic in the way that it does the settings management, it just goes "what's in settings right now? store that for next time", so it does result in a lot of bloat in the settings.json file.

Everything needs to be in the settings.json file so that we can use the built in Settings Sync (I no longer support third-party sync extensions), so there's kind of no way around making that file larger the more you have in there.

I agree a better job could be done when it comes to shared config, whether it's theme, editor formatters, etc., but I feel like the code required to support a layering approach you describe would add complexity while only targeting a small set of users, such as yourself.

I'm unconvinced around there being performance improvements though, as the process you're describing (assuming I understood it correctly) would result in more layers of settings needing to be merged, rather than single layer which it currently is. Instead, performance gains would be better achieved by improving the extension add/remove process, as it is basic.

That said, if you're willing to provide a PR, I'm happy to review it, but I make no promises beyond that.

NerdyDeedsLLC commented 3 years ago

@aaronpowell

Hey man, that's totally fair. Indeed, it's entirely possible that I represent the extreme minority of use cases here, and this would be introducing unnecessary complexity and overhead. In fairness, I am one of those devs that is frequently brought in as a consultant to spot-problemsove, so I'm willing to admit that I have a tendency to rank edge-cases as similar in priority to happy-path cases, as usually I'm not involved for happy-path, lol. I tend to get brought in to support that happy path: to help identify when this shoulder needs to be widened, or that turn needs to be banked, or "maybe we should shore up this here bridge before we have a chasm fulla cars?"

As a consequence, I have a... call it a blind spot when it comes to prioritization of criticality (that's the PO's job, lol); I just identify where something's lacking guardrails or has the possibility of causing a pile-up and design solutions.

But, and I want to stress this, the small tome I dropped into your issue log was absolutely NOT a criticism. I was more surprised than anything, and automatic behaviors took over. Indeed, I had a strong suspicion from the get-go I was the edge case on this one and, though the impact of having a 30k config file is trivial - and it largely is; certainly is from a storage, retention, and file management standpoint; drive space is cheap, and I hemorrhage WAY more than 30k in bandwidth every time I hit a site that uses jQuery), it's also a performance hit, and a scalability risk.

Imagine if one ran, say, multiple extensions that all used the same mechanism for retention; that is, they ingested the entirety of the JSON into their own node:

activeProfileSwitcherConfig : {
     {whole of the config}
}
                 ⇓
extension2Config: {
     {activeProfileSwitcherConfig},
     ...{whole of the config}
}
                 ⇓
/* START NEW PROJECT */
                 ⇓
activeProfileSwitcherConfig : {
     {extension2Config},
     {whole of the config}
}
                 ⇓
extension2Config: {
     {activeProfileSwitcherConfig},
     ...whole of the config
}

...see my concern? Toss a third into the mix and you're gonna eat up a bigger footprint than photoshop, and right quick. Worse, if one of them happens to update its snapshot automatically on file change, now you're looking at an exponential expansion instead of "only" a geometric one. If BOTH did it, well, I don't think a logarithmic expansion would take long to test the whole "drive space is cheap" bit ("but wait! There's Moore!"). I want to say I recall VSCode having a check in place to simply identify a config file that's gotten too out of hand as corrupt, dump it, and start over, but, even if I'm not misremembering there, I have no clue what that would do when given that condition combined with a third-party piece of software restoring an offsite backup every time said "refresh and renew" protocol hit.

Edge case? Even corner case? Well, yes, I suppose that'd be a fair assessment. But we're not talking about "aligning the Seven Relics of Power at the center of the Ley Line Nexus on the Day of Solstice at the precise moment of the Planetary Convergence". In this case, it's closer to, "toss your 20kg ball of uranium in the bucket with the other one(s) and lets go get lunch!" (though I admit this example does overinflate the - heh. - criticality of the issue): there's precious few scenarios out side of this flavor extension I can think of in which one would need/want/bother with the full schema. Less still the number that a user would be running concurrently. But the risk is definitely non-zero, and as I mentioned before: the (sub-?)demographic that will be drawn towards such a tool will increase it further, too.

I really do love the idea here, and I really would be happy to contribute. Your call, of course, whether you wish to merge any or all of the code; do what's best for the program. I'm really not trying to come stomping in being a rude, pedantic douche; there's no ego here, I'm just voicing a concern that only occurred to me at all because I brushed up against the edges of it.

I'll create a local branch and see if I cannot offer you more than exposition and theory. If I cannot, then thank you for taking the time to read all of this. Either way, good work on this. It's a nifty piece of code regardless.

...though, once I do fork, I intend to be be damn sure I'm not running both copies at once. Yanno... just in case, lol.